title: KYC页面流程 description: 用户在前端页面提交KYC认证申请,输入相关信息并点击提交,前端调用后端API接口进行KYC认证流程的处理,后端验证用户信息并更新数据库状态... date: "2026-03-10" tags: [RWA, Web3, Next.js] cover: /images/rwa-cover.jpg
1.完整 KYC 模块
整体流程:
用户点击 KYC申请
↓
填写 KYC 表单
↓
提交 /api/rwa/kyc/apply
↓
数据库 status = pending
↓
管理员审核
↓
调用 /api/rwa/whitelist
↓
合约 setWhitelist(address,true)
1.1 数据库表设计(KYC)
create table rwa_kyc_requests (
id uuid primary key default gen_random_uuid(),
wallet_address text not null,
full_name text,
country text,
id_type text,
id_number text,
id_front_url text,
id_back_url text,
selfie_url text,
status text default 'pending',
reviewer text,
reviewed_at timestamptz,
created_at timestamptz default now()
);
-- status: pending, approved, rejected
二、项目目录结构
app
├ api
│ └ rwa
│ ├ kyc
│ │ ├ apply
│ │ │ └ route.ts
│ │ └ status
│ │ └ route.ts
│ └ whitelist
│ └ route.ts
│
├ kyc
│ └ apply
│ └ page.tsx
│
└ admin
└ kyc
└ page.tsx
三、KYC申请按钮(你的按钮升级版)
<Button
variant="contained"
size="large"
fullWidth
sx={{ borderRadius: 3 }}
onClick={() => router.push("/kyc/apply")}
>
KYC 认证申请
</Button>
四、KYC申请页面(MUI V7)
/app/kyc/apply/page.tsx
五、KYC提交 API
/api/rwa/kyc/apply/route.ts
import pool from "@/lib/db";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest){
const body = await req.json()
const { wallet, full_name, country, id_type, id_number } = body
await pool.query(
`insert into rwa_kyc_requests
(wallet_address,full_name,country,id_type,id_number)
values ($1,$2,$3,$4,$5)
`,
[wallet,full_name,country,id_type,id_number]
)
return NextResponse.json({ success:true })
}
六、KYC状态查询 API
GET /api/rwa/kyc/status?wallet=xxx
export async function GET(req: NextRequest){
const wallet = req.nextUrl.searchParams.get("wallet")
const { rows } = await pool.query(
`select status from rwa_kyc_requests
where wallet_address=$1
order by created_at desc
limit 1`,
[wallet]
)
return NextResponse.json(rows[0] || { status:"none"})
}
七、管理员审核页面(核心)
/admin/kyc
"use client";
import { useEffect, useState } from "react";
import {
Box,
Typography,
Card,
CardContent,
Stack,
Chip,
Button,
TextField,
MenuItem,
Drawer,
Divider
} from "@mui/material";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
type KycRow = {
id: string;
wallet_address: string;
full_name: string;
country: string;
id_type: string;
id_number: string;
status: string;
created_at: string;
};
export default function AdminKycPage() {
const [rows, setRows] = useState<KycRow[]>([]);
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState("pending");
const [search, setSearch] = useState("");
const [selected, setSelected] = useState<KycRow | null>(null);
const load = async () => {
setLoading(true);
let active = true;
try {
const res = await fetch(`/api/admin/kyc?status=${status}`);
const data = await res.json();
if (active) setRows(data);
} finally {
if (active) setLoading(false);
}
return () => { active = false };
};
useEffect(() => {
const load = async () => {
setLoading(true);
const res = await fetch(`/api/admin/kyc?status=${status}`);
const data = await res.json();
setRows(data);
setLoading(false);
};
load();
}, [status]);
const approve = async (wallet: string) => {
await fetch("/api/rwa/whitelist", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet }),
});
load();
};
const reject = async (id: string) => {
await fetch("/api/admin/kyc/reject", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
load();
};
const columns: GridColDef[] = [
{
field: "wallet_address",
headerName: "钱包地址",
flex: 1.2,
},
{
field: "full_name",
headerName: "姓名",
flex: 1,
},
{
field: "country",
headerName: "国家/地区",
width: 120,
},
{
field: "id_type",
headerName: "证件类型",
width: 140,
},
{
field: "status",
headerName: "状态",
width: 130,
renderCell: (params) => {
const s = params.value;
if (s === "pending")
return <Chip label="待审核" color="warning" size="small" />;
if (s === "approved")
return <Chip label="已通过" color="success" size="small" />;
return <Chip label="已拒绝" color="error" size="small" />;
},
},
{
field: "created_at",
headerName: "提交时间",
width: 180,
},
{
field: "actions",
headerName: "操作",
width: 220,
renderCell: (params) => (
<Stack direction="row" spacing={1}>
<Button
size="small"
variant="outlined"
onClick={() => setSelected(params.row)}
>
查看
</Button>
{params.row.status === "pending" && (
<>
<Button
size="small"
variant="contained"
color="success"
onClick={() => approve(params.row.wallet_address)}
>
通过
</Button>
<Button
size="small"
variant="outlined"
color="error"
onClick={() => reject(params.row.id)}
>
拒绝
</Button>
</>
)}
</Stack>
),
},
];
const filtered = rows.filter((r) => {
if (!search) return true;
return (
r.wallet_address.toLowerCase().includes(search.toLowerCase()) ||
r.full_name?.toLowerCase().includes(search.toLowerCase())
);
});
return (
<Box p={4} sx={{ mt: 7, maxWidth: 1600, mx: "auto" }}>
<Typography variant="h4" fontWeight={700} mb={3}>
KYC 审核管理
</Typography>
<Card sx={{ borderRadius: 4 }}>
<CardContent>
<Stack
direction={{ xs: "column", md: "row" }}
spacing={2}
mb={3}
>
<TextField
label="搜索钱包 / 姓名"
size="small"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<TextField
select
label="状态筛选"
size="small"
value={status}
onChange={(e) => setStatus(e.target.value)}
sx={{ width: 160 }}
>
<MenuItem value="pending">待审核</MenuItem>
<MenuItem value="approved">已通过</MenuItem>
<MenuItem value="rejected">已拒绝</MenuItem>
</TextField>
<Button variant="outlined" onClick={load}>
刷新
</Button>
</Stack>
<DataGrid
rows={filtered}
columns={columns}
loading={loading}
autoHeight
pageSizeOptions={[10, 20, 50]}
disableRowSelectionOnClick
sx={{
border: 0,
"& .MuiDataGrid-columnHeaders": {
fontWeight: 700,
},
}}
/>
</CardContent>
</Card>
{/* Drawer 详情 */}
<Drawer
anchor="right"
open={!!selected}
onClose={() => setSelected(null)}
>
<Box width={420} p={3}>
{selected && (
<>
<Typography variant="h6" fontWeight={700}>
KYC 详情
</Typography>
<Divider sx={{ my: 2 }} />
<Stack spacing={2}>
<TextField
label="钱包地址"
value={selected.wallet_address}
fullWidth
InputProps={{ readOnly: true }}
/>
<TextField
label="姓名"
value={selected.full_name}
fullWidth
InputProps={{ readOnly: true }}
/>
<TextField
label="国家/地区"
value={selected.country}
fullWidth
InputProps={{ readOnly: true }}
/>
<TextField
label="证件类型"
value={selected.id_type}
fullWidth
InputProps={{ readOnly: true }}
/>
<TextField
label="证件号码"
value={selected.id_number}
fullWidth
InputProps={{ readOnly: true }}
/>
<TextField
label="提交时间"
value={selected.created_at}
fullWidth
InputProps={{ readOnly: true }}
/>
<Divider />
{selected.status === "pending" && (
<Stack direction="row" spacing={2}>
<Button
fullWidth
variant="contained"
color="success"
onClick={() => approve(selected.wallet_address)}
>
通过
</Button>
<Button
fullWidth
variant="outlined"
color="error"
onClick={() => reject(selected.id)}
>
拒绝
</Button>
</Stack>
)}
</Stack>
</>
)}
</Box>
</Drawer>
</Box>
);
}
2.KYC通过后,同步白名单列表,同步链上数据
结果
Whitelist updated successfully for wallet:
{success: true, txHash: '0x0db22ac5671ac59f022448688f84809506bc10dc243772bda76ce21aa142791c', blockNumber: 10419577}
const approve = async (id: string) => {
console.log("Approving KYC ID:", id);
const res = await fetch("/api/admin/kyc",{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id, status: "approved" })
});
const data = await res.json();
console.log("拿到钱包地址",data.wallet_address); // 拿到钱包地址
if(res.ok){
console.log("KYC approved successfully for ID:", id);
mutate(); // 刷新列表
}
uptoBlockChain(data.wallet_address) // 同步链上白名单
};
//同步链上白名单
const uptoBlockChain = async (wallet_address: string) => {
const res = await fetch("/api/rwa/whitelist",{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: wallet_address, ok: true })
});
const data = await res.json();
if(res.ok){
console.log("Whitelist updated successfully for wallet:", data);
}
}
