第 2 阶段:实名认证,mock测试ok
This commit is contained in:
@@ -24,6 +24,12 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 开发态短信与实名
|
||||||
|
|
||||||
|
- 短信验证码使用 mock 适配器,验证码会打印在后端日志中。
|
||||||
|
- 实名认证使用 mock 适配器,登录后请求 `POST /api/realname/start`,提交合法姓名和 18 位身份证号会直接通过。
|
||||||
|
- 实名状态可通过 `GET /api/realname/status` 查询。
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- [项目计划](docs/project-plan.md)
|
- [项目计划](docs/project-plan.md)
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/modules/auth"
|
||||||
|
"hfb_sys/backend/pkg/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RequireRealname(users *auth.UserRepository) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if users == nil {
|
||||||
|
response.ServiceUnavailable(c, "数据库未连接")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value, ok := c.Get(ContextUserID)
|
||||||
|
if !ok {
|
||||||
|
response.Unauthorized(c, "缺少用户上下文")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := users.FindByID(value.(uint64))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
response.Unauthorized(c, "用户不存在")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
response.ServiceUnavailable(c, "用户查询失败")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user.RealnameStatus != "verified" {
|
||||||
|
response.Error(c, 403, "realname_required", "请先完成实名认证")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type UserRealname struct {
|
||||||
|
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||||
|
UserID uint64 `gorm:"not null;uniqueIndex" json:"user_id"`
|
||||||
|
Provider string `gorm:"size:32;not null" json:"provider"`
|
||||||
|
ProviderOrderNo string `gorm:"size:128;not null;default:''" json:"provider_order_no"`
|
||||||
|
Status string `gorm:"size:32;not null;default:'pending'" json:"status"`
|
||||||
|
MaskedName string `gorm:"size:64;not null;default:''" json:"masked_name"`
|
||||||
|
MaskedIDNo string `gorm:"size:64;not null;default:''" json:"masked_id_no"`
|
||||||
|
VerifiedAt *time.Time `json:"verified_at"`
|
||||||
|
FailReason string `gorm:"size:255;not null;default:''" json:"fail_reason"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UserRealname) TableName() string {
|
||||||
|
return "user_realname"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Realname Module
|
||||||
|
|
||||||
|
实名认证状态流转、第三方实名服务适配器、发布前实名拦截。
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package realname
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/middleware"
|
||||||
|
"hfb_sys/backend/pkg/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service *Service
|
||||||
|
}
|
||||||
|
|
||||||
|
type StartRealnameRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
IDNo string `json:"id_no" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service *Service) *Handler {
|
||||||
|
return &Handler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Start(c *gin.Context) {
|
||||||
|
userID, ok := currentUserID(c)
|
||||||
|
if !ok {
|
||||||
|
response.Unauthorized(c, "缺少用户上下文")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req StartRealnameRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "姓名和身份证号不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := h.service.Start(c.Request.Context(), userID, req.Name, req.IDNo)
|
||||||
|
if err != nil {
|
||||||
|
writeRealnameError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, ToPublicStatus(record))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Status(c *gin.Context) {
|
||||||
|
userID, ok := currentUserID(c)
|
||||||
|
if !ok {
|
||||||
|
response.Unauthorized(c, "缺少用户上下文")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := h.service.Status(userID)
|
||||||
|
if err != nil {
|
||||||
|
writeRealnameError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentUserID(c *gin.Context) (uint64, bool) {
|
||||||
|
value, ok := c.Get(middleware.ContextUserID)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
userID, ok := value.(uint64)
|
||||||
|
return userID, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRealnameError(c *gin.Context, err error) {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrDependencyUnavailable):
|
||||||
|
response.ServiceUnavailable(c, "数据库未连接")
|
||||||
|
case errors.Is(err, ErrInvalidRealnameInput):
|
||||||
|
response.BadRequest(c, "姓名或身份证号格式不正确")
|
||||||
|
default:
|
||||||
|
response.Error(c, http.StatusInternalServerError, "internal_error", "实名认证服务暂时不可用")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package realname
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
Start(ctx context.Context, req StartRequest) (ProviderResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StartRequest struct {
|
||||||
|
UserID uint64
|
||||||
|
Name string
|
||||||
|
IDNo string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderResult struct {
|
||||||
|
ProviderOrderNo string
|
||||||
|
Status string
|
||||||
|
MaskedName string
|
||||||
|
MaskedIDNo string
|
||||||
|
VerifiedAt *time.Time
|
||||||
|
FailReason string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockProvider struct{}
|
||||||
|
|
||||||
|
func NewMockProvider() *MockProvider {
|
||||||
|
return &MockProvider{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MockProvider) Start(_ context.Context, req StartRequest) (ProviderResult, error) {
|
||||||
|
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.IDNo) == "" {
|
||||||
|
return ProviderResult{}, errors.New("name and id no are required")
|
||||||
|
}
|
||||||
|
orderNo, err := randomOrderNo()
|
||||||
|
if err != nil {
|
||||||
|
return ProviderResult{}, err
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
return ProviderResult{
|
||||||
|
ProviderOrderNo: orderNo,
|
||||||
|
Status: StatusVerified,
|
||||||
|
MaskedName: maskName(req.Name),
|
||||||
|
MaskedIDNo: maskIDNo(req.IDNo),
|
||||||
|
VerifiedAt: &now,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomOrderNo() (string, error) {
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "mock_rn_" + hex.EncodeToString(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskName(name string) string {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if len([]rune(name)) <= 1 {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
runes := []rune(name)
|
||||||
|
return string(runes[0]) + strings.Repeat("*", len(runes)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskIDNo(idNo string) string {
|
||||||
|
idNo = strings.TrimSpace(idNo)
|
||||||
|
if len(idNo) <= 8 {
|
||||||
|
return idNo
|
||||||
|
}
|
||||||
|
return idNo[:4] + strings.Repeat("*", len(idNo)-8) + idNo[len(idNo)-4:]
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package realname
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(db *gorm.DB) *Repository {
|
||||||
|
return &Repository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) FindByUserID(userID uint64) (*model.UserRealname, error) {
|
||||||
|
var record model.UserRealname
|
||||||
|
if err := r.db.Where("user_id = ?", userID).First(&record).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) SaveResult(userID uint64, provider string, result ProviderResult) (*model.UserRealname, error) {
|
||||||
|
record := model.UserRealname{
|
||||||
|
UserID: userID,
|
||||||
|
Provider: provider,
|
||||||
|
ProviderOrderNo: result.ProviderOrderNo,
|
||||||
|
Status: result.Status,
|
||||||
|
MaskedName: result.MaskedName,
|
||||||
|
MaskedIDNo: result.MaskedIDNo,
|
||||||
|
VerifiedAt: result.VerifiedAt,
|
||||||
|
FailReason: result.FailReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "user_id"}},
|
||||||
|
DoUpdates: clause.Assignments(map[string]any{
|
||||||
|
"provider": record.Provider,
|
||||||
|
"provider_order_no": record.ProviderOrderNo,
|
||||||
|
"status": record.Status,
|
||||||
|
"masked_name": record.MaskedName,
|
||||||
|
"masked_id_no": record.MaskedIDNo,
|
||||||
|
"verified_at": record.VerifiedAt,
|
||||||
|
"fail_reason": record.FailReason,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}),
|
||||||
|
}).Create(&record).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userStatus := "realname_failed"
|
||||||
|
if result.Status == StatusVerified {
|
||||||
|
userStatus = "verified"
|
||||||
|
}
|
||||||
|
if result.Status == StatusPending {
|
||||||
|
userStatus = "pending"
|
||||||
|
}
|
||||||
|
return tx.Model(&model.User{}).Where("id = ?", userID).Update("realname_status", userStatus).Error
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.FindByUserID(userID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package realname
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusPending = "pending"
|
||||||
|
StatusVerified = "verified"
|
||||||
|
StatusFailed = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||||
|
ErrInvalidRealnameInput = errors.New("invalid realname input")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repo *Repository
|
||||||
|
provider Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublicStatus struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
ProviderOrderNo string `json:"provider_order_no,omitempty"`
|
||||||
|
MaskedName string `json:"masked_name,omitempty"`
|
||||||
|
MaskedIDNo string `json:"masked_id_no,omitempty"`
|
||||||
|
VerifiedAt *string `json:"verified_at,omitempty"`
|
||||||
|
FailReason string `json:"fail_reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo *Repository, provider Provider) *Service {
|
||||||
|
return &Service{repo: repo, provider: provider}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Start(ctx context.Context, userID uint64, name string, idNo string) (*model.UserRealname, error) {
|
||||||
|
if s.repo == nil || s.provider == nil {
|
||||||
|
return nil, ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
idNo = strings.TrimSpace(idNo)
|
||||||
|
if !validName(name) || !validIDNo(idNo) {
|
||||||
|
return nil, ErrInvalidRealnameInput
|
||||||
|
}
|
||||||
|
result, err := s.provider.Start(ctx, StartRequest{
|
||||||
|
UserID: userID,
|
||||||
|
Name: name,
|
||||||
|
IDNo: idNo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.repo.SaveResult(userID, "mock", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Status(userID uint64) (PublicStatus, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return PublicStatus{}, ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
record, err := s.repo.FindByUserID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return PublicStatus{Status: "unverified"}, nil
|
||||||
|
}
|
||||||
|
return ToPublicStatus(record), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPublicStatus(record *model.UserRealname) PublicStatus {
|
||||||
|
var verifiedAt *string
|
||||||
|
if record.VerifiedAt != nil {
|
||||||
|
value := record.VerifiedAt.Format("2006-01-02 15:04:05")
|
||||||
|
verifiedAt = &value
|
||||||
|
}
|
||||||
|
return PublicStatus{
|
||||||
|
Status: record.Status,
|
||||||
|
Provider: record.Provider,
|
||||||
|
ProviderOrderNo: record.ProviderOrderNo,
|
||||||
|
MaskedName: record.MaskedName,
|
||||||
|
MaskedIDNo: record.MaskedIDNo,
|
||||||
|
VerifiedAt: verifiedAt,
|
||||||
|
FailReason: record.FailReason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validName(name string) bool {
|
||||||
|
return len([]rune(name)) >= 2 && len([]rune(name)) <= 30
|
||||||
|
}
|
||||||
|
|
||||||
|
func validIDNo(idNo string) bool {
|
||||||
|
return regexp.MustCompile(`^\d{17}[\dXx]$`).MatchString(idNo)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"hfb_sys/backend/internal/handler"
|
"hfb_sys/backend/internal/handler"
|
||||||
"hfb_sys/backend/internal/middleware"
|
"hfb_sys/backend/internal/middleware"
|
||||||
"hfb_sys/backend/internal/modules/auth"
|
"hfb_sys/backend/internal/modules/auth"
|
||||||
|
"hfb_sys/backend/internal/modules/realname"
|
||||||
"hfb_sys/backend/internal/modules/user"
|
"hfb_sys/backend/internal/modules/user"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -31,6 +32,12 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
authService := auth.NewService(userRepo, deps.Redis, jwtManager, logger)
|
authService := auth.NewService(userRepo, deps.Redis, jwtManager, logger)
|
||||||
authHandler := auth.NewHandler(authService)
|
authHandler := auth.NewHandler(authService)
|
||||||
userHandler := user.NewHandler(userRepo)
|
userHandler := user.NewHandler(userRepo)
|
||||||
|
var realnameRepo *realname.Repository
|
||||||
|
if deps.DB != nil {
|
||||||
|
realnameRepo = realname.NewRepository(deps.DB)
|
||||||
|
}
|
||||||
|
realnameService := realname.NewService(realnameRepo, realname.NewMockProvider())
|
||||||
|
realnameHandler := realname.NewHandler(realnameService)
|
||||||
requireAuth := middleware.Auth(jwtManager)
|
requireAuth := middleware.Auth(jwtManager)
|
||||||
|
|
||||||
api := engine.Group("/api")
|
api := engine.Group("/api")
|
||||||
@@ -46,6 +53,12 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
api.GET("/me", requireAuth, userHandler.Me)
|
api.GET("/me", requireAuth, userHandler.Me)
|
||||||
|
|
||||||
|
realnameRoutes := api.Group("/realname", requireAuth)
|
||||||
|
{
|
||||||
|
realnameRoutes.POST("/start", realnameHandler.Start)
|
||||||
|
realnameRoutes.GET("/status", realnameHandler.Status)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return engine
|
return engine
|
||||||
|
|||||||
+10
@@ -12,3 +12,13 @@ API 规划以 [项目计划](project-plan.md) 第 10 章为准。
|
|||||||
- 钱包:余额、流水、提现预留。
|
- 钱包:余额、流水、提现预留。
|
||||||
- 通知:站内信列表、标记已读。
|
- 通知:站内信列表、标记已读。
|
||||||
- 后台:审核、订单、仲裁、资金流水、系统配置、审计日志。
|
- 后台:审核、订单、仲裁、资金流水、系统配置、审计日志。
|
||||||
|
|
||||||
|
## 已实现接口
|
||||||
|
|
||||||
|
- `POST /api/auth/sms/send`
|
||||||
|
- `POST /api/auth/sms/login`
|
||||||
|
- `POST /api/auth/refresh`
|
||||||
|
- `POST /api/auth/logout`
|
||||||
|
- `GET /api/me`
|
||||||
|
- `POST /api/realname/start`
|
||||||
|
- `GET /api/realname/status`
|
||||||
|
|||||||
@@ -10,3 +10,11 @@
|
|||||||
- 账号交接采用号主手动交接。
|
- 账号交接采用号主手动交接。
|
||||||
- 资金先做账务模型,不直接接真实支付和提现。
|
- 资金先做账务模型,不直接接真实支付和提现。
|
||||||
- 押金、交接超时、归还超时、短信限流等阈值进入系统配置。
|
- 押金、交接超时、归还超时、短信限流等阈值进入系统配置。
|
||||||
|
|
||||||
|
## 开发态实名认证
|
||||||
|
|
||||||
|
- 当前实现为 mock 实名服务。
|
||||||
|
- 提交合法姓名和 18 位身份证号后直接通过认证。
|
||||||
|
- 后端只返回脱敏姓名和脱敏证件号。
|
||||||
|
- 用户表 `realname_status` 会同步更新为 `verified`。
|
||||||
|
- 后续接入阿里云或腾讯云实名服务时,保持 `POST /api/realname/start` 和 `GET /api/realname/status` 的业务语义不变。
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { apiClient } from './client'
|
||||||
|
|
||||||
|
export interface RealnameStatus {
|
||||||
|
status: string
|
||||||
|
provider?: string
|
||||||
|
provider_order_no?: string
|
||||||
|
masked_name?: string
|
||||||
|
masked_id_no?: string
|
||||||
|
verified_at?: string
|
||||||
|
fail_reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: string
|
||||||
|
message: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startRealname(name: string, idNo: string) {
|
||||||
|
const { data } = await apiClient.post<ApiResponse<RealnameStatus>>('/realname/start', {
|
||||||
|
name,
|
||||||
|
id_no: idNo,
|
||||||
|
})
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRealnameStatus() {
|
||||||
|
const { data } = await apiClient.get<ApiResponse<RealnameStatus>>('/realname/status')
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
@@ -151,6 +151,37 @@ h1 {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
max-width: 520px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel {
|
||||||
|
max-width: 520px;
|
||||||
|
margin-top: 24px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel span {
|
||||||
|
color: #6b7785;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #0f766e;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #52616f;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -1,9 +1,77 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
|
import { getRealnameStatus, startRealname, type RealnameStatus } from '@/api/realname'
|
||||||
|
import { useSessionStore } from '@/stores/session'
|
||||||
|
|
||||||
|
const session = useSessionStore()
|
||||||
|
const loading = ref(false)
|
||||||
|
const status = ref<RealnameStatus | null>(null)
|
||||||
|
const form = reactive({
|
||||||
|
name: '',
|
||||||
|
idNo: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(loadStatus)
|
||||||
|
|
||||||
|
async function loadStatus() {
|
||||||
|
if (!session.token) return
|
||||||
|
try {
|
||||||
|
status.value = await getRealnameStatus()
|
||||||
|
} catch {
|
||||||
|
status.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
status.value = await startRealname(form.name, form.idNo)
|
||||||
|
await session.loadMe()
|
||||||
|
ElMessage.success('实名认证已通过')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(readError(error, '实名认证失败'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readError(error: unknown, fallback: string) {
|
||||||
|
if (typeof error === 'object' && error && 'response' in error) {
|
||||||
|
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||||
|
return response?.data?.message || fallback
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<p class="eyebrow">Realname</p>
|
<p class="eyebrow">Realname</p>
|
||||||
<h1>实名认证</h1>
|
<h1>实名认证</h1>
|
||||||
<p>号主发布前必须完成实名认证。</p>
|
<p>号主发布前必须完成实名认证。当前是开发态 mock 认证,提交合法姓名和身份证号会直接通过。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-alert v-if="!session.token" class="notice" type="warning" :closable="false" title="请先登录后再实名认证" />
|
||||||
|
|
||||||
|
<div v-if="status" class="status-panel">
|
||||||
|
<span>当前状态</span>
|
||||||
|
<strong>{{ status.status }}</strong>
|
||||||
|
<p v-if="status.masked_name">姓名:{{ status.masked_name }}</p>
|
||||||
|
<p v-if="status.masked_id_no">证件号:{{ status.masked_id_no }}</p>
|
||||||
|
<p v-if="status.verified_at">通过时间:{{ status.verified_at }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form class="login-form" label-position="top">
|
||||||
|
<el-form-item label="姓名">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入真实姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="身份证号">
|
||||||
|
<el-input v-model="form.idNo" maxlength="18" placeholder="请输入 18 位身份证号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-button type="primary" :disabled="!session.token" :loading="loading" @click="handleSubmit">提交认证</el-button>
|
||||||
|
</el-form>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user