Files
past-go/past-go/internal/handler/admin.go
T
2026-10-30 23:46:55 +08:00

830 lines
30 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"fmt"
"math"
"regexp"
"strings"
"past/internal/httpapi"
"past/internal/store"
)
func registerAdmin(r *httpapi.Router) {
r.Handle("admin_login", adminLogin)
r.Handle("admin_check_auth", adminCheckAuth)
r.Handle("admin_logout", adminLogout)
r.Handle("admin_stats", adminStats)
r.Handle("admin_stats_detail", adminStatsDetail)
r.Handle("admin_couples", adminCouples)
r.Handle("admin_couple_detail", adminCoupleDetail)
r.Handle("admin_pet_detail", adminPetDetail)
r.Handle("admin_pets", adminPets)
r.Handle("admin_users", adminUsers)
r.Handle("admin_fix_last_login", adminFixLastLogin)
r.Handle("admin_clean_locations", adminCleanLocations)
r.Handle("admin_user_detail", adminUserDetail)
r.Handle("admin_change_password", adminChangePassword)
r.Handle("admin_change_username", adminChangeUsername)
r.Handle("admin_update_user", adminUpdateUser)
r.Handle("admin_delete_user", adminDeleteUser)
r.Handle("admin_unbind_requests", adminUnbindRequests)
r.Handle("admin_force_unbind", adminForceUnbind)
r.Handle("admin_restore_couple", adminRestoreCouple)
r.Handle("admin_delete_couple_data", adminDeleteCoupleData)
r.Handle("admin_delete_content", adminDeleteContent)
r.Handle("admin_contents", adminContents)
r.Handle("admin_reset_user_password", adminResetUserPassword)
r.Handle("admin_activity_log", adminActivityLog)
r.Handle("admin_subscribe_logs", adminSubscribeLogs)
r.Handle("admin_get_global_bg", adminGetGlobalBg)
r.Handle("admin_set_global_bg", adminSetGlobalBg)
r.Handle("admin_clear_global_bg", adminClearGlobalBg)
r.Handle("admin_couple_bg_list", adminCoupleBgList)
r.Handle("admin_clear_couple_bg", adminClearCoupleBg)
}
func adminLogin(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
admin, err := c.DB.QueryOne("SELECT * FROM admins WHERE username = ?", c.InTrim("username"))
if err != nil {
return err
}
if admin != nil && verifyPassword(c.InStr("password"), admin.Str("password")) {
c.AdminLogin(admin.Int("id"), admin.Str("username"))
return c.OK(M{"success": true, "username": admin["username"]})
}
return c.Err(401, "用户名或密码错误")
}
func adminCheckAuth(c *httpapi.Ctx) error {
return c.OK(M{"logged_in": c.AdminSession() != nil})
}
func adminLogout(c *httpapi.Ctx) error {
c.AdminLogout()
return c.OK(M{"success": true})
}
// tableCount 对应 $db->query("SELECT COUNT(*) as c FROM t")->fetch()['c'](原样,未 intval)
func tableCount(c *httpapi.Ctx, table string) any {
return countRaw(c, "c", "SELECT COUNT(*) as c FROM "+table)
}
func adminStats(c *httpapi.Ctx) error {
return c.OK(M{
"total_users": tableCount(c, "users"), "total_couples": tableCount(c, "couples"),
"total_timelines": tableCount(c, "timeline"), "total_diaries": tableCount(c, "diaries"),
"total_albums": tableCount(c, "albums"), "total_photos": tableCount(c, "photos"),
"total_songs": tableCount(c, "songs"), "total_checkins": tableCount(c, "checkins"),
"total_pets": tableCount(c, "pets"), "total_pet_achievements": tableCount(c, "pet_achievements"),
})
}
var ymdRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
func adminStatsDetail(c *httpapi.Ctx) error {
date := c.Q("date")
if !c.QHas("date") {
date = today()
}
if !ymdRe.MatchString(date) {
return c.Err(400, "日期格式错误,请使用 YYYY-MM-DD")
}
month := date[:7]
dateCount := func(table string) int64 {
return countC(c, "SELECT COUNT(*) as c FROM `"+table+"` WHERE DATE(created_at) = ?", date)
}
contents := M{}
var total int64
for _, t := range []string{"timeline", "diaries", "albums", "photos", "songs", "checkins", "checkin_logs"} {
n := dateCount(t)
contents[t] = n
total += n
}
return c.OK(M{
"date": date,
"today_registers": dateCount("users"),
"month_registers": countC(c, "SELECT COUNT(*) as c FROM users WHERE DATE_FORMAT(created_at, '%Y-%m') = ?", month),
"date_registers": dateCount("users"),
"date_couples": dateCount("couples"),
"date_contents": contents,
"date_contents_total": total,
"total_users": tableCount(c, "users"),
"total_couples": tableCount(c, "couples"),
"total_storage_used": allStorageUsedBytes(),
})
}
// paged 后台列表分页包装:query 参数 page / page_size(默认 10,最大 100),返回 {items,total,page,page_size,total_pages}
func paged(c *httpapi.Ctx, all []store.Row) M {
page, pageSize, offset := pageParams(c, 10, 100)
total := int64(len(all))
end := offset + pageSize
if offset > total {
offset = total
}
if end > total {
end = total
}
return M{
"items": rows(all[offset:end]), "total": total, "page": page, "page_size": pageSize,
"total_pages": int64(math.Ceil(float64(total) / float64(pageSize))),
}
}
func adminCouples(c *httpapi.Ctx) error {
rs, err := c.DB.Query("SELECT c.*, c.couple_space_id, u1.nickname as user1_nick, u1.username as user1_name, u1.gender as user1_gender, u1.age as user1_age, u1.birthday as user1_birthday, u2.nickname as user2_nick, u2.username as user2_name, u2.gender as user2_gender, u2.age as user2_age, u2.birthday as user2_birthday, (SELECT COUNT(*) FROM timeline WHERE couple_id = c.id) as timeline_count, (SELECT COUNT(*) FROM diaries WHERE couple_id = c.id) as diary_count, (SELECT COUNT(*) FROM albums WHERE couple_id = c.id) as album_count, (SELECT COUNT(*) FROM songs WHERE couple_id = c.id) as songs_count, (SELECT COUNT(*) FROM checkins WHERE couple_id = c.id) as checkin_count FROM couples c LEFT JOIN users u1 ON c.user1_id = u1.id LEFT JOIN users u2 ON c.user2_id = u2.id ORDER BY c.created_at DESC")
if err != nil {
return err
}
return c.OK(paged(c, rs))
}
// petFullDetail 宠物详情各子表(admin_couple_detail / admin_pet_detail 共用)
func petFullDetail(c *httpapi.Ctx, cid int64) (dailies, achievements, dynamics, items []store.Row, err error) {
if dailies, err = c.DB.Query("SELECT * FROM pet_dailies WHERE couple_id = ? ORDER BY date DESC LIMIT 60", cid); err != nil {
return
}
if achievements, err = c.DB.Query("SELECT pa.* FROM pet_achievements pa WHERE pa.couple_id = ? ORDER BY pa.unlocked_at DESC", cid); err != nil {
return
}
if dynamics, err = c.DB.Query("SELECT * FROM pet_dynamics WHERE couple_id = ? ORDER BY created_at DESC LIMIT 30", cid); err != nil {
return
}
items, err = c.DB.Query("SELECT * FROM pet_items WHERE couple_id = ? ORDER BY created_at DESC", cid)
return
}
// rowOrFalse 对应 PDO fetch() 无结果返回 false
func rowOrFalse(r store.Row) any {
if r == nil {
return false
}
return r
}
func adminCoupleDetail(c *httpapi.Ctx) error {
var cid int64
if c.Method == "GET" {
cid = c.QInt("couple_id")
} else {
cid = c.InInt("couple_id")
}
if cid == 0 {
return c.Err(400, "缺少情侣ID")
}
data := M{}
users, err := c.DB.Query("SELECT id, user_id, username, nickname, avatar FROM users WHERE couple_id = ?", cid)
if err != nil {
return err
}
data["users"] = rows(users)
data["user1_id"], data["user2_id"] = int64(0), int64(0)
if len(users) > 0 {
data["user1_id"] = users[0]["id"]
}
if len(users) > 1 {
data["user2_id"] = users[1]["id"]
}
q := func(key, sql string) error {
rs, err := c.DB.Query(sql, cid)
if err != nil {
return err
}
data[key] = rows(rs)
return nil
}
if err := q("timeline", "SELECT * FROM timeline WHERE couple_id = ? ORDER BY event_date DESC"); err != nil {
return err
}
if err := q("diaries", "SELECT * FROM diaries WHERE couple_id = ? ORDER BY diary_date DESC"); err != nil {
return err
}
if err := q("albums", "SELECT * FROM albums WHERE couple_id = ?"); err != nil {
return err
}
data["photos"] = []store.Row{}
if albums := data["albums"].([]store.Row); len(albums) > 0 {
ids := make([]any, 0, len(albums))
for _, a := range albums {
ids = append(ids, a["id"])
}
photos, err := c.DB.Query("SELECT * FROM photos WHERE album_id IN ("+placeholders(len(ids))+") ORDER BY album_id, sort_order ASC, created_at DESC", ids...)
if err != nil {
return err
}
data["photos"] = rows(photos)
}
if err := q("songs", "SELECT * FROM songs WHERE couple_id = ? ORDER BY created_at DESC"); err != nil {
return err
}
if err := q("checkins", "SELECT * FROM checkins WHERE couple_id = ? ORDER BY created_at DESC"); err != nil {
return err
}
pet, err := c.DB.QueryOne("SELECT * FROM pets WHERE couple_id = ?", cid)
if err != nil {
return err
}
if pet != nil {
pet["achievement_count"] = countC(c, "SELECT COUNT(*) as c FROM pet_achievements WHERE couple_id = ?", cid)
pet["daily_count"] = countC(c, "SELECT COUNT(*) as c FROM pet_dailies WHERE couple_id = ?", cid)
dailies, achs, dyns, items, err := petFullDetail(c, cid)
if err != nil {
return err
}
pet["dailies"], pet["achievements"], pet["dynamics"], pet["items"] = rows(dailies), rows(achs), rows(dyns), rows(items)
}
data["pet"] = rowOrFalse(pet)
return c.OK(data)
}
func adminPetDetail(c *httpapi.Ctx) error {
cid := c.QInt("couple_id")
if cid == 0 {
return c.Err(400, "缺少情侣ID")
}
pet, err := c.DB.QueryOne("SELECT * FROM pets WHERE couple_id = ?", cid)
if err != nil {
return c.Err(500, "服务器错误: "+err.Error())
}
dailies, achs, dyns, items, err := petFullDetail(c, cid)
if err != nil {
return c.Err(500, "服务器错误: "+err.Error())
}
return c.OK(M{"pet": rowOrFalse(pet), "dailies": rows(dailies), "achievements": rows(achs), "dynamics": rows(dyns), "items": rows(items)})
}
func adminPets(c *httpapi.Ctx) error {
var where []string
var params []any
if store.Truthy(c.Q("breed")) {
where = append(where, "p.breed = ?")
params = append(params, c.Q("breed"))
}
if store.Truthy(c.Q("stage")) {
where = append(where, "p.stage = ?")
params = append(params, c.QInt("stage"))
}
whereSQL := ""
if len(where) > 0 {
whereSQL = "WHERE " + strings.Join(where, " AND ")
}
rs, err := c.DB.Query(`SELECT p.*, u1.nickname as user1_name, u2.nickname as user2_name FROM pets p
LEFT JOIN users u1 ON p.created_by = u1.id
LEFT JOIN couples c ON p.couple_id = c.id
LEFT JOIN users u2 ON (CASE WHEN c.user1_id = p.created_by THEN c.user2_id ELSE c.user1_id END) = u2.id
`+whereSQL+` ORDER BY p.level DESC, p.updated_at DESC`, params...)
if err != nil {
return err
}
return c.OK(paged(c, rs))
}
func adminUsers(c *httpapi.Ctx) error {
search := strings.TrimSpace(c.Q("search"))
bind := strings.TrimSpace(c.Q("bind"))
if !c.QHas("bind") {
bind = "all"
}
sort := strings.TrimSpace(c.Q("sort"))
if !c.QHas("sort") {
sort = "newest"
}
sql := "SELECT u.id, u.user_id, u.openid, u.username, u.nickname, u.avatar, u.gender, u.age, u.birthday, u.couple_id, u.created_at, COALESCE(u.last_login_at, u.created_at) as last_login_at, u.location, u.last_login_location, u.last_ip, CASE WHEN u.couple_id IS NOT NULL THEN '已绑定' ELSE '未绑定' END as status FROM users u"
var where []string
var params []any
if bind == "bound" {
where = append(where, "u.couple_id IS NOT NULL")
} else if bind == "unbound" {
where = append(where, "u.couple_id IS NULL")
}
if search != "" {
like := "%" + search + "%"
if isNumeric(search) {
where = append(where, "(u.id = ? OR u.user_id = ? OR u.nickname LIKE ? OR u.username LIKE ?)")
params = append(params, store.ToInt(search), store.ToInt(search), like, like)
} else {
where = append(where, "(u.nickname LIKE ? OR u.username LIKE ?)")
params = append(params, like, like)
}
}
if len(where) > 0 {
sql += " WHERE " + strings.Join(where, " AND ")
}
if sort == "last_login" {
sql += " ORDER BY COALESCE(u.last_login_at, u.created_at) DESC"
} else {
sql += " ORDER BY u.created_at DESC"
}
rs, err := c.DB.Query(sql, params...)
if err != nil {
return err
}
pg := paged(c, rs)
items := pg["items"].([]store.Row)
for _, it := range items {
uid, cid := it.Int("id"), it.Int("couple_id")
it["used_bytes"] = storageUsedBytes(c, uid, cid)
it["storage_vip"] = isStorageVip(c, uid)
it["storage_vip_expire_at"] = storageVipExpireAt(c, uid)
}
pg["items"] = items
return c.OK(pg)
}
func adminFixLastLogin(c *httpapi.Ctx) error {
res, err := c.DB.Exec("UPDATE users SET last_login_at = created_at WHERE last_login_at IS NULL")
if err != nil {
return err
}
n, _ := res.RowsAffected()
return c.OK(M{"success": true, "count": n, "msg": fmt.Sprintf("已补全 %d 个用户的最后登录时间", n)})
}
func adminCleanLocations(c *httpapi.Ctx) error {
res, err := c.DB.Exec(`UPDATE users SET location = '' WHERE location REGEXP '^-?[0-9]+(\\.[0-9]+)?,-?[0-9]+(\\.[0-9]+)?$'`)
if err != nil {
return err
}
n, _ := res.RowsAffected()
return c.OK(M{"success": true, "count": n, "msg": fmt.Sprintf("已清理 %d 个经纬度格式的位置数据", n)})
}
func adminUserDetail(c *httpapi.Ctx) error {
uid := c.QInt("user_id")
if uid == 0 {
return c.Err(400, "缺少用户ID")
}
user, err := c.DB.QueryOne("SELECT id, user_id, openid, username, nickname, avatar, gender, age, birthday, couple_id, created_at, last_login_at, region, location, last_login_location FROM users WHERE id = ?", uid)
if err != nil {
return err
}
if user == nil {
return c.Err(404, "用户不存在")
}
if user.Int("couple_id") != 0 {
partner, _ := c.DB.QueryOne("SELECT id, user_id, username, nickname, avatar, gender, age, birthday, location, last_login_location FROM users WHERE couple_id = ? AND id != ?", user.Int("couple_id"), uid)
user["partner"] = rowOrFalse(partner)
}
logs, err := c.DB.Query("SELECT cl.*, c.title as checkin_title, c.icon as checkin_icon, u2.nickname as creator_name FROM checkin_logs cl LEFT JOIN checkins c ON cl.checkin_id = c.id LEFT JOIN users u2 ON c.created_by = u2.id WHERE cl.user_id = ? ORDER BY cl.created_at DESC LIMIT 100", uid)
if err != nil {
return err
}
user["checkin_logs"] = rows(logs)
checkins, err := c.DB.Query("SELECT * FROM checkins WHERE created_by = ? ORDER BY created_at DESC LIMIT 50", uid)
if err != nil {
return err
}
for _, ck := range checkins {
l, _ := c.DB.Query("SELECT cl.*, u.nickname, u.avatar FROM checkin_logs cl LEFT JOIN users u ON cl.user_id = u.id WHERE cl.checkin_id = ? ORDER BY cl.created_at DESC LIMIT 30", ck.Int("id"))
ck["logs"] = rows(l)
}
user["checkins"] = rows(checkins)
diaries, err := c.DB.Query("SELECT * FROM diaries WHERE created_by = ? ORDER BY diary_date DESC LIMIT 50", uid)
if err != nil {
return err
}
user["diaries"] = rows(diaries)
albums, err := c.DB.Query("SELECT * FROM albums WHERE created_by = ? ORDER BY created_at DESC LIMIT 50", uid)
if err != nil {
return err
}
user["albums"] = rows(albums)
return c.OK(user)
}
func adminChangePassword(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
if len(c.InStr("new_password")) < 6 {
return c.Err(400, "新密码至少6位")
}
sess := c.AdminSession()
admin, _ := c.DB.QueryOne("SELECT * FROM admins WHERE id = ?", sess.AdminID)
if admin != nil && verifyPassword(c.InStr("old_password"), admin.Str("password")) {
if _, err := c.DB.Exec("UPDATE admins SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", hashPassword(c.InStr("new_password")), sess.AdminID); err != nil {
return err
}
return c.OK(M{"success": true})
}
return c.Err(400, "原密码错误")
}
func adminChangeUsername(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
name := c.InTrim("new_username")
if !store.Truthy(name) {
return c.Err(400, "用户名不能为空")
}
sess := c.AdminSession()
if _, err := c.DB.Exec("UPDATE admins SET username = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", name, sess.AdminID); err != nil {
return err
}
sess.Username = name
return c.OK(M{"success": true})
}
func adminUpdateUser(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
userID := c.InInt("user_id")
if userID <= 0 {
return c.Err(400, "用户ID无效")
}
var fields []string
var params []any
if nick := c.InTrim("nickname"); store.Truthy(nick) {
fields = append(fields, "nickname = ?")
params = append(params, nick)
}
if g := c.InStr("gender"); store.Truthy(g) {
fields = append(fields, "gender = ?")
params = append(params, g)
}
if c.InHas("age") {
fields = append(fields, "age = ?")
params = append(params, c.InInt("age"))
}
if b := c.InTrim("birthday"); b != "" {
fields = append(fields, "birthday = ?")
params = append(params, b)
}
if len(fields) > 0 {
fields = append(fields, "updated_at = CURRENT_TIMESTAMP")
params = append(params, userID)
if _, err := c.DB.Exec("UPDATE users SET "+strings.Join(fields, ", ")+" WHERE id = ?", params...); err != nil {
return err
}
}
return c.OK(M{"success": true})
}
func adminDeleteUser(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
target := c.InInt("user_id")
if target <= 0 {
return c.Err(400, "用户ID无效")
}
tu, _ := c.DB.QueryOne("SELECT id, couple_id FROM users WHERE id = ?", target)
if tu == nil {
return c.Err(404, "用户不存在")
}
deleteIDs := []any{target}
if cid := tu.Int("couple_id"); cid != 0 {
partners, _ := c.DB.Query("SELECT id FROM users WHERE couple_id = ? AND id != ?", cid, target)
for _, p := range partners {
deleteIDs = append(deleteIDs, p.Int("id"))
}
for _, t := range []string{"timeline", "diaries", "songs", "checkin_logs", "checkins"} {
c.DB.Exec("DELETE FROM "+t+" WHERE couple_id = ?", cid)
}
albums, _ := c.DB.Query("SELECT id FROM albums WHERE couple_id = ?", cid)
for _, a := range albums {
c.DB.Exec("DELETE FROM photos WHERE album_id = ?", a.Int("id"))
}
c.DB.Exec("DELETE FROM albums WHERE couple_id = ?", cid)
c.DB.Exec("DELETE FROM couples WHERE id = ?", cid)
}
ph := placeholders(len(deleteIDs))
c.DB.Exec("DELETE FROM invitations WHERE from_user_id IN ("+ph+") OR to_user_id IN ("+ph+")", append(append([]any{}, deleteIDs...), deleteIDs...)...)
c.DB.Exec("DELETE FROM invite_links WHERE user_id IN ("+ph+")", deleteIDs...)
if _, err := c.DB.Exec("DELETE FROM users WHERE id IN ("+ph+")", deleteIDs...); err != nil {
return err
}
msg := "已删除用户"
if len(deleteIDs) > 1 {
msg = fmt.Sprintf("已删除用户及伴侣(共%d个账号)及相关情侣数据", len(deleteIDs))
}
return c.OK(M{"success": true, "msg": msg, "deleted_count": len(deleteIDs)})
}
var unbindStatusLabels = map[string]string{
"pending": "待对方回应", "cooling": "冷静期", "completed": "解绑完成", "rejected": "已拒绝", "cancelled": "已撤销", "expired": "已过期",
}
func adminUnbindRequests(c *httpapi.Ctx) error {
if err := c.RequireGET(); err != nil {
return err
}
rs, err := c.DB.Query(`
SELECT
ur.id, ur.couple_id, ur.status, ur.created_at, ur.responded_at, ur.cooling_until, ur.completed_at, ur.cancelled_at,
cancel_user.nickname AS cancelled_by_name,
r.id as requester_id, r.user_id as requester_uid, r.nickname as requester_name, r.avatar as requester_avatar,
c.couple_space_id, c.data_deleted, c.status as couple_status,
u1.nickname as user1_name, u1.user_id as user1_uid,
u2.nickname as user2_name, u2.user_id as user2_uid
FROM unbind_requests ur
JOIN users r ON ur.requester_id = r.id
LEFT JOIN users cancel_user ON ur.cancelled_by = cancel_user.id
JOIN couples c ON ur.couple_id = c.id
LEFT JOIN users u1 ON c.user1_id = u1.id
LEFT JOIN users u2 ON c.user2_id = u2.id
WHERE NOT (c.status = 'active' AND ur.status = 'agreed')
UNION
SELECT
NULL as id, c.id as couple_id, 'completed' as status, NULL as created_at, NULL as responded_at, NULL as cooling_until, NULL as completed_at, NULL as cancelled_at, NULL as cancelled_by_name,
NULL as requester_id, NULL as requester_uid, NULL as requester_name, NULL as requester_avatar,
c.couple_space_id, c.data_deleted, c.status as couple_status,
u1.nickname as user1_name, u1.user_id as user1_uid,
u2.nickname as user2_name, u2.user_id as user2_uid
FROM couples c
LEFT JOIN users u1 ON c.user1_id = u1.id
LEFT JOIN users u2 ON c.user2_id = u2.id
WHERE c.status = 'unbound'
AND c.id NOT IN (SELECT DISTINCT couple_id FROM unbind_requests)
ORDER BY created_at DESC`)
if err != nil {
return err
}
filter := c.Q("status")
out := make([]store.Row, 0, len(rs))
for _, r := range rs {
st := r.Str("status")
if filter != "" && st != filter {
continue
}
if l, ok := unbindStatusLabels[st]; ok {
r["status_label"] = l
} else {
r["status_label"] = r["status"]
}
out = append(out, r)
}
return c.OK(paged(c, out))
}
// coupleUserNames 对应 array_map(fn($u) => $u['nickname'] ?: '#' . $u['user_id'], $users) 后 implode(' & ')
func coupleUserNames(users []store.Row) string {
names := make([]string, 0, len(users))
for _, u := range users {
n := u.Str("nickname")
if !store.Truthy(n) {
n = "#" + u.Str("user_id")
}
names = append(names, n)
}
return strings.Join(names, " & ")
}
func adminForceUnbind(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
cid := c.InInt("couple_id")
if cid == 0 {
return c.Err(400, "缺少情侣ID")
}
couple, _ := c.DB.QueryOne("SELECT * FROM couples WHERE id = ?", cid)
if couple == nil {
return c.Err(404, "情侣不存在")
}
if err := c.DB.AdvanceUnbindings(); err != nil {
return err
}
current, err := c.DB.QueryOne("SELECT status FROM couples WHERE id=?", cid)
if err != nil {
return err
}
if current.Str("status") != "unbound" {
return c.Err(409, "需双方同意并等待72小时冷静期结束,不能提前解绑")
}
return c.OK(M{"success": true, "msg": "冷静期已结束,系统已自动解绑并处理共同数据"})
}
func adminRestoreCouple(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
cid := c.InInt("couple_id")
if cid == 0 {
return c.Err(400, "缺少情侣ID")
}
couple, _ := c.DB.QueryOne("SELECT * FROM couples WHERE id = ? AND status = 'unbound' AND data_deleted = 0", cid)
if couple == nil {
return c.Err(400, "该情侣记录不存在或数据已被删除,无法恢复")
}
return c.Err(409, "旧空间已封存,恢复关系请由双方重新发起绑定,后台不能覆盖当前关系")
}
func adminDeleteCoupleData(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
cid := c.InInt("couple_id")
if cid == 0 {
return c.Err(400, "缺少情侣ID")
}
couple, _ := c.DB.QueryOne("SELECT * FROM couples WHERE id = ? AND status = 'unbound'", cid)
if couple == nil {
return c.Err(400, "该情侣记录不存在或状态异常,无法删除数据")
}
if couple.Int("data_deleted") == 1 {
return c.OK(M{"success": true, "msg": "共同数据已删除,解绑记录保留"})
}
users, _ := c.DB.Query("SELECT id, user_id, nickname FROM users WHERE id IN (?, ?)", couple.Int("user1_id"), couple.Int("user2_id"))
c.DB.Exec("UPDATE couples SET data_deleted = 1 WHERE id = ?", cid)
executeUnbind(c.DB, cid)
return c.OK(M{"success": true, "msg": "已删除情侣「" + coupleUserNames(users) + "」的所有数据记录"})
}
func adminDeleteContent(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
typ := c.InStr("type")
id, coupleID := c.InInt("id"), c.InInt("couple_id")
if id == 0 || coupleID == 0 {
return c.Err(400, "参数不完整")
}
switch typ {
case "timeline":
c.DB.Exec("DELETE FROM timeline WHERE id = ? AND couple_id = ?", id, coupleID)
case "diary":
c.DB.Exec("DELETE FROM diaries WHERE id = ? AND couple_id = ?", id, coupleID)
case "album":
c.DB.Exec("DELETE FROM photos WHERE album_id = ?", id)
c.DB.Exec("DELETE FROM albums WHERE id = ? AND couple_id = ?", id, coupleID)
case "photo":
if p, _ := c.DB.QueryOne("SELECT path FROM photos WHERE id = ?", id); p != nil {
up.Remove(p.Str("path"))
c.DB.Exec("DELETE FROM photos WHERE id = ?", id)
}
case "song":
c.DB.Exec("DELETE FROM songs WHERE id = ? AND couple_id = ?", id, coupleID)
case "checkin":
c.DB.Exec("DELETE FROM checkin_logs WHERE checkin_id = ?", id)
c.DB.Exec("DELETE FROM checkins WHERE id = ? AND couple_id = ?", id, coupleID)
default:
return c.Err(400, "不支持的内容类型: "+typ)
}
return c.OK(M{"success": true, "msg": "已删除"})
}
func adminContents(c *httpapi.Ctx) error {
typ := c.Q("type")
if !c.QHas("type") {
typ = "diary"
}
coupleID, userID := c.QInt("couple_id"), c.QInt("user_id")
startDate, endDate := c.Q("start_date"), c.Q("end_date")
page, pageSize, offset := pageParams(c, 10, 50)
var where []string
var params []any
if coupleID > 0 {
where = append(where, "t.couple_id = ?")
params = append(params, coupleID)
}
if userID > 0 {
where = append(where, "t.created_by = ?")
params = append(params, userID)
}
if startDate != "" {
where = append(where, "t.created_at >= ?")
params = append(params, startDate+" 00:00:00")
}
if endDate != "" {
where = append(where, "t.created_at <= ?")
params = append(params, endDate+" 23:59:59")
}
whereClause := ""
if len(where) > 0 {
whereClause = "WHERE " + strings.Join(where, " AND ")
}
coupleList, err := c.DB.Query("SELECT c.id, c.couple_space_id, u1.nickname as user1_name, u2.nickname as user2_name FROM couples c LEFT JOIN users u1 ON c.user1_id = u1.id LEFT JOIN users u2 ON c.user2_id = u2.id ORDER BY c.id DESC")
if err != nil {
return err
}
userList, err := c.DB.Query("SELECT id, user_id, nickname, username FROM users ORDER BY id DESC")
if err != nil {
return err
}
const cols = "t.*, u.nickname as author_name, u.user_id as author_display_id, u.avatar as author_avatar"
var sql, countSQL string
switch typ {
case "diary":
sql = "SELECT " + cols + ", t.couple_id FROM diaries t LEFT JOIN users u ON t.created_by = u.id " + whereClause + " ORDER BY t.created_at DESC LIMIT ? OFFSET ?"
countSQL = "SELECT COUNT(*) as cnt FROM diaries t " + whereClause
case "timeline":
sql = "SELECT " + cols + ", t.couple_id FROM timeline t LEFT JOIN users u ON t.created_by = u.id " + whereClause + " ORDER BY t.created_at DESC LIMIT ? OFFSET ?"
countSQL = "SELECT COUNT(*) as cnt FROM timeline t " + whereClause
case "album":
sql = "SELECT " + cols + ", t.couple_id, (SELECT COUNT(*) FROM photos WHERE album_id = t.id) as photo_count FROM albums t LEFT JOIN users u ON t.created_by = u.id " + whereClause + " ORDER BY t.created_at DESC LIMIT ? OFFSET ?"
countSQL = "SELECT COUNT(*) as cnt FROM albums t " + whereClause
case "photo":
sql = "SELECT " + cols + ", a.couple_id FROM photos t JOIN albums a ON t.album_id = a.id LEFT JOIN users u ON t.created_by = u.id " + whereClause + " ORDER BY t.created_at DESC LIMIT ? OFFSET ?"
countSQL = "SELECT COUNT(*) as cnt FROM photos t JOIN albums a ON t.album_id = a.id " + whereClause
case "song":
sql = "SELECT " + cols + ", t.couple_id FROM songs t LEFT JOIN users u ON t.created_by = u.id " + whereClause + " ORDER BY t.created_at DESC LIMIT ? OFFSET ?"
countSQL = "SELECT COUNT(*) as cnt FROM songs t " + whereClause
case "checkin":
sql = "SELECT " + cols + ", t.couple_id FROM checkins t LEFT JOIN users u ON t.created_by = u.id " + whereClause + " ORDER BY t.created_at DESC LIMIT ? OFFSET ?"
countSQL = "SELECT COUNT(*) as cnt FROM checkins t " + whereClause
default:
return c.Err(400, "不支持的内容类型")
}
total := countOf(c, countSQL, params...)
items, err := c.DB.Query(sql, append(append([]any{}, params...), pageSize, offset)...)
if err != nil {
return err
}
for _, it := range items {
if _, ok := it["images"]; ok && it["images"] != nil {
it["images"] = jsonDecodeList(it["images"])
}
}
return c.OK(M{
"items": rows(items), "total": total, "page": page, "page_size": pageSize,
"total_pages": math.Ceil(float64(total) / float64(pageSize)),
"couples": rows(coupleList), "users": rows(userList),
})
}
func adminResetUserPassword(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
userID := c.InInt("user_id")
pw := c.InStr("new_password")
if userID <= 0 {
return c.Err(400, "用户ID无效")
}
if len(pw) < 6 {
return c.Err(400, "新密码至少6位")
}
if _, err := c.DB.Exec("UPDATE users SET password = ? WHERE id = ?", hashPassword(pw), userID); err != nil {
return err
}
return c.OK(M{"success": true, "message": "密码重置成功"})
}
func adminActivityLog(c *httpapi.Ctx) error {
cid := c.QInt("couple_id")
if cid == 0 {
return c.Err(400, "缺少情侣ID")
}
rs, err := c.DB.Query("SELECT al.*, u.nickname, u.gender FROM activity_log al JOIN users u ON al.user_id = u.id WHERE al.couple_id = ? ORDER BY al.created_at DESC LIMIT 100", cid)
if err != nil {
return err
}
return c.OK(rows(rs))
}
func adminGetGlobalBg(c *httpapi.Ctx) error {
row, _ := c.DB.QueryOne("SELECT `value` FROM settings WHERE `key` = 'bg_global'")
if row == nil {
return c.OK(M{"bg_image": nil})
}
return c.OK(M{"bg_image": row["value"]})
}
func adminSetGlobalBg(c *httpapi.Ctx) error {
fh := c.File("bg")
if fh == nil {
return c.Err(400, "没有上传文件")
}
res := up.Handle(fh, "", "global")
if res.Error != "" {
return c.Err(400, res.Error)
}
if _, err := c.DB.Exec("INSERT INTO settings (`key`, `value`) VALUES ('bg_global', ?) ON DUPLICATE KEY UPDATE `value` = ?, updated_at = CURRENT_TIMESTAMP", res.Path, res.Path); err != nil {
return err
}
return c.OK(M{"success": true, "path": res.Path})
}
func adminClearGlobalBg(c *httpapi.Ctx) error {
if _, err := c.DB.Exec("DELETE FROM settings WHERE `key` = 'bg_global'"); err != nil {
return err
}
return c.OK(M{"success": true})
}
func adminCoupleBgList(c *httpapi.Ctx) error {
rs, err := c.DB.Query("SELECT c.id as couple_id, c.couple_space_id, u1.nickname as user1_name, u2.nickname as user2_name, CONCAT(u1.nickname, ' & ', u2.nickname) as couple_name, s1.value as bg_image FROM couples c LEFT JOIN users u1 ON c.user1_id = u1.id LEFT JOIN users u2 ON c.user2_id = u2.id LEFT JOIN settings s1 ON s1.`key` = CONCAT('bg_', c.id) ORDER BY c.id DESC")
if err != nil {
return err
}
return c.OK(rows(rs))
}
func adminClearCoupleBg(c *httpapi.Ctx) error {
cid := c.InInt("couple_id")
if cid == 0 {
return c.Err(400, "缺少情侣ID")
}
if _, err := c.DB.Exec("DELETE FROM settings WHERE `key` = ? OR `key` = ?", fmt.Sprintf("bg_%d", cid), fmt.Sprintf("bgver_%d", cid)); err != nil {
return err
}
return c.OK(M{"success": true})
}