136 lines
5.6 KiB
Go
136 lines
5.6 KiB
Go
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})
|
|
}
|