资产织造

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);
    }
  }