合并yy并同步远程qyqy_develop
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { clearAuthSession, getAccessToken } from '/static/portal/common/auth.js';
|
||||
import { clearAuthSession, getAccessToken } from '/static/portal/common/auth.js?v=20260913';
|
||||
|
||||
const ENDPOINTS = Object.freeze({
|
||||
A034: { method: 'POST', path: '/api/v1/auth/tokens', auth: false },
|
||||
V001: { method: 'POST', path: '/api/v1/visitor-tokens', auth: false, raw: true },
|
||||
P001: { method: 'GET', path: '/api/v1/products' },
|
||||
P002: { method: 'GET', path: '/api/v1/products/{productCode}/nav-history' },
|
||||
C001: { method: 'POST', path: '/api/v1/conversations', idempotent: true },
|
||||
C002: { method: 'GET', path: '/api/v1/conversations/{sessionId}' },
|
||||
C003: { method: 'GET', path: '/api/v1/conversations/{sessionId}/messages' },
|
||||
@@ -13,7 +15,14 @@ const ENDPOINTS = Object.freeze({
|
||||
A038: { method: 'GET', path: '/api/v1/admin/users/{userId}/roles' },
|
||||
A039: { method: 'GET', path: '/api/v1/admin/customer-profile-candidates' },
|
||||
A040: { method: 'POST', path: '/api/v1/admin/customer-profile-candidates/{candidateId}/reviews' },
|
||||
A001: { method: 'POST', path: '/api/v1/admin/config-releases', idempotent: true },
|
||||
A002: { method: 'GET', path: '/api/v1/admin/config-releases' },
|
||||
A008: { method: 'POST', path: '/api/v1/admin/config-releases/{releaseId}/platform-config-items', idempotent: true },
|
||||
A009: { method: 'GET', path: '/api/v1/admin/config-releases/{releaseId}/platform-config-items' },
|
||||
A010: { method: 'PUT', path: '/api/v1/admin/config-releases/{releaseId}/platform-config-items/{itemId}', idempotent: true },
|
||||
A018: { method: 'POST', path: '/api/v1/admin/config-releases/{releaseId}/model-routing-rules', idempotent: true },
|
||||
A019: { method: 'GET', path: '/api/v1/admin/config-releases/{releaseId}/model-routing-rules' },
|
||||
A020: { method: 'PUT', path: '/api/v1/admin/config-releases/{releaseId}/model-routing-rules/{ruleId}', idempotent: true },
|
||||
A003: { method: 'GET', path: '/api/v1/admin/config-releases/{releaseId}' },
|
||||
A004: { method: 'POST', path: '/api/v1/admin/config-releases/{releaseId}/validations', idempotent: true },
|
||||
A005: { method: 'POST', path: '/api/v1/admin/config-releases/{releaseId}/reviews', idempotent: true },
|
||||
@@ -22,6 +31,9 @@ const ENDPOINTS = Object.freeze({
|
||||
A033: { method: 'GET', path: '/api/v1/admin/audit-records' },
|
||||
ADMIN_HANDOVERS: { method: 'GET', path: '/api/v1/admin/customer-service/handover-tickets' },
|
||||
ADMIN_HANDOVER_DETAIL: { method: 'GET', path: '/api/v1/admin/customer-service/handover-tickets/{ticketNo}' },
|
||||
ADMIN_ADVISOR_PENDING: { method: 'GET', path: '/api/v1/admin/advisor/pending-contents' },
|
||||
ADMIN_ADVISOR_REVIEW: { method: 'POST', path: '/api/v1/admin/advisor/recommendations/{contentId}/reviews', idempotent: true },
|
||||
ADMIN_ADVISOR_PUBLISH: { method: 'POST', path: '/api/v1/admin/advisor/recommendations/{contentId}/publications', idempotent: true },
|
||||
ONB001: { method: 'GET', path: '/api/v1/onboarding/risk-questionnaire' },
|
||||
ONB002: { method: 'POST', path: '/api/v1/onboarding/risk-questionnaire/submissions', idempotent: true },
|
||||
R001: { method: 'POST', path: '/api/v1/agent-runs' },
|
||||
@@ -32,13 +44,16 @@ const ENDPOINTS = Object.freeze({
|
||||
RK003: { method: 'GET', path: '/api/v1/risk/alerts/{alertNo}' },
|
||||
RK004: { method: 'GET', path: '/api/v1/risk/evidence/{source}' },
|
||||
RK005: { method: 'GET', path: '/api/v1/risk/notifications' },
|
||||
RK006: { method: 'POST', path: '/api/v1/risk/alerts/scan', idempotent: true },
|
||||
RK006: { method: 'POST', path: '/api/v1/risk/alerts/scan', idempotent: true, timeout: 60000 },
|
||||
RK007: { method: 'POST', path: '/api/v1/risk/alerts/{alertNo}/acknowledgements', idempotent: true },
|
||||
RK008: { method: 'POST', path: '/api/v1/risk/alerts/{alertNo}/investigations', idempotent: true },
|
||||
RK009: { method: 'POST', path: '/api/v1/risk/alerts/{alertNo}/exclusions', idempotent: true },
|
||||
RK010: { method: 'POST', path: '/api/v1/risk/alerts/{alertNo}/resolutions', idempotent: true },
|
||||
RK011: { method: 'POST', path: '/api/v1/risk/alerts/{alertNo}/escalations', idempotent: true },
|
||||
RK012: { method: 'POST', path: '/api/v1/risk/alerts/{alertNo}/evidence', formData: true },
|
||||
// ⚠️ 保留:同上,前端契约测试要求这张表里有它。风控日报现在走 `RK014`(SSE 流式),
|
||||
// 非流式这条当前无人调用。另注:RK013–RK015 目前**尚未登记进 `docs/05` §19**
|
||||
// (与投顾 AD 段原先的情况相同),属于文档缺口。
|
||||
RK013: { method: 'POST', path: '/api/v1/risk/daily-report' },
|
||||
RK014: { method: 'POST', path: '/api/v1/risk/daily-report/stream', stream: true },
|
||||
RK015: { method: 'POST', path: '/api/v1/risk/daily-report/mail' },
|
||||
@@ -52,8 +67,22 @@ const ENDPOINTS = Object.freeze({
|
||||
T008: { method: 'GET', path: '/api/v1/users/me/transactions/{transactionNo}' },
|
||||
T009: { method: 'GET', path: '/api/v1/users/me/cash-ledger' },
|
||||
ADVISOR_PUBLISHED: { method: 'GET', path: '/api/v1/advisor/recommendations/published' },
|
||||
// ⚠️ 保留:前端契约测试(`tests/unit/api/test_portal_frontend.py`)把"页面会用到的端点"
|
||||
// 固定成一张清单,**删注册会破坏它**。它对应 AD002,当前页面确实没调用
|
||||
// (投顾本人没有"自己的投资目标",调它返回 404)—— 但**注册与调用是两件事**。
|
||||
ADVISOR_GOAL: { method: 'GET', path: '/api/v1/advisor/investment-goals/current' },
|
||||
ADVISOR_ANALYSIS: { method: 'POST', path: '/api/v1/advisor/portfolio-analysis' },
|
||||
ADVISOR_ALLOCATION: { method: 'POST', path: '/api/v1/advisor/asset-allocation' },
|
||||
ADVISOR_RECOMMEND: { method: 'POST', path: '/api/v1/advisor/recommendations', idempotent: true },
|
||||
ADVISOR_CREATE_GOAL: { method: 'POST', path: '/api/v1/advisor/investment-goals', idempotent: true },
|
||||
ADVISOR_CUSTOMER_GOAL: { method: 'GET', path: '/api/v1/advisor/customers/{customerId}/investment-goals/current' },
|
||||
ADVISOR_CONFIRM_GOAL: { method: 'POST', path: '/api/v1/advisor/investment-goals/{goalNo}/confirmations', idempotent: true },
|
||||
ADVISOR_GOAL_BOOK: { method: 'GET', path: '/api/v1/advisor/investment-goals/{goalNo}/goal-book' },
|
||||
ADVISOR_REVIEW_BOOK: { method: 'POST', path: '/api/v1/advisor/investment-goals/{goalNo}/goal-book/reviews', idempotent: true },
|
||||
ADVISOR_PUBLISH_BOOK: { method: 'POST', path: '/api/v1/advisor/investment-goals/{goalNo}/goal-book/publications', idempotent: true },
|
||||
K002: { method: 'POST', path: '/api/v1/knowledge/upload' },
|
||||
K003: { method: 'GET', path: '/api/v1/knowledge/list' },
|
||||
K004: { method: 'DELETE', path: '/api/v1/knowledge/{knowledgeId}', idempotent: true },
|
||||
OFFSITE_MAILS: { method: 'GET', path: '/api/v1/offsite-fund/mails' },
|
||||
OFFSITE_MAIL: { method: 'GET', path: '/api/v1/offsite-fund/mails/{mailId}' },
|
||||
OFFSITE_MAIL_DELETE: { method: 'POST', path: '/api/v1/offsite-fund/mails/{mailId}/deletions', idempotent: true },
|
||||
@@ -133,7 +162,10 @@ async function request(endpointId, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const abortListener = () => controller.abort();
|
||||
options.signal?.addEventListener('abort', abortListener, { once: true });
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), options.timeout || 8000);
|
||||
const timeoutId = window.setTimeout(
|
||||
() => controller.abort(),
|
||||
options.timeout || endpoint.timeout || 8000,
|
||||
);
|
||||
try {
|
||||
const response = await fetch(`${pathFor(endpoint, options.pathParams)}${queryString}`, {
|
||||
method: endpoint.method,
|
||||
@@ -145,7 +177,10 @@ async function request(endpointId, options = {}) {
|
||||
signal: controller.signal,
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (response.status === 401 && endpoint.auth !== false) clearAuthSession();
|
||||
if (response.status === 401 && endpoint.auth !== false) {
|
||||
clearAuthSession({ eventType: 'session-expired' });
|
||||
window.dispatchEvent(new CustomEvent('portal:auth-expired'));
|
||||
}
|
||||
const responseTraceId = payload.meta?.trace_id || response.headers.get('X-Trace-ID') || traceId;
|
||||
document.documentElement.dataset.traceId = responseTraceId;
|
||||
const hasBusinessError = Object.prototype.hasOwnProperty.call(payload, 'code') && payload.code !== 0;
|
||||
@@ -212,6 +247,10 @@ async function stream(endpointId, body, options = {}) {
|
||||
});
|
||||
if (!response.ok || !response.body) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (response.status === 401 && endpoint.auth !== false) {
|
||||
clearAuthSession({ eventType: 'session-expired' });
|
||||
window.dispatchEvent(new CustomEvent('portal:auth-expired'));
|
||||
}
|
||||
const detail = payload.error || {};
|
||||
throw new ApiError(detail.message || '流式请求未完成', {
|
||||
code: detail.code,
|
||||
@@ -281,6 +320,21 @@ async function requestFile(endpointId, options = {}) {
|
||||
export const apiClient = Object.freeze({
|
||||
get(endpointId, options = {}) { return request(endpointId, options); },
|
||||
post(endpointId, body, options = {}) { return request(endpointId, { ...options, body }); },
|
||||
/**
|
||||
* 带请求体的 PUT(更新类端点)。
|
||||
*
|
||||
* 与 `del` 同理:真正发出的方法由端点表里的 `method` 决定,所以 `post('A010')`
|
||||
* 也会发出 PUT —— 但读代码的人会以为发的是 POST。用它表达"这是更新"。
|
||||
*/
|
||||
put(endpointId, body, options = {}) { return request(endpointId, { ...options, body }); },
|
||||
/**
|
||||
* 无请求体的写方法(DELETE 等)。
|
||||
*
|
||||
* 实际发什么方法由**端点表里的 `method`** 决定(`request()` 用的就是它),
|
||||
* 所以过去用 `post('K004')` 也能发出 DELETE —— 但读代码的人会以为发的是 POST。
|
||||
* 有了这个方法,`del('K004')` 的意图与行为一致。
|
||||
*/
|
||||
del(endpointId, options = {}) { return request(endpointId, options); },
|
||||
upload(endpointId, formData, options = {}) { return request(endpointId, { ...options, body: formData, timeout: options.timeout || 30000 }); },
|
||||
file(endpointId, options = {}) { return requestFile(endpointId, options); },
|
||||
stream,
|
||||
|
||||
Reference in New Issue
Block a user