610 lines
19 KiB
Go
610 lines
19 KiB
Go
package controllers
|
||
|
||
// ==================== 游客(免登录模块)接口 ====================
|
||
//
|
||
// POST /api/guest/mini-login 微信手机号授权换取游客令牌(公开,不在受保护前缀内)
|
||
// POST /api/guest/merge 正式登录后把游客数据并入正式账号(需正式令牌)
|
||
// GET /api/guest/data 通用游客数据列表(需游客令牌,新模块直接复用)
|
||
// POST /api/guest/data 新增一条
|
||
// PUT /api/guest/data/:id 覆盖一条
|
||
// DELETE /api/guest/data/:id 软删一条
|
||
//
|
||
// 便捷工具(记事本/日程/密码库)不走通用 data 接口:为保持前端零改动,
|
||
// 由各自控制器按 identity.IsGuest 分流进 gueststore(见 app_notebook.go 等)。
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"strings"
|
||
"time"
|
||
|
||
"server/models"
|
||
"server/pkg/identity"
|
||
"server/pkg/jwtutil"
|
||
authsvc "server/services/auth"
|
||
"server/services/gueststore"
|
||
"server/services/wechatmini"
|
||
|
||
"github.com/beego/beego/v2/client/orm"
|
||
"github.com/beego/beego/v2/core/logs"
|
||
beego "github.com/beego/beego/v2/server/web"
|
||
)
|
||
|
||
// guestTokenTTL 游客令牌有效期。签发即授权一次,30 天内免打扰;
|
||
// 过期后 nav 游客态会重新弹授权(微信手机号验证按次计费,不宜缩短)。
|
||
const guestTokenTTL = 30 * 24 * time.Hour
|
||
|
||
type GuestController struct {
|
||
beego.Controller
|
||
}
|
||
|
||
func (c *GuestController) gReply(status int, msg string, data interface{}) {
|
||
c.Ctx.Output.SetStatus(status)
|
||
c.Data["json"] = map[string]interface{}{"code": status, "msg": msg, "data": data}
|
||
_ = c.ServeJSON()
|
||
}
|
||
|
||
// loadGuestMiniConfig 与小程序登录同源:appid 指定则按多应用配置选择,否则取第一条启用配置。
|
||
func loadGuestMiniConfig(appID string) (*wechatmini.Config, error) {
|
||
if appID = strings.TrimSpace(appID); appID != "" {
|
||
return wechatmini.LoadEnabledAppByAppID(appID)
|
||
}
|
||
return wechatmini.LoadEnabledConfig()
|
||
}
|
||
|
||
// maskPhone 手机号脱敏:138****8000。游客令牌 Username 与响应里都不出现完整号码。
|
||
func maskPhone(phone string) string {
|
||
phone = strings.TrimSpace(phone)
|
||
if len(phone) < 7 {
|
||
return phone
|
||
}
|
||
return phone[:3] + "****" + phone[len(phone)-4:]
|
||
}
|
||
|
||
// MiniLogin POST /api/guest/mini-login
|
||
// body: { "code": "wx.login的code(可选,用于记录openid)", "phone_code": "getPhoneNumber动态code", "appid": "可选" }
|
||
func (c *GuestController) MiniLogin() {
|
||
var req struct {
|
||
Code string `json:"code"`
|
||
PhoneCode string `json:"phone_code"`
|
||
AppID string `json:"appid"`
|
||
}
|
||
body := c.Ctx.Input.RequestBody
|
||
if len(body) == 0 {
|
||
body, _ = io.ReadAll(c.Ctx.Request.Body)
|
||
}
|
||
if err := json.Unmarshal(body, &req); err != nil {
|
||
c.gReply(400, "参数错误", nil)
|
||
return
|
||
}
|
||
|
||
cfg, err := loadGuestMiniConfig(req.AppID)
|
||
if err != nil {
|
||
c.gReply(400, "小程序未配置,无法获取手机号", nil)
|
||
return
|
||
}
|
||
|
||
// openid 仅作溯源与身份识别(已绑定过正式身份时按 openid 认人),拿不到不阻断游客签发
|
||
var openID, unionID string
|
||
session := guestMiniSession(cfg, req.Code)
|
||
if session != nil {
|
||
openID, unionID = session.OpenID, session.UnionID
|
||
}
|
||
|
||
if strings.TrimSpace(req.PhoneCode) == "" {
|
||
c.gReply(400, "缺少手机号授权凭证", nil)
|
||
return
|
||
}
|
||
phone, perr := wechatmini.GetPhoneNumber(cfg, req.PhoneCode)
|
||
if perr != nil {
|
||
msg := perr.Error()
|
||
if hint, ok := wechatmini.ExplainError(perr); ok {
|
||
msg = hint
|
||
}
|
||
logs.Warn("[guest] 获取手机号失败: %v", perr)
|
||
c.gReply(400, "手机号授权失败:"+msg, nil)
|
||
return
|
||
}
|
||
mobile := strings.TrimSpace(phone.PurePhoneNumber)
|
||
if mobile == "" {
|
||
mobile = strings.TrimSpace(phone.PhoneNumber)
|
||
}
|
||
if mobile == "" {
|
||
c.gReply(400, "未获取到手机号", nil)
|
||
return
|
||
}
|
||
|
||
// 手机号已注册温新云账号(或该微信已绑定正式身份):直接签发正式令牌按登录处理。
|
||
// (与 /auth/wechat/miniapp/login 同一套识别与签发逻辑)
|
||
if c.tryUserLogin(mobile, session) {
|
||
return
|
||
}
|
||
|
||
guest, err := gueststore.Upsert(mobile, openID, unionID)
|
||
if err != nil {
|
||
logs.Error("[guest] upsert 失败: %v", err)
|
||
c.gReply(500, "游客身份创建失败", nil)
|
||
return
|
||
}
|
||
|
||
token, err := jwtutil.SignToken(jwtutil.TokenOptions{
|
||
UserID: int(guest.ID),
|
||
Username: maskPhone(mobile),
|
||
TenantID: 0,
|
||
UserType: string(identity.EndGuest),
|
||
Amr: "guest_phone",
|
||
TTL: guestTokenTTL,
|
||
})
|
||
if err != nil {
|
||
logs.Error("[guest] 签发游客令牌失败: %v", err)
|
||
c.gReply(500, "签发游客令牌失败", nil)
|
||
return
|
||
}
|
||
|
||
c.gReply(200, "success", map[string]interface{}{
|
||
"token": token,
|
||
"guest_id": guest.ID,
|
||
"phone_mask": maskPhone(mobile),
|
||
"expires_in": int(guestTokenTTL.Seconds()),
|
||
})
|
||
}
|
||
|
||
// identityByPhone 按微信已验证手机号定位认证中心身份。
|
||
// 租户账号手机号可能落在三处,逐层兜底(先查认证中心,查不到再回退业务表):
|
||
// 1. yz_auth_identity.mobile/account/email(FindIdentityByAccount)
|
||
// 2. yz_auth_tenant_user.phone → identity_id
|
||
// 3. yz_system_tenant_user.phone → uid(全量迁移后 uid == identity_id)
|
||
func identityByPhone(mobile string) *models.AuthIdentity {
|
||
if ai, err := authsvc.FindIdentityByAccount(mobile); err == nil && ai != nil {
|
||
return ai
|
||
}
|
||
var identityID uint64
|
||
var tu models.AuthTenantUser
|
||
if err := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||
Filter("phone", mobile).Filter("delete_time__isnull", true).
|
||
OrderBy("-is_default", "id").One(&tu); err == nil {
|
||
identityID = tu.IdentityID
|
||
} else {
|
||
var su models.SystemTenantUser
|
||
if err2 := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||
Filter("phone", mobile).Filter("delete_time__isnull", true).
|
||
OrderBy("-is_default", "id").One(&su); err2 == nil {
|
||
identityID = su.Uid
|
||
}
|
||
}
|
||
if identityID == 0 {
|
||
return nil
|
||
}
|
||
ai := &models.AuthIdentity{}
|
||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||
Filter("id", identityID).Filter("delete_time__isnull", true).One(ai); err != nil {
|
||
return nil
|
||
}
|
||
return ai
|
||
}
|
||
|
||
// phoneOfIdentity 取身份手机号:identity.mobile 为空时回退租户绑定/业务用户表。
|
||
func phoneOfIdentity(identityID uint64) string {
|
||
var tus []models.AuthTenantUser
|
||
if _, err := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
||
Filter("identity_id", identityID).Filter("delete_time__isnull", true).
|
||
OrderBy("-is_default", "id").All(&tus); err == nil {
|
||
for _, tu := range tus {
|
||
if tu.Phone != nil {
|
||
if p := strings.TrimSpace(*tu.Phone); p != "" {
|
||
return p
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var sus []models.SystemTenantUser
|
||
if _, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||
Filter("uid", identityID).Filter("delete_time__isnull", true).
|
||
OrderBy("-is_default", "id").All(&sus); err == nil {
|
||
for _, su := range sus {
|
||
if su.Phone != nil {
|
||
if p := strings.TrimSpace(*su.Phone); p != "" {
|
||
return p
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// guestMiniSession 拿 wx.login code 换 openid/unionid;失败不阻断(返回 nil)。
|
||
func guestMiniSession(cfg *wechatmini.Config, code string) *wechatmini.Session {
|
||
if strings.TrimSpace(code) == "" {
|
||
return nil
|
||
}
|
||
session, err := wechatmini.Code2Session(cfg, code)
|
||
if err != nil {
|
||
logs.Warn("[guest] code 换取 openid 失败: %v", err)
|
||
return nil
|
||
}
|
||
return session
|
||
}
|
||
|
||
// identityByWechat 按 openid/unionid 查已绑定的正式身份(登录页静默 bind 的落点)。
|
||
// 与小程序一键登录 resolveIdentity 同源:手机号查不到时的第二级认人方式。
|
||
func identityByWechat(openID, unionID string) *models.AuthIdentity {
|
||
var bind models.AuthIdentityThird
|
||
qs := models.Orm.QueryTable(new(models.AuthIdentityThird)).Filter("provider", "wechat_mini")
|
||
found := false
|
||
if openID != "" {
|
||
if err := qs.Filter("open_id", openID).One(&bind); err == nil {
|
||
found = true
|
||
}
|
||
}
|
||
if !found && unionID != "" {
|
||
if err := models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||
Filter("provider", "wechat_mini").Filter("union_id", unionID).One(&bind); err == nil {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
return nil
|
||
}
|
||
ai := &models.AuthIdentity{}
|
||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||
Filter("id", bind.IdentityID).Filter("delete_time__isnull", true).One(ai); err != nil {
|
||
return nil
|
||
}
|
||
return ai
|
||
}
|
||
|
||
// bindWechatMini 把当前小程序 openid 绑到该身份。手机号已经微信验证,
|
||
// 属于强证据,允许覆盖旧绑定(与 controllers/auth syncBinding 的 verified 分支一致)。
|
||
func bindWechatMini(session *wechatmini.Session, identityID uint64) {
|
||
var bind models.AuthIdentityThird
|
||
qs := models.Orm.QueryTable(new(models.AuthIdentityThird)).Filter("provider", "wechat_mini")
|
||
if err := qs.Filter("open_id", session.OpenID).One(&bind); err == nil {
|
||
if bind.IdentityID == identityID {
|
||
return
|
||
}
|
||
_, _ = models.Orm.QueryTable(new(models.AuthIdentityThird)).Filter("id", bind.ID).Delete()
|
||
}
|
||
item := &models.AuthIdentityThird{IdentityID: identityID, Provider: "wechat_mini", OpenID: session.OpenID}
|
||
if session.UnionID != "" {
|
||
uid := session.UnionID
|
||
item.UnionID = &uid
|
||
}
|
||
_, _ = models.Orm.Insert(item)
|
||
}
|
||
|
||
// tryUserLogin 一键授权时的正式身份识别:手机号命中启用账号且该账号有企业,
|
||
// 则签发正式令牌并返回 type=user(对齐小程序一键登录:单企业直接进入,
|
||
// 多企业返回待选令牌由企业选择接口完成切换)。已处理返回 true。
|
||
// 未命中 / 账号无企业 / 签发失败 → 返回 false,回落到游客签发流程。
|
||
func (c *GuestController) tryUserLogin(mobile string, session *wechatmini.Session) bool {
|
||
var authIdentity *models.AuthIdentity
|
||
if session != nil {
|
||
authIdentity = identityByWechat(session.OpenID, session.UnionID)
|
||
}
|
||
if authIdentity == nil {
|
||
authIdentity = identityByPhone(mobile)
|
||
}
|
||
if authIdentity == nil {
|
||
return false
|
||
}
|
||
// 手机号已通过微信验证:把 openid 补绑/改绑到该身份(下次无手机号也能认人)
|
||
if session != nil && session.OpenID != "" {
|
||
bindWechatMini(session, authIdentity.ID)
|
||
}
|
||
if authIdentity.Status != models.AuthIdentityStatusEnabled {
|
||
c.gReply(403, "账号已被禁用或锁定,无法使用", nil)
|
||
return true
|
||
}
|
||
tenants, lerr := authsvc.ListTenantOptions(authIdentity.ID)
|
||
if lerr != nil || len(tenants) == 0 {
|
||
return false // 账号未加入任何企业:按游客模式继续使用
|
||
}
|
||
|
||
needChoose := len(tenants) > 1
|
||
tid := tenants[0].Tid
|
||
accessTTL := 0
|
||
if needChoose {
|
||
tid = authsvc.PendingTenantID
|
||
accessTTL = 600 // 待选状态令牌只够用户完成企业选择
|
||
}
|
||
|
||
username := "微信用户"
|
||
if authIdentity.Nickname != nil && strings.TrimSpace(*authIdentity.Nickname) != "" {
|
||
username = strings.TrimSpace(*authIdentity.Nickname)
|
||
} else if authIdentity.Mobile != nil && *authIdentity.Mobile != "" {
|
||
username = *authIdentity.Mobile
|
||
}
|
||
|
||
sess, serr := authsvc.CreateSession(authsvc.SessionInfo{
|
||
IdentityID: authIdentity.ID,
|
||
Tid: tid,
|
||
ClientID: "yz-miniapp",
|
||
IP: c.Ctx.Input.IP(),
|
||
UserAgent: c.Ctx.Request.UserAgent(),
|
||
LoginType: "wechat_mini",
|
||
Amr: "wechat_mini",
|
||
})
|
||
if serr != nil {
|
||
logs.Error("[guest] 正式登录创建会话失败: %v", serr)
|
||
return false
|
||
}
|
||
tokens, terr := authsvc.IssueTokens(authsvc.TokenIssue{
|
||
IdentityID: authIdentity.ID,
|
||
Tid: tid,
|
||
ClientID: "yz-miniapp",
|
||
Sid: sess.Sid,
|
||
Username: username,
|
||
UserType: "app",
|
||
Amr: "wechat_mini",
|
||
AccessTTL: accessTTL,
|
||
})
|
||
if terr != nil {
|
||
logs.Error("[guest] 正式登录签发令牌失败: %v", terr)
|
||
return false
|
||
}
|
||
|
||
identityIDCopy := authIdentity.ID
|
||
tidCopy := tid
|
||
amr := "wechat_mini"
|
||
_, _ = models.Orm.Insert(&models.AuthLoginLog{
|
||
Tid: &tidCopy,
|
||
IdentityID: &identityIDCopy,
|
||
UserName: username,
|
||
ClientID: "yz-miniapp",
|
||
LoginType: "wechat_mini",
|
||
Amr: &amr,
|
||
Status: 1,
|
||
Message: "小程序手机号一键登录成功(免登录入口识别为正式账号)",
|
||
IP: c.Ctx.Input.IP(),
|
||
UserAgent: c.Ctx.Request.UserAgent(),
|
||
})
|
||
|
||
c.gReply(200, "success", map[string]interface{}{
|
||
"type": "user",
|
||
"tokens": tokens,
|
||
"tenants": tenants,
|
||
"need_choose_tenant": needChoose,
|
||
})
|
||
return true
|
||
}
|
||
|
||
// findGuestForIdentity 找回当前身份对应的未合并游客记录:
|
||
// 优先按手机号,查不到再按该身份已绑定的微信 openid/unionid
|
||
// (微信验证号可能与账号注册号不一致,openid 是另一条可靠对应关系)。
|
||
func findGuestForIdentity(identityID uint64, mobile string) (models.GuestUser, bool) {
|
||
var guest models.GuestUser
|
||
if mobile != "" {
|
||
err := models.Orm.QueryTable(new(models.GuestUser)).
|
||
Filter("phone", mobile).Filter("merged_uid", 0).One(&guest)
|
||
if err == nil {
|
||
return guest, true
|
||
}
|
||
}
|
||
var binds []models.AuthIdentityThird
|
||
if _, err := models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||
Filter("identity_id", identityID).Filter("provider", "wechat_mini").All(&binds); err == nil {
|
||
for _, b := range binds {
|
||
if b.OpenID != "" {
|
||
if err := models.Orm.QueryTable(new(models.GuestUser)).
|
||
Filter("openid", b.OpenID).Filter("merged_uid", 0).One(&guest); err == nil {
|
||
return guest, true
|
||
}
|
||
}
|
||
if b.UnionID != nil && *b.UnionID != "" {
|
||
if err := models.Orm.QueryTable(new(models.GuestUser)).
|
||
Filter("unionid", *b.UnionID).Filter("merged_uid", 0).One(&guest); err == nil {
|
||
return guest, true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return guest, false
|
||
}
|
||
|
||
// Merge POST /api/guest/merge(携带正式令牌)
|
||
// 按当前身份手机号找回游客记录,把 yz_guest_data 各模块数据并入正式账号。
|
||
func (c *GuestController) Merge() {
|
||
claims, err := identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp, identity.EndBackend)
|
||
if err != nil || claims.UserID <= 0 {
|
||
c.gReply(401, "未登录或无权限", nil)
|
||
return
|
||
}
|
||
|
||
var ai models.AuthIdentity
|
||
if err = models.Orm.QueryTable(new(models.AuthIdentity)).Filter("id", claims.UserID).One(&ai); err != nil {
|
||
if err == orm.ErrNoRows {
|
||
c.gReply(200, "无可合并数据", map[string]interface{}{"merged": false})
|
||
return
|
||
}
|
||
c.gReply(500, "查询身份失败", nil)
|
||
return
|
||
}
|
||
// 回查手机号用于定位游客记录:identity.mobile 为空时回退租户绑定/业务用户表
|
||
var mobile string
|
||
if ai.Mobile != nil {
|
||
mobile = strings.TrimSpace(*ai.Mobile)
|
||
}
|
||
if mobile == "" {
|
||
mobile = phoneOfIdentity(ai.ID)
|
||
}
|
||
|
||
guest, found := findGuestForIdentity(ai.ID, mobile)
|
||
if !found {
|
||
c.gReply(200, "无可合并数据", map[string]interface{}{"merged": false})
|
||
return
|
||
}
|
||
|
||
counts, merr := mergeGuestToUser(&guest, uint64(claims.UserID), claims.TenantId, claims.Username)
|
||
if merr != nil {
|
||
logs.Error("[guest] 合并失败 guest=%d: %v", guest.ID, merr)
|
||
c.gReply(500, "合并游客数据失败", nil)
|
||
return
|
||
}
|
||
|
||
now := time.Now()
|
||
_, err = models.Orm.QueryTable(new(models.GuestUser)).Filter("id", guest.ID).Update(map[string]interface{}{
|
||
"merged_uid": uint64(claims.UserID),
|
||
"merged_time": now,
|
||
})
|
||
if err != nil {
|
||
logs.Error("[guest] 标记合并状态失败 guest=%d: %v", guest.ID, err)
|
||
}
|
||
|
||
c.gReply(200, "success", map[string]interface{}{"merged": true, "counts": counts})
|
||
}
|
||
|
||
// ---------- 通用游客数据接口(供后续免登录模块直接复用) ----------
|
||
|
||
func (c *GuestController) guestClaims() (*jwtutil.Claims, error) {
|
||
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndGuest)
|
||
}
|
||
|
||
// DataList GET /api/guest/data?module=xxx
|
||
func (c *GuestController) DataList() {
|
||
claims, err := c.guestClaims()
|
||
if err != nil || claims.UserID <= 0 {
|
||
c.gReply(identity.Status(err), "游客令牌无效", nil)
|
||
return
|
||
}
|
||
module := strings.TrimSpace(c.GetString("module"))
|
||
if module == "" {
|
||
c.gReply(400, "缺少 module 参数", nil)
|
||
return
|
||
}
|
||
rows, err := gueststore.List(uint64(claims.UserID), module)
|
||
if err != nil {
|
||
c.gReply(500, "查询失败", nil)
|
||
return
|
||
}
|
||
list := make([]map[string]interface{}, 0, len(rows))
|
||
for _, r := range rows {
|
||
list = append(list, guestRowToMap(r))
|
||
}
|
||
c.gReply(200, "success", map[string]interface{}{"list": list, "total": len(list)})
|
||
}
|
||
|
||
// DataCreate POST /api/guest/data body: { "module": "xxx", "payload": {...} }
|
||
func (c *GuestController) DataCreate() {
|
||
claims, err := c.guestClaims()
|
||
if err != nil || claims.UserID <= 0 {
|
||
c.gReply(identity.Status(err), "游客令牌无效", nil)
|
||
return
|
||
}
|
||
var req struct {
|
||
Module string `json:"module"`
|
||
Payload json.RawMessage `json:"payload"`
|
||
}
|
||
if e := c.bindJSON(&req); e != nil {
|
||
c.gReply(400, "参数错误", nil)
|
||
return
|
||
}
|
||
req.Module = strings.TrimSpace(req.Module)
|
||
if req.Module == "" || len(req.Payload) == 0 {
|
||
c.gReply(400, "module 与 payload 不能为空", nil)
|
||
return
|
||
}
|
||
id, err := gueststore.Insert(uint64(claims.UserID), req.Module, req.Payload)
|
||
if err != nil {
|
||
c.gReply(500, "保存失败", nil)
|
||
return
|
||
}
|
||
c.gReply(200, "success", map[string]interface{}{"id": id})
|
||
}
|
||
|
||
// DataUpdate PUT /api/guest/data/:id body: { "module": "xxx", "payload": {...} }
|
||
func (c *GuestController) DataUpdate() {
|
||
claims, err := c.guestClaims()
|
||
if err != nil || claims.UserID <= 0 {
|
||
c.gReply(identity.Status(err), "游客令牌无效", nil)
|
||
return
|
||
}
|
||
id := c.parseIDParam()
|
||
if id == 0 {
|
||
c.gReply(400, "无效ID", nil)
|
||
return
|
||
}
|
||
var req struct {
|
||
Module string `json:"module"`
|
||
Payload json.RawMessage `json:"payload"`
|
||
}
|
||
if e := c.bindJSON(&req); e != nil {
|
||
c.gReply(400, "参数错误", nil)
|
||
return
|
||
}
|
||
if strings.TrimSpace(req.Module) == "" || len(req.Payload) == 0 {
|
||
c.gReply(400, "module 与 payload 不能为空", nil)
|
||
return
|
||
}
|
||
if err = gueststore.Update(uint64(claims.UserID), req.Module, id, req.Payload); err != nil {
|
||
if err == gueststore.ErrNotFound {
|
||
c.gReply(404, "记录不存在", nil)
|
||
return
|
||
}
|
||
c.gReply(500, "更新失败", nil)
|
||
return
|
||
}
|
||
c.gReply(200, "success", nil)
|
||
}
|
||
|
||
// DataDelete DELETE /api/guest/data/:id?module=xxx
|
||
func (c *GuestController) DataDelete() {
|
||
claims, err := c.guestClaims()
|
||
if err != nil || claims.UserID <= 0 {
|
||
c.gReply(identity.Status(err), "游客令牌无效", nil)
|
||
return
|
||
}
|
||
id := c.parseIDParam()
|
||
module := strings.TrimSpace(c.GetString("module"))
|
||
if id == 0 || module == "" {
|
||
c.gReply(400, "无效ID或缺少 module", nil)
|
||
return
|
||
}
|
||
if err = gueststore.Delete(uint64(claims.UserID), module, id); err != nil {
|
||
if err == gueststore.ErrNotFound {
|
||
c.gReply(404, "记录不存在", nil)
|
||
return
|
||
}
|
||
c.gReply(500, "删除失败", nil)
|
||
return
|
||
}
|
||
c.gReply(200, "success", nil)
|
||
}
|
||
|
||
// ---------- 小工具 ----------
|
||
|
||
func (c *GuestController) bindJSON(v interface{}) error {
|
||
body := c.Ctx.Input.RequestBody
|
||
if len(body) == 0 {
|
||
body, _ = io.ReadAll(c.Ctx.Request.Body)
|
||
}
|
||
return json.Unmarshal(body, v)
|
||
}
|
||
|
||
func (c *GuestController) parseIDParam() uint64 {
|
||
idStr := c.Ctx.Input.Param(":id")
|
||
var id uint64
|
||
for _, ch := range idStr {
|
||
if ch < '0' || ch > '9' {
|
||
return 0
|
||
}
|
||
id = id*10 + uint64(ch-'0')
|
||
}
|
||
return id
|
||
}
|
||
|
||
// guestRowToMap 把存储行转成前端条目:payload 展开 + 以表 ID 覆盖 id + 时间戳。
|
||
func guestRowToMap(r gueststore.Row) map[string]interface{} {
|
||
item := map[string]interface{}{}
|
||
if len(r.Payload) > 0 {
|
||
_ = json.Unmarshal(r.Payload, &item)
|
||
}
|
||
item["id"] = r.ID
|
||
item["created_at"] = r.CreateTime.Format("2006-01-02 15:04:05")
|
||
if r.UpdateTime != nil {
|
||
item["updated_at"] = r.UpdateTime.Format("2006-01-02 15:04:05")
|
||
} else {
|
||
item["updated_at"] = item["created_at"]
|
||
}
|
||
return item
|
||
}
|