feat(web): 基金转换演示页与 agent 镜像表迁移; Core seed 修复
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
-1
@@ -8,7 +8,10 @@
|
||||
**2026-09-12 · 基金转换 T+1**(分支 **`integrate/risk-convert`** · `risk-control-agent` → `merger` 方案 A · 待合入 `merger`)
|
||||
|
||||
- [x] merge 冲突解完(15 文件)· `main.py` 保留平台/问数/顾问 + convert 路由 · `core_ro` 并集 + 顾问行情别名
|
||||
- [ ] 本机 `reset.ps1` 灌 Core convert 种子 · 全量 pytest 回归 · fast-forward **`merger`**
|
||||
- [x] fast-forward **`merger`** → `03fce88`(`integrate/risk-convert`)
|
||||
- [x] 本机 Core convert 种子(`verify_convert_seed.py` + nav-history 可选)· `verify_convert_api.py` 9/9
|
||||
- [x] 前端:`/app/risk/convert` + `web/src/api/convert.ts`
|
||||
- [ ] 全量 pytest 回归
|
||||
|
||||
**2026-09-12 · 投资顾问 Agent 合并**(分支 **`integrate/advisor-agent`** · 目标合入 **`merger`**)
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
USE jinrong_agent;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS risk_convert_detail (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
convert_group_id VARCHAR(64) NOT NULL,
|
||||
status ENUM('pending','completed','failed','cancelled','expired') NOT NULL DEFAULT 'pending',
|
||||
out_trade_id VARCHAR(64) NULL,
|
||||
in_trade_id VARCHAR(64) NULL,
|
||||
related_trade_id VARCHAR(64) NULL,
|
||||
nav DECIMAL(10,4) NULL,
|
||||
nav_date DATE NULL,
|
||||
fee_amount DECIMAL(18,2) NULL,
|
||||
hold_days_min INT NULL,
|
||||
hold_days_max INT NULL,
|
||||
estimated TINYINT(1) NOT NULL DEFAULT 1,
|
||||
nav_stale TINYINT(1) NOT NULL DEFAULT 0,
|
||||
client_request_id VARCHAR(64) NULL,
|
||||
cancelled_at DATETIME(3) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
UNIQUE KEY uk_group (convert_group_id),
|
||||
UNIQUE KEY uk_idem (client_request_id),
|
||||
KEY idx_out (out_trade_id),
|
||||
KEY idx_in (in_trade_id)
|
||||
) ENGINE=InnoDB COMMENT='基金转换详情(Agent 侧)';
|
||||
@@ -1,18 +1,30 @@
|
||||
USE jinrong_core;
|
||||
USE jinrong_core;
|
||||
|
||||
-- 基金净值种子:**真实历史净值,非人造数值**
|
||||
-- 生成方式:python scripts/core/fetch_nav.py --days 60
|
||||
-- 数据来源:天天基金(东方财富)公开接口 https://fund.eastmoney.com/pingzhongdata/{代码}.js
|
||||
-- 抓取时间:2026-09-12 14:56:55
|
||||
-- 区间:2026-06-22 ~ 2026-09-11(共 60 个净值发布日)
|
||||
--
|
||||
-- 产品 ↔ 净值来源映射(产品 ID 为虚构,数值借用下列真实基金,按类型/风险等级配对):
|
||||
-- PROD-000002 <- 014437 鹏华中证同业存单AAA指数7天持有(bond/R1:同业存单,日波动 0.01%)
|
||||
-- PROD-003095 <- 003095 中欧医疗健康混合A(stock/R4:医药主题,代码即真身)
|
||||
-- PROD-005827 <- 005827 易方达蓝筹精选混合(mixed/R3:代码即真身,股债平衡)
|
||||
-- PROD-005828 <- 001182 易方达安心回馈混合A(mixed/R2:低波混合,日波动 0.73%)
|
||||
-- PROD-110022 <- 000191 富国信用债债券A/B(bond/R1:纯债,日波动 0.02%)
|
||||
-- PROD-110023 <- 000032 易方达信用债债券A(bond/R2:信用债,日波动 0.09%)
|
||||
-- PROD-161725 <- 161631 融通人工智能指数(LOF)A(stock/R4:科技主题,日波动 2.57%)
|
||||
-- PROD-161726 <- 110022 易方达消费行业股票(stock/R4:消费主题,代码即真身)
|
||||
-- PROD-510300 <- 510300 沪深300ETF华泰柏瑞(index/R3:宽基指数,代码即真身)
|
||||
-- PROD-510500 <- 510500 中证500ETF南方(index/R4:宽基指数,代码即真身)
|
||||
-- PROD-PRIV01 <- 519062 海富通阿尔法对冲混合A(private_fund/R4:量化对冲,日波动 0.33%)
|
||||
-- PROD-WMG001 <- 004155 中信保诚至泰中短债A(wealth_mgmt/R2:中短债,日波动 0.02%)
|
||||
-- PROD-XYZ999 <- 320007 诺安成长混合A(stock/R5:高波动成长,日波动 1.76%)
|
||||
-- PROD-000001 <- 恒定 1.0000(现金宝货币(money/R1:货基单位净值恒为 1 元,收益以份额结转))
|
||||
--
|
||||
-- ⚠️ 真实净值只到抓取当日。之后运行需重跑 fetch_nav.py 增量补拉,
|
||||
-- 否则 T+1 确认会取不到 T 日净值(真实业务同样是「净值 T+1 才公告」)。
|
||||
|
||||
-- 鍩洪噾鍑€鍊肩瀛愶細**鐪熷疄鍘嗗彶鍑€鍊硷紝闈炰汉閫犳暟鍊?*
|
||||
-- 鐢熸垚鏂瑰紡锛歱ython scripts/core/fetch_nav.py --days 60
|
||||
-- 鏁版嵁鏉ユ簮锛氬ぉ澶╁熀閲戯紙涓滄柟璐㈠瘜锛夊叕寮€鎺ュ彛 https://fund.eastmoney.com/pingzhongdata/{浠g爜}.js
|
||||
-- 鎶撳彇鏃堕棿锛?026-09-12 14:56:55
|
||||
-- 鍖洪棿锛?026-06-22 ~ 2026-09-11锛堝叡 60 涓噣鍊煎彂甯冩棩锛?--
|
||||
-- 浜у搧 鈫?鍑€鍊兼潵婧愭槧灏勶紙浜у搧 ID 涓鸿櫄鏋勶紝鏁板€煎€熺敤涓嬪垪鐪熷疄鍩洪噾锛屾寜绫诲瀷/椋庨櫓绛夌骇閰嶅锛夛細
|
||||
-- PROD-000002 <- 014437 楣忓崕涓瘉鍚屼笟瀛樺崟AAA鎸囨暟7澶╂寔鏈夛紙bond/R1锛氬悓涓氬瓨鍗曪紝鏃ユ尝鍔?0.01%锛?-- PROD-003095 <- 003095 涓鍖荤枟鍋ュ悍娣峰悎A锛坰tock/R4锛氬尰鑽富棰橈紝浠g爜鍗崇湡韬級
|
||||
-- PROD-005827 <- 005827 鏄撴柟杈捐摑绛圭簿閫夋贩鍚堬紙mixed/R3锛氫唬鐮佸嵆鐪熻韩锛岃偂鍊哄钩琛★級
|
||||
-- PROD-005828 <- 001182 鏄撴柟杈惧畨蹇冨洖棣堟贩鍚圓锛坢ixed/R2锛氫綆娉㈡贩鍚堬紝鏃ユ尝鍔?0.73%锛?-- PROD-110022 <- 000191 瀵屽浗淇$敤鍊哄€哄埜A/B锛坆ond/R1锛氱函鍊猴紝鏃ユ尝鍔?0.02%锛?-- PROD-110023 <- 000032 鏄撴柟杈句俊鐢ㄥ€哄€哄埜A锛坆ond/R2锛氫俊鐢ㄥ€猴紝鏃ユ尝鍔?0.09%锛?-- PROD-161725 <- 161631 铻嶉€氫汉宸ユ櫤鑳芥寚鏁?LOF)A锛坰tock/R4锛氱鎶€涓婚锛屾棩娉㈠姩 2.57%锛?-- PROD-161726 <- 110022 鏄撴柟杈炬秷璐硅涓氳偂绁紙stock/R4锛氭秷璐逛富棰橈紝浠g爜鍗崇湡韬級
|
||||
-- PROD-510300 <- 510300 娌繁300ETF鍗庢嘲鏌忕憺锛坕ndex/R3锛氬鍩烘寚鏁帮紝浠g爜鍗崇湡韬級
|
||||
-- PROD-510500 <- 510500 涓瘉500ETF鍗楁柟锛坕ndex/R4锛氬鍩烘寚鏁帮紝浠g爜鍗崇湡韬級
|
||||
-- PROD-PRIV01 <- 519062 娴峰瘜閫氶樋灏旀硶瀵瑰啿娣峰悎A锛坧rivate_fund/R4锛氶噺鍖栧鍐诧紝鏃ユ尝鍔?0.33%锛?-- PROD-WMG001 <- 004155 涓俊淇濊瘹鑷虫嘲涓煭鍊篈锛坵ealth_mgmt/R2锛氫腑鐭€猴紝鏃ユ尝鍔?0.02%锛?-- PROD-XYZ999 <- 320007 璇哄畨鎴愰暱娣峰悎A锛坰tock/R5锛氶珮娉㈠姩鎴愰暱锛屾棩娉㈠姩 1.76%锛?-- PROD-000001 <- 鎭掑畾 1.0000锛堢幇閲戝疂璐у竵锛坢oney/R1锛氳揣鍩哄崟浣嶅噣鍊兼亽涓?1 鍏冿紝鏀剁泭浠ヤ唤棰濈粨杞級锛?--
|
||||
-- 鈿狅笍 鐪熷疄鍑€鍊煎彧鍒版姄鍙栧綋鏃ャ€備箣鍚庤繍琛岄渶閲嶈窇 fetch_nav.py 澧為噺琛ユ媺锛?-- 鍚﹀垯 T+1 纭浼氬彇涓嶅埌 T 鏃ュ噣鍊硷紙鐪熷疄涓氬姟鍚屾牱鏄€屽噣鍊?T+1 鎵嶅叕鍛娿€嶏級銆?
|
||||
INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date) VALUES
|
||||
('PROD-000001', 1.0000, 0.0000, '2026-06-22'),
|
||||
('PROD-000001', 1.0000, 0.0000, '2026-06-23'),
|
||||
|
||||
@@ -23,6 +23,7 @@ import { ProductNavDetailPage } from './pages/market/ProductNavDetailPage'
|
||||
import { RiskAlertsPage } from './pages/risk/RiskAlertsPage'
|
||||
import { RiskChatPage } from './pages/risk/RiskChatPage'
|
||||
import { RiskSimulatePage } from './pages/risk/RiskSimulatePage'
|
||||
import { RiskConvertPage } from './pages/risk/RiskConvertPage'
|
||||
import { RiskSuitabilityPage } from './pages/risk/RiskSuitabilityPage'
|
||||
import { RiskAmlScanPage } from './pages/risk/RiskAmlScanPage'
|
||||
import { ChatPanel } from './components/chat'
|
||||
@@ -109,6 +110,7 @@ export default function AppRoutes() {
|
||||
<Route path="risk/suitability" element={<RiskSuitabilityPage />} />
|
||||
<Route path="risk/aml-scan" element={<RiskAmlScanPage />} />
|
||||
<Route path="risk/simulate" element={<RiskSimulatePage />} />
|
||||
<Route path="risk/convert" element={<RiskConvertPage />} />
|
||||
<Route path="risk/chat" element={<RiskChatPage />} />
|
||||
|
||||
<Route path="market" element={<MarketQuotesPage />} />
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { apiFetch } from './client'
|
||||
import type { AgentType } from './chat'
|
||||
|
||||
function platformHeaders(token: string, agentType: AgentType = 'risk'): HeadersInit {
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Agent-Type': agentType,
|
||||
}
|
||||
}
|
||||
|
||||
export type ConvertAcceptPayload = {
|
||||
customer_id: string
|
||||
trade_type: 'convert'
|
||||
from_product_id: string
|
||||
to_product_id: string
|
||||
qty: number
|
||||
client_request_id?: string
|
||||
}
|
||||
|
||||
export type ConvertAcceptResponse = {
|
||||
accepted?: boolean
|
||||
convert_group_id?: string
|
||||
status?: string
|
||||
blocked?: boolean
|
||||
message?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** POST /api/simulate/trade — trade_type=convert,成功受理为 HTTP 202。 */
|
||||
export async function submitConvertAccept(token: string, payload: ConvertAcceptPayload) {
|
||||
const { data } = await apiFetch<ConvertAcceptResponse>('/api/simulate/trade', {
|
||||
method: 'POST',
|
||||
token,
|
||||
headers: platformHeaders(token, 'risk'),
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export type ConvertQueryResponse = Record<string, unknown>
|
||||
|
||||
/** GET /api/simulate/trade/convert/{convert_group_id} */
|
||||
export async function getConvertRequest(token: string, convertGroupId: string) {
|
||||
const { data } = await apiFetch<ConvertQueryResponse>(
|
||||
`/api/simulate/trade/convert/${encodeURIComponent(convertGroupId)}`,
|
||||
{ token, headers: platformHeaders(token, 'risk') },
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
/** POST /api/simulate/trade/convert/{convert_group_id}/cancel */
|
||||
export async function cancelConvertRequest(token: string, convertGroupId: string) {
|
||||
const { data } = await apiFetch<Record<string, unknown>>(
|
||||
`/api/simulate/trade/convert/${encodeURIComponent(convertGroupId)}/cancel`,
|
||||
{ method: 'POST', token, headers: platformHeaders(token, 'risk'), body: JSON.stringify({}) },
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
/** POST /api/admin/convert/confirm?accept_date=YYYY-MM-DD — 需 risk_officer。 */
|
||||
export async function confirmConvertBatch(token: string, acceptDate: string) {
|
||||
const q = new URLSearchParams({ accept_date: acceptDate })
|
||||
const { data } = await apiFetch<Record<string, unknown>>(`/api/admin/convert/confirm?${q}`, {
|
||||
method: 'POST',
|
||||
token,
|
||||
headers: platformHeaders(token, 'risk'),
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { Alert, Form, Input, InputNumber, Tabs, Typography } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { ApiErrorResult } from '../../components/ApiErrorResult'
|
||||
import { PageShell } from '../../components/PageShell'
|
||||
import { Button } from '../../components/ui'
|
||||
import {
|
||||
cancelConvertRequest,
|
||||
confirmConvertBatch,
|
||||
getConvertRequest,
|
||||
submitConvertAccept,
|
||||
} from '../../api/convert'
|
||||
import { ApiError } from '../../api/client'
|
||||
import { RiskJsonResult } from '../../components/risk/RiskJsonResult'
|
||||
import { useAppAuth } from '../../layouts/AppLayout'
|
||||
|
||||
const ACCEPT_PRESETS = [
|
||||
{
|
||||
key: 'demo',
|
||||
label: '演示全转(CUST-1001 · 110022→161725 · 30000 份)',
|
||||
values: {
|
||||
customer_id: 'CUST-1001',
|
||||
from_product_id: 'PROD-110022',
|
||||
to_product_id: 'PROD-161725',
|
||||
qty: 30000,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export function RiskConvertPage() {
|
||||
const auth = useAppAuth()
|
||||
const [acceptForm] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [acceptResult, setAcceptResult] = useState<Record<string, unknown> | null>(null)
|
||||
const [queryId, setQueryId] = useState('')
|
||||
const [queryResult, setQueryResult] = useState<Record<string, unknown> | null>(null)
|
||||
const [confirmDate, setConfirmDate] = useState('2026-09-15')
|
||||
const [confirmResult, setConfirmResult] = useState<Record<string, unknown> | null>(null)
|
||||
|
||||
const onAccept = async () => {
|
||||
const values = await acceptForm.validateFields()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setAcceptResult(null)
|
||||
try {
|
||||
const data = await submitConvertAccept(auth.accessToken, {
|
||||
customer_id: values.customer_id,
|
||||
trade_type: 'convert',
|
||||
from_product_id: values.from_product_id,
|
||||
to_product_id: values.to_product_id,
|
||||
qty: values.qty,
|
||||
client_request_id: values.client_request_id || undefined,
|
||||
})
|
||||
setAcceptResult(data as Record<string, unknown>)
|
||||
if (data.convert_group_id) setQueryId(String(data.convert_group_id))
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e : new Error('accept failed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onQuery = async () => {
|
||||
if (!queryId.trim()) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await getConvertRequest(auth.accessToken, queryId.trim())
|
||||
setQueryResult(data)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e : new Error('query failed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = async () => {
|
||||
if (!queryId.trim()) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await cancelConvertRequest(auth.accessToken, queryId.trim())
|
||||
setQueryResult(data)
|
||||
await onQuery()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e : new Error('cancel failed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirmBatch = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setConfirmResult(null)
|
||||
try {
|
||||
const data = await confirmConvertBatch(auth.accessToken, confirmDate)
|
||||
setConfirmResult(data)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e : new Error('confirm failed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
title="基金转换(T+1)"
|
||||
crumbs={[
|
||||
{ title: '风控工作台', path: '/app/risk/home' },
|
||||
{ title: '基金转换' },
|
||||
]}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-6"
|
||||
message="演示说明"
|
||||
description={
|
||||
<>
|
||||
受理 <Typography.Text code>POST /api/simulate/trade</Typography.Text>(convert)· 查询/撤单{' '}
|
||||
<Typography.Text code>/api/simulate/trade/convert/…</Typography.Text> · 确认批处理{' '}
|
||||
<Typography.Text code>POST /api/admin/convert/confirm</Typography.Text>(需 risk_officer,如 STAFF-30001)。
|
||||
Core 需已灌 convert 种子(<Typography.Text code>verify_convert_seed.py</Typography.Text>)。
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{error && <ApiErrorResult error={error as ApiError} />}
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'accept',
|
||||
label: '受理',
|
||||
children: (
|
||||
<div className="max-w-xl space-y-4">
|
||||
<Form form={acceptForm} layout="vertical" initialValues={ACCEPT_PRESETS[0].values}>
|
||||
<Form.Item label="快捷场景">
|
||||
<Input
|
||||
readOnly
|
||||
value={ACCEPT_PRESETS[0].label}
|
||||
onClick={() => acceptForm.setFieldsValue(ACCEPT_PRESETS[0].values)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="customer_id" label="客户 ID" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="from_product_id" label="转出产品" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="to_product_id" label="转入产品" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="qty" label="申请份额" rules={[{ required: true }]}>
|
||||
<InputNumber min={0.01} className="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item name="client_request_id" label="幂等键(可选)">
|
||||
<Input placeholder="client_request_id" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Button variant="primary" loading={loading} onClick={onAccept}>
|
||||
提交受理
|
||||
</Button>
|
||||
<RiskJsonResult title="受理回执" data={acceptResult} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'query',
|
||||
label: '查询 / 撤单',
|
||||
children: (
|
||||
<div className="max-w-xl space-y-4">
|
||||
<Input
|
||||
value={queryId}
|
||||
onChange={(e) => setQueryId(e.target.value)}
|
||||
placeholder="convert_group_id"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="primary" loading={loading} onClick={onQuery}>
|
||||
查询
|
||||
</Button>
|
||||
<Button loading={loading} onClick={onCancel}>
|
||||
撤单
|
||||
</Button>
|
||||
</div>
|
||||
<RiskJsonResult title="查询结果" data={queryResult} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'confirm',
|
||||
label: '确认批处理',
|
||||
children: (
|
||||
<div className="max-w-md space-y-4">
|
||||
<Input
|
||||
value={confirmDate}
|
||||
onChange={(e) => setConfirmDate(e.target.value)}
|
||||
placeholder="业务确认日 YYYY-MM-DD"
|
||||
/>
|
||||
<Button variant="primary" loading={loading} onClick={onConfirmBatch}>
|
||||
触发 confirm_batch
|
||||
</Button>
|
||||
<RiskJsonResult title="批处理结果" data={confirmResult} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</PageShell>
|
||||
)
|
||||
}
|
||||
@@ -129,6 +129,7 @@ export function buildMenuGroups(roleLabel: string): RouteMenuGroup[] {
|
||||
{ key: '/app/risk/suitability', icon: <CheckCircleOutlined />, label: '适当性校验', path: '/app/risk/suitability' },
|
||||
{ key: '/app/risk/aml-scan', icon: <SafetyCertificateOutlined />, label: 'AML 扫描', path: '/app/risk/aml-scan' },
|
||||
{ key: '/app/risk/simulate', icon: <SwapOutlined />, label: '模拟交易', path: '/app/risk/simulate' },
|
||||
{ key: '/app/risk/convert', icon: <SwapOutlined />, label: '基金转换', path: '/app/risk/convert' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user