后台增加撤销用户实名功能
实名姓名与收款账户不一致会导致收款码登记失败,新增后台撤销实名能力: - 新增 user:revoke_realname 权限,授予超级管理员与运营角色 - 撤销时重置用户实名状态为未认证并删除实名记录,使其可重新认证,写入审计日志 - 用户管理页新增实名状态列与"撤销实名"操作(含原因弹窗) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8192344027
commit
4d54f1b03a
@@ -24,6 +24,10 @@ type FreezeRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type RevokeRealnameRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type DepositFreeQuotaRequest struct {
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
}
|
||||
|
||||
@@ -110,6 +110,26 @@ func (h *Handler) SetDepositFreeQuota(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) RevokeRealname(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
userID, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req RevokeRealnameRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
item, err := h.service.RevokeRealname(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 {
|
||||
@@ -142,6 +162,8 @@ func writeAdminUserError(c *gin.Context, err error) {
|
||||
response.ServiceUnavailable(c, "数据库未连接")
|
||||
case errors.Is(err, ErrInvalidUser):
|
||||
response.BadRequest(c, "用户不符合规则")
|
||||
case errors.Is(err, ErrRealnameNotRevocable):
|
||||
response.Error(c, http.StatusConflict, "realname_not_revocable", "该用户当前没有可撤销的实名信息")
|
||||
case IsNotFound(err):
|
||||
response.Error(c, http.StatusNotFound, "not_found", "用户不存在")
|
||||
default:
|
||||
|
||||
@@ -136,6 +136,43 @@ func (r *Repository) updateStatus(ctx context.Context, adminID uint64, userID ui
|
||||
return r.Find(ctx, userID)
|
||||
}
|
||||
|
||||
// RevokeRealname 撤销用户实名:将用户实名状态重置为未认证,并删除实名记录,
|
||||
// 使用户可重新发起实名认证(用于实名姓名与收款账户不一致导致收款码登记失败的场景)。
|
||||
func (r *Repository) RevokeRealname(ctx context.Context, adminID uint64, userID uint64, req RevokeRealnameRequest, meta AuditMeta) (*UserDTO, error) {
|
||||
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
|
||||
}
|
||||
var realnameCount int64
|
||||
if err := tx.Model(&model.UserRealname{}).Where("user_id = ?", userID).Count(&realnameCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 已是未认证且无实名记录,无可撤销内容。
|
||||
if user.RealnameStatus == "unverified" && realnameCount == 0 {
|
||||
return ErrRealnameNotRevocable
|
||||
}
|
||||
beforeRealnameStatus := user.RealnameStatus
|
||||
user.RealnameStatus = "unverified"
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&model.UserRealname{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return appendAuditLog(tx, adminID, "admin_user.revoke_realname", user.ID, meta, map[string]any{
|
||||
"user_id": user.ID,
|
||||
"reason": req.Reason,
|
||||
"before_realname_status": beforeRealnameStatus,
|
||||
"after_realname_status": user.RealnameStatus,
|
||||
})
|
||||
})
|
||||
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").
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
var (
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
ErrInvalidUser = errors.New("invalid user")
|
||||
ErrRealnameNotRevocable = errors.New("realname not revocable")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -54,3 +55,13 @@ func (s *Service) SetDepositFreeQuota(ctx context.Context, adminID uint64, userI
|
||||
}
|
||||
return s.repo.SetDepositFreeQuota(ctx, adminID, userID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) RevokeRealname(ctx context.Context, adminID uint64, userID uint64, req RevokeRealnameRequest, meta AuditMeta) (*UserDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if userID == 0 {
|
||||
return nil, ErrInvalidUser
|
||||
}
|
||||
return s.repo.RevokeRealname(ctx, adminID, userID, req, meta)
|
||||
}
|
||||
|
||||
@@ -482,6 +482,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/revoke-realname", requirePerm("user:revoke_realname"), adminUserHandler.RevokeRealname)
|
||||
adminRoutes.GET("/orders", requirePerm("order:view"), orderHandler.AdminList)
|
||||
adminRoutes.GET("/orders/:id", requirePerm("order:view"), orderHandler.AdminDetail)
|
||||
adminRoutes.GET("/orders/:id/handoff-records", requirePerm("order:view"), orderHandler.AdminHandoffRecords)
|
||||
|
||||
Reference in New Issue
Block a user