248 lines
11 KiB
Go
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, ¬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})
|
|
}
|