From 18eba36ef9b262016666732cbf4692cb286c6cfe Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 28 Jun 2026 01:06:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=96=B0=E5=A2=9E=E2=80=9C=E4=BA=BA=E5=B7=A5=E5=AE=9E?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/adminuser/dto.go | 6 + backend/internal/modules/adminuser/handler.go | 27 +++++ .../internal/modules/adminuser/repository.go | 114 +++++++++++++++++- .../modules/adminuser/repository_test.go | 106 ++++++++++++++++ backend/internal/modules/adminuser/service.go | 32 ++++- .../modules/realname/cloudmarket_provider.go | 6 +- .../realname/cloudmarket_provider_test.go | 31 ++++- backend/internal/modules/realname/handler.go | 4 +- backend/internal/router/router.go | 3 +- ...000019_user_manual_realname_permission.sql | 28 +++++ frontend/src/features/admin/api/adminUsers.ts | 15 +++ .../features/admin/views/AdminUsersView.vue | 97 ++++++++++++++- 12 files changed, 453 insertions(+), 16 deletions(-) create mode 100644 backend/migrations/000019_user_manual_realname_permission.sql diff --git a/backend/internal/modules/adminuser/dto.go b/backend/internal/modules/adminuser/dto.go index d7022fa..148a387 100644 --- a/backend/internal/modules/adminuser/dto.go +++ b/backend/internal/modules/adminuser/dto.go @@ -28,6 +28,12 @@ type RevokeRealnameRequest struct { Reason string `json:"reason"` } +type ManualRealnameRequest struct { + Name string `json:"name"` + IDNo string `json:"id_no"` + Reason string `json:"reason"` +} + type DepositFreeQuotaRequest struct { AmountCent int64 `json:"amount_cent"` } diff --git a/backend/internal/modules/adminuser/handler.go b/backend/internal/modules/adminuser/handler.go index 7c1361f..a13a94c 100644 --- a/backend/internal/modules/adminuser/handler.go +++ b/backend/internal/modules/adminuser/handler.go @@ -140,6 +140,29 @@ func (h *Handler) RevokeRealname(c *gin.Context) { response.OK(c, item) } +func (h *Handler) ManualRealname(c *gin.Context) { + adminID, ok := currentAdminID(c) + if !ok { + response.Unauthorized(c, "缺少管理员上下文") + return + } + userID, ok := parseID(c) + if !ok { + return + } + var req ManualRealnameRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "姓名和身份证号不能为空") + return + } + item, err := h.service.ManualRealname(c.Request.Context(), adminID, userID, req, auditMeta(c)) + if err != nil { + writeAdminUserError(c, err) + return + } + response.OK(c, item) +} + func currentAdminID(c *gin.Context) (uint64, bool) { value, ok := c.Get(middleware.ContextAdminID) if !ok { @@ -172,6 +195,10 @@ func writeAdminUserError(c *gin.Context, err error) { response.ServiceUnavailable(c, "数据库未连接") case errors.Is(err, ErrInvalidUser): response.BadRequest(c, "用户不符合规则") + case errors.Is(err, ErrInvalidRealnameInput): + response.BadRequest(c, "姓名或身份证号格式不正确") + case errors.Is(err, ErrRealnameAlreadyVerified): + response.Error(c, http.StatusConflict, "realname_already_verified", "该用户已完成实名认证,如需修改请先撤销实名") case errors.Is(err, ErrRealnameNotRevocable): response.Error(c, http.StatusConflict, "realname_not_revocable", "该用户当前没有可撤销的实名信息") case IsNotFound(err): diff --git a/backend/internal/modules/adminuser/repository.go b/backend/internal/modules/adminuser/repository.go index ab88a5e..6af9470 100644 --- a/backend/internal/modules/adminuser/repository.go +++ b/backend/internal/modules/adminuser/repository.go @@ -5,22 +5,29 @@ import ( "errors" "strconv" "strings" + "time" "hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/model" + "hfb_sys/backend/pkg/crypto" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { - db *gorm.DB + db *gorm.DB + encryptor crypto.Encryptor } type AuditMeta = auditlog.Meta -func NewRepository(db *gorm.DB) *Repository { - return &Repository{db: db} +func NewRepository(db *gorm.DB, encryptors ...crypto.Encryptor) *Repository { + encryptor := crypto.Encryptor(&crypto.MockEncryptor{}) + if len(encryptors) > 0 && encryptors[0] != nil { + encryptor = encryptors[0] + } + return &Repository{db: db, encryptor: encryptor} } func (r *Repository) List(ctx context.Context, page, pageSize int, query ListQuery) (*PaginatedResult, error) { @@ -176,6 +183,86 @@ func (r *Repository) RevokeRealname(ctx context.Context, adminID uint64, userID return r.Find(ctx, userID) } +// ManualRealname 人工实名:第三方实名服务不可用或异常时,由后台审核后补录实名信息。 +func (r *Repository) ManualRealname(ctx context.Context, adminID uint64, userID uint64, req ManualRealnameRequest, meta AuditMeta) (*UserDTO, error) { + name := strings.TrimSpace(req.Name) + idNo := strings.ToUpper(strings.TrimSpace(req.IDNo)) + reason := strings.TrimSpace(req.Reason) + if !validManualRealname(name, idNo) { + return nil, ErrInvalidRealnameInput + } + + encryptedName, err := r.encryptor.Encrypt(name) + if err != nil { + return nil, err + } + encryptedIDNo, err := r.encryptor.Encrypt(idNo) + if err != nil { + return nil, err + } + + now := time.Now() + record := model.UserRealname{ + UserID: userID, + Provider: "admin_manual", + ProviderOrderNo: manualRealnameOrderNo(adminID, now), + Status: "verified", + MaskedName: maskManualName(name), + EncryptedName: encryptedName, + MaskedIDNo: maskManualIDNo(idNo), + EncryptedIDNo: encryptedIDNo, + VerifiedAt: &now, + FailReason: "", + } + + err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var user model.User + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil { + return err + } + if user.RealnameStatus == "verified" { + return ErrRealnameAlreadyVerified + } + + beforeRealnameStatus := user.RealnameStatus + user.RealnameStatus = "verified" + if err := tx.Save(&user).Error; err != nil { + return err + } + 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, + "encrypted_name": record.EncryptedName, + "masked_id_no": record.MaskedIDNo, + "encrypted_id_no": record.EncryptedIDNo, + "verified_at": record.VerifiedAt, + "fail_reason": record.FailReason, + "updated_at": now, + }), + }).Create(&record).Error; err != nil { + return err + } + + return appendAuditLog(tx, adminID, "admin_user.manual_realname", user.ID, meta, map[string]any{ + "user_id": user.ID, + "reason": reason, + "before_realname_status": beforeRealnameStatus, + "after_realname_status": user.RealnameStatus, + "masked_name": record.MaskedName, + "masked_id_no": record.MaskedIDNo, + "provider_order_no": record.ProviderOrderNo, + }) + }) + if err != nil { + return nil, err + } + return r.Find(ctx, userID) +} + func (r *Repository) Find(ctx context.Context, userID uint64) (*UserDTO, error) { var row userRow err := r.db.WithContext(ctx).Table("users AS u"). @@ -234,6 +321,27 @@ func depositFreeQuotaAmountCent(req DepositFreeQuotaRequest) int64 { return req.AmountCent } +func manualRealnameOrderNo(adminID uint64, verifiedAt time.Time) string { + return "manual_" + strconv.FormatUint(adminID, 10) + "_" + strconv.FormatInt(verifiedAt.UnixNano(), 10) +} + +func maskManualName(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 maskManualIDNo(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:] +} + func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error { return auditlog.Append(tx, auditlog.Entry{ ActorType: "admin", diff --git a/backend/internal/modules/adminuser/repository_test.go b/backend/internal/modules/adminuser/repository_test.go index cdbe73e..3d45b00 100644 --- a/backend/internal/modules/adminuser/repository_test.go +++ b/backend/internal/modules/adminuser/repository_test.go @@ -1,9 +1,12 @@ package adminuser import ( + "errors" "strings" "testing" + "hfb_sys/backend/internal/model" + "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -76,3 +79,106 @@ func TestApplyUserFilterHasDepositFreeQuota(t *testing.T) { t.Fatalf("筛选变量不正确:%v", stmt.Vars) } } + +func TestManualRealnameUpdatesUserRecordAndAudit(t *testing.T) { + db := setupAdminUserTestDB(t) + user := model.User{ + Phone: "13900000001", + Nickname: "待实名用户", + RealnameStatus: "rejected", + Status: "active", + } + if err := db.Create(&user).Error; err != nil { + t.Fatalf("创建用户失败:%v", err) + } + + repo := NewRepository(db) + got, err := repo.ManualRealname(t.Context(), 99, user.ID, ManualRealnameRequest{ + Name: "周奕男", + IDNo: "130682200604080615", + Reason: "第三方服务异常,客服人工核验通过", + }, AuditMeta{IP: "127.0.0.1", UserAgent: "test", RequestID: "req-1"}) + if err != nil { + t.Fatalf("ManualRealname() error = %v", err) + } + if got.RealnameStatus != "verified" { + t.Fatalf("用户实名状态 = %s, want verified", got.RealnameStatus) + } + + var savedUser model.User + if err := db.First(&savedUser, user.ID).Error; err != nil { + t.Fatalf("查询用户失败:%v", err) + } + if savedUser.RealnameStatus != "verified" { + t.Fatalf("数据库用户实名状态 = %s, want verified", savedUser.RealnameStatus) + } + + var record model.UserRealname + if err := db.Where("user_id = ?", user.ID).First(&record).Error; err != nil { + t.Fatalf("查询实名记录失败:%v", err) + } + if record.Provider != "admin_manual" || record.Status != "verified" { + t.Fatalf("实名记录不正确:provider=%s status=%s", record.Provider, record.Status) + } + if record.MaskedName != "周**" || record.MaskedIDNo != "1306**********0615" { + t.Fatalf("脱敏信息不正确:name=%s id=%s", record.MaskedName, record.MaskedIDNo) + } + if record.EncryptedName != "周奕男" || record.EncryptedIDNo != "130682200604080615" { + t.Fatalf("加密字段未写入:name=%s id=%s", record.EncryptedName, record.EncryptedIDNo) + } + if record.VerifiedAt == nil { + t.Fatal("VerifiedAt 不能为空") + } + + var audit model.AuditLog + if err := db.Where("action = ?", "admin_user.manual_realname").First(&audit).Error; err != nil { + t.Fatalf("查询审计日志失败:%v", err) + } + if audit.ActorID != 99 || audit.BizID == nil || *audit.BizID != user.ID { + t.Fatalf("审计日志主体不正确:actor=%d biz=%v", audit.ActorID, audit.BizID) + } + if strings.Contains(string(audit.Detail), "130682200604080615") { + t.Fatalf("审计日志不应包含完整身份证号:%s", string(audit.Detail)) + } +} + +func TestManualRealnameRejectsVerifiedUser(t *testing.T) { + db := setupAdminUserTestDB(t) + user := model.User{ + Phone: "13900000002", + Nickname: "已实名用户", + RealnameStatus: "verified", + Status: "active", + } + if err := db.Create(&user).Error; err != nil { + t.Fatalf("创建用户失败:%v", err) + } + + repo := NewRepository(db) + _, err := repo.ManualRealname(t.Context(), 99, user.ID, ManualRealnameRequest{ + Name: "周奕男", + IDNo: "130682200604080615", + }, AuditMeta{}) + if !errors.Is(err, ErrRealnameAlreadyVerified) { + t.Fatalf("ManualRealname() error = %v, want ErrRealnameAlreadyVerified", err) + } +} + +func setupAdminUserTestDB(t *testing.T) *gorm.DB { + t.Helper() + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("打开测试数据库失败:%v", err) + } + if err := db.AutoMigrate( + &model.User{}, + &model.UserRealname{}, + &model.AuditLog{}, + &model.RentalOrder{}, + &model.RentalListing{}, + &model.Dispute{}, + ); err != nil { + t.Fatalf("数据库迁移失败:%v", err) + } + return db +} diff --git a/backend/internal/modules/adminuser/service.go b/backend/internal/modules/adminuser/service.go index 0153dad..7f43a2b 100644 --- a/backend/internal/modules/adminuser/service.go +++ b/backend/internal/modules/adminuser/service.go @@ -3,14 +3,20 @@ package adminuser import ( "context" "errors" + "regexp" + "strings" ) var ( - ErrDependencyUnavailable = errors.New("dependency unavailable") - ErrInvalidUser = errors.New("invalid user") - ErrRealnameNotRevocable = errors.New("realname not revocable") + ErrDependencyUnavailable = errors.New("dependency unavailable") + ErrInvalidUser = errors.New("invalid user") + ErrInvalidRealnameInput = errors.New("invalid realname input") + ErrRealnameAlreadyVerified = errors.New("realname already verified") + ErrRealnameNotRevocable = errors.New("realname not revocable") ) +var manualRealnameIDPattern = regexp.MustCompile(`^\d{17}[\dXx]$`) + type Service struct { repo *Repository } @@ -65,3 +71,23 @@ func (s *Service) RevokeRealname(ctx context.Context, adminID uint64, userID uin } return s.repo.RevokeRealname(ctx, adminID, userID, req, meta) } + +func (s *Service) ManualRealname(ctx context.Context, adminID uint64, userID uint64, req ManualRealnameRequest, meta AuditMeta) (*UserDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + if userID == 0 { + return nil, ErrInvalidUser + } + if !validManualRealname(req.Name, req.IDNo) { + return nil, ErrInvalidRealnameInput + } + return s.repo.ManualRealname(ctx, adminID, userID, req, meta) +} + +func validManualRealname(name string, idNo string) bool { + name = strings.TrimSpace(name) + idNo = strings.TrimSpace(idNo) + nameLen := len([]rune(name)) + return nameLen >= 2 && nameLen <= 30 && manualRealnameIDPattern.MatchString(idNo) +} diff --git a/backend/internal/modules/realname/cloudmarket_provider.go b/backend/internal/modules/realname/cloudmarket_provider.go index dd0856c..db65cdc 100644 --- a/backend/internal/modules/realname/cloudmarket_provider.go +++ b/backend/internal/modules/realname/cloudmarket_provider.go @@ -79,7 +79,7 @@ func (p *CloudMarketProvider) Start(ctx context.Context, req StartRequest) (Prov resp, err := p.client.Do(httpReq) if err != nil { - return ProviderResult{}, fmt.Errorf("realname cloud market request failed: %w", err) + return ProviderResult{}, fmt.Errorf("%w: realname cloud market request failed: %v", ErrProviderUnavailable, err) } defer resp.Body.Close() @@ -165,7 +165,7 @@ func cloudMarketHTTPError(statusCode int, reason string) error { func parseCloudMarketResult(raw json.RawMessage) (int, error) { if len(raw) == 0 || string(raw) == "null" { - return 0, fmt.Errorf("realname cloud market result missing") + return 0, fmt.Errorf("%w: realname cloud market result missing", ErrProviderUnavailable) } var number int if err := json.Unmarshal(raw, &number); err == nil { @@ -184,7 +184,7 @@ func parseCloudMarketResult(raw json.RawMessage) (int, error) { return 110, nil } } - return 0, fmt.Errorf("realname cloud market result invalid: %s", string(raw)) + return 0, fmt.Errorf("%w: realname cloud market result invalid: %s", ErrProviderUnavailable, string(raw)) } func firstNonEmpty(values ...string) string { diff --git a/backend/internal/modules/realname/cloudmarket_provider_test.go b/backend/internal/modules/realname/cloudmarket_provider_test.go index 380fd06..30f78ec 100644 --- a/backend/internal/modules/realname/cloudmarket_provider_test.go +++ b/backend/internal/modules/realname/cloudmarket_provider_test.go @@ -28,7 +28,7 @@ func TestCloudMarketProviderStartVerified(t *testing.T) { defer server.Close() provider, err := NewCloudMarketProvider(CloudMarketConfig{ - URL: server.URL, + URL: server.URL, AppCode: "test-code", Encryptor: &crypto.MockEncryptor{}, }) @@ -62,7 +62,7 @@ func TestCloudMarketProviderStartRejected(t *testing.T) { defer server.Close() provider, err := NewCloudMarketProvider(CloudMarketConfig{ - URL: server.URL, + URL: server.URL, AppCode: "test-code", Encryptor: &crypto.MockEncryptor{}, }) @@ -93,7 +93,7 @@ func TestCloudMarketProviderStartRateLimited(t *testing.T) { defer server.Close() provider, err := NewCloudMarketProvider(CloudMarketConfig{ - URL: server.URL, + URL: server.URL, AppCode: "test-code", Encryptor: &crypto.MockEncryptor{}, }) @@ -110,3 +110,28 @@ func TestCloudMarketProviderStartRateLimited(t *testing.T) { t.Fatalf("期望限流错误,实际:%v", err) } } + +func TestCloudMarketProviderStartUnavailableOnInvalidResult(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"request_id":"TID999","status":"OK","result":"busy"}`)) + })) + defer server.Close() + + provider, err := NewCloudMarketProvider(CloudMarketConfig{ + URL: server.URL, + AppCode: "test-code", + Encryptor: &crypto.MockEncryptor{}, + }) + if err != nil { + t.Fatalf("创建 Provider 失败:%v", err) + } + + _, err = provider.Start(t.Context(), StartRequest{ + UserID: 1, + Name: "赵六", + IDNo: "110101199001011234", + }) + if !errors.Is(err, ErrProviderUnavailable) { + t.Fatalf("期望服务不可用错误,实际:%v", err) + } +} diff --git a/backend/internal/modules/realname/handler.go b/backend/internal/modules/realname/handler.go index 5ffea31..d688119 100644 --- a/backend/internal/modules/realname/handler.go +++ b/backend/internal/modules/realname/handler.go @@ -79,8 +79,8 @@ func writeRealnameError(c *gin.Context, err error) { case errors.Is(err, ErrProviderRateLimited): response.Error(c, http.StatusTooManyRequests, "realname_rate_limited", "实名认证请求过于频繁,请稍后再试") case errors.Is(err, ErrProviderUnavailable): - response.Error(c, http.StatusBadGateway, "realname_provider_unavailable", "实名认证服务调用失败,请稍后再试") + response.Error(c, http.StatusBadGateway, "realname_provider_unavailable", "实名认证服务调用失败,请稍后重试或联系客服处理") default: - response.Error(c, http.StatusInternalServerError, "internal_error", "实名认证服务暂时不可用") + response.Error(c, http.StatusInternalServerError, "internal_error", "实名认证服务暂时不可用,请稍后重试或联系客服处理") } } diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 0c031ee..a87d222 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -137,7 +137,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { adminFinanceHandler := adminfinance.NewHandler(adminFinanceService) var adminUserRepo *adminuser.Repository if deps.DB != nil { - adminUserRepo = adminuser.NewRepository(deps.DB) + adminUserRepo = adminuser.NewRepository(deps.DB, fieldEncryptor) } adminUserService := adminuser.NewService(adminUserRepo) adminUserHandler := adminuser.NewHandler(adminUserService) @@ -511,6 +511,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { adminRoutes.POST("/users/:id/freeze", requirePerm("user:freeze"), adminUserHandler.Freeze) adminRoutes.POST("/users/:id/unfreeze", requirePerm("user:unfreeze"), adminUserHandler.Unfreeze) adminRoutes.POST("/users/:id/deposit-free-quota", requirePerm("user:deposit_free"), adminUserHandler.SetDepositFreeQuota) + adminRoutes.POST("/users/:id/manual-realname", requirePerm("user:manual_realname"), adminUserHandler.ManualRealname) adminRoutes.POST("/users/:id/revoke-realname", requirePerm("user:revoke_realname"), adminUserHandler.RevokeRealname) adminRoutes.GET("/orders", requirePerm("order:view"), orderHandler.AdminList) adminRoutes.GET("/listing-orders/:id/latest", requirePerm("order:view"), orderHandler.AdminLatestByListing) diff --git a/backend/migrations/000019_user_manual_realname_permission.sql b/backend/migrations/000019_user_manual_realname_permission.sql new file mode 100644 index 0000000..8373082 --- /dev/null +++ b/backend/migrations/000019_user_manual_realname_permission.sql @@ -0,0 +1,28 @@ +-- +goose Up + +-- 新增“人工实名认证”权限:第三方实名服务不可用或异常时, +-- 由后台用户管理补录已核验的实名信息。 +INSERT INTO permissions (code, name, resource, action) VALUES +('user:manual_realname', '人工实名认证', 'user', 'manual_realname') +ON DUPLICATE KEY UPDATE + name = VALUES(name), + resource = VALUES(resource), + action = VALUES(action); + +-- super_admin 拥有全部权限,补授新权限。 +INSERT IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id FROM roles r, permissions p +WHERE r.code = 'super_admin' AND p.code = 'user:manual_realname'; + +-- ops(运营)负责用户管理,授予人工实名权限。 +INSERT IGNORE INTO role_permissions (role_id, permission_id) +SELECT r.id, p.id FROM roles r, permissions p +WHERE r.code = 'ops' AND p.code = 'user:manual_realname'; + +-- +goose Down + +DELETE rp FROM role_permissions rp +JOIN permissions p ON p.id = rp.permission_id +WHERE p.code = 'user:manual_realname'; + +DELETE FROM permissions WHERE code = 'user:manual_realname'; diff --git a/frontend/src/features/admin/api/adminUsers.ts b/frontend/src/features/admin/api/adminUsers.ts index 85d39a4..341ee7e 100644 --- a/frontend/src/features/admin/api/adminUsers.ts +++ b/frontend/src/features/admin/api/adminUsers.ts @@ -69,3 +69,18 @@ export async function revokeAdminUserRealname(id: number, reason: string) { ) return data.data } + +export async function manualAdminUserRealname( + id: number, + payload: { name: string; idNo: string; reason: string } +) { + const { data } = await apiClient.post>( + `/admin/users/${id}/manual-realname`, + { + name: payload.name, + id_no: payload.idNo, + reason: payload.reason, + } + ) + return data.data +} diff --git a/frontend/src/features/admin/views/AdminUsersView.vue b/frontend/src/features/admin/views/AdminUsersView.vue index 5279e3e..3bee250 100644 --- a/frontend/src/features/admin/views/AdminUsersView.vue +++ b/frontend/src/features/admin/views/AdminUsersView.vue @@ -7,6 +7,7 @@ import { reactive, ref } from 'vue' import { fetchAdminUsers, freezeAdminUser, + manualAdminUserRealname, revokeAdminUserRealname, setAdminUserDepositFreeQuota, unfreezeAdminUser, @@ -23,9 +24,15 @@ const submitting = ref(false) const activeUser = ref(null) const quotaUser = ref(null) const realnameUser = ref(null) +const manualRealnameUser = ref(null) const freezeReason = ref('') const revokeRealnameReason = ref('') const quotaAmount = ref(0) +const manualRealnameForm = reactive({ + name: '', + idNo: '', + reason: '', +}) const filters = reactive({ keyword: '', @@ -114,6 +121,40 @@ function openRevokeRealname(row: AdminUserItem) { revokeRealnameReason.value = '' } +function openManualRealname(row: AdminUserItem) { + manualRealnameUser.value = row + manualRealnameForm.name = '' + manualRealnameForm.idNo = '' + manualRealnameForm.reason = '' +} + +async function handleManualRealname() { + if (!manualRealnameUser.value) return + if (!manualRealnameForm.name.trim()) { + ElMessage.warning('请输入真实姓名') + return + } + if (!manualRealnameForm.idNo.trim() || manualRealnameForm.idNo.trim().length !== 18) { + ElMessage.warning('请输入18位身份证号') + return + } + submitting.value = true + try { + await manualAdminUserRealname(manualRealnameUser.value.id, { + name: manualRealnameForm.name, + idNo: manualRealnameForm.idNo, + reason: manualRealnameForm.reason, + }) + ElMessage.success('已完成人工实名') + manualRealnameUser.value = null + await loadUsers() + } catch (error) { + ElMessage.error(readError(error, '人工实名失败')) + } finally { + submitting.value = false + } +} + async function handleRevokeRealname() { if (!realnameUser.value) return submitting.value = true @@ -206,9 +247,18 @@ function moneyCent(value: number | string | undefined) { - +