更新
This commit is contained in:
1 parent
5d7ed43570
commit
012f8edf8b
4 files changed
+778
No files matched your search
@@ -0,0 +1,355 @@
|
||||
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"
|
||||
"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 仅作溯源,拿不到不阻断游客签发
|
||||
var openID, unionID string
|
||||
if req.Code != "" {
|
||||
if session, serr := wechatmini.Code2Session(cfg, req.Code); serr == nil && 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
|
||||
}
|
||||
|
||||
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()),
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
if ai.Mobile == nil || strings.TrimSpace(*ai.Mobile) == "" {
|
||||
c.gReply(200, "无可合并数据", map[string]interface{}{"merged": false})
|
||||
return
|
||||
}
|
||||
|
||||
var guest models.GuestUser
|
||||
err = models.Orm.QueryTable(new(models.GuestUser)).
|
||||
Filter("phone", strings.TrimSpace(*ai.Mobile)).
|
||||
Filter("merged_uid", 0).
|
||||
One(&guest)
|
||||
if err == orm.ErrNoRows {
|
||||
c.gReply(200, "无可合并数据", map[string]interface{}{"merged": false})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.gReply(500, "查询游客记录失败", nil)
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package controllers
|
||||
|
||||
// ==================== 游客数据转正合并 ====================
|
||||
//
|
||||
// 正式登录后按手机号把 yz_guest_data 里的条目迁入各模块业务表。
|
||||
// 新免登录模块接入时:在 guestMergeAdapters 注册一个适配器即可,
|
||||
// rows 为该模块全部有效游客条目,返回成功迁移的条数。
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/services/gueststore"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
)
|
||||
|
||||
// 游客数据所属模块编码(与 gueststore 的 module 字段一致)
|
||||
const (
|
||||
guestModuleNotebook = "notebook"
|
||||
guestModuleSchedule = "schedule"
|
||||
guestModulePasswordStore = "passwordStore"
|
||||
)
|
||||
|
||||
// guestNoteItem 游客记事本条目:与 /api/notebook 登录态响应字段一致,
|
||||
// 前端拿到的形状不因游客/登录而变化。
|
||||
type guestNoteItem struct {
|
||||
ID uint64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Pinned bool `json:"pinned"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// guestScheduleItem 游客日程条目:与 /api/schedule 登录态响应字段一致。
|
||||
// 游客日程不接提醒发送链路(无短信/邮件推送),仅作待办记录。
|
||||
type guestScheduleItem struct {
|
||||
ID uint64 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
ScheduleTime string `json:"schedule_time"`
|
||||
RemindChannels []string `json:"remind_channels"`
|
||||
AdvanceMinutes int `json:"advance_minutes"`
|
||||
RepeatIntervalMinutes int `json:"repeat_interval_minutes"`
|
||||
MaxSendCount int `json:"max_send_count"`
|
||||
IsFinished bool `json:"is_finished"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// mergeGuestToUser 把游客的全部模块数据并入正式身份(AuthIdentity.ID=uid, 租户 tid)。
|
||||
// 单条失败只记日志不中断;完成后游客数据行统一置 is_deleted=2(保留行本身)。
|
||||
func mergeGuestToUser(guest *models.GuestUser, uid uint64, tid int, username string) (map[string]int, error) {
|
||||
counts := map[string]int{}
|
||||
|
||||
// ---- 记事本 ----
|
||||
if rows, err := gueststore.List(guest.ID, guestModuleNotebook); err != nil {
|
||||
logs.Error("[guest-merge] list notebook failed guest=%d: %v", guest.ID, err)
|
||||
} else {
|
||||
for _, r := range rows {
|
||||
var item guestNoteItem
|
||||
if json.Unmarshal(r.Payload, &item) != nil {
|
||||
continue
|
||||
}
|
||||
userID, name := uid, username
|
||||
pinned := int8(0)
|
||||
if item.Pinned {
|
||||
pinned = 1
|
||||
}
|
||||
if _, err := models.Orm.Insert(&models.BackendNotebook{
|
||||
Tid: tid,
|
||||
Title: item.Title,
|
||||
Content: item.Content,
|
||||
Pinned: pinned,
|
||||
UserID: &userID,
|
||||
UserName: &name,
|
||||
}); err != nil {
|
||||
logs.Error("[guest-merge] insert notebook row=%d: %v", r.ID, err)
|
||||
continue
|
||||
}
|
||||
counts[guestModuleNotebook]++
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 日程 ----
|
||||
if rows, err := gueststore.List(guest.ID, guestModuleSchedule); err != nil {
|
||||
logs.Error("[guest-merge] list schedule failed guest=%d: %v", guest.ID, err)
|
||||
} else {
|
||||
for _, r := range rows {
|
||||
var item guestScheduleItem
|
||||
if json.Unmarshal(r.Payload, &item) != nil {
|
||||
continue
|
||||
}
|
||||
schedTime, terr := time.ParseInLocation("2006-01-02 15:04:05", item.ScheduleTime, time.Local)
|
||||
if terr != nil {
|
||||
logs.Warn("[guest-merge] schedule row=%d 时间非法,跳过", r.ID)
|
||||
continue
|
||||
}
|
||||
title := firstLineOf(item.Content, "日程提醒")
|
||||
schedule := models.BackendSchedule{
|
||||
Tid: tid,
|
||||
Title: title,
|
||||
Content: item.Content,
|
||||
ScheduleTime: schedTime,
|
||||
UserID: uid,
|
||||
}
|
||||
schedID, ierr := models.Orm.Insert(&schedule)
|
||||
if ierr != nil {
|
||||
logs.Error("[guest-merge] insert schedule row=%d: %v", r.ID, ierr)
|
||||
continue
|
||||
}
|
||||
// 提醒行按登录态创建逻辑重建;已完成的不再发送
|
||||
status := int8(0)
|
||||
if item.IsFinished {
|
||||
status = 2
|
||||
}
|
||||
for _, ch := range normalizeRemindChannels(item.RemindChannels) {
|
||||
reminder := models.BackendScheduleReminder{
|
||||
Tid: tid,
|
||||
ScheduleID: uint64(schedID),
|
||||
RemindChannel: ch,
|
||||
AdvanceMinutes: item.AdvanceMinutes,
|
||||
NextRemindTime: schedTime.Add(-time.Duration(item.AdvanceMinutes) * time.Minute),
|
||||
ReceiverUserID: uid,
|
||||
RemindStatus: status,
|
||||
RepeatIntervalMinutes: item.RepeatIntervalMinutes,
|
||||
MaxSendCount: item.MaxSendCount,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
if reminder.MaxSendCount <= 0 {
|
||||
reminder.MaxSendCount = 1
|
||||
}
|
||||
if _, rerr := models.Orm.Insert(&reminder); rerr != nil {
|
||||
logs.Error("[guest-merge] insert reminder row=%d ch=%s: %v", r.ID, ch, rerr)
|
||||
}
|
||||
}
|
||||
counts[guestModuleSchedule]++
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 密码库 ----
|
||||
if rows, err := gueststore.List(guest.ID, guestModulePasswordStore); err != nil {
|
||||
logs.Error("[guest-merge] list passwordStore failed guest=%d: %v", guest.ID, err)
|
||||
} else {
|
||||
for _, r := range rows {
|
||||
var item PasswordStoreItemDTO
|
||||
if json.Unmarshal(r.Payload, &item) != nil {
|
||||
continue
|
||||
}
|
||||
if item.Platform == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := models.Orm.Insert(&models.BackendPasswordStore{
|
||||
Tid: tid,
|
||||
UserID: uid,
|
||||
Platform: item.Platform,
|
||||
URL: item.URL,
|
||||
Accounts: accountsToJSON(item.Accounts),
|
||||
Remark: item.Remark,
|
||||
}); err != nil {
|
||||
logs.Error("[guest-merge] insert passwordStore row=%d: %v", r.ID, err)
|
||||
continue
|
||||
}
|
||||
counts[guestModulePasswordStore]++
|
||||
}
|
||||
}
|
||||
|
||||
if err := gueststore.MarkMerged(guest.ID); err != nil {
|
||||
return counts, err
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// firstLineOf 取首行作为标题(与登录态日程创建逻辑一致)。
|
||||
func firstLineOf(content, fallback string) string {
|
||||
title := strings.SplitN(content, "\n", 2)[0]
|
||||
if len(title) > 60 {
|
||||
title = title[:60]
|
||||
}
|
||||
if title == "" {
|
||||
title = fallback
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
// normalizeRemindChannels 与 AppReminderController.Create 相同的通道白名单:
|
||||
// APP 归一为 SITE_MSG,非法值丢弃。
|
||||
func normalizeRemindChannels(list []string) []string {
|
||||
out := make([]string, 0, len(list))
|
||||
for _, ch := range list {
|
||||
ch = strings.ToUpper(strings.TrimSpace(ch))
|
||||
if ch == "APP" {
|
||||
ch = "SITE_MSG"
|
||||
}
|
||||
if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" {
|
||||
continue
|
||||
}
|
||||
out = append(out, ch)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// ==================== 游客(免登录模块)轻身份与通用数据 ====================
|
||||
//
|
||||
// 设计:免登录模块「不登录也要能存数据」。小程序用手机号授权换取游客轻身份
|
||||
// (yz_guest_user),业务条目按 guest_id + module 文档式存入 yz_guest_data,
|
||||
// 永久保留不清理;正式登录后按手机号把数据合并进正式账号(见 controllers/guest_merge.go)。
|
||||
|
||||
// GuestUser 游客轻身份,以微信验证手机号为唯一标识。
|
||||
type GuestUser struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Phone string `orm:"column(phone);size(20)" json:"phone"`
|
||||
OpenID string `orm:"column(openid);size(64);default('')" json:"openid"`
|
||||
UnionID string `orm:"column(unionid);size(64);default('')" json:"unionid"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
LastActiveTime *time.Time `orm:"column(last_active_time);type(datetime);null" json:"last_active_time"`
|
||||
MergedUID uint64 `orm:"column(merged_uid);default(0)" json:"merged_uid"`
|
||||
MergedTime *time.Time `orm:"column(merged_time);type(datetime);null" json:"merged_time"`
|
||||
}
|
||||
|
||||
func (*GuestUser) TableName() string { return "yz_guest_user" }
|
||||
|
||||
// GuestData 游客通用数据行:一条业务条目 = 一行 JSON 文档。
|
||||
// module 取值与免登录业务对应(notebook / schedule / passwordStore / …),
|
||||
// 新模块直接复用本表,无需再加业务表。
|
||||
type GuestData struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
GuestID uint64 `orm:"column(guest_id)" json:"guest_id"`
|
||||
Module string `orm:"column(module);size(50)" json:"module"`
|
||||
Payload string `orm:"column(payload);type(text)" json:"payload"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` // 0正常 1已删除 2已合并
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
}
|
||||
|
||||
func (*GuestData) TableName() string { return "yz_guest_data" }
|
||||
@@ -0,0 +1,181 @@
|
||||
// Package gueststore 游客(免登录模块)通用存储。
|
||||
//
|
||||
// 一条业务条目 = yz_guest_data 一行 JSON 文档,按 guest_id + module 隔离;
|
||||
// 删除只做软删(is_deleted=1)永久保留,合并进正式账号后置 is_deleted=2。
|
||||
// 业务控制器通过 identity.IsGuest 分流到这里,响应结构与登录态接口保持一致。
|
||||
package gueststore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// ErrNotFound 条目不存在(或不属于该游客 / 已被删除)。
|
||||
var ErrNotFound = errors.New("记录不存在")
|
||||
|
||||
// Row 一条游客数据文档(对外统一用表 ID 作为条目 ID,payload 内的 id 仅冗余)。
|
||||
type Row struct {
|
||||
ID uint64
|
||||
Payload json.RawMessage
|
||||
CreateTime time.Time
|
||||
UpdateTime *time.Time
|
||||
}
|
||||
|
||||
// Upsert 按手机号定位/创建游客身份,并刷新 openid / 活跃时间。
|
||||
// 已合并(merged_uid>0)的旧记录会被复位为新的游客周期:
|
||||
// 历史数据行在合并时已置 is_deleted=2,不会再次出现在列表里。
|
||||
func Upsert(phone, openid, unionid string) (*models.GuestUser, error) {
|
||||
phone = strings.TrimSpace(phone)
|
||||
if phone == "" {
|
||||
return nil, errors.New("手机号不能为空")
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
var row models.GuestUser
|
||||
err := models.Orm.QueryTable(new(models.GuestUser)).Filter("phone", phone).One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
row = models.GuestUser{Phone: phone, OpenID: openid, UnionID: unionid, LastActiveTime: &now}
|
||||
id, ierr := models.Orm.Insert(&row)
|
||||
if ierr != nil {
|
||||
return nil, ierr
|
||||
}
|
||||
row.ID = uint64(id)
|
||||
return &row, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upd := map[string]interface{}{"last_active_time": now}
|
||||
if openid != "" {
|
||||
upd["openid"] = openid
|
||||
}
|
||||
if unionid != "" {
|
||||
upd["unionid"] = unionid
|
||||
}
|
||||
if row.MergedUID > 0 {
|
||||
// 复位游客周期,允许同一手机号再次以游客身份产生新数据
|
||||
upd["merged_uid"] = uint64(0)
|
||||
upd["merged_time"] = nil
|
||||
row.MergedUID = 0
|
||||
}
|
||||
if _, err = models.Orm.QueryTable(new(models.GuestUser)).Filter("id", row.ID).Update(upd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row.LastActiveTime = &now
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// Insert 写入一条业务条目,返回行 ID(即条目 ID)。
|
||||
func Insert(guestID uint64, module string, payload interface{}) (uint64, error) {
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
now := time.Now()
|
||||
id, err := models.Orm.Insert(&models.GuestData{
|
||||
GuestID: guestID,
|
||||
Module: module,
|
||||
Payload: string(b),
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint64(id), nil
|
||||
}
|
||||
|
||||
// Get 读取单条(限本游客 + 本模块 + 未删除)。
|
||||
func Get(guestID uint64, module string, id uint64) (Row, error) {
|
||||
var d models.GuestData
|
||||
err := models.Orm.QueryTable(new(models.GuestData)).
|
||||
Filter("id", id).
|
||||
Filter("guest_id", guestID).
|
||||
Filter("module", module).
|
||||
Filter("is_deleted", 0).
|
||||
One(&d)
|
||||
if err == orm.ErrNoRows {
|
||||
return Row{}, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return Row{}, err
|
||||
}
|
||||
return toRow(d), nil
|
||||
}
|
||||
|
||||
// List 列出某游客某模块的全部有效条目(新→旧)。
|
||||
func List(guestID uint64, module string) ([]Row, error) {
|
||||
var rows []models.GuestData
|
||||
_, err := models.Orm.QueryTable(new(models.GuestData)).
|
||||
Filter("guest_id", guestID).
|
||||
Filter("module", module).
|
||||
Filter("is_deleted", 0).
|
||||
OrderBy("-id").
|
||||
All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]Row, 0, len(rows))
|
||||
for _, d := range rows {
|
||||
out = append(out, toRow(d))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Update 覆盖一条条目的 payload。
|
||||
func Update(guestID uint64, module string, id uint64, payload interface{}) error {
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.GuestData)).
|
||||
Filter("id", id).
|
||||
Filter("guest_id", guestID).
|
||||
Filter("module", module).
|
||||
Filter("is_deleted", 0).
|
||||
Update(map[string]interface{}{"payload": string(b), "update_time": time.Now()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 软删除一条条目(保留数据行,永久不清理)。
|
||||
func Delete(guestID uint64, module string, id uint64) error {
|
||||
n, err := models.Orm.QueryTable(new(models.GuestData)).
|
||||
Filter("id", id).
|
||||
Filter("guest_id", guestID).
|
||||
Filter("module", module).
|
||||
Filter("is_deleted", 0).
|
||||
Update(map[string]interface{}{"is_deleted": 1, "update_time": time.Now()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkMerged 游客转正后:该游客全部有效数据行置 is_deleted=2(保留行本身)。
|
||||
func MarkMerged(guestID uint64) error {
|
||||
_, err := models.Orm.QueryTable(new(models.GuestData)).
|
||||
Filter("guest_id", guestID).
|
||||
Filter("is_deleted", 0).
|
||||
Update(map[string]interface{}{"is_deleted": 2, "update_time": time.Now()})
|
||||
return err
|
||||
}
|
||||
|
||||
func toRow(d models.GuestData) Row {
|
||||
return Row{ID: d.ID, Payload: json.RawMessage(d.Payload), CreateTime: d.CreateTime, UpdateTime: d.UpdateTime}
|
||||
}
|
||||
Reference in new issue
Block a user