package app import ( "crypto/hmac" "crypto/sha256" "database/sql" "encoding/json" "net/http" "strings" ) func (a *App) verificationDocumentHash(value string) []byte { key := a.config.ConfigEncryptionKey if key == "" { key = a.config.JWTSecret + ":development-verification" } mac := hmac.New(sha256.New, []byte(key+":verification-document")) _, _ = mac.Write([]byte(strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(value), " ", "")))) return mac.Sum(nil) } func maskDocumentNumber(value string) string { runes := []rune(strings.TrimSpace(value)) if len(runes) <= 4 { return strings.Repeat("*", len(runes)) } if len(runes) <= 8 { return string(runes[:1]) + strings.Repeat("*", len(runes)-2) + string(runes[len(runes)-1:]) } return string(runes[:3]) + strings.Repeat("*", len(runes)-7) + string(runes[len(runes)-4:]) } func (a *App) myVerification(w http.ResponseWriter, r *http.Request) { requestedType := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type"))) if requestedType == "" { requestedType = "real_name" } if requestedType != "real_name" && requestedType != "occupation" { fail(w, 400, 20001, "认证类型无效") return } var verificationType, status, realName, documentMask, remark, evidenceJSON string var submittedAt, reviewedAt sql.NullTime err := a.db.QueryRowContext(r.Context(), `SELECT verification_type,status,real_name,document_mask,remark,evidence_json,submitted_at,reviewed_at FROM user_verifications WHERE user_id=? AND verification_type=?`, current(r).ID, requestedType).Scan(&verificationType, &status, &realName, &documentMask, &remark, &evidenceJSON, &submittedAt, &reviewedAt) if err == sql.ErrNoRows { reply(w, map[string]any{"status": "UNVERIFIED", "type": "real_name", "evidence": []string{}}) return } if err != nil { fail(w, http.StatusInternalServerError, 50001, "查询认证状态失败") return } evidence := []string{} _ = json.Unmarshal([]byte(evidenceJSON), &evidence) reply(w, map[string]any{ "type": verificationType, "status": status, "realName": realName, "documentMask": documentMask, "remark": remark, "evidence": evidence, "submittedAt": nullableTime(submittedAt), "reviewedAt": nullableTime(reviewedAt), }) } func (a *App) submitVerification(w http.ResponseWriter, r *http.Request) { var req struct { Type string `json:"type"` RealName string `json:"realName"` DocumentNumber string `json:"documentNumber"` Evidence []string `json:"evidence"` } if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "认证资料格式错误") return } req.Type = strings.ToLower(strings.TrimSpace(req.Type)) if req.Type != "real_name" && req.Type != "occupation" { fail(w, http.StatusBadRequest, 20001, "认证类型无效") return } req.RealName = strings.TrimSpace(req.RealName) documentNumber := strings.ReplaceAll(strings.TrimSpace(req.DocumentNumber), " ", "") if len([]rune(req.RealName)) < 2 || len([]rune(req.RealName)) > 50 || len([]rune(documentNumber)) < 6 || len([]rune(documentNumber)) > 80 { fail(w, http.StatusBadRequest, 20001, "姓名或证件号码格式无效") return } if len(req.Evidence) == 0 || len(req.Evidence) > 3 { fail(w, http.StatusBadRequest, 20001, "请上传 1 至 3 张认证材料") return } tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted}) if err != nil { fail(w, http.StatusInternalServerError, 50001, "提交认证失败") return } defer func() { _ = tx.Rollback() }() var existingStatus string err = tx.QueryRowContext(r.Context(), `SELECT status FROM user_verifications WHERE user_id=? AND verification_type=? FOR UPDATE`, current(r).ID, req.Type).Scan(&existingStatus) if err != nil && err != sql.ErrNoRows { fail(w, http.StatusInternalServerError, 50001, "读取认证状态失败") return } if existingStatus == "PENDING" { fail(w, http.StatusConflict, 20001, "认证资料正在审核,请勿重复提交") return } if existingStatus == "VERIFIED" { fail(w, http.StatusConflict, 20001, "账号已完成认证") return } evidence := make([]string, 0, len(req.Evidence)) seen := map[string]bool{} for _, rawURL := range req.Evidence { mediaURL := strings.TrimSpace(rawURL) if mediaURL == "" || seen[mediaURL] { fail(w, http.StatusBadRequest, 20001, "认证材料地址无效或重复") return } seen[mediaURL] = true var exists int if err = tx.QueryRowContext(r.Context(), `SELECT EXISTS(SELECT 1 FROM media_assets WHERE owner_user_id=? AND public_url=? AND media_type='image' AND status=1)`, current(r).ID, mediaURL).Scan(&exists); err != nil || exists != 1 { fail(w, http.StatusBadRequest, 20001, "认证材料必须由当前账号上传") return } evidence = append(evidence, mediaURL) } evidenceJSON, _ := json.Marshal(evidence) _, err = tx.ExecContext(r.Context(), `INSERT INTO user_verifications(user_id,verification_type,status,real_name,document_mask,document_hash,evidence_json,remark,reviewer_admin_id,submitted_at,reviewed_at) VALUES(?,?,'PENDING',?,?,?,?, '',NULL,NOW(3),NULL) ON DUPLICATE KEY UPDATE verification_type=VALUES(verification_type),status='PENDING',real_name=VALUES(real_name),document_mask=VALUES(document_mask),document_hash=VALUES(document_hash),evidence_json=VALUES(evidence_json),remark='',reviewer_admin_id=NULL,submitted_at=NOW(3),reviewed_at=NULL`, current(r).ID, req.Type, req.RealName, maskDocumentNumber(documentNumber), a.verificationDocumentHash(documentNumber), string(evidenceJSON)) if err != nil || tx.Commit() != nil { fail(w, http.StatusInternalServerError, 50001, "提交认证失败") return } reply(w, map[string]any{"status": "PENDING", "submitted": true}) }