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 }