Files
past-go/past-go/internal/handler/misc.go
T

298 lines
8.5 KiB
Go

package handler
import (
"math"
"strings"
"time"
"past/internal/httpapi"
"past/internal/store"
"past/internal/upload"
)
// ========== 通用上传 / 背景图 ==========
func registerBg(r *httpapi.Router) {
r.Handle("upload", uploadGeneric)
r.Handle("get_bg_config", getBgConfig)
r.Handle("set_bg_image", setBgImage)
r.Handle("delete_bg_image", deleteBgImage)
}
// uploadResult 统一处理 handleUpload 结果的响应(成功/违规/失败)
func uploadResult(c *httpapi.Ctx, res upload.Result, onSuccess func(path string) error) error {
if res.Path != "" {
return onSuccess(res.Path)
}
if strings.Contains(res.Error, "违规") {
return c.ErrCode(400, res.Error, upload.ErrHandleFailed)
}
return c.ErrCode(400, "上传失败: "+orDefault(res.Error, "未知错误"), upload.ErrHandleFailed)
}
func uploadGeneric(c *httpapi.Ctx) error {
if c.Method != "POST" {
return c.ErrCode(405, "方法不允许", upload.ErrMethodNotAllowed)
}
subDir := "photos"
if c.FormHas("sub_dir") {
subDir = c.Form("sub_dir")
}
if strings.ContainsAny(subDir, "/\\.") || subDir == "" {
return c.Err(400, "无效的上传目录")
}
uid := c.UID()
if uid == 0 {
return c.ErrCode(401, "请先登录", upload.ErrNotLoggedIn)
}
prefix := upload.UserFilePath(uid, subDir)
var res *upload.Result
for _, f := range []string{"file", "avatar", "photo", "image"} {
if fh := c.File(f); fh != nil {
r := up.Handle(fh, "", prefix)
res = &r
break
}
}
if res == nil {
// 兼容某些 Android 端:原始请求体
if raw := c.Body(); len(raw) > 100 {
r := up.HandleBytes(raw, "upload_"+time.Now().Format("20060102150405")+".jpg", "", prefix)
res = &r
}
}
if res == nil {
return c.ErrCode(400, "未接收到文件", upload.ErrNoFileReceived)
}
return uploadResult(c, *res, func(path string) error {
return c.OK(M{"success": true, "path": path})
})
}
func settingValue(c *httpapi.Ctx, key string) (string, bool) {
row, _ := c.DB.QueryOne("SELECT `value` FROM settings WHERE `key` = ?", key)
if row == nil {
return "", false
}
return row.Str("value"), true
}
func setSetting(c *httpapi.Ctx, key string, value any) error {
_, err := c.DB.Exec("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?", key, value, value)
return err
}
func bgKeys(cid int64) (string, string) {
if cid != 0 {
return "bg_" + store.ToStr(cid), "bgver_" + store.ToStr(cid)
}
return "bg_global", "bgver_global"
}
func getBgConfig(c *httpapi.Ctx) error {
bgKey, verKey := bgKeys(c.CoupleID())
img, _ := settingValue(c, bgKey)
ver, _ := settingValue(c, verKey)
return c.OK(M{"image": img, "version": store.ToInt(ver)})
}
func setBgImage(c *httpapi.Ctx) error {
if c.Method != "POST" {
return c.ErrCode(405, "方法不允许", upload.ErrMethodNotAllowed)
}
cid := c.CoupleID()
if cid == 0 {
return c.ErrCode(400, "请先绑定情侣", upload.ErrNotBound)
}
couplePath := upload.CouplePath(coupleSpaceID(c, cid), "covers")
bgKey, verKey := bgKeys(cid)
nextVersion := func() int64 {
if old, ok := settingValue(c, verKey); ok {
return store.ToInt(old) + 1
}
return 1
}
saveBg := func(path string, version int64) error {
if err := setSetting(c, bgKey, path); err != nil {
return err
}
if err := setSetting(c, verKey, version); err != nil {
return err
}
return c.OK(M{"success": true, "path": path, "version": version})
}
fh := c.File("bg")
if fh == nil {
fh = c.File("file")
}
if fh != nil {
version := nextVersion()
return uploadResult(c, up.Handle(fh, "", couplePath), func(path string) error { return saveBg(path, version) })
}
if raw := c.Body(); len(raw) > 100 {
res := up.SaveRaw(raw, "bg_", "jpg", "", couplePath)
if res.Path != "" {
return saveBg(res.Path, nextVersion())
}
if strings.Contains(res.Error, "违规") {
return c.ErrCode(400, res.Error, upload.ErrHandleFailed)
}
return c.ErrCode(400, "原始数据保存失败", upload.ErrRawBodySave)
}
return c.ErrCode(400, "未接收到文件", upload.ErrNoFileReceived)
}
func deleteBgImage(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
cid, err := requireCoupleID(c)
if err != nil {
return err
}
bgKey, verKey := bgKeys(cid)
if v, ok := settingValue(c, bgKey); ok && v != "" {
up.Remove(v)
}
if _, err := c.DB.Exec("DELETE FROM settings WHERE `key` = ? OR `key` = ?", bgKey, verKey); err != nil {
return err
}
return c.OK(M{"success": true})
}
// ========== 站点公开 ==========
func registerSite(r *httpapi.Router) {
r.Handle("site_info", siteInfo)
r.Handle("site_stats", siteStats)
}
func siteInfo(c *httpapi.Ctx) error {
rs, err := c.DB.Query("SELECT `key`, `value` FROM settings")
if err != nil {
return err
}
if len(rs) == 0 {
return c.OK([]any{}) // PHP 空数组 → []
}
out := M{}
for _, r := range rs {
out[r.Str("key")] = r["value"]
}
return c.OK(out)
}
func siteStats(c *httpapi.Ctx) error {
raw := func(t string) any { return countRaw(c, "cnt", "SELECT COUNT(*) as cnt FROM "+t) }
cnt := func(t string) int64 { return countOf(c, "SELECT COUNT(*) as cnt FROM "+t) }
couples, users := raw("couples"), raw("users")
memories := cnt("timeline") + cnt("photos") + cnt("diaries")
var days any = 0
if e, _ := c.DB.QueryOne("SELECT MIN(created_at) as d FROM users"); e != nil && e.Str("d") != "" {
if t, err := parseDate(e.Str("d")); err == nil {
d := math.Floor(time.Since(t).Seconds() / 86400)
if d < 1 {
d = 1
}
days = d
}
}
return c.OK(M{"couples": couples, "users": users, "memories": memories, "days": days})
}
// ========== 操作记录 / 订阅 ==========
func registerActivity(r *httpapi.Router) {
r.Handle("activity_log_list", activityLogList)
}
func registerSubscribe(r *httpapi.Router) {
r.Handle("subscribe_template", subscribeTemplate)
r.Handle("unsubscribe_template", unsubscribeTemplate)
r.Handle("my_subscribe_templates", mySubscribeTemplates)
}
// pageParams 对应 max(1, intval(page)) 与 min(maxSize, max(1, intval(page_size)))
func pageParams(c *httpapi.Ctx, defSize, maxSize int64) (page, pageSize, offset int64) {
page = 1
if c.QHas("page") {
page = c.QInt("page")
}
if page < 1 {
page = 1
}
pageSize = defSize
if c.QHas("page_size") {
pageSize = c.QInt("page_size")
}
if pageSize < 1 {
pageSize = 1
}
if pageSize > maxSize {
pageSize = maxSize
}
return page, pageSize, (page - 1) * pageSize
}
func activityLogList(c *httpapi.Ctx) error {
ids := coupleUserIds(c, 0)
if len(ids) == 0 {
return c.OK([]any{})
}
page, pageSize, offset := pageParams(c, 6, 100)
totalRaw := countRaw(c, "cnt", "SELECT COUNT(*) as cnt FROM activity_log WHERE user_id IN (?, ?)", ids[0], ids[1])
total := store.ToInt(totalRaw)
items, err := c.DB.Query("SELECT al.*, u.nickname, u.avatar, u.gender FROM activity_log al JOIN users u ON al.user_id = u.id WHERE al.user_id IN (?, ?) ORDER BY al.created_at DESC LIMIT ? OFFSET ?", ids[0], ids[1], pageSize, offset)
if err != nil {
return err
}
return c.OK(M{"items": rows(items), "total": totalRaw, "page": page, "page_size": pageSize, "total_pages": math.Ceil(float64(total) / float64(pageSize))})
}
func subscribeTemplate(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
templateType, templateID := c.InTrim("template_type"), c.InTrim("template_id")
if !store.Truthy(templateType) || !store.Truthy(templateID) {
return c.Err(400, "缺少参数")
}
if _, ok := c.App.Wechat.Templates()[templateType]; !ok {
return c.Err(400, "不支持的模板类型")
}
if _, err := c.DB.Exec("INSERT INTO subscribe_templates (user_id, template_id, template_type) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE template_id = VALUES(template_id)", uid, templateID, templateType); err != nil {
return err
}
return c.OK(M{"success": true})
}
func unsubscribeTemplate(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
templateType := c.InTrim("template_type")
if !store.Truthy(templateType) {
return c.Err(400, "缺少参数")
}
if _, err := c.DB.Exec("DELETE FROM subscribe_templates WHERE user_id = ? AND template_type = ?", c.UID(), templateType); err != nil {
return err
}
return c.OK(M{"success": true})
}
func mySubscribeTemplates(c *httpapi.Ctx) error {
rs, err := c.DB.Query("SELECT template_type, template_id FROM subscribe_templates WHERE user_id = ?", c.UID())
if err != nil {
return err
}
if len(rs) == 0 {
return c.OK(M{"templates": []any{}}) // PHP 空数组 → []
}
out := M{}
for _, r := range rs {
out[r.Str("template_type")] = r["template_id"]
}
return c.OK(M{"templates": out})
}