init: initial commit
This commit is contained in:
@@ -0,0 +1,816 @@
|
||||
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_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": "待回应", "agreed": "已同意(已解绑)", "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,
|
||||
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
|
||||
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, 'agreed' as status, NULL as created_at, NULL as responded_at,
|
||||
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 req, _ := c.DB.QueryOne("SELECT id, requester_id, responded_at FROM unbind_requests WHERE couple_id = ? AND status = 'agreed' ORDER BY id DESC LIMIT 1", cid); req == nil {
|
||||
return c.Err(400, "双方尚未同意解绑,无法执行操作。请先让双方在微信小程序中完成解绑确认")
|
||||
}
|
||||
users, _ := c.DB.Query("SELECT id, user_id, nickname FROM users WHERE couple_id = ?", cid)
|
||||
softUnbind(c.DB, cid)
|
||||
return c.OK(M{"success": true, "msg": "已执行解绑操作(双方已同意),情侣「" + coupleUserNames(users) + "」的关系已解除,所有数据已保留"})
|
||||
}
|
||||
|
||||
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, "该情侣记录不存在或数据已被删除,无法恢复")
|
||||
}
|
||||
u1, u2 := couple.Int("user1_id"), couple.Int("user2_id")
|
||||
users, _ := c.DB.Query("SELECT id, user_id, nickname FROM users WHERE id IN (?, ?)", u1, u2)
|
||||
if len(users) < 2 {
|
||||
return c.Err(400, "用户数据异常,无法恢复情侣关系")
|
||||
}
|
||||
c.DB.Exec("UPDATE couples SET status = 'active' WHERE id = ?", cid)
|
||||
c.DB.Exec("UPDATE users SET couple_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (?, ?)", cid, u1, u2)
|
||||
c.DB.Exec("DELETE FROM unbind_requests WHERE couple_id = ?", cid)
|
||||
return c.OK(M{"success": true, "msg": "已恢复情侣关系「" + coupleUserNames(users) + "」,所有数据已恢复"})
|
||||
}
|
||||
|
||||
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, "该情侣记录不存在或状态异常,无法删除数据")
|
||||
}
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user