package app import ( "context" "database/sql" "fmt" "net/http" "strings" "time" ) func (a *App) loadAdminUserDetail(w http.ResponseWriter, r *http.Request, id int64) { var publicID, nickname, avatar, cover, cityCode, city, occupation, bio string var phoneCipher []byte var birthday sql.NullString var created time.Time var lastActive sql.NullTime var status, risk, gender, height, profileScore, vip, vipLevel int var followingCount, followerCount, postCount, likeCount int err := a.db.QueryRowContext(r.Context(), `SELECT u.public_id,u.phone_cipher,u.status,u.risk_level,u.created_at, p.nickname,p.avatar_url,p.cover_url,p.gender,DATE_FORMAT(p.birthday,'%Y-%m-%d'),COALESCE(p.height_cm,0),p.city_code,p.city_name,p.occupation,p.bio,p.profile_score,p.is_vip,p.vip_level,p.last_active_at, (SELECT COUNT(*) FROM user_follows WHERE user_id=u.id),(SELECT COUNT(*) FROM user_follows WHERE target_user_id=u.id),(SELECT COUNT(*) FROM posts WHERE user_id=u.id AND deleted_at IS NULL),(SELECT COUNT(*) FROM post_likes pl JOIN posts po ON po.id=pl.post_id WHERE po.user_id=u.id) FROM users u JOIN user_profiles p ON p.user_id=u.id WHERE u.id=? AND u.deleted_at IS NULL`, id).Scan( &publicID, &phoneCipher, &status, &risk, &created, &nickname, &avatar, &cover, &gender, &birthday, &height, &cityCode, &city, &occupation, &bio, &profileScore, &vip, &vipLevel, &lastActive, &followingCount, &followerCount, &postCount, &likeCount) if err != nil { fail(w, http.StatusNotFound, 30001, "用户不存在") return } phone, decryptErr := a.decryptPhone(phoneCipher) if decryptErr != nil { fail(w, http.StatusInternalServerError, 50001, "用户手机号解密失败") return } verification := map[string]any{"status": "UNVERIFIED", "type": "real_name", "realName": "", "documentMask": "", "remark": ""} var verificationType, verificationStatus, realName, documentMask, verificationRemark string var submittedAt, reviewedAt any if a.db.QueryRowContext(r.Context(), `SELECT verification_type,status,real_name,document_mask,remark,submitted_at,reviewed_at FROM user_verifications WHERE user_id=?`, id).Scan(&verificationType, &verificationStatus, &realName, &documentMask, &verificationRemark, &submittedAt, &reviewedAt) == nil { verification = map[string]any{"type": verificationType, "status": verificationStatus, "realName": realName, "documentMask": documentMask, "remark": verificationRemark, "submittedAt": submittedAt, "reviewedAt": reviewedAt} } membership := map[string]any{"active": vip == 1, "level": vipLevel, "name": "普通用户"} if vip == 1 { membership["name"] = "历史会员资料" } var subscriptionID, planID int64 var planName string var level int var startedAt, expiresAt time.Time if a.db.QueryRowContext(r.Context(), `SELECT s.id,s.plan_id,p.name,p.level,s.started_at,s.expires_at FROM subscriptions s JOIN membership_plans p ON p.id=s.plan_id WHERE s.user_id=? AND s.status=1 AND s.expires_at>NOW(3) ORDER BY p.level DESC,s.expires_at DESC LIMIT 1`, id).Scan(&subscriptionID, &planID, &planName, &level, &startedAt, &expiresAt) == nil { membership = map[string]any{"active": true, "subscriptionId": subscriptionID, "planId": planID, "name": planName, "level": level, "startedAt": startedAt, "expiresAt": expiresAt} } devices := []map[string]any{} rows, _ := a.db.QueryContext(r.Context(), `SELECT device_id,platform,device_model,os_version,app_version,last_ip,last_active_at,status FROM user_devices WHERE user_id=? ORDER BY COALESCE(last_active_at,created_at) DESC LIMIT 20`, id) if rows != nil { defer rows.Close() for rows.Next() { var deviceID, platform, model, osVersion, appVersion, ip string var active any var deviceStatus int _ = rows.Scan(&deviceID, &platform, &model, &osVersion, &appVersion, &ip, &active, &deviceStatus) devices = append(devices, map[string]any{"deviceId": deviceID, "platform": platform, "model": model, "osVersion": osVersion, "appVersion": appVersion, "ip": ip, "lastActiveAt": active, "status": deviceStatus}) } } var activeSessions, orderCount, paidCent int64 _ = a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM user_sessions WHERE user_id=? AND revoked_at IS NULL AND expires_at>NOW(3)`, id).Scan(&activeSessions) _ = a.db.QueryRowContext(r.Context(), `SELECT COUNT(*),COALESCE(SUM(IF(status='PAID',amount_cent,0)),0) FROM orders WHERE user_id=? AND deleted_at IS NULL`, id).Scan(&orderCount, &paidCent) reply(w, map[string]any{ "id": id, "publicId": publicID, "phone": phone, "status": status, "riskLevel": risk, "createdAt": created, "profile": map[string]any{"id": id, "publicId": publicID, "nickname": nickname, "avatar": avatar, "cover": cover, "gender": gender, "birthday": nullableString(birthday), "height": height, "cityCode": cityCode, "city": city, "occupation": occupation, "bio": bio, "profileScore": profileScore, "vip": vip == 1, "vipLevel": vipLevel, "lastActiveAt": nullableTime(lastActive), "followingCount": followingCount, "followerCount": followerCount, "postCount": postCount, "likeCount": likeCount}, "verification": verification, "membership": membership, "sanctions": a.sanctionList(r.Context(), id), "devices": devices, "security": map[string]any{"activeSessions": activeSessions}, "orderSummary": map[string]any{"count": orderCount, "paidCent": paidCent}, }) } func nullableTime(value sql.NullTime) any { if value.Valid { return value.Time } return nil } func (a *App) adminUpdateUserProfile(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { fail(w, 400, 20001, "用户编号无效") return } var req struct { Phone string `json:"phone"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` Cover string `json:"cover"` Gender int `json:"gender"` Birthday string `json:"birthday"` Height int `json:"height"` CityCode string `json:"cityCode"` City string `json:"city"` Occupation string `json:"occupation"` Bio string `json:"bio"` } if decode(r, &req) != nil || strings.TrimSpace(req.Nickname) == "" || len([]rune(req.Nickname)) > 50 || req.Gender < 0 || req.Gender > 2 || req.Height < 0 || req.Height > 260 { fail(w, 400, 20001, "用户资料格式不正确") return } if req.Birthday != "" { if _, err = time.Parse("2006-01-02", req.Birthday); err != nil { fail(w, 400, 20001, "生日格式应为 YYYY-MM-DD") return } } tx, err := a.db.BeginTx(r.Context(), nil) if err != nil { fail(w, 500, 50001, "保存失败") return } defer func() { _ = tx.Rollback() }() if strings.TrimSpace(req.Phone) != "" { if !validPhone(req.Phone) { fail(w, 400, 20001, "手机号格式不正确") return } phoneCipher, encryptErr := a.encryptPhone(req.Phone) if encryptErr != nil { fail(w, 500, 50001, "加密手机号失败") return } if _, err = tx.ExecContext(r.Context(), `UPDATE users SET phone_hash=?,phone_cipher=? WHERE id=?`, phoneHash(req.Phone), phoneCipher, id); err != nil { fail(w, http.StatusConflict, 20001, "手机号已被其他账号使用") return } } _, err = tx.ExecContext(r.Context(), `UPDATE user_profiles SET nickname=?,avatar_url=?,cover_url=?,gender=?,birthday=NULLIF(?,''),height_cm=NULLIF(?,0),city_code=?,city_name=?,occupation=?,bio=?,profile_score=GREATEST(profile_score,80) WHERE user_id=?`, strings.TrimSpace(req.Nickname), strings.TrimSpace(req.Avatar), strings.TrimSpace(req.Cover), req.Gender, req.Birthday, req.Height, strings.TrimSpace(req.CityCode), strings.TrimSpace(req.City), strings.TrimSpace(req.Occupation), strings.TrimSpace(req.Bio), id) if err != nil || tx.Commit() != nil { fail(w, 500, 50001, "保存失败") return } a.audit(r, "update_profile", "user", id, map[string]any{"nickname": req.Nickname, "phoneChanged": req.Phone != "", "city": req.City}) reply(w, map[string]bool{"success": true}) } func (a *App) adminUpdateVerification(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { fail(w, 400, 20001, "用户编号无效") return } var req struct { Type string `json:"type"` Status string `json:"status"` RealName string `json:"realName"` DocumentMask string `json:"documentMask"` Remark string `json:"remark"` } if decode(r, &req) != nil { fail(w, 400, 20001, "认证资料格式错误") return } req.Status = strings.ToUpper(strings.TrimSpace(req.Status)) if req.Type == "" { req.Type = "real_name" } if req.Status != "UNVERIFIED" && req.Status != "PENDING" && req.Status != "VERIFIED" && req.Status != "REJECTED" { fail(w, 400, 20001, "认证状态无效") return } _, err = a.db.ExecContext(r.Context(), `INSERT INTO user_verifications(user_id,verification_type,status,real_name,document_mask,remark,reviewer_admin_id,submitted_at,reviewed_at) VALUES(?,?,?,?,?,?,?,IF(?='PENDING',NOW(3),NULL),IF(? IN ('VERIFIED','REJECTED'),NOW(3),NULL)) ON DUPLICATE KEY UPDATE verification_type=VALUES(verification_type),status=VALUES(status),real_name=VALUES(real_name),document_mask=VALUES(document_mask),remark=VALUES(remark),reviewer_admin_id=VALUES(reviewer_admin_id),submitted_at=IF(VALUES(status)='PENDING',COALESCE(submitted_at,NOW(3)),submitted_at),reviewed_at=IF(VALUES(status) IN ('VERIFIED','REJECTED'),NOW(3),NULL)`, id, req.Type, req.Status, strings.TrimSpace(req.RealName), strings.TrimSpace(req.DocumentMask), strings.TrimSpace(req.Remark), current(r).ID, req.Status, req.Status) if err != nil { fail(w, 500, 50001, "保存认证结果失败") return } a.audit(r, "verify", "user", id, map[string]any{"status": req.Status, "type": req.Type, "remark": req.Remark}) reply(w, map[string]bool{"success": true}) } func parseAdminExpiry(value string, fallbackDays int) (time.Time, error) { if strings.TrimSpace(value) == "" { return time.Now().AddDate(0, 0, fallbackDays), nil } if parsed, err := time.Parse(time.RFC3339, value); err == nil { return parsed, nil } parsed, err := time.ParseInLocation("2006-01-02", value, time.Local) if err != nil { return time.Time{}, err } return parsed.Add(23*time.Hour + 59*time.Minute + 59*time.Second), nil } func (a *App) adminUpdateMembership(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil { fail(w, 400, 20001, "用户编号无效") return } var req struct { Operation string `json:"operation"` PlanID int64 `json:"planId"` ExpiresAt string `json:"expiresAt"` Reason string `json:"reason"` } if decode(r, &req) != nil { fail(w, 400, 20001, "会员设置格式错误") return } if req.Operation == "" { req.Operation = "grant" } tx, err := a.db.BeginTx(r.Context(), nil) if err != nil { fail(w, 500, 50001, "会员设置失败") return } defer func() { _ = tx.Rollback() }() if req.Operation == "revoke" { _, err = tx.ExecContext(r.Context(), `UPDATE subscriptions SET status=0 WHERE user_id=? AND status=1`, id) if err == nil { _, err = tx.ExecContext(r.Context(), `UPDATE user_profiles SET is_vip=0,vip_level=0 WHERE user_id=?`, id) } } else { var durationDays, level int if err = tx.QueryRowContext(r.Context(), `SELECT duration_days,level FROM membership_plans WHERE id=? AND deleted_at IS NULL`, req.PlanID).Scan(&durationDays, &level); err != nil { fail(w, 400, 20001, "会员套餐不存在") return } expiresAt, parseErr := parseAdminExpiry(req.ExpiresAt, durationDays) if parseErr != nil || !expiresAt.After(time.Now()) { fail(w, 400, 20001, "会员到期时间无效") return } _, err = tx.ExecContext(r.Context(), `UPDATE subscriptions SET status=0 WHERE user_id=? AND status=1`, id) if err == nil { _, err = tx.ExecContext(r.Context(), `INSERT INTO subscriptions(user_id,plan_id,source,status,started_at,expires_at) VALUES(?,?,'admin',1,NOW(3),?)`, id, req.PlanID, expiresAt) } if err == nil { _, err = tx.ExecContext(r.Context(), `UPDATE user_profiles SET is_vip=1,vip_level=? WHERE user_id=?`, level, id) } } if err != nil || tx.Commit() != nil { fail(w, 500, 50001, "会员设置失败") return } a.audit(r, "membership_"+req.Operation, "user", id, map[string]any{"planId": req.PlanID, "expiresAt": req.ExpiresAt, "reason": req.Reason}) reply(w, map[string]bool{"success": true}) } func (a *App) forceLogoutUser(ctx context.Context, userID, adminID int64, passwordReset bool) error { if _, err := a.db.ExecContext(ctx, `UPDATE user_sessions SET revoked_at=NOW(3) WHERE user_id=? AND revoked_at IS NULL`, userID); err != nil { return err } if passwordReset { _, err := a.db.ExecContext(ctx, `INSERT INTO user_security_controls(user_id,token_version,force_logout_at,password_reset_at,last_operator_admin_id) VALUES(?,1,NOW(3),NOW(3),?) ON DUPLICATE KEY UPDATE token_version=token_version+1,force_logout_at=VALUES(force_logout_at),password_reset_at=VALUES(password_reset_at),last_operator_admin_id=VALUES(last_operator_admin_id)`, userID, adminID) if err == nil { a.hub.disconnect(userID) } return err } _, err := a.db.ExecContext(ctx, `INSERT INTO user_security_controls(user_id,token_version,force_logout_at,last_operator_admin_id) VALUES(?,1,NOW(3),?) ON DUPLICATE KEY UPDATE token_version=token_version+1,force_logout_at=VALUES(force_logout_at),last_operator_admin_id=VALUES(last_operator_admin_id)`, userID, adminID) if err == nil { a.hub.disconnect(userID) } return err } func (a *App) adminResetUserPassword(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) var req struct { NewPassword string `json:"newPassword"` } if err != nil || decode(r, &req) != nil || !validUserPassword(req.NewPassword) { fail(w, 400, 20001, "新密码需为 8-72 位并同时包含字母和数字") return } hash, err := hashPassword(req.NewPassword) if err != nil { fail(w, 500, 50001, "密码加密失败") return } result, err := a.db.ExecContext(r.Context(), `UPDATE users SET password_hash=? WHERE id=? AND deleted_at IS NULL`, hash, id) if err != nil { fail(w, 500, 50001, "重置密码失败") return } affected, _ := result.RowsAffected() if affected == 0 || a.forceLogoutUser(r.Context(), id, current(r).ID, true) != nil { fail(w, 500, 50001, "重置密码失败") return } a.audit(r, "reset_password", "user", id, map[string]any{"forceLogout": true}) reply(w, map[string]bool{"success": true}) } func (a *App) adminForceLogout(w http.ResponseWriter, r *http.Request) { id, err := pathID(r) if err != nil || a.forceLogoutUser(r.Context(), id, current(r).ID, false) != nil { fail(w, 500, 50001, "强制下线失败") return } a.audit(r, "force_logout", "user", id, nil) reply(w, map[string]bool{"success": true}) } func (a *App) sanctionList(ctx context.Context, userID int64) []map[string]any { items := []map[string]any{} rows, err := a.db.QueryContext(ctx, `SELECT s.id,s.sanction_type,s.reason,s.starts_at,s.expires_at,s.status,s.operator_admin_id,COALESCE(a.real_name,''),s.revoked_at,s.created_at FROM user_sanctions s LEFT JOIN admin_users a ON a.id=s.operator_admin_id WHERE s.user_id=? ORDER BY s.created_at DESC LIMIT 100`, userID) if err != nil { return items } defer rows.Close() for rows.Next() { var id, operatorID int64 var typ, reason, status, operatorName string var startsAt, createdAt time.Time var expiresAt, revokedAt any _ = rows.Scan(&id, &typ, &reason, &startsAt, &expiresAt, &status, &operatorID, &operatorName, &revokedAt, &createdAt) items = append(items, map[string]any{"id": id, "type": typ, "reason": reason, "startsAt": startsAt, "expiresAt": expiresAt, "status": status, "operatorId": operatorID, "operatorName": operatorName, "revokedAt": revokedAt, "createdAt": createdAt}) } return items } func (a *App) createSanction(ctx context.Context, adminID, userID int64, typ, reason string, expiresAt *time.Time) (int64, error) { result, err := a.db.ExecContext(ctx, `INSERT INTO user_sanctions(user_id,sanction_type,reason,expires_at,operator_admin_id) VALUES(?,?,?,?,?)`, userID, typ, reason, expiresAt, adminID) if err != nil { return 0, err } id, _ := result.LastInsertId() if typ == "FREEZE" || typ == "BAN" { status := 2 if typ == "BAN" { status = 3 } if _, err = a.db.ExecContext(ctx, `UPDATE users SET status=? WHERE id=?`, status, userID); err == nil { err = a.forceLogoutUser(ctx, userID, adminID, false) } } if err == nil { title := map[string]string{"WARNING": "违规警告", "MUTE": "禁言通知", "CONTENT_LIMIT": "内容发布限制", "FREEZE": "账号冻结", "BAN": "账号封禁"}[typ] _, _ = a.db.ExecContext(ctx, `INSERT INTO notifications(user_id,type,title,content,biz_type) VALUES(?,'system',?,?,'sanction')`, userID, title, reason) } return id, err } func (a *App) adminUserSanctions(w http.ResponseWriter, r *http.Request) { userID, err := pathID(r) if err != nil { fail(w, 400, 20001, "用户编号无效") return } if r.Method == http.MethodGet { reply(w, map[string]any{"items": a.sanctionList(r.Context(), userID)}) return } var req struct { Type string `json:"type"` Reason string `json:"reason"` ExpiresAt string `json:"expiresAt"` } if decode(r, &req) != nil { fail(w, 400, 20001, "处罚信息格式错误") return } req.Type = strings.ToUpper(strings.TrimSpace(req.Type)) allowed := map[string]bool{"WARNING": true, "FREEZE": true, "BAN": true, "MUTE": true, "CONTENT_LIMIT": true} if !allowed[req.Type] || strings.TrimSpace(req.Reason) == "" { fail(w, 400, 20001, "请选择处罚类型并填写原因") return } var expiry *time.Time if req.ExpiresAt != "" { parsed, parseErr := parseAdminExpiry(req.ExpiresAt, 0) if parseErr != nil || !parsed.After(time.Now()) { fail(w, 400, 20001, "处罚到期时间无效") return } expiry = &parsed } sanctionID, err := a.createSanction(r.Context(), current(r).ID, userID, req.Type, strings.TrimSpace(req.Reason), expiry) if err != nil { fail(w, 500, 50001, "执行处罚失败") return } a.audit(r, "sanction", "user", userID, map[string]any{"sanctionId": sanctionID, "type": req.Type, "reason": req.Reason, "expiresAt": req.ExpiresAt}) reply(w, map[string]any{"id": sanctionID}) } func (a *App) adminRevokeSanction(w http.ResponseWriter, r *http.Request) { sanctionID, err := pathID(r) if err != nil { fail(w, 400, 20001, "处罚编号无效") return } var userID int64 var typ string if err = a.db.QueryRowContext(r.Context(), `SELECT user_id,sanction_type FROM user_sanctions WHERE id=? AND status='ACTIVE'`, sanctionID).Scan(&userID, &typ); err != nil { fail(w, 404, 30001, "有效处罚不存在") return } result, err := a.db.ExecContext(r.Context(), `UPDATE user_sanctions SET status='REVOKED',revoked_by=?,revoked_at=NOW(3) WHERE id=? AND status='ACTIVE'`, current(r).ID, sanctionID) affected, _ := result.RowsAffected() if err != nil || affected == 0 { fail(w, 500, 50001, "撤销处罚失败") return } if typ == "FREEZE" || typ == "BAN" { var bans, freezes int _ = a.db.QueryRowContext(r.Context(), `SELECT SUM(sanction_type='BAN'),SUM(sanction_type='FREEZE') FROM user_sanctions WHERE user_id=? AND status='ACTIVE' AND (expires_at IS NULL OR expires_at>NOW(3))`, userID).Scan(&bans, &freezes) status := 1 if bans > 0 { status = 3 } else if freezes > 0 { status = 2 } _, _ = a.db.ExecContext(r.Context(), `UPDATE users SET status=? WHERE id=?`, status, userID) } a.audit(r, "revoke_sanction", "user", userID, map[string]any{"sanctionId": sanctionID, "type": typ}) reply(w, map[string]bool{"success": true}) } func (a *App) isSanctionActive(ctx context.Context, userID int64, typ string) bool { var count int _ = a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM user_sanctions WHERE user_id=? AND sanction_type=? AND status='ACTIVE' AND (expires_at IS NULL OR expires_at>NOW(3))`, userID, typ).Scan(&count) return count > 0 } func (a *App) normalizeUserStatus(ctx context.Context, userID int64, status int) int { if status != 2 && status != 3 { return status } var total int _ = a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM user_sanctions WHERE user_id=? AND sanction_type IN ('FREEZE','BAN')`, userID).Scan(&total) if total == 0 { return status } _, _ = a.db.ExecContext(ctx, `UPDATE user_sanctions SET status='EXPIRED' WHERE user_id=? AND sanction_type IN ('FREEZE','BAN') AND status='ACTIVE' AND expires_at<=NOW(3)`, userID) var bans, freezes int _ = a.db.QueryRowContext(ctx, `SELECT SUM(sanction_type='BAN'),SUM(sanction_type='FREEZE') FROM user_sanctions WHERE user_id=? AND status='ACTIVE' AND (expires_at IS NULL OR expires_at>NOW(3))`, userID).Scan(&bans, &freezes) resolved := 1 if bans > 0 { resolved = 3 } else if freezes > 0 { resolved = 2 } if resolved != status { _, _ = a.db.ExecContext(ctx, `UPDATE users SET status=? WHERE id=?`, resolved, userID) } return resolved } func (a *App) recomputeMembershipTx(ctx context.Context, tx *sql.Tx, userID int64) error { var level sql.NullInt64 if err := tx.QueryRowContext(ctx, `SELECT MAX(p.level) FROM subscriptions s JOIN membership_plans p ON p.id=s.plan_id WHERE s.user_id=? AND s.status=1 AND s.expires_at>NOW(3)`, userID).Scan(&level); err != nil { return err } if !level.Valid { _, err := tx.ExecContext(ctx, `UPDATE user_profiles SET is_vip=0,vip_level=0 WHERE user_id=?`, userID) return err } _, err := tx.ExecContext(ctx, `UPDATE user_profiles SET is_vip=1,vip_level=? WHERE user_id=?`, level.Int64, userID) return err } func (a *App) adminOrderTransition(w http.ResponseWriter, r *http.Request, action string) { id, err := pathID(r) if err != nil { fail(w, 400, 20001, "订单编号无效") return } tx, err := a.db.BeginTx(r.Context(), nil) if err != nil { fail(w, 500, 50001, "订单操作失败") return } defer func() { _ = tx.Rollback() }() var userID, planID int64 var status string if err = tx.QueryRowContext(r.Context(), `SELECT user_id,product_id,status FROM orders WHERE id=? AND deleted_at IS NULL FOR UPDATE`, id).Scan(&userID, &planID, &status); err != nil { fail(w, 404, 30001, "订单不存在") return } switch action { case "pay": if status != "CREATED" { fail(w, 400, 20001, "只有待支付订单可标记为已支付") return } var durationDays, level int if err = tx.QueryRowContext(r.Context(), `SELECT duration_days,level FROM membership_plans WHERE id=? AND deleted_at IS NULL`, planID).Scan(&durationDays, &level); err == nil { _, err = tx.ExecContext(r.Context(), `UPDATE orders SET status='PAID',paid_at=NOW(3) WHERE id=?`, id) } if err == nil { _, err = tx.ExecContext(r.Context(), `INSERT INTO subscriptions(user_id,plan_id,source,status,started_at,expires_at) VALUES(?,?,?,1,NOW(3),DATE_ADD(NOW(3),INTERVAL ? DAY))`, userID, planID, fmt.Sprintf("admin_order:%d", id), durationDays) } if err == nil { _, err = tx.ExecContext(r.Context(), `UPDATE user_profiles SET is_vip=1,vip_level=GREATEST(vip_level,?) WHERE user_id=?`, level, userID) } case "close": if status != "CREATED" { fail(w, 400, 20001, "只有待支付订单可关闭") return } _, err = tx.ExecContext(r.Context(), `UPDATE orders SET status='CLOSED' WHERE id=?`, id) case "refund": if status != "PAID" { fail(w, 400, 20001, "只有已支付订单可退款") return } _, err = tx.ExecContext(r.Context(), `UPDATE orders SET status='REFUNDED' WHERE id=?`, id) if err == nil { result, updateErr := tx.ExecContext(r.Context(), `UPDATE subscriptions SET status=0 WHERE user_id=? AND plan_id=? AND status=1 AND source IN (?,?)`, userID, planID, fmt.Sprintf("order:%d", id), fmt.Sprintf("admin_order:%d", id)) err = updateErr affected, _ := result.RowsAffected() if err == nil && affected == 0 { _, err = tx.ExecContext(r.Context(), `UPDATE subscriptions SET status=0 WHERE id=(SELECT id FROM (SELECT id FROM subscriptions WHERE user_id=? AND plan_id=? AND status=1 ORDER BY started_at DESC LIMIT 1) latest)`, userID, planID) } } if err == nil { err = a.recomputeMembershipTx(r.Context(), tx, userID) } default: fail(w, 400, 20001, "订单操作无效") return } if err != nil || tx.Commit() != nil { fail(w, 500, 50001, "订单操作失败") return } a.audit(r, action, "order", id, map[string]any{"userId": userID, "previousStatus": status}) reply(w, map[string]bool{"success": true}) } func (a *App) adminMarkOrderPaid(w http.ResponseWriter, r *http.Request) { if a.configPlain(r.Context(), "payment.mode", "sandbox") == "live" { fail(w, http.StatusBadRequest, 20001, "生产支付订单只能由已验签的支付回调确认入账") return } a.adminOrderTransition(w, r, "pay") } func (a *App) adminCloseOrder(w http.ResponseWriter, r *http.Request) { a.adminOrderTransition(w, r, "close") }