Files
past-go/past-go/internal/handler/couple.go
T
2026-10-07 09:28:43 +08:00

591 lines
21 KiB
Go

package handler
import (
"fmt"
"math"
"regexp"
"strings"
"time"
"past/internal/geo"
"past/internal/httpapi"
"past/internal/store"
)
var targetIDRe = regexp.MustCompile(`^520\d{3,}$`)
func registerCouple(r *httpapi.Router) {
r.Handle("generate_invite_link", generateInviteLink)
r.Handle("check_invite_data_status", checkInviteDataStatus)
r.Handle("check_invite_code", checkInviteCode)
r.Handle("invite_by_phone", inviteByPhone)
r.Handle("invite_partner", invitePartner)
r.Handle("my_invitations", myInvitations)
r.Handle("respond_invitation", respondInvitation)
r.Handle("accept_share_invite", acceptShareInvite)
r.Handle("couple_info", coupleInfo)
r.Handle("update_couple_info", updateCoupleInfo)
r.Handle("unbind_request", unbindRequest)
r.Handle("unbind_respond", unbindRespond)
r.Handle("unbind_cancel", unbindCancel)
r.Handle("unbind_status", unbindStatus)
r.Handle("unbind_couple", unbindCouple)
}
// parseDate 对应 new DateTime($str)(仅支持常见格式)
func parseDate(s string) (time.Time, error) {
s = strings.TrimSpace(s)
for _, layout := range []string{"2006-01-02 15:04:05", "2006-01-02", "2006/01/02", "2006-1-2", "2006.01.02"} {
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("DateTime::__construct(): Failed to parse time string (%s)", s)
}
// diffDays 对应 DateTime::diff()->days(整天数,绝对值)
func diffDays(a, b time.Time) int64 {
d := a.Sub(b)
if d < 0 {
d = -d
}
return int64(math.Floor(d.Hours() / 24))
}
// birthdayCountdown 按日历日计算下一次生日。生日当天仍是 0 天,不会因为当天零点已过而跳到下一年。
func birthdayCountdown(nowT, bday time.Time, currentAge int64) (time.Time, int64, int64) {
today := time.Date(nowT.Year(), nowT.Month(), nowT.Day(), 0, 0, 0, 0, nowT.Location())
next := time.Date(today.Year(), bday.Month(), bday.Day(), 0, 0, 0, 0, nowT.Location())
if next.Before(today) {
next = next.AddDate(1, 0, 0)
}
// 用 UTC 中的日历日做差,避免当前时分秒导致“明天生日却显示 0 天”。
todayDate := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC)
nextDate := time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, time.UTC)
daysLeft := int64(nextDate.Sub(todayDate) / (24 * time.Hour))
return next, daysLeft, currentAge + 1
}
func generateInviteLink(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
user, _ := c.DB.QueryOne("SELECT couple_id FROM users WHERE id = ?", uid)
if user != nil && user.Int("couple_id") != 0 {
return c.Err(400, "您已绑定情侣")
}
if ex, _ := c.DB.QueryOne("SELECT code FROM invite_links WHERE user_id = ? AND used = 0 ORDER BY created_at DESC LIMIT 1", uid); ex != nil {
return c.OK(M{"success": true, "code": ex.Str("code"), "url": "?invite=" + ex.Str("code")})
}
code := randomHex(8)
if _, err := c.DB.Exec("INSERT INTO invite_links (user_id, code) VALUES (?, ?)", uid, code); err != nil {
return err
}
return c.OK(M{"success": true, "code": code, "url": "?invite=" + code})
}
func checkInviteDataStatus(c *httpapi.Ctx) error {
uid := c.UID()
cp, _ := c.DB.QueryOne("SELECT c.id, c.data_deleted, c.status FROM couples c WHERE (c.user1_id = ? OR c.user2_id = ?) AND c.status = 'unbound' AND c.data_deleted = 0 LIMIT 1", uid, uid)
if cp != nil {
return c.OK(M{"has_pending_data": true, "msg": "请等待系统删除之前的情侣数据后再进行绑定操作"})
}
return c.OK(M{"has_pending_data": false})
}
func checkInviteCode(c *httpapi.Ctx) error {
code := strings.TrimSpace(c.Q("code"))
if code == "" {
return c.Err(400, "缺少邀请码")
}
link, err := c.DB.QueryOne("SELECT il.*, u.nickname, u.avatar FROM invite_links il JOIN users u ON il.user_id = u.id WHERE il.code = ? AND il.used = 0", code)
if err != nil {
return err
}
if link == nil {
return c.Err(404, "邀请链接无效或已使用")
}
// PHP 中 $link['inviter_couple_id'] ?? $link['couple_id'] 两个键都不存在,判断恒为假,此处保持一致
return c.OK(M{"success": true, "inviter": M{"nickname": link["nickname"], "avatar": link["avatar"]}})
}
func inviteByPhone(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
target := c.InTrim("username")
if !c.InHas("username") {
target = c.InTrim("phone")
}
message := c.InTrim("message")
if target == "" {
return c.Err(400, "请输入对方用户名")
}
if pass, msg := c.App.Wechat.CheckText(message, userOpenId(c.DB, uid)); !pass {
return c.Err(400, msg)
}
me, _ := c.DB.QueryOne("SELECT username, couple_id FROM users WHERE id = ?", uid)
if me != nil && me.Int("couple_id") != 0 {
return c.Err(400, "您已绑定情侣")
}
if me != nil && me.Str("username") == target {
return c.Err(400, "不能邀请自己")
}
t, _ := c.DB.QueryOne("SELECT id, username, couple_id FROM users WHERE username = ?", target)
if t == nil {
return c.Err(400, "该用户不存在")
}
if t.Int("couple_id") != 0 {
return c.Err(400, "对方已绑定情侣")
}
if ex, _ := c.DB.QueryOne("SELECT id FROM invitations WHERE from_user_id = ? AND to_user_id = ? AND status = 'pending'", uid, t.Int("id")); ex != nil {
return c.Err(400, "已发送过邀请,等待对方回应")
}
if _, err := c.DB.Exec("INSERT INTO invitations (from_user_id, to_user_id, to_phone, message, type) VALUES (?, ?, ?, ?, 'username')", uid, t.Int("id"), target, message); err != nil {
return err
}
return c.OK(M{"success": true, "msg": "邀请已发送"})
}
func invitePartner(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
targetID := c.InTrim("target_user_id")
message := c.InTrim("message")
if targetID == "" {
return c.Err(400, "请输入对方ID")
}
if !targetIDRe.MatchString(targetID) {
return c.Err(400, "ID格式不正确,请输入6位数字ID")
}
me, _ := c.DB.QueryOne("SELECT user_id, couple_id, gender FROM users WHERE id = ?", uid)
if me != nil && me.Int("couple_id") != 0 {
return c.Err(400, "您已绑定情侣")
}
if me != nil && me.Str("user_id") == targetID {
return c.Err(400, "不能绑定自己")
}
if ex, _ := c.DB.QueryOne("SELECT c.id FROM couples c WHERE (c.user1_id = ? OR c.user2_id = ?) AND c.status = 'unbound' AND c.data_deleted = 0 LIMIT 1", uid, uid); ex != nil {
return c.Err(400, "请等待系统删除之前的情侣数据后再进行绑定操作")
}
t, _ := c.DB.QueryOne("SELECT id, user_id, couple_id, nickname, gender FROM users WHERE user_id = ?", targetID)
if t == nil {
return c.Err(400, "该ID不存在,请确认对方ID是否正确")
}
if t.Int("couple_id") != 0 {
return c.Err(400, "对方已绑定情侣")
}
if ex, _ := c.DB.QueryOne("SELECT id FROM invitations WHERE from_user_id = ? AND to_user_id = ? AND status = 'pending'", uid, t.Int("id")); ex != nil {
return c.Err(400, "已发送过邀请,等待对方回应")
}
id, err := c.DB.Insert("INSERT INTO invitations (from_user_id, to_user_id, message, type) VALUES (?, ?, ?, 'id')", uid, t.Int("id"), message)
if err != nil {
return err
}
return c.OK(M{"success": true, "invitation_id": fmt.Sprint(id), "msg": "邀请已发送,等待对方同意"})
}
func myInvitations(c *httpapi.Ctx) error {
uid := c.UID()
user, _ := c.DB.QueryOne("SELECT couple_id FROM users WHERE id = ?", uid)
var coupleID int64
if user != nil {
coupleID = user.Int("couple_id")
}
received, err := c.DB.Query("SELECT i.*, u.username as from_username, u.nickname as from_nickname, u.avatar as from_avatar FROM invitations i JOIN users u ON i.from_user_id = u.id WHERE i.to_user_id = ? ORDER BY i.created_at DESC", uid)
if err != nil {
return err
}
sent, err := c.DB.Query("SELECT i.*, u.username as to_username, u.nickname as to_nickname, u.avatar as to_avatar FROM invitations i JOIN users u ON i.to_user_id = u.id WHERE i.from_user_id = ? ORDER BY i.created_at DESC", uid)
if err != nil {
return err
}
links, err := c.DB.Query("SELECT * FROM invite_links WHERE user_id = ? ORDER BY created_at DESC", uid)
if err != nil {
return err
}
return c.OK(M{"received": rows(received), "sent": rows(sent), "links": rows(links), "couple_id": coupleID})
}
// bindCouple 创建情侣关系(事务):couples 记录、空间 ID、双方 couple_id;返回 coupleId
func bindCouple(c *httpapi.Ctx, user1, user2 int64, extra func(tx *store.Tx) error) (int64, error) {
tx, err := c.DB.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
coupleID, err := tx.Insert("INSERT INTO couples (user1_id, user2_id) VALUES (?, ?)", user1, user2)
if err != nil {
return 0, err
}
if _, err := tx.Exec("UPDATE couples SET couple_space_id = ? WHERE id = ?", generateCoupleSpaceId(coupleID), coupleID); err != nil {
return 0, err
}
if _, err := tx.Exec("UPDATE users SET couple_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (?, ?)", coupleID, user1, user2); err != nil {
return 0, err
}
if extra != nil {
if err := extra(tx); err != nil {
return 0, err
}
}
if err := tx.Commit(); err != nil {
return 0, err
}
ensureUserDirs(user1)
ensureUserDirs(user2)
return coupleID, nil
}
func respondInvitation(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
invID := c.InInt("invitation_id")
accept := c.InBool("accept")
inv, err := c.DB.QueryOne("SELECT * FROM invitations WHERE id = ? AND to_user_id = ? AND status = 'pending'", invID, uid)
if err != nil {
return err
}
if inv == nil {
return c.Err(404, "邀请不存在或已处理")
}
fromID := inv.Int("from_user_id")
rs, _ := c.DB.Query("SELECT couple_id FROM users WHERE id IN (?, ?)", uid, fromID)
for _, r := range rs {
if r.Int("couple_id") != 0 {
return c.Err(400, "已绑定情侣,无法接受")
}
}
if accept {
coupleID, err := bindCouple(c, fromID, uid, func(tx *store.Tx) error {
_, err := tx.Exec("UPDATE invitations SET status = 'accepted', responded_at = CURRENT_TIMESTAMP WHERE id = ?", invID)
return err
})
if err != nil {
return err
}
createDefaultAlbums(c.DB, coupleID, uid)
return c.OK(M{"success": true, "couple_id": fmt.Sprint(coupleID)})
}
c.DB.Exec("UPDATE invitations SET status = 'rejected', responded_at = CURRENT_TIMESTAMP WHERE id = ?", invID)
return c.OK(M{"success": true, "msg": "已拒绝邀请"})
}
func acceptShareInvite(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
inviterDisplayID := c.InTrim("inviter_id")
if inviterDisplayID == "" {
return c.Err(400, "缺少邀请人ID")
}
me, _ := c.DB.QueryOne("SELECT user_id, couple_id, gender, nickname FROM users WHERE id = ?", uid)
if me != nil && me.Int("couple_id") != 0 {
return c.Err(400, "您已绑定情侣")
}
inviter, _ := c.DB.QueryOne("SELECT id, user_id, couple_id, gender, nickname FROM users WHERE user_id = ?", inviterDisplayID)
if inviter == nil {
return c.Err(400, "邀请人不存在")
}
if inviter.Int("couple_id") != 0 {
return c.Err(400, "邀请人已绑定情侣")
}
if me != nil && me.Str("user_id") == inviter.Str("user_id") {
return c.Err(400, "不能绑定自己")
}
if me != nil && me.Str("gender") != "" && inviter.Str("gender") != "" && me.Str("gender") == inviter.Str("gender") {
return c.Err(400, "双方性别一致,仅支持异性绑定")
}
inviterID := inviter.Int("id")
existing, _ := c.DB.QueryOne("SELECT id FROM invitations WHERE ((from_user_id = ? AND to_user_id = ?) OR (from_user_id = ? AND to_user_id = ?)) AND status = 'pending'", uid, inviterID, inviterID, uid)
if existing != nil {
c.DB.Exec("UPDATE invitations SET status = 'accepted', responded_at = CURRENT_TIMESTAMP WHERE id = ?", existing.Int("id"))
}
coupleID, err := bindCouple(c, uid, inviterID, func(tx *store.Tx) error {
if existing == nil {
_, err := tx.Exec("INSERT INTO invitations (from_user_id, to_user_id, status, responded_at, type) VALUES (?, ?, 'accepted', CURRENT_TIMESTAMP, 'share')", inviterID, uid)
return err
}
return nil
})
if err != nil {
return err
}
createDefaultAlbums(c.DB, coupleID, uid)
return c.OK(M{"success": true, "couple_id": fmt.Sprint(coupleID), "partner_nickname": inviter["nickname"]})
}
func coupleInfo(c *httpapi.Ctx) error {
cid := c.CoupleID()
uid := c.UID()
if cid == 0 {
user, _ := c.DB.QueryOne("SELECT id, user_id, username, nickname, avatar, gender, age, birthday, created_at FROM users WHERE id = ?", uid)
regDate := today()
if user != nil {
ca := user.Str("created_at")
if len(ca) >= 10 {
regDate = ca[:10]
} else {
regDate = ca
}
}
var days int64
if regDate != "" {
t, err := parseDate(regDate)
if err != nil {
return err
}
days = diffDays(time.Now(), t)
}
var p1 any
if user != nil {
p1 = user
}
return c.OK(M{"not_bound": true, "partner1": p1, "partner2": nil, "love_start_date": regDate, "days_together": days})
}
info, err := c.DB.QueryOne("SELECT * FROM couples WHERE id = ?", cid)
if err != nil {
return err
}
if info == nil {
info = store.Row{}
}
partners, err := c.DB.Query("SELECT id, user_id, username, nickname, avatar, gender, age, birthday, region, location, lat, lng FROM users WHERE couple_id = ? ORDER BY id ASC", cid)
if err != nil {
return err
}
info["partner1"] = nil
info["partner2"] = nil
if len(partners) > 0 {
info["partner1"] = partners[0]
}
if len(partners) > 1 {
info["partner2"] = partners[1]
}
info["distance"] = nil
if len(partners) >= 2 {
lat1, lng1 := store.ToFloat(partners[0]["lat"]), store.ToFloat(partners[0]["lng"])
lat2, lng2 := store.ToFloat(partners[1]["lat"]), store.ToFloat(partners[1]["lng"])
if lat1 != 0 && lng1 != 0 && lat2 != 0 && lng2 != 0 {
info["distance"] = geo.Haversine(lat1, lng1, lat2, lng2)
}
}
var partner store.Row
for _, p := range partners {
if p.Int("id") != uid {
partner = p
}
}
if partner != nil && partner.Str("birthday") != "" {
bday, err := parseDate(partner.Str("birthday"))
if err != nil {
return err
}
next, daysLeft, ageNext := birthdayCountdown(time.Now(), bday, partner.Int("age"))
info["partner_birthday_countdown"] = M{
"birthday": partner["birthday"],
"next_date": next.Format("2006-01-02"),
"days_left": daysLeft,
"age_next": ageNext,
}
}
if store.Truthy(info["love_start_date"]) {
start, err := parseDate(info.Str("love_start_date"))
if err != nil {
return err
}
info["days_together"] = diffDays(start, time.Now())
} else {
info["days_together"] = 0
}
return c.OK(info)
}
func updateCoupleInfo(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
cid := c.CoupleID()
if cid == 0 {
return c.Err(400, "请先绑定情侣")
}
uid := c.UID()
if pass, msg := c.App.Wechat.CheckText(c.InTrim("love_story"), userOpenId(c.DB, uid)); !pass {
return c.Err(400, msg)
}
if _, err := c.DB.Exec("UPDATE couples SET love_start_date = ?, love_story = ? WHERE id = ?", c.InStr("love_start_date"), c.InStr("love_story"), cid); err != nil {
return err
}
if store.Truthy(c.InStr("love_start_date")) {
logActivity(c.DB, cid, uid, "couple_edit", "设置了恋爱纪念日", "")
}
return c.OK(M{"success": true})
}
func userCoupleID(c *httpapi.Ctx, uid int64) int64 {
u, _ := c.DB.QueryOne("SELECT couple_id FROM users WHERE id = ?", uid)
if u == nil {
return 0
}
return u.Int("couple_id")
}
func unbindRequest(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
cid := userCoupleID(c, uid)
if cid == 0 {
return c.Err(400, "未绑定情侣")
}
existing, _ := c.DB.QueryOne("SELECT id, status, requester_id, created_at FROM unbind_requests WHERE couple_id = ? AND status IN ('pending','agreed') ORDER BY id DESC LIMIT 1", cid)
if existing != nil {
if existing.Str("status") == "pending" {
return c.Err(400, "已有待处理的解绑请求,请等待对方回应")
}
if existing.Str("status") == "agreed" {
softUnbind(c.DB, cid)
return c.OK(M{"success": true, "msg": "对方已同意解绑,情侣关系已解除,所有数据已保留"})
}
return nil
}
if _, err := c.DB.Exec("INSERT INTO unbind_requests (couple_id, requester_id, status, created_at) VALUES (?, ?, 'pending', NOW())", cid, uid); err != nil {
return err
}
logActivity(c.DB, cid, uid, "unbind_request", "发起了解除情侣绑定请求", "")
return c.OK(M{"success": true, "msg": "解绑请求已发送,等待对方确认(72小时内有效)"})
}
func unbindRespond(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
// $input = json_decode(...) ?: $_POST
agree := c.InBool("agree")
if len(c.Input()) == 0 {
agree = store.Truthy(c.Form("agree"))
}
cid := userCoupleID(c, uid)
if cid == 0 {
return c.Err(400, "未绑定情侣")
}
req, _ := c.DB.QueryOne("SELECT id, requester_id, created_at FROM unbind_requests WHERE couple_id = ? AND status = 'pending' AND requester_id != ? ORDER BY id DESC LIMIT 1", cid, uid)
if req == nil {
return c.Err(400, "没有待处理的解绑请求")
}
if created, err := parseDate(req.Str("created_at")); err == nil && time.Since(created) > 72*time.Hour {
c.DB.Exec("UPDATE unbind_requests SET status = 'expired', responded_at = NOW() WHERE id = ?", req.Int("id"))
return c.Err(400, "解绑请求已超过72小时,已自动取消")
}
if agree {
c.DB.Exec("UPDATE unbind_requests SET status = 'agreed', responded_at = NOW() WHERE id = ?", req.Int("id"))
logActivity(c.DB, cid, uid, "unbind_agreed", "同意了解除情侣绑定请求", "")
softUnbind(c.DB, cid)
return c.OK(M{"success": true, "msg": "已同意解绑,情侣关系已解除,所有数据已保留"})
}
c.DB.Exec("UPDATE unbind_requests SET status = 'rejected', responded_at = NOW() WHERE id = ?", req.Int("id"))
logActivity(c.DB, cid, uid, "unbind_rejected", "拒绝了解除情侣绑定请求", "")
return c.OK(M{"success": true, "msg": "已拒绝解绑请求"})
}
func unbindCancel(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
cid := userCoupleID(c, uid)
if cid == 0 {
return c.Err(400, "未绑定情侣")
}
req, _ := c.DB.QueryOne("SELECT id, status FROM unbind_requests WHERE couple_id = ? AND requester_id = ? AND status IN ('pending','agreed') ORDER BY id DESC LIMIT 1", cid, uid)
if req == nil {
return c.Err(400, "没有可撤销的解绑请求")
}
c.DB.Exec("UPDATE unbind_requests SET status = 'cancelled', responded_at = NOW() WHERE id = ?", req.Int("id"))
logActivity(c.DB, cid, uid, "unbind_cancelled", "撤销了解除情侣绑定请求", "")
return c.OK(M{"success": true, "msg": "已撤销解绑请求"})
}
func unbindStatus(c *httpapi.Ctx) error {
uid := c.UID()
// $cid 来源不同类型不同:来自 users 行为字符串;来自 unbind_requests 反查时经过 intval 为整数
var cidVal any
u, _ := c.DB.QueryOne("SELECT couple_id FROM users WHERE id = ?", uid)
cid := int64(0)
if u != nil && u.Int("couple_id") != 0 {
cid, cidVal = u.Int("couple_id"), u["couple_id"]
}
if cid == 0 {
found, _ := c.DB.QueryOne("SELECT ur.couple_id FROM unbind_requests ur WHERE ur.requester_id = ? OR ur.id IN (SELECT ur2.id FROM unbind_requests ur2 JOIN couples c ON ur2.couple_id = c.id WHERE (c.user1_id = ? OR c.user2_id = ?)) ORDER BY ur.id DESC LIMIT 1", uid, uid, uid)
if found != nil {
cid, cidVal = found.Int("couple_id"), found.Int("couple_id")
}
}
if cid == 0 {
return c.OK(M{"status": "none", "msg": "未绑定情侣"})
}
req, err := c.DB.QueryOne("SELECT id, requester_id, status, created_at, responded_at FROM unbind_requests WHERE couple_id = ? ORDER BY id DESC LIMIT 1", cid)
if err != nil {
return err
}
dataDeleted := int64(0)
if cd, _ := c.DB.QueryOne("SELECT data_deleted FROM couples WHERE id = ?", cid); cd != nil {
dataDeleted = cd.Int("data_deleted")
}
if req == nil {
return c.OK(M{"status": "none", "msg": "无解绑请求", "couple_id": cidVal, "data_deleted": dataDeleted})
}
if req.Str("status") == "pending" {
created, _ := parseDate(req.Str("created_at"))
elapsed := int64(time.Since(created).Seconds())
remaining := int64(72*3600) - elapsed
if remaining < 0 {
remaining = 0
}
if elapsed > 72*3600 {
c.DB.Exec("UPDATE unbind_requests SET status = 'expired', responded_at = NOW() WHERE id = ?", req.Int("id"))
req["status"] = "expired"
} else {
req["remaining_seconds"] = remaining
}
}
req["is_requester"] = req.Int("requester_id") == uid
requester, _ := c.DB.QueryOne("SELECT nickname, username FROM users WHERE id = ?", req.Int("requester_id"))
name := "未知"
if requester != nil {
name = requester.Str("nickname")
if name == "" {
name = requester.Str("username")
}
}
req["requester_name"] = name
req["couple_id"] = cid
req["data_deleted"] = dataDeleted
return c.OK(req)
}
func unbindCouple(c *httpapi.Ctx) error {
if err := c.RequirePOST(); err != nil {
return err
}
uid := c.UID()
cid := userCoupleID(c, uid)
if cid == 0 {
return c.Err(400, "未绑定情侣")
}
if ag, _ := c.DB.QueryOne("SELECT id FROM unbind_requests WHERE couple_id = ? AND status = 'agreed' ORDER BY id DESC LIMIT 1", cid); ag == nil {
return c.Err(400, "没有已确认的解绑请求,请先发起解绑")
}
softUnbind(c.DB, cid)
return c.OK(M{"success": true, "msg": "已解除绑定"})
}