package app import ( "fmt" "net/http" "strings" ) // Two things the console could not do: act on a risk score, and act on more // than one row at a time. A moderator looking at forty spam posts had to click // forty times, which is how backlogs turn into "we stopped looking". // --- 风险中心 ------------------------------------------------------------- func (a *App) adminWhitelistRiskUser(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { fail(w, http.StatusBadRequest, 20001, "编号无效") return } var req struct { On bool `json:"on"` Note string `json:"note"` } if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "参数错误") return } req.Note = strings.TrimSpace(req.Note) // 加白等于对后面所有告警说"这个人我看过了",没有理由就没法复核。 if req.On && req.Note == "" { fail(w, http.StatusBadRequest, 20001, "加白必须写明理由") return } if len([]rune(req.Note)) > 200 { fail(w, http.StatusBadRequest, 20001, "理由过长") return } result, err := a.db.ExecContext(r.Context(), `UPDATE user_risk_profiles SET whitelisted=?,whitelist_note=?,reviewed_by=?,reviewed_at=NOW(3) WHERE user_id=?`, req.On, req.Note, current(r).ID, id) if err != nil { fail(w, http.StatusInternalServerError, 50001, "操作失败") return } if affected, _ := result.RowsAffected(); affected == 0 { fail(w, http.StatusNotFound, 30001, "这个用户没有风险档案") return } a.audit(r, "risk_whitelist", "user", id, map[string]any{"on": req.On, "note": req.Note}) reply(w, map[string]bool{"success": true}) } func (a *App) adminHandleRiskEvent(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { fail(w, http.StatusBadRequest, 20001, "编号无效") return } var req struct { Note string `json:"note"` } if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "参数错误") return } req.Note = strings.TrimSpace(req.Note) if len([]rune(req.Note)) > 200 { fail(w, http.StatusBadRequest, 20001, "备注过长") return } result, err := a.db.ExecContext(r.Context(), `UPDATE risk_events SET handled_at=NOW(3),handled_by=?,handled_note=? WHERE id=? AND handled_at IS NULL`, current(r).ID, req.Note, id) if err != nil { fail(w, http.StatusInternalServerError, 50001, "操作失败") return } if affected, _ := result.RowsAffected(); affected == 0 { fail(w, http.StatusBadRequest, 20001, "这条事件已经处理过了") return } a.audit(r, "risk_event_handled", "risk_event", id, map[string]any{"note": req.Note}) reply(w, map[string]bool{"success": true}) } // --- 批量处置 ------------------------------------------------------------- type batchRequest struct { IDs []int64 `json:"ids"` Action string `json:"action"` Reason string `json:"reason"` } func validBatch(w http.ResponseWriter, req *batchRequest, allowed map[string]bool) bool { req.Reason = strings.TrimSpace(req.Reason) if len(req.IDs) == 0 { fail(w, http.StatusBadRequest, 20001, "请先勾选要处理的内容") return false } // 一次几百条的批量下架,点错一次就要一条条撤回;限量能把误操作的代价压住。 if len(req.IDs) > 100 { fail(w, http.StatusBadRequest, 20001, "一次最多处理 100 条") return false } if !allowed[req.Action] { fail(w, http.StatusBadRequest, 20001, "操作无效") return false } if len([]rune(req.Reason)) > 200 { fail(w, http.StatusBadRequest, 20001, "原因过长") return false } return true } func placeholdersFor(ids []int64) (string, []any) { marks := make([]string, 0, len(ids)) args := make([]any, 0, len(ids)) for _, id := range ids { marks = append(marks, "?") args = append(args, id) } return strings.Join(marks, ","), args } func (a *App) adminBatchPosts(w http.ResponseWriter, r *http.Request) { var req batchRequest if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "参数错误") return } if !validBatch(w, &req, map[string]bool{"remove": true, "restore": true}) { return } marks, args := placeholdersFor(req.IDs) query := `UPDATE posts SET status=0,deleted_at=NOW(3) WHERE status=1 AND id IN (` + marks + `)` if req.Action == "restore" { query = `UPDATE posts SET status=1,deleted_at=NULL WHERE status=0 AND id IN (` + marks + `)` } result, err := a.db.ExecContext(r.Context(), query, args...) if err != nil { fail(w, http.StatusInternalServerError, 50001, "批量操作失败") return } affected, _ := result.RowsAffected() a.audit(r, "batch_"+req.Action, "post", 0, map[string]any{"ids": req.IDs, "affected": affected, "reason": req.Reason}) reply(w, map[string]any{"success": true, "affected": affected}) } func (a *App) adminBatchMessages(w http.ResponseWriter, r *http.Request) { var req batchRequest if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "参数错误") return } if !validBatch(w, &req, map[string]bool{"remove": true, "restore": true}) { return } if req.Action == "remove" && req.Reason == "" { fail(w, http.StatusBadRequest, 20001, "批量下架消息必须写明原因") return } marks, args := placeholdersFor(req.IDs) query := `UPDATE im_messages SET admin_removed_at=NOW(3),admin_remove_reason=? WHERE admin_removed_at IS NULL AND id IN (` + marks + `)` params := append([]any{req.Reason}, args...) if req.Action == "restore" { query = `UPDATE im_messages SET admin_removed_at=NULL,admin_remove_reason='' WHERE admin_removed_at IS NOT NULL AND id IN (` + marks + `)` params = args } result, err := a.db.ExecContext(r.Context(), query, params...) if err != nil { fail(w, http.StatusInternalServerError, 50001, "批量操作失败") return } affected, _ := result.RowsAffected() a.audit(r, "batch_"+req.Action, "message", 0, map[string]any{"ids": req.IDs, "affected": affected, "reason": req.Reason}) reply(w, map[string]any{"success": true, "affected": affected}) } // adminBatchReports only closes reports without punishing anyone. Sanctions // stay one at a time on purpose: batching a punishment is how the wrong person // gets banned. func (a *App) adminBatchReports(w http.ResponseWriter, r *http.Request) { var req batchRequest if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "参数错误") return } if !validBatch(w, &req, map[string]bool{"dismiss": true}) { return } if req.Reason == "" { fail(w, http.StatusBadRequest, 20001, "批量驳回必须写明理由") return } marks, args := placeholdersFor(req.IDs) params := append([]any{req.Reason, current(r).ID}, args...) result, err := a.db.ExecContext(r.Context(), `UPDATE reports SET status='REJECTED',action_type='NONE',handle_remark=?, handled_by=?,handled_at=NOW(3) WHERE status='PENDING' AND id IN (`+marks+`)`, params...) if err != nil { fail(w, http.StatusInternalServerError, 50001, "批量操作失败") return } affected, _ := result.RowsAffected() a.audit(r, "batch_dismiss", "report", 0, map[string]any{"ids": req.IDs, "affected": affected, "reason": req.Reason}) reply(w, map[string]any{"success": true, "affected": affected}) } // adminBatchPetListings mirrors the same shape for the pet module queue. func (a *App) adminBatchPetListings(w http.ResponseWriter, r *http.Request) { var req struct { batchRequest Kind string `json:"kind"` } if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "参数错误") return } table, ok := petListingTables[strings.TrimSpace(req.Kind)] if !ok { fail(w, http.StatusBadRequest, 20001, "类型无效") return } if !validBatch(w, &req.batchRequest, map[string]bool{"approve": true, "reject": true}) { return } if req.Action == "reject" && req.Reason == "" { fail(w, http.StatusBadRequest, 20001, "批量驳回必须写明原因") return } marks, args := placeholdersFor(req.IDs) status := 1 if req.Action == "reject" { status = 2 } params := append([]any{status, req.Reason}, args...) result, err := a.db.ExecContext(r.Context(), fmt.Sprintf( `UPDATE %s SET moderation_status=?,moderation_note=? WHERE moderation_status=0 AND deleted_at IS NULL AND id IN (%s)`, table, marks), params...) if err != nil { fail(w, http.StatusInternalServerError, 50001, "批量操作失败") return } affected, _ := result.RowsAffected() a.audit(r, "batch_"+req.Action, "pet_"+req.Kind, 0, map[string]any{"ids": req.IDs, "affected": affected, "reason": req.Reason}) reply(w, map[string]any{"success": true, "affected": affected}) }