第 2 阶段:实名认证,mock测试ok
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user