- 新增存储空间管理:会员套餐配置、用户容量/到期管理、后台支付记录查询 - 接入个人主体小程序虚拟支付:下单/发货推送/兜底查单(query_order) - 修复 adminStorageUsers 等接口鉴权、out_trade_no 长度、前端 requestVirtualPayment 参数 - 前端上传前容量预检,超容引导升级无限空间 - 我的页会员卡片展示「无限空间」与已用容量/到期时间 - 后台「存储空间管理」新增支付记录标签页
648 lines
21 KiB
Go
648 lines
21 KiB
Go
package handler
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"golang.org/x/crypto/bcrypt"
|
||
|
||
"past/internal/geo"
|
||
"past/internal/httpapi"
|
||
"past/internal/store"
|
||
"past/internal/upload"
|
||
)
|
||
|
||
var phoneRe = regexp.MustCompile(`^1[3-9]\d{9}$`)
|
||
|
||
// hashPassword 对应 password_hash($pw, PASSWORD_DEFAULT),输出 $2y$ 前缀与 PHP 一致
|
||
func hashPassword(pw string) string {
|
||
h, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||
return "$2y$" + strings.TrimPrefix(string(h), "$2a$")
|
||
}
|
||
|
||
// verifyPassword 对应 password_verify()
|
||
func verifyPassword(pw, hash string) bool {
|
||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil
|
||
}
|
||
|
||
func randomHex(n int) string {
|
||
b := make([]byte, n)
|
||
rand.Read(b)
|
||
return hex.EncodeToString(b)
|
||
}
|
||
|
||
// last4 对应 substr($s, -4)
|
||
func last4(s string) string {
|
||
if len(s) <= 4 {
|
||
return s
|
||
}
|
||
return s[len(s)-4:]
|
||
}
|
||
|
||
// ensureUserDirs 创建用户独立文件夹 users/{uid}/{photos,diaries,covers,timeline}
|
||
func ensureUserDirs(uid int64) {
|
||
base := filepath.Join(up.Dir, "users", fmt.Sprint(uid))
|
||
for _, t := range []string{"photos", "diaries", "covers", "timeline"} {
|
||
os.MkdirAll(filepath.Join(base, t), 0755)
|
||
}
|
||
}
|
||
|
||
func registerUser(r *httpapi.Router) {
|
||
r.Handle("register", register)
|
||
r.Handle("register_by_invite", registerByInvite)
|
||
r.Handle("login", login)
|
||
r.Handle("wechat_login", wechatLogin)
|
||
r.Handle("check_auth", checkAuth)
|
||
r.Handle("logout", logout)
|
||
r.Handle("my_info", myInfo)
|
||
r.Handle("update_profile", updateProfile)
|
||
r.Handle("update_location", updateLocation)
|
||
r.Handle("update_ip_location", updateIPLocation)
|
||
r.Handle("update_partner_profile", updatePartnerProfile)
|
||
r.Handle("upload_avatar", uploadAvatar)
|
||
r.Handle("upload_partner_avatar", uploadPartnerAvatar)
|
||
}
|
||
|
||
func register(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
phone := c.InTrim("phone")
|
||
password := c.InStr("password")
|
||
nickname := c.InTrim("nickname")
|
||
if phone == "" {
|
||
return c.Err(400, "请输入手机号")
|
||
}
|
||
if !phoneRe.MatchString(phone) {
|
||
return c.Err(400, "请输入正确的手机号")
|
||
}
|
||
if ex, _ := c.DB.QueryOne("SELECT id FROM users WHERE phone = ? AND phone != ''", phone); ex != nil {
|
||
return c.Err(400, "该手机号已注册")
|
||
}
|
||
if len(password) < 6 {
|
||
return c.Err(400, "密码至少6位")
|
||
}
|
||
if pass, msg := c.App.Wechat.CheckText(nickname, ""); !pass {
|
||
return c.Err(400, msg)
|
||
}
|
||
displayID, err := generateUserId(c.DB)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
displayName := nickname
|
||
if displayName == "" {
|
||
displayName = "用户" + last4(phone)
|
||
}
|
||
gender := c.InStr("gender")
|
||
if gender != "male" && gender != "female" {
|
||
gender = ""
|
||
}
|
||
region := c.InTrim("region")
|
||
userID, err := c.DB.Insert("INSERT INTO users (user_id, phone, username, password, nickname, gender, region) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||
displayID, phone, phone, hashPassword(password), displayName, gender, region)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// PHP 每次请求都会执行 install.php 的回填 UPDATE users SET last_login_at = created_at WHERE last_login_at IS NULL,
|
||
// 效果等价于注册后下一请求即回填;Go 只在启动时执行迁移,因此在此处直接回填
|
||
c.DB.Exec("UPDATE users SET last_login_at = created_at WHERE id = ? AND last_login_at IS NULL", userID)
|
||
token := httpapi.NewToken()
|
||
c.SaveUserToken(userID, token)
|
||
// PHP: user_id 来自 lastInsertId(),为字符串
|
||
return c.OK(M{"success": true, "user_id": fmt.Sprint(userID), "display_id": displayID, "username": phone, "nickname": displayName, "couple_id": nil, "region": region, "token": token})
|
||
}
|
||
|
||
func registerByInvite(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
phone := c.InTrim("phone")
|
||
password := c.InStr("password")
|
||
nickname := c.InTrim("nickname")
|
||
inviteCode := c.InTrim("invite_code")
|
||
if phone == "" {
|
||
return c.Err(400, "请输入手机号")
|
||
}
|
||
if !phoneRe.MatchString(phone) {
|
||
return c.Err(400, "请输入正确的手机号")
|
||
}
|
||
if ex, _ := c.DB.QueryOne("SELECT id FROM users WHERE phone = ? AND phone != ''", phone); ex != nil {
|
||
return c.Err(400, "该手机号已注册")
|
||
}
|
||
if len(password) < 6 {
|
||
return c.Err(400, "密码至少6位")
|
||
}
|
||
if inviteCode == "" {
|
||
return c.Err(400, "缺少邀请码")
|
||
}
|
||
if pass, msg := c.App.Wechat.CheckText(nickname, ""); !pass {
|
||
return c.Err(400, msg)
|
||
}
|
||
link, err := c.DB.QueryOne("SELECT il.*, u.couple_id as inviter_couple_id FROM invite_links il JOIN users u ON il.user_id = u.id WHERE il.code = ? AND il.used = 0", inviteCode)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if link == nil {
|
||
return c.Err(400, "邀请链接无效或已使用")
|
||
}
|
||
if link.Int("inviter_couple_id") != 0 {
|
||
return c.Err(400, "邀请人已绑定情侣")
|
||
}
|
||
displayID, err := generateUserId(c.DB)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
displayName := nickname
|
||
if displayName == "" {
|
||
displayName = "用户" + last4(phone)
|
||
}
|
||
tx, err := c.DB.Begin()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer tx.Rollback()
|
||
newUserID, err := tx.Insert("INSERT INTO users (user_id, phone, username, password, nickname) VALUES (?, ?, ?, ?, ?)",
|
||
displayID, phone, phone, hashPassword(password), displayName)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
inviterID := link.Int("user_id")
|
||
coupleID, err := tx.Insert("INSERT INTO couples (user1_id, user2_id) VALUES (?, ?)", inviterID, newUserID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
spaceID := generateCoupleSpaceId(coupleID)
|
||
if _, err := tx.Exec("UPDATE couples SET couple_space_id = ? WHERE id = ?", spaceID, coupleID); err != nil {
|
||
return err
|
||
}
|
||
ensureUserDirs(inviterID)
|
||
ensureUserDirs(newUserID)
|
||
if _, err := tx.Exec("UPDATE users SET couple_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (?, ?)", coupleID, inviterID, newUserID); err != nil {
|
||
return err
|
||
}
|
||
if _, err := tx.Exec("UPDATE invite_links SET used = 1 WHERE id = ?", link.Int("id")); err != nil {
|
||
return err
|
||
}
|
||
if err := tx.Commit(); err != nil {
|
||
return err
|
||
}
|
||
createDefaultAlbums(c.DB, coupleID, newUserID)
|
||
c.DB.Exec("UPDATE users SET last_login_at = NOW() WHERE id = ?", newUserID)
|
||
token := httpapi.NewToken()
|
||
c.SaveUserToken(newUserID, token)
|
||
return c.OK(M{"success": true, "user_id": fmt.Sprint(newUserID), "display_id": displayID, "username": phone, "couple_id": fmt.Sprint(coupleID), "nickname": displayName, "token": token})
|
||
}
|
||
|
||
func login(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
loginField := c.InTrim("phone")
|
||
if loginField == "" {
|
||
loginField = c.InTrim("username")
|
||
}
|
||
password := c.InStr("password")
|
||
if loginField == "" {
|
||
return c.Err(400, "请输入手机号")
|
||
}
|
||
user, err := c.DB.QueryOne("SELECT * FROM users WHERE username = ? OR phone = ?", loginField, loginField)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if user == nil {
|
||
return c.Err(400, "用户不存在,请先注册")
|
||
}
|
||
if !verifyPassword(password, user.Str("password")) {
|
||
return c.Err(401, "用户名或密码错误")
|
||
}
|
||
token := httpapi.NewToken()
|
||
uid := user.Int("id")
|
||
c.SaveUserToken(uid, token)
|
||
c.DB.Exec("UPDATE users SET last_login_at = NOW() WHERE id = ?", uid)
|
||
if loc := resolveLoginLocation(c, ""); loc != "" {
|
||
c.DB.Exec("UPDATE users SET last_login_location = ? WHERE id = ?", loc, uid)
|
||
}
|
||
if user.Int("couple_id") != 0 {
|
||
logActivity(c.DB, user.Int("couple_id"), uid, "login", "登录了小程序", "")
|
||
}
|
||
return c.OK(M{"success": true, "user_id": user["id"], "display_id": user["user_id"], "username": user["username"], "nickname": user["nickname"],
|
||
"couple_id": user["couple_id"], "gender": user.Str("gender"), "avatar": user.Str("avatar"), "token": token})
|
||
}
|
||
|
||
func wechatLogin(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
code := c.InTrim("code")
|
||
if code == "" {
|
||
return c.Err(400, "缺少登录凭证")
|
||
}
|
||
openid, sessionKey, wxErr, available := c.App.Wechat.Jscode2Session(code)
|
||
if !available {
|
||
return c.Err(500, "微信登录服务暂不可用")
|
||
}
|
||
if wxErr != "" {
|
||
return c.Err(400, "微信登录失败: "+wxErr)
|
||
}
|
||
if openid == "" {
|
||
return c.Err(400, "获取用户标识失败")
|
||
}
|
||
user, err := c.DB.QueryOne("SELECT id, user_id, nickname, avatar, gender, birthday, couple_id, region FROM users WHERE openid = ?", openid)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if user != nil {
|
||
uid := user.Int("id")
|
||
token := httpapi.NewToken()
|
||
c.SaveUserToken(uid, token)
|
||
if sessionKey != "" {
|
||
c.DB.Exec("UPDATE users SET last_login_at = NOW(), session_key = ? WHERE id = ?", sessionKey, uid)
|
||
} else {
|
||
c.DB.Exec("UPDATE users SET last_login_at = NOW() WHERE id = ?", uid)
|
||
}
|
||
region := c.InTrim("region")
|
||
if region != "" && user.Str("region") == "" {
|
||
c.DB.Exec("UPDATE users SET region = ? WHERE id = ?", region, uid)
|
||
user["region"] = region
|
||
}
|
||
if loc := resolveLoginLocation(c, region); loc != "" {
|
||
c.DB.Exec("UPDATE users SET last_login_location = ? WHERE id = ?", loc, uid)
|
||
}
|
||
if cid := user.Int("couple_id"); cid != 0 {
|
||
actID := logActivity(c.DB, cid, uid, "login", "登录了小程序", "")
|
||
if partnerID := partnerUserId(c.DB, uid); partnerID != 0 {
|
||
nick := user.Str("nickname")
|
||
if nick == "" {
|
||
nick = "你的TA"
|
||
}
|
||
c.App.Wechat.SendSubscribeMessage(partnerID, "notify", map[string]string{
|
||
"thing1": nick + " 登录了小程序",
|
||
"thing2": "TA已上线,快来互动吧 💕",
|
||
"date3": today(),
|
||
"thing4": "点击查看详情",
|
||
}, "pages/index/index", actID, uid)
|
||
}
|
||
}
|
||
return c.OK(M{
|
||
"success": true, "is_new": false,
|
||
"user_id": user["id"], "display_id": user["user_id"],
|
||
"couple_id": user["couple_id"], "nickname": user["nickname"],
|
||
"avatar": user.Str("avatar"), "gender": user.Str("gender"),
|
||
"birthday": user.Str("birthday"), "region": user.Str("region"),
|
||
"token": token,
|
||
})
|
||
}
|
||
displayID, err := generateUserId(c.DB)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
region := c.InTrim("region")
|
||
defaultNick := "用户" + last4(displayID)
|
||
newID, err := c.DB.Insert("INSERT INTO users (user_id, openid, username, password, nickname, region, session_key) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||
displayID, openid, "wx_"+openid, hashPassword(randomHex(8)), defaultNick, region, sessionKey)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
c.DB.Exec("UPDATE users SET last_login_at = NOW() WHERE id = ?", newID)
|
||
if loc := resolveLoginLocation(c, region); loc != "" {
|
||
c.DB.Exec("UPDATE users SET last_login_location = ? WHERE id = ?", loc, newID)
|
||
}
|
||
token := httpapi.NewToken()
|
||
c.SaveUserToken(newID, token)
|
||
return c.OK(M{
|
||
"success": true, "is_new": true,
|
||
"user_id": fmt.Sprint(newID), "display_id": displayID,
|
||
"couple_id": nil, "nickname": defaultNick,
|
||
"avatar": "", "gender": "", "birthday": "", "region": region,
|
||
"token": token,
|
||
})
|
||
}
|
||
|
||
func checkAuth(c *httpapi.Ctx) error {
|
||
if err := c.DB.AdvanceUnbindings(); err != nil {
|
||
return err
|
||
}
|
||
c.CoupleID()
|
||
uid := c.UID()
|
||
if uid == 0 {
|
||
return c.OK(M{"logged_in": false})
|
||
}
|
||
// 触发登录记录(5 分钟去重)——逻辑与 requireUserLogin 相同
|
||
if user, _ := c.DB.QueryOne("SELECT couple_id, last_login_at FROM users WHERE id = ?", uid); user != nil {
|
||
if needLoginRecord(user.Str("last_login_at")) {
|
||
c.DB.Exec("UPDATE users SET last_login_at = NOW() WHERE id = ?", uid)
|
||
if user.Int("couple_id") != 0 {
|
||
c.DB.Exec("INSERT INTO activity_log (couple_id, user_id, action_type, description, detail, created_at) VALUES (?, ?, 'login', '登录了小程序', '', NOW())", user.Int("couple_id"), uid)
|
||
}
|
||
}
|
||
}
|
||
user, err := c.DB.QueryOne("SELECT id as user_id, user_id as display_id, phone, username, nickname, avatar, gender, couple_id FROM users WHERE id = ?", uid)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return c.OK(M{"logged_in": true, "user": user})
|
||
}
|
||
|
||
func logout(c *httpapi.Ctx) error {
|
||
if uid := c.UID(); uid != 0 {
|
||
// PHP 只销毁 session(token 仍可通过 user_tokens 兜底);Go 版本同时失效 token,语义更合理
|
||
c.DB.Exec("DELETE FROM user_tokens WHERE token = ?", c.Token())
|
||
}
|
||
return c.OK(M{"success": true})
|
||
}
|
||
|
||
func myInfo(c *httpapi.Ctx) error {
|
||
uid := c.UID()
|
||
user, err := c.DB.QueryOne("SELECT u.id, u.username, u.nickname, u.avatar, u.gender, u.age, u.birthday, u.region, u.location, u.couple_id, u.created_at FROM users u WHERE u.id = ?", uid)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if user == nil {
|
||
return c.OK(false)
|
||
}
|
||
if user.Int("couple_id") != 0 {
|
||
partner, err := c.DB.QueryOne("SELECT u.id, u.username, u.nickname, u.avatar, u.gender, u.age, u.birthday, u.region, u.location, u.lat, u.lng FROM users u INNER JOIN couples c ON (u.id = c.user1_id OR u.id = c.user2_id) WHERE c.id = ? AND u.id != ?", user.Int("couple_id"), uid)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if partner == nil {
|
||
user["partner"] = false
|
||
} else {
|
||
user["partner"] = partner
|
||
}
|
||
}
|
||
return c.OK(user)
|
||
}
|
||
|
||
func updateProfile(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
uid := c.UID()
|
||
nickname := c.InTrim("nickname")
|
||
avatar := c.InStr("avatar")
|
||
gender := c.InStr("gender")
|
||
if nickname != "" {
|
||
if pass, msg := c.App.Wechat.CheckText(nickname, userOpenId(c.DB, uid)); !pass {
|
||
return c.Err(400, msg)
|
||
}
|
||
}
|
||
hasAge := c.InHas("age")
|
||
age := c.InInt("age")
|
||
birthday := c.InTrim("birthday")
|
||
region := c.InTrim("region")
|
||
var fields []string
|
||
var params []any
|
||
var changed []string
|
||
if store.Truthy(nickname) {
|
||
fields = append(fields, "nickname = ?")
|
||
params = append(params, nickname)
|
||
changed = append(changed, "昵称")
|
||
}
|
||
if store.Truthy(avatar) {
|
||
fields = append(fields, "avatar = ?")
|
||
params = append(params, avatar)
|
||
changed = append(changed, "头像")
|
||
}
|
||
if store.Truthy(gender) {
|
||
fields = append(fields, "gender = ?")
|
||
params = append(params, gender)
|
||
changed = append(changed, "性别")
|
||
}
|
||
if hasAge {
|
||
fields = append(fields, "age = ?")
|
||
params = append(params, age)
|
||
changed = append(changed, "年龄")
|
||
}
|
||
if birthday != "" {
|
||
fields = append(fields, "birthday = ?")
|
||
params = append(params, birthday)
|
||
changed = append(changed, "生日")
|
||
}
|
||
if region != "" {
|
||
fields = append(fields, "region = ?")
|
||
params = append(params, region)
|
||
changed = append(changed, "地区")
|
||
}
|
||
if len(fields) > 0 {
|
||
fields = append(fields, "updated_at = CURRENT_TIMESTAMP")
|
||
params = append(params, uid)
|
||
if _, err := c.DB.Exec("UPDATE users SET "+strings.Join(fields, ", ")+" WHERE id = ?", params...); err != nil {
|
||
return err
|
||
}
|
||
if cid := c.CoupleID(); cid != 0 && len(changed) > 0 {
|
||
logActivity(c.DB, cid, uid, "profile_edit", "修改了个人资料:"+strings.Join(changed, "、"), "")
|
||
}
|
||
}
|
||
return c.OK(M{"success": true})
|
||
}
|
||
|
||
func updateLocation(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
uid := c.UID()
|
||
location := c.InTrim("location")
|
||
if location == "" {
|
||
return c.Err(400, "位置不能为空")
|
||
}
|
||
saved := location
|
||
var lat, lng float64
|
||
if strings.Contains(location, ",") {
|
||
parts := strings.Split(location, ",")
|
||
if len(parts) == 2 && isNumeric(strings.TrimSpace(parts[0])) && isNumeric(strings.TrimSpace(parts[1])) {
|
||
lat = store.ToFloat(strings.TrimSpace(parts[0]))
|
||
lng = store.ToFloat(strings.TrimSpace(parts[1]))
|
||
if addr := c.App.Geo.ReverseGeocode(lat, lng); addr != "" {
|
||
saved = addr
|
||
} else {
|
||
old, _ := c.DB.QueryOne("SELECT location FROM users WHERE id = ?", uid)
|
||
if old != nil && old.Str("location") != "" && !strings.Contains(old.Str("location"), ",") {
|
||
saved = old.Str("location")
|
||
} else {
|
||
saved = ""
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if _, err := c.DB.Exec("UPDATE users SET location = ?, lat = ?, lng = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", saved, lat, lng, uid); err != nil {
|
||
return err
|
||
}
|
||
var distance any
|
||
if cid := c.CoupleID(); cid != 0 {
|
||
partner, _ := c.DB.QueryOne("SELECT lat, lng FROM users WHERE couple_id = ? AND id != ? AND lat > 0 AND lng > 0", cid, uid)
|
||
if partner != nil && lat != 0 && lng != 0 {
|
||
distance = geo.Haversine(lat, lng, store.ToFloat(partner["lat"]), store.ToFloat(partner["lng"]))
|
||
}
|
||
}
|
||
return c.OK(M{"success": true, "location": saved, "lat": lat, "lng": lng, "distance": distance})
|
||
}
|
||
|
||
func updateIPLocation(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
uid := c.UID()
|
||
var newLat, newLng float64
|
||
if c.InHas("lat") {
|
||
newLat = c.InFloat("lat")
|
||
}
|
||
if c.InHas("lng") {
|
||
newLng = c.InFloat("lng")
|
||
}
|
||
u, _ := c.DB.QueryOne("SELECT location, lat, lng, last_ip FROM users WHERE id = ?", uid)
|
||
current := ""
|
||
var oldLat, oldLng float64
|
||
if u != nil {
|
||
current = u.Str("location")
|
||
oldLat = store.ToFloat(u["lat"])
|
||
oldLng = store.ToFloat(u["lng"])
|
||
}
|
||
if newLat != 0 && newLng != 0 && oldLat != 0 && oldLng != 0 {
|
||
dist := geo.Haversine(oldLat, oldLng, newLat, newLng)
|
||
if dist <= 5.0 {
|
||
return c.OK(M{"success": true, "location": current, "cached": true, "distance": math.Round(dist*100) / 100})
|
||
}
|
||
}
|
||
clientIP := c.ClientIP()
|
||
address := ""
|
||
if newLat != 0 && newLng != 0 {
|
||
address = c.App.Geo.ReverseGeocode(newLat, newLng)
|
||
}
|
||
if address == "" {
|
||
address = c.App.Geo.IPGeolocate(clientIP)
|
||
}
|
||
if address != "" {
|
||
if _, err := c.DB.Exec("UPDATE users SET location = ?, lat = ?, lng = ?, last_ip = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", address, newLat, newLng, clientIP, uid); err != nil {
|
||
return err
|
||
}
|
||
return c.OK(M{"success": true, "location": address, "cached": false})
|
||
}
|
||
return c.OK(M{"success": true, "location": current, "cached": true})
|
||
}
|
||
|
||
// verifyPartner 校验 partner_id 是当前用户的伴侣;失败时已写出响应
|
||
func verifyPartner(c *httpapi.Ctx, uid, partnerID int64) error {
|
||
user, _ := c.DB.QueryOne("SELECT couple_id FROM users WHERE id = ?", uid)
|
||
if user == nil || user.Int("couple_id") == 0 {
|
||
return c.Err(400, "你还没有绑定情侣")
|
||
}
|
||
partner, _ := c.DB.QueryOne("SELECT id FROM users WHERE id = ? AND couple_id = ?", partnerID, user.Int("couple_id"))
|
||
if partner == nil {
|
||
return c.Err(400, "无法修改此用户信息")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func updatePartnerProfile(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
uid := c.UID()
|
||
partnerID := c.InInt("partner_id")
|
||
nickname := c.InTrim("nickname")
|
||
avatar := c.InStr("avatar")
|
||
gender := c.InTrim("gender")
|
||
hasAge := c.InHas("age")
|
||
age := c.InInt("age")
|
||
birthday := c.InTrim("birthday")
|
||
if err := verifyPartner(c, uid, partnerID); err != nil {
|
||
return err
|
||
}
|
||
var fields []string
|
||
var params []any
|
||
var changed []string
|
||
if store.Truthy(nickname) {
|
||
fields = append(fields, "nickname = ?")
|
||
params = append(params, nickname)
|
||
changed = append(changed, "昵称")
|
||
}
|
||
if store.Truthy(avatar) {
|
||
fields = append(fields, "avatar = ?")
|
||
params = append(params, avatar)
|
||
changed = append(changed, "头像")
|
||
}
|
||
if store.Truthy(gender) {
|
||
fields = append(fields, "gender = ?")
|
||
params = append(params, gender)
|
||
changed = append(changed, "性别")
|
||
}
|
||
if hasAge {
|
||
fields = append(fields, "age = ?")
|
||
params = append(params, age)
|
||
changed = append(changed, "年龄")
|
||
}
|
||
if birthday != "" {
|
||
fields = append(fields, "birthday = ?")
|
||
params = append(params, birthday)
|
||
changed = append(changed, "生日")
|
||
}
|
||
if len(fields) > 0 {
|
||
fields = append(fields, "updated_at = CURRENT_TIMESTAMP")
|
||
params = append(params, partnerID)
|
||
if _, err := c.DB.Exec("UPDATE users SET "+strings.Join(fields, ", ")+" WHERE id = ?", params...); err != nil {
|
||
return err
|
||
}
|
||
if cid := c.CoupleID(); cid != 0 && len(changed) > 0 {
|
||
logActivity(c.DB, cid, uid, "partner_edit", "修改了TA的资料:"+strings.Join(changed, "、"), "")
|
||
}
|
||
}
|
||
return c.OK(M{"success": true})
|
||
}
|
||
|
||
// doAvatarUpload 头像上传公共逻辑;文件字段兼容 avatar / file
|
||
func doAvatarUpload(c *httpapi.Ctx, targetUID int64) error {
|
||
u, _ := c.DB.QueryOne("SELECT user_id FROM users WHERE id = ?", targetUID)
|
||
uidPath := fmt.Sprintf("uid%d", targetUID)
|
||
if u != nil {
|
||
uidPath = u.Str("user_id")
|
||
}
|
||
fh := c.File("avatar")
|
||
if fh == nil {
|
||
fh = c.File("file")
|
||
}
|
||
if fh == nil {
|
||
return c.ErrCode(400, "未接收到文件", upload.ErrNoFileReceived)
|
||
}
|
||
res := up.Handle(fh, "avatars", upload.AvatarPath(uidPath))
|
||
if res.Path != "" {
|
||
if _, err := c.DB.Exec("UPDATE users SET avatar = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", res.Path, targetUID); err != nil {
|
||
return err
|
||
}
|
||
return c.OK(M{"success": true, "path": res.Path})
|
||
}
|
||
if strings.Contains(res.Error, "违规") {
|
||
return c.ErrCode(400, res.Error, upload.ErrHandleFailed)
|
||
}
|
||
msg := res.Error
|
||
if msg == "" {
|
||
msg = "未知错误"
|
||
}
|
||
return c.ErrCode(400, "上传失败: "+msg, upload.ErrHandleFailed)
|
||
}
|
||
|
||
func uploadAvatar(c *httpapi.Ctx) error {
|
||
if c.Method != "POST" {
|
||
return c.ErrCode(405, "方法不允许", upload.ErrMethodNotAllowed)
|
||
}
|
||
return doAvatarUpload(c, c.UID())
|
||
}
|
||
|
||
func uploadPartnerAvatar(c *httpapi.Ctx) error {
|
||
if c.Method != "POST" {
|
||
return c.ErrCode(405, "方法不允许", upload.ErrMethodNotAllowed)
|
||
}
|
||
uid := c.UID()
|
||
partnerID := store.ToInt(c.Form("partner_id"))
|
||
if err := verifyPartner(c, uid, partnerID); err != nil {
|
||
return err
|
||
}
|
||
return doAvatarUpload(c, partnerID)
|
||
}
|