更新最新代码

This commit is contained in:
CodeBuddy
2026-10-26 17:32:47 +08:00
commit 0f5dc01674
315 changed files with 42640 additions and 0 deletions
+819
View File
@@ -0,0 +1,819 @@
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"),
})
}
// 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
}
return c.OK(paged(c, rs))
}
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})
}