package app import ( "database/sql" "encoding/json" "net/http" "strconv" "strings" "time" ) func decodeStringArray(value string) []string { items := []string{} _ = json.Unmarshal([]byte(value), &items) return items } func (a *App) adminFeedback(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { page, size, offset := pagination(r) status := strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("status"))) where, args := "", []any{} if status != "" { where = " WHERE f.status=?" args = append(args, status) } var total int64 if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM user_feedback f`+where, args...).Scan(&total); err != nil { fail(w, http.StatusInternalServerError, 50001, "查询反馈失败") return } queryArgs := append(append([]any{}, args...), size, offset) rows, err := a.db.QueryContext(r.Context(), `SELECT f.id,f.user_id,u.public_id,p.nickname,p.avatar_url,f.category,f.content,f.contact,COALESCE(f.evidence_json,'[]'),f.status,f.reply_content,f.handled_by,f.handled_at,f.created_at,f.updated_at FROM user_feedback f JOIN users u ON u.id=f.user_id JOIN user_profiles p ON p.user_id=f.user_id`+where+` ORDER BY f.created_at DESC LIMIT ? OFFSET ?`, queryArgs...) if err != nil { fail(w, http.StatusInternalServerError, 50001, "查询反馈失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var id, userID int64 var publicID, nickname, avatar, category, content, contact, evidence, itemStatus, replyContent string var handledBy sql.NullInt64 var handledAt sql.NullTime var createdAt, updatedAt time.Time if err = rows.Scan(&id, &userID, &publicID, &nickname, &avatar, &category, &content, &contact, &evidence, &itemStatus, &replyContent, &handledBy, &handledAt, &createdAt, &updatedAt); err != nil { fail(w, http.StatusInternalServerError, 50001, "读取反馈失败") return } items = append(items, map[string]any{"id": id, "userId": userID, "publicId": publicID, "nickname": nickname, "avatar": avatar, "category": category, "content": content, "contact": contact, "evidence": decodeStringArray(evidence), "status": itemStatus, "replyContent": replyContent, "handledBy": nullableInt64(handledBy), "handledAt": nullableTime(handledAt), "createdAt": createdAt, "updatedAt": updatedAt}) } reply(w, pageResult{Items: items, Total: total, Page: page, Size: size}) return } id, err := pathID(r) var req struct { Status string `json:"status"` Reply string `json:"reply"` } if err != nil || decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "反馈参数无效") return } req.Status = strings.ToUpper(strings.TrimSpace(req.Status)) if req.Status != "PROCESSING" && req.Status != "RESOLVED" && req.Status != "CLOSED" { fail(w, http.StatusBadRequest, 20001, "反馈状态无效") return } if req.Status == "RESOLVED" && strings.TrimSpace(req.Reply) == "" { fail(w, http.StatusBadRequest, 20001, "解决反馈时必须填写回复") return } var userID int64 if err = a.db.QueryRowContext(r.Context(), `SELECT user_id FROM user_feedback WHERE id=?`, id).Scan(&userID); err != nil { fail(w, http.StatusNotFound, 30001, "反馈不存在") return } result, err := a.db.ExecContext(r.Context(), `UPDATE user_feedback SET status=?,reply_content=?,handled_by=?,handled_at=NOW(3) WHERE id=?`, req.Status, strings.TrimSpace(req.Reply), current(r).ID, id) if err != nil { fail(w, http.StatusInternalServerError, 50001, "更新反馈失败") return } affected, _ := result.RowsAffected() if affected == 0 { fail(w, http.StatusNotFound, 30001, "反馈不存在") return } if strings.TrimSpace(req.Reply) != "" { a.notifyUser(r.Context(), userID, "system", "反馈处理结果", strings.TrimSpace(req.Reply), "feedback", id) } a.audit(r, "handle_feedback", "feedback", id, map[string]any{"status": req.Status}) reply(w, map[string]bool{"success": true}) } func (a *App) adminAccountClosures(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { page, size, offset := pagination(r) status := strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("status"))) where, args := "", []any{} if status != "" { where = " WHERE c.status=?" args = append(args, status) } var total int64 if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM user_account_closures c`+where, args...).Scan(&total); err != nil { fail(w, http.StatusInternalServerError, 50001, "查询注销申请失败") return } queryArgs := append(append([]any{}, args...), size, offset) rows, err := a.db.QueryContext(r.Context(), `SELECT c.user_id,u.public_id,p.nickname,p.avatar_url,c.reason,c.status,c.requested_at,c.execute_after,c.cancelled_at,c.completed_at FROM user_account_closures c JOIN users u ON u.id=c.user_id JOIN user_profiles p ON p.user_id=c.user_id`+where+` ORDER BY c.requested_at DESC LIMIT ? OFFSET ?`, queryArgs...) if err != nil { fail(w, http.StatusInternalServerError, 50001, "查询注销申请失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var userID int64 var publicID, nickname, avatar, reason, itemStatus string var requestedAt, executeAfter time.Time var cancelledAt, completedAt sql.NullTime if err = rows.Scan(&userID, &publicID, &nickname, &avatar, &reason, &itemStatus, &requestedAt, &executeAfter, &cancelledAt, &completedAt); err != nil { fail(w, http.StatusInternalServerError, 50001, "读取注销申请失败") return } items = append(items, map[string]any{"userId": userID, "publicId": publicID, "nickname": nickname, "avatar": avatar, "reason": reason, "status": itemStatus, "requestedAt": requestedAt, "executeAfter": executeAfter, "cancelledAt": nullableTime(cancelledAt), "completedAt": nullableTime(completedAt)}) } reply(w, pageResult{Items: items, Total: total, Page: page, Size: size}) return } userID, err := pathID(r) var req struct { Action string `json:"action"` Reason string `json:"reason"` } if err != nil || decode(r, &req) != nil || strings.ToLower(req.Action) != "cancel" || strings.TrimSpace(req.Reason) == "" { fail(w, http.StatusBadRequest, 20001, "必须填写取消注销的原因") return } result, err := a.db.ExecContext(r.Context(), `UPDATE user_account_closures SET status='CANCELLED',cancelled_at=NOW(3) WHERE user_id=? AND status='PENDING'`, userID) if err != nil { fail(w, http.StatusInternalServerError, 50001, "取消注销申请失败") return } affected, _ := result.RowsAffected() if affected == 0 { fail(w, http.StatusBadRequest, 20001, "只有待执行的注销申请可以取消") return } a.notifyUser(r.Context(), userID, "system", "注销申请已取消", strings.TrimSpace(req.Reason), "account_closure", nil) a.audit(r, "cancel_account_closure", "user", userID, map[string]any{"reason": req.Reason}) reply(w, map[string]bool{"success": true}) } func (a *App) adminAppVersions(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { rows, err := a.db.QueryContext(r.Context(), `SELECT id,platform,version,build_number,force_update,download_url,release_notes,status,created_at FROM app_versions ORDER BY platform,build_number DESC`) if err != nil { fail(w, http.StatusInternalServerError, 50001, "查询客户端版本失败") return } defer rows.Close() items := []map[string]any{} for rows.Next() { var id int64 var platform, version, downloadURL, notes string var build, force, status int var createdAt time.Time if rows.Scan(&id, &platform, &version, &build, &force, &downloadURL, ¬es, &status, &createdAt) == nil { items = append(items, map[string]any{"id": id, "platform": platform, "version": version, "buildNumber": build, "forceUpdate": force == 1, "downloadUrl": downloadURL, "releaseNotes": notes, "status": status, "createdAt": createdAt}) } } reply(w, map[string]any{"items": items, "total": len(items)}) return } if r.Method == http.MethodDelete { id, err := pathID(r) if err != nil { fail(w, http.StatusBadRequest, 20001, "版本编号无效") return } result, err := a.db.ExecContext(r.Context(), `DELETE FROM app_versions WHERE id=?`, id) if err != nil { fail(w, http.StatusInternalServerError, 50001, "删除版本失败") return } affected, _ := result.RowsAffected() if affected == 0 { fail(w, http.StatusNotFound, 30001, "版本不存在") return } a.audit(r, "delete_app_version", "app_version", id, nil) reply(w, map[string]bool{"success": true}) return } var req struct { ID int64 `json:"id"` Platform string `json:"platform"` Version string `json:"version"` BuildNumber int `json:"buildNumber"` ForceUpdate bool `json:"forceUpdate"` DownloadURL string `json:"downloadUrl"` ReleaseNotes string `json:"releaseNotes"` Status int `json:"status"` } if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "版本参数无效") return } req.Platform = strings.ToLower(strings.TrimSpace(req.Platform)) if req.Platform != "android" && req.Platform != "ios" && req.Platform != "h5" || strings.TrimSpace(req.Version) == "" || req.BuildNumber <= 0 || req.Status < 0 || req.Status > 1 { fail(w, http.StatusBadRequest, 20001, "平台、版本号或构建号无效") return } force := 0 if req.ForceUpdate { force = 1 } if r.Method == http.MethodPost { result, err := a.db.ExecContext(r.Context(), `INSERT INTO app_versions(platform,version,build_number,force_update,download_url,release_notes,status) VALUES(?,?,?,?,?,?,?)`, req.Platform, strings.TrimSpace(req.Version), req.BuildNumber, force, strings.TrimSpace(req.DownloadURL), strings.TrimSpace(req.ReleaseNotes), req.Status) if err != nil { fail(w, http.StatusConflict, 20001, "该平台构建号已存在") return } id, _ := result.LastInsertId() a.audit(r, "create_app_version", "app_version", id, map[string]any{"platform": req.Platform, "version": req.Version, "buildNumber": req.BuildNumber}) reply(w, map[string]any{"id": id}) return } id, err := pathID(r) if err != nil { fail(w, http.StatusBadRequest, 20001, "版本编号无效") return } result, err := a.db.ExecContext(r.Context(), `UPDATE app_versions SET platform=?,version=?,build_number=?,force_update=?,download_url=?,release_notes=?,status=? WHERE id=?`, req.Platform, strings.TrimSpace(req.Version), req.BuildNumber, force, strings.TrimSpace(req.DownloadURL), strings.TrimSpace(req.ReleaseNotes), req.Status, id) if err != nil { fail(w, http.StatusConflict, 20001, "该平台构建号已存在") return } affected, _ := result.RowsAffected() if affected == 0 { fail(w, http.StatusNotFound, 30001, "版本不存在") return } a.audit(r, "update_app_version", "app_version", id, map[string]any{"platform": req.Platform, "version": req.Version, "buildNumber": req.BuildNumber, "status": strconv.Itoa(req.Status)}) reply(w, map[string]bool{"success": true}) }