Files
kefu/im/backend/internal/app/admin_client_operations.go
T
2026-09-03 08:38:17 +08:00

248 lines
11 KiB
Go

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, &notes, &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})
}