Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ff960fbd | ||
|
|
012f8edf8b |
No files matched your search
@@ -125,6 +125,117 @@ func (c *AppAggregateController) Home() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// publicModuleItem 移动端免登录可见的模块卡片:只暴露展示与安全开关,不含菜单/KPI。
|
||||||
|
// 供未登录用户渲染 nav 宫格,require_login 决定点击是「进登录引导」还是「直接进入」。
|
||||||
|
type publicModuleItem struct {
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
RequireLogin int8 `json:"require_login"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicModules GET /api/modules/public
|
||||||
|
//
|
||||||
|
// 公开接口(无需登录,路径不在 jwt 中间件 protectedPrefixes 内):
|
||||||
|
// 返回移动端可见的模块宫格 + 每个模块的 require_login 标记,
|
||||||
|
// 让未登录用户也能进入 nav 默认页并分辨「哪些能免登录用、哪些必须先登录」。
|
||||||
|
func (c *AppAggregateController) PublicModules() {
|
||||||
|
var rows []models.SystemModules
|
||||||
|
if _, err := models.Orm.QueryTable(new(models.SystemModules)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
Filter("status", 1).
|
||||||
|
Filter("is_show", 1).
|
||||||
|
OrderBy("sort", "id").
|
||||||
|
All(&rows); err != nil {
|
||||||
|
c.serveJSON(map[string]interface{}{"code": 200, "msg": "success", "data": []publicModuleItem{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 移动端只展示 app 可见模块(未配置 platforms 视为全端)
|
||||||
|
rows = services.FilterModulesByPlatform(rows, models.PlatformApp)
|
||||||
|
|
||||||
|
out := make([]publicModuleItem, 0, len(rows))
|
||||||
|
for _, m := range rows {
|
||||||
|
out = append(out, publicModuleItem{
|
||||||
|
ID: m.ID,
|
||||||
|
Code: m.Code,
|
||||||
|
Name: m.Name,
|
||||||
|
Icon: m.Icon,
|
||||||
|
Path: m.Path,
|
||||||
|
Description: m.Description,
|
||||||
|
RequireLogin: m.RequireLogin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.serveJSON(map[string]interface{}{"code": 200, "msg": "success", "data": out})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicMenus GET /api/menus/public
|
||||||
|
//
|
||||||
|
// 公开接口(无需登录):返回移动端免登录菜单(require_login=0)。
|
||||||
|
// 免登录控制按菜单粒度配置(Platform 菜单管理),游客持游客令牌直接使用;
|
||||||
|
// nav 宫格据此判定模块卡片是否可免登录进入(模块级 require_login 不再作为判定依据)。
|
||||||
|
// 不做角色/套餐过滤:免登录功能对所有人开放,未配 platforms 的菜单继承所属模块。
|
||||||
|
func (c *AppAggregateController) PublicMenus() {
|
||||||
|
var rows []models.SystemMenu
|
||||||
|
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).
|
||||||
|
Filter("status", 1).
|
||||||
|
Filter("require_login", 0).
|
||||||
|
All(&rows); err != nil {
|
||||||
|
c.serveJSON(map[string]interface{}{"code": 200, "msg": "success", "data": []appMenuItem{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allModules := c.allModules()
|
||||||
|
sortedModules := services.SortedModulesForScope(allModules)
|
||||||
|
|
||||||
|
visible := make([]models.SystemMenu, 0, len(rows))
|
||||||
|
for _, m := range rows {
|
||||||
|
if m.Type == 3 { // 接口类菜单是权限标识载体,不下发
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m.IsVisible != nil && *m.IsVisible == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !services.MenuVisibleOn(m, sortedModules, allModules, models.PlatformApp) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visible = append(visible, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公开集合内统计子节点数:目录若只包含未放开(或不存在)的子菜单,leaf 为 true 但不跳任何页,
|
||||||
|
// 前端按 NATIVE_RULES 解析不到路由时会落「开发中」占位,不会误入受保护页。
|
||||||
|
childCount := map[int64]int{}
|
||||||
|
for _, m := range visible {
|
||||||
|
if m.Pid != 0 {
|
||||||
|
childCount[m.Pid]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]appMenuItem, 0, len(visible))
|
||||||
|
for _, m := range visible {
|
||||||
|
item := appMenuItem{
|
||||||
|
ID: m.ID,
|
||||||
|
Pid: m.Pid,
|
||||||
|
Type: m.Type,
|
||||||
|
Sort: m.Sort,
|
||||||
|
Title: strings.TrimSpace(m.Title),
|
||||||
|
}
|
||||||
|
if m.Path != nil {
|
||||||
|
item.Path = strings.TrimSpace(*m.Path)
|
||||||
|
}
|
||||||
|
if m.Icon != nil {
|
||||||
|
item.Icon = strings.TrimSpace(*m.Icon)
|
||||||
|
}
|
||||||
|
item.Module = appModuleCodeOfPath(item.Path, allModules)
|
||||||
|
item.Leaf = childCount[int64(m.ID)] == 0
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
sort.SliceStable(out, func(i, j int) bool { return out[i].Sort < out[j].Sort })
|
||||||
|
c.serveJSON(map[string]interface{}{"code": 200, "msg": "success", "data": out})
|
||||||
|
}
|
||||||
|
|
||||||
// appQuickMenuIds 读取当前用户自定义的常用功能菜单 ID 列表(有序)。
|
// appQuickMenuIds 读取当前用户自定义的常用功能菜单 ID 列表(有序)。
|
||||||
// 未设置或解析失败一律返回空数组,前端据此回落到默认取前 N 个叶子菜单。
|
// 未设置或解析失败一律返回空数组,前端据此回落到默认取前 N 个叶子菜单。
|
||||||
func (c *AppAggregateController) appQuickMenuIds(tid, uid uint64) []uint64 {
|
func (c *AppAggregateController) appQuickMenuIds(tid, uid uint64) []uint64 {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"server/models"
|
"server/models"
|
||||||
"server/pkg/identity"
|
"server/pkg/identity"
|
||||||
"server/pkg/jwtutil"
|
"server/pkg/jwtutil"
|
||||||
|
"server/services/gueststore"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -19,9 +21,9 @@ type AppNotebookController struct {
|
|||||||
beego.Controller
|
beego.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只放行移动端与租户端令牌:/api/** 仅被 uniapp 调用,平台令牌不该能打移动端接口。
|
// 放行移动端、租户端与游客令牌:游客调用时不进业务表,分流到 gueststore。
|
||||||
func (c *AppNotebookController) appNotebookClaims() (*jwtutil.Claims, error) {
|
func (c *AppNotebookController) appNotebookClaims() (*jwtutil.Claims, error) {
|
||||||
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp, identity.EndBackend)
|
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp, identity.EndBackend, identity.EndGuest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AppNotebookController) nbJsonErr(httpStatus, bizCode int, msg string) {
|
func (c *AppNotebookController) nbJsonErr(httpStatus, bizCode int, msg string) {
|
||||||
@@ -45,6 +47,11 @@ func (c *AppNotebookController) GetList() {
|
|||||||
|
|
||||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestList(claims, keyword)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
qs := models.Orm.QueryTable(new(models.BackendNotebook)).
|
qs := models.Orm.QueryTable(new(models.BackendNotebook)).
|
||||||
Filter("is_deleted", 0).
|
Filter("is_deleted", 0).
|
||||||
Filter("user_id", claims.UserID)
|
Filter("user_id", claims.UserID)
|
||||||
@@ -109,6 +116,11 @@ func (c *AppNotebookController) GetDetail() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestDetail(claims, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var note models.BackendNotebook
|
var note models.BackendNotebook
|
||||||
err = models.Orm.QueryTable(new(models.BackendNotebook)).
|
err = models.Orm.QueryTable(new(models.BackendNotebook)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
@@ -164,6 +176,11 @@ func (c *AppNotebookController) Create() {
|
|||||||
title = "无标题"
|
title = "无标题"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestCreateItem(claims, title, payload.Content, payload.Pinned)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userID := uint64(claims.UserID)
|
userID := uint64(claims.UserID)
|
||||||
pinned := int8(0)
|
pinned := int8(0)
|
||||||
if payload.Pinned {
|
if payload.Pinned {
|
||||||
@@ -224,6 +241,11 @@ func (c *AppNotebookController) Update() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestUpdateItem(claims, id, payload.Title, payload.Content, payload.Pinned)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var note models.BackendNotebook
|
var note models.BackendNotebook
|
||||||
err = models.Orm.QueryTable(new(models.BackendNotebook)).
|
err = models.Orm.QueryTable(new(models.BackendNotebook)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
@@ -286,6 +308,19 @@ func (c *AppNotebookController) Delete() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
if err := gueststore.Delete(uint64(claims.UserID), guestModuleNotebook, id); err != nil {
|
||||||
|
if err == gueststore.ErrNotFound {
|
||||||
|
c.nbJsonErr(404, 404, "笔记不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.nbJsonErr(500, 500, "删除失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.nbOk(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var note models.BackendNotebook
|
var note models.BackendNotebook
|
||||||
err = models.Orm.QueryTable(new(models.BackendNotebook)).
|
err = models.Orm.QueryTable(new(models.BackendNotebook)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
@@ -327,6 +362,11 @@ func (c *AppNotebookController) TogglePin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestTogglePin(claims, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var note models.BackendNotebook
|
var note models.BackendNotebook
|
||||||
err = models.Orm.QueryTable(new(models.BackendNotebook)).
|
err = models.Orm.QueryTable(new(models.BackendNotebook)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
@@ -361,3 +401,107 @@ func (c *AppNotebookController) TogglePin() {
|
|||||||
"pinned": newPinned == 1,
|
"pinned": newPinned == 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 游客分支(免登录态):数据存 yz_guest_data,响应结构与登录态一致 ----------
|
||||||
|
|
||||||
|
// guestNoteFull 读取并补全一条游客笔记(ID/时间以存储行为准)。
|
||||||
|
func guestNoteFull(r gueststore.Row) guestNoteItem {
|
||||||
|
var it guestNoteItem
|
||||||
|
if json.Unmarshal(r.Payload, &it) != nil {
|
||||||
|
it = guestNoteItem{Title: "无标题"}
|
||||||
|
}
|
||||||
|
it.ID = r.ID
|
||||||
|
it.CreatedAt = r.CreateTime.Format("2006-01-02 15:04:05")
|
||||||
|
it.UpdatedAt = it.CreatedAt
|
||||||
|
if r.UpdateTime != nil {
|
||||||
|
it.UpdatedAt = r.UpdateTime.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppNotebookController) guestList(claims *jwtutil.Claims, keyword string) {
|
||||||
|
rows, err := gueststore.List(uint64(claims.UserID), guestModuleNotebook)
|
||||||
|
if err != nil {
|
||||||
|
c.nbJsonErr(500, 500, "查询失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]guestNoteItem, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
it := guestNoteFull(r)
|
||||||
|
if keyword != "" && !strings.Contains(it.Title, keyword) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, it)
|
||||||
|
}
|
||||||
|
// 与登录态一致:置顶优先,再按更新时间倒序
|
||||||
|
sort.SliceStable(items, func(i, j int) bool {
|
||||||
|
if items[i].Pinned != items[j].Pinned {
|
||||||
|
return items[i].Pinned
|
||||||
|
}
|
||||||
|
return items[i].UpdatedAt > items[j].UpdatedAt
|
||||||
|
})
|
||||||
|
c.nbOk(map[string]interface{}{"list": items, "total": len(items)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppNotebookController) guestDetail(claims *jwtutil.Claims, id uint64) {
|
||||||
|
r, err := gueststore.Get(uint64(claims.UserID), guestModuleNotebook, id)
|
||||||
|
if err != nil {
|
||||||
|
c.nbJsonErr(404, 404, "笔记不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.nbOk(guestNoteFull(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppNotebookController) guestCreateItem(claims *jwtutil.Claims, title, content string, pinned bool) {
|
||||||
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
it := guestNoteItem{Title: title, Content: content, Pinned: pinned, CreatedAt: now, UpdatedAt: now}
|
||||||
|
newID, err := gueststore.Insert(uint64(claims.UserID), guestModuleNotebook, it)
|
||||||
|
if err != nil {
|
||||||
|
c.nbJsonErr(500, 500, "创建失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.nbOk(map[string]interface{}{
|
||||||
|
"id": newID, "title": title, "content": content, "pinned": pinned,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppNotebookController) guestUpdateItem(claims *jwtutil.Claims, id uint64, title, content string, pinned *bool) {
|
||||||
|
r, err := gueststore.Get(uint64(claims.UserID), guestModuleNotebook, id)
|
||||||
|
if err != nil {
|
||||||
|
c.nbJsonErr(404, 404, "笔记不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it := guestNoteFull(r)
|
||||||
|
if t := strings.TrimSpace(title); t != "" {
|
||||||
|
it.Title = t
|
||||||
|
}
|
||||||
|
it.Content = content
|
||||||
|
if pinned != nil {
|
||||||
|
it.Pinned = *pinned
|
||||||
|
}
|
||||||
|
it.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
if err = gueststore.Update(uint64(claims.UserID), guestModuleNotebook, id, it); err != nil {
|
||||||
|
c.nbJsonErr(500, 500, "更新失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.nbOk(map[string]interface{}{
|
||||||
|
"id": it.ID, "title": it.Title, "content": it.Content, "pinned": it.Pinned,
|
||||||
|
"created_at": it.CreatedAt, "updated_at": it.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppNotebookController) guestTogglePin(claims *jwtutil.Claims, id uint64) {
|
||||||
|
r, err := gueststore.Get(uint64(claims.UserID), guestModuleNotebook, id)
|
||||||
|
if err != nil {
|
||||||
|
c.nbJsonErr(404, 404, "笔记不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it := guestNoteFull(r)
|
||||||
|
it.Pinned = !it.Pinned
|
||||||
|
it.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
if err = gueststore.Update(uint64(claims.UserID), guestModuleNotebook, id, it); err != nil {
|
||||||
|
c.nbJsonErr(500, 500, "操作失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.nbOk(map[string]interface{}{"pinned": it.Pinned})
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"server/middleware"
|
"server/middleware"
|
||||||
"server/models"
|
"server/models"
|
||||||
"server/pkg/identity"
|
"server/pkg/identity"
|
||||||
"server/pkg/jwtutil"
|
"server/pkg/jwtutil"
|
||||||
|
"server/services/gueststore"
|
||||||
|
|
||||||
"github.com/beego/beego/v2/client/orm"
|
"github.com/beego/beego/v2/client/orm"
|
||||||
"github.com/beego/beego/v2/core/logs"
|
"github.com/beego/beego/v2/core/logs"
|
||||||
@@ -24,12 +27,18 @@ type AppPasswordStoreController struct {
|
|||||||
beego.Controller
|
beego.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只放行移动端与租户端令牌:/api/** 仅被 uniapp 调用,平台令牌不该能打移动端接口。
|
// 放行移动端、租户端与游客令牌:游客 tid=0 合法,数据进 gueststore 而非业务表。
|
||||||
func (c *AppPasswordStoreController) appPwdClaims() (*jwtutil.Claims, error) {
|
func (c *AppPasswordStoreController) appPwdClaims() (*jwtutil.Claims, error) {
|
||||||
claims, err := identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp, identity.EndBackend)
|
claims, err := identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp, identity.EndBackend, identity.EndGuest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
if claims.UserID <= 0 {
|
||||||
|
return nil, orm.ErrNoRows
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
if claims.UserID <= 0 || claims.TenantId <= 0 {
|
if claims.UserID <= 0 || claims.TenantId <= 0 {
|
||||||
return nil, orm.ErrNoRows
|
return nil, orm.ErrNoRows
|
||||||
}
|
}
|
||||||
@@ -43,6 +52,10 @@ func (c *AppPasswordStoreController) GetList() {
|
|||||||
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if identity.IsGuest(cl) {
|
||||||
|
c.guestList(cl, strings.TrimSpace(c.GetString("keyword")))
|
||||||
|
return
|
||||||
|
}
|
||||||
qs := models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
qs := models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
||||||
Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID)
|
Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID)
|
||||||
if k := strings.TrimSpace(c.GetString("keyword")); k != "" {
|
if k := strings.TrimSpace(c.GetString("keyword")); k != "" {
|
||||||
@@ -82,6 +95,10 @@ func (c *AppPasswordStoreController) GetDetail() {
|
|||||||
passwordReply(&c.Controller, 400, "参数错误", nil)
|
passwordReply(&c.Controller, 400, "参数错误", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if identity.IsGuest(cl) {
|
||||||
|
c.guestDetail(cl, uint64(id))
|
||||||
|
return
|
||||||
|
}
|
||||||
var m models.BackendPasswordStore
|
var m models.BackendPasswordStore
|
||||||
e = models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
e = models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
||||||
Filter("id", id).Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID).One(&m)
|
Filter("id", id).Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID).One(&m)
|
||||||
@@ -121,6 +138,10 @@ func (c *AppPasswordStoreController) Create() {
|
|||||||
passwordReply(&c.Controller, 400, e.Error(), nil)
|
passwordReply(&c.Controller, 400, e.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if identity.IsGuest(cl) {
|
||||||
|
c.guestCreate(cl, p)
|
||||||
|
return
|
||||||
|
}
|
||||||
qs := models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
qs := models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
||||||
Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID)
|
Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID)
|
||||||
if existing, e := findSamePlatformRecord(qs, 0, p.Platform, p.URL); e != nil {
|
if existing, e := findSamePlatformRecord(qs, 0, p.Platform, p.URL); e != nil {
|
||||||
@@ -172,6 +193,10 @@ func (c *AppPasswordStoreController) Update() {
|
|||||||
passwordReply(&c.Controller, 400, e.Error(), nil)
|
passwordReply(&c.Controller, 400, e.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if identity.IsGuest(cl) {
|
||||||
|
c.guestUpdate(cl, uint64(id), p)
|
||||||
|
return
|
||||||
|
}
|
||||||
var m models.BackendPasswordStore
|
var m models.BackendPasswordStore
|
||||||
e = models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
e = models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
||||||
Filter("id", id).Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID).One(&m)
|
Filter("id", id).Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID).One(&m)
|
||||||
@@ -221,6 +246,18 @@ func (c *AppPasswordStoreController) Delete() {
|
|||||||
passwordReply(&c.Controller, 400, "参数错误", nil)
|
passwordReply(&c.Controller, 400, "参数错误", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if identity.IsGuest(cl) {
|
||||||
|
if err := gueststore.Delete(uint64(cl.UserID), guestModulePasswordStore, uint64(id)); err != nil {
|
||||||
|
if err == gueststore.ErrNotFound {
|
||||||
|
passwordReply(&c.Controller, 404, "记录不存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
passwordReply(&c.Controller, 500, "删除失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
passwordReply(&c.Controller, 200, "success", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
var m models.BackendPasswordStore
|
var m models.BackendPasswordStore
|
||||||
e = models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
e = models.Orm.QueryTable(new(models.BackendPasswordStore)).
|
||||||
Filter("id", id).Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID).One(&m)
|
Filter("id", id).Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID).One(&m)
|
||||||
@@ -241,3 +278,125 @@ func (c *AppPasswordStoreController) Delete() {
|
|||||||
middleware.WriteDeleteLog(uint64(cl.UserID), cl.TenantId, "passwordStore", m.Platform)
|
middleware.WriteDeleteLog(uint64(cl.UserID), cl.TenantId, "passwordStore", m.Platform)
|
||||||
passwordReply(&c.Controller, 200, "success", nil)
|
passwordReply(&c.Controller, 200, "success", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 游客分支(免登录态):数据存 yz_guest_data,响应结构与登录态一致 ----------
|
||||||
|
|
||||||
|
func guestPwdFull(r gueststore.Row) PasswordStoreItemDTO {
|
||||||
|
var it PasswordStoreItemDTO
|
||||||
|
if json.Unmarshal(r.Payload, &it) != nil {
|
||||||
|
it = PasswordStoreItemDTO{}
|
||||||
|
}
|
||||||
|
it.ID = r.ID
|
||||||
|
it.Tid = 0
|
||||||
|
it.UserID = 0
|
||||||
|
it.CreateTime = r.CreateTime
|
||||||
|
it.UpdateTime = r.UpdateTime
|
||||||
|
if it.Accounts == nil {
|
||||||
|
it.Accounts = []models.PasswordAccountItem{}
|
||||||
|
}
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
// guestPwdSamePlatform 与登录态 findSamePlatformRecord 同语义:平台+网址重复。
|
||||||
|
func guestPwdSamePlatform(it PasswordStoreItemDTO, platform, url string, excludeID uint64) bool {
|
||||||
|
if it.ID == excludeID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.EqualFold(strings.TrimSpace(it.Platform), strings.TrimSpace(platform)) &&
|
||||||
|
strings.TrimSpace(it.URL) == strings.TrimSpace(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func guestPwdKeywordHit(it PasswordStoreItemDTO, k string) bool {
|
||||||
|
k = strings.ToLower(k)
|
||||||
|
if strings.Contains(strings.ToLower(it.Platform), k) ||
|
||||||
|
strings.Contains(strings.ToLower(it.URL), k) ||
|
||||||
|
strings.Contains(strings.ToLower(it.Remark), k) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, a := range it.Accounts {
|
||||||
|
if strings.Contains(strings.ToLower(a.Username), k) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppPasswordStoreController) guestList(cl *jwtutil.Claims, keyword string) {
|
||||||
|
rows, err := gueststore.List(uint64(cl.UserID), guestModulePasswordStore)
|
||||||
|
if err != nil {
|
||||||
|
passwordReply(&c.Controller, 500, "查询失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := make([]PasswordStoreItemDTO, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
it := guestPwdFull(r)
|
||||||
|
if keyword != "" && !guestPwdKeywordHit(it, keyword) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res = append(res, it)
|
||||||
|
}
|
||||||
|
passwordReply(&c.Controller, 200, "success", map[string]interface{}{"list": res, "total": len(res)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppPasswordStoreController) guestDetail(cl *jwtutil.Claims, id uint64) {
|
||||||
|
r, err := gueststore.Get(uint64(cl.UserID), guestModulePasswordStore, id)
|
||||||
|
if err != nil {
|
||||||
|
passwordReply(&c.Controller, 404, "记录不存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
passwordReply(&c.Controller, 200, "success", guestPwdFull(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppPasswordStoreController) guestCreate(cl *jwtutil.Claims, p passwordPayload) {
|
||||||
|
rows, err := gueststore.List(uint64(cl.UserID), guestModulePasswordStore)
|
||||||
|
if err != nil {
|
||||||
|
passwordReply(&c.Controller, 500, "查询失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, r := range rows {
|
||||||
|
if guestPwdSamePlatform(guestPwdFull(r), p.Platform, p.URL, 0) {
|
||||||
|
passwordReply(&c.Controller, 400, "同平台同网址记录已存在,请勿重复添加", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it := PasswordStoreItemDTO{
|
||||||
|
Platform: p.Platform, URL: p.URL, Accounts: p.Accounts, Remark: p.Remark,
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
}
|
||||||
|
newID, err := gueststore.Insert(uint64(cl.UserID), guestModulePasswordStore, it)
|
||||||
|
if err != nil {
|
||||||
|
passwordReply(&c.Controller, 500, "创建失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it.ID = newID
|
||||||
|
passwordReply(&c.Controller, 200, "success", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppPasswordStoreController) guestUpdate(cl *jwtutil.Claims, id uint64, p passwordPayload) {
|
||||||
|
r, err := gueststore.Get(uint64(cl.UserID), guestModulePasswordStore, id)
|
||||||
|
if err != nil {
|
||||||
|
passwordReply(&c.Controller, 404, "记录不存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := gueststore.List(uint64(cl.UserID), guestModulePasswordStore)
|
||||||
|
if err != nil {
|
||||||
|
passwordReply(&c.Controller, 500, "查询失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, rr := range rows {
|
||||||
|
if guestPwdSamePlatform(guestPwdFull(rr), p.Platform, p.URL, id) {
|
||||||
|
passwordReply(&c.Controller, 400, "同平台同网址记录已存在,请勿重复添加", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it := guestPwdFull(r)
|
||||||
|
it.Platform = p.Platform
|
||||||
|
it.URL = p.URL
|
||||||
|
it.Accounts = p.Accounts
|
||||||
|
it.Remark = p.Remark
|
||||||
|
if err = gueststore.Update(uint64(cl.UserID), guestModulePasswordStore, id, it); err != nil {
|
||||||
|
passwordReply(&c.Controller, 500, "更新失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
passwordReply(&c.Controller, 200, "success", it)
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"server/models"
|
"server/models"
|
||||||
"server/pkg/identity"
|
"server/pkg/identity"
|
||||||
"server/pkg/jwtutil"
|
"server/pkg/jwtutil"
|
||||||
|
"server/services/gueststore"
|
||||||
|
|
||||||
"github.com/beego/beego/v2/client/orm"
|
"github.com/beego/beego/v2/client/orm"
|
||||||
beego "github.com/beego/beego/v2/server/web"
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
@@ -22,9 +23,9 @@ type AppReminderController struct {
|
|||||||
beego.Controller
|
beego.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只放行移动端与租户端令牌:/api/** 仅被 uniapp 调用,平台令牌不该能打移动端接口。
|
// 放行移动端、租户端与游客令牌:游客调用时不进业务表,分流到 gueststore。
|
||||||
func (c *AppReminderController) appClaims() (*jwtutil.Claims, error) {
|
func (c *AppReminderController) appClaims() (*jwtutil.Claims, error) {
|
||||||
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp, identity.EndBackend)
|
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp, identity.EndBackend, identity.EndGuest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AppReminderController) jsonErr(httpStatus, bizCode int, msg string) {
|
func (c *AppReminderController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||||
@@ -75,6 +76,11 @@ func (c *AppReminderController) GetList() {
|
|||||||
pageSize = 20
|
pageSize = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestList(claims, page, pageSize, keyword, status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
qs := models.Orm.QueryTable(new(models.BackendSchedule)).
|
qs := models.Orm.QueryTable(new(models.BackendSchedule)).
|
||||||
Filter("user_id", claims.UserID).
|
Filter("user_id", claims.UserID).
|
||||||
Filter("tid", claims.TenantId)
|
Filter("tid", claims.TenantId)
|
||||||
@@ -171,6 +177,11 @@ func (c *AppReminderController) GetDetail() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestDetail(claims, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var schedule models.BackendSchedule
|
var schedule models.BackendSchedule
|
||||||
err = models.Orm.QueryTable(new(models.BackendSchedule)).
|
err = models.Orm.QueryTable(new(models.BackendSchedule)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
@@ -272,6 +283,11 @@ func (c *AppReminderController) Create() {
|
|||||||
title = "日程提醒"
|
title = "日程提醒"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestCreate(claims, p, content, schedTime)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
schedule := models.BackendSchedule{
|
schedule := models.BackendSchedule{
|
||||||
Tid: claims.TenantId,
|
Tid: claims.TenantId,
|
||||||
Title: title,
|
Title: title,
|
||||||
@@ -363,6 +379,11 @@ func (c *AppReminderController) Update() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestUpdate(claims, id, p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var schedule models.BackendSchedule
|
var schedule models.BackendSchedule
|
||||||
err = models.Orm.QueryTable(new(models.BackendSchedule)).
|
err = models.Orm.QueryTable(new(models.BackendSchedule)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
@@ -482,6 +503,11 @@ func (c *AppReminderController) Delete() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestDelete(claims, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var schedule models.BackendSchedule
|
var schedule models.BackendSchedule
|
||||||
err = models.Orm.QueryTable(new(models.BackendSchedule)).
|
err = models.Orm.QueryTable(new(models.BackendSchedule)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
@@ -543,6 +569,11 @@ func (c *AppReminderController) ToggleComplete() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if identity.IsGuest(claims) {
|
||||||
|
c.guestToggle(claims, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var schedule models.BackendSchedule
|
var schedule models.BackendSchedule
|
||||||
err = models.Orm.QueryTable(new(models.BackendSchedule)).
|
err = models.Orm.QueryTable(new(models.BackendSchedule)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
@@ -587,3 +618,175 @@ func (c *AppReminderController) ToggleComplete() {
|
|||||||
"is_finished": newStatus == 2,
|
"is_finished": newStatus == 2,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 游客分支(免登录态):数据存 yz_guest_data,响应结构与登录态一致。
|
||||||
|
// 游客日程不接提醒发送链路(无短信/邮件推送),仅作待办记录;转正合并时重建提醒。 ----------
|
||||||
|
|
||||||
|
func guestScheduleItemMap(it guestScheduleItem) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"id": it.ID,
|
||||||
|
"content": it.Content,
|
||||||
|
"schedule_time": it.ScheduleTime,
|
||||||
|
"remind_channels": it.RemindChannels,
|
||||||
|
"is_finished": it.IsFinished,
|
||||||
|
"advance_minutes": it.AdvanceMinutes,
|
||||||
|
"repeat_interval_minutes": it.RepeatIntervalMinutes,
|
||||||
|
"max_send_count": it.MaxSendCount,
|
||||||
|
"created_at": it.CreatedAt,
|
||||||
|
"updated_at": it.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func guestScheduleFull(r gueststore.Row) guestScheduleItem {
|
||||||
|
var it guestScheduleItem
|
||||||
|
if json.Unmarshal(r.Payload, &it) != nil {
|
||||||
|
it = guestScheduleItem{Content: "(数据异常)"}
|
||||||
|
}
|
||||||
|
it.ID = r.ID
|
||||||
|
it.CreatedAt = r.CreateTime.Format("2006-01-02 15:04:05")
|
||||||
|
it.UpdatedAt = it.CreatedAt
|
||||||
|
if r.UpdateTime != nil {
|
||||||
|
it.UpdatedAt = r.UpdateTime.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
if it.RemindChannels == nil {
|
||||||
|
it.RemindChannels = []string{}
|
||||||
|
}
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppReminderController) guestList(claims *jwtutil.Claims, page, pageSize int, keyword, status string) {
|
||||||
|
rows, err := gueststore.List(uint64(claims.UserID), guestModuleSchedule)
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(500, 500, "查询失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]map[string]interface{}, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
it := guestScheduleFull(r)
|
||||||
|
if keyword != "" && !strings.Contains(it.Content, keyword) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if status == "pending" && it.IsFinished {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if status == "done" && !it.IsFinished {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, guestScheduleItemMap(it))
|
||||||
|
}
|
||||||
|
total := len(items)
|
||||||
|
start := (page - 1) * pageSize
|
||||||
|
if start > total {
|
||||||
|
start = total
|
||||||
|
}
|
||||||
|
end := start + pageSize
|
||||||
|
if end > total {
|
||||||
|
end = total
|
||||||
|
}
|
||||||
|
c.ok(map[string]interface{}{
|
||||||
|
"list": items[start:end], "total": total, "page": page, "pageSize": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppReminderController) guestDetail(claims *jwtutil.Claims, id uint64) {
|
||||||
|
r, err := gueststore.Get(uint64(claims.UserID), guestModuleSchedule, id)
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(404, 404, "日程未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(guestScheduleItemMap(guestScheduleFull(r)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppReminderController) guestCreate(claims *jwtutil.Claims, p appSchedulePayload, content string, schedTime time.Time) {
|
||||||
|
_ = schedTime
|
||||||
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
channels := p.RemindChannels
|
||||||
|
if channels == nil {
|
||||||
|
channels = []string{}
|
||||||
|
}
|
||||||
|
it := guestScheduleItem{
|
||||||
|
Content: content,
|
||||||
|
ScheduleTime: p.ScheduleTime,
|
||||||
|
RemindChannels: channels,
|
||||||
|
AdvanceMinutes: p.AdvanceMinutes,
|
||||||
|
RepeatIntervalMinutes: p.RepeatIntervalMinutes,
|
||||||
|
MaxSendCount: p.MaxSendCount,
|
||||||
|
IsFinished: false,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
newID, err := gueststore.Insert(uint64(claims.UserID), guestModuleSchedule, it)
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(500, 500, "保存日程失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(map[string]interface{}{"id": newID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppReminderController) guestUpdate(claims *jwtutil.Claims, id uint64, p appSchedulePayload) {
|
||||||
|
r, err := gueststore.Get(uint64(claims.UserID), guestModuleSchedule, id)
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(404, 404, "日程未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it := guestScheduleFull(r)
|
||||||
|
if it.IsFinished {
|
||||||
|
c.jsonErr(400, 400, "该日程提醒已全部结束,无法编辑")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if content := strings.TrimSpace(p.Content); content != "" {
|
||||||
|
it.Content = content
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(p.ScheduleTime) != "" {
|
||||||
|
if _, perr := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local); perr != nil {
|
||||||
|
c.jsonErr(400, 400, "日程时间格式不合法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it.ScheduleTime = p.ScheduleTime
|
||||||
|
}
|
||||||
|
if p.RemindChannels != nil {
|
||||||
|
it.RemindChannels = p.RemindChannels
|
||||||
|
}
|
||||||
|
it.AdvanceMinutes = p.AdvanceMinutes
|
||||||
|
it.RepeatIntervalMinutes = p.RepeatIntervalMinutes
|
||||||
|
it.MaxSendCount = p.MaxSendCount
|
||||||
|
it.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
if err = gueststore.Update(uint64(claims.UserID), guestModuleSchedule, id, it); err != nil {
|
||||||
|
c.jsonErr(500, 500, "更新失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppReminderController) guestDelete(claims *jwtutil.Claims, id uint64) {
|
||||||
|
r, err := gueststore.Get(uint64(claims.UserID), guestModuleSchedule, id)
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(404, 404, "日程未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if guestScheduleFull(r).IsFinished {
|
||||||
|
c.jsonErr(400, 400, "该日程提醒已全部结束,无法删除")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = gueststore.Delete(uint64(claims.UserID), guestModuleSchedule, id); err != nil {
|
||||||
|
c.jsonErr(500, 500, "删除失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AppReminderController) guestToggle(claims *jwtutil.Claims, id uint64) {
|
||||||
|
r, err := gueststore.Get(uint64(claims.UserID), guestModuleSchedule, id)
|
||||||
|
if err != nil {
|
||||||
|
c.jsonErr(404, 404, "日程未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it := guestScheduleFull(r)
|
||||||
|
it.IsFinished = !it.IsFinished
|
||||||
|
it.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
if err = gueststore.Update(uint64(claims.UserID), guestModuleSchedule, id, it); err != nil {
|
||||||
|
c.jsonErr(500, 500, "操作失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.ok(map[string]interface{}{"is_finished": it.IsFinished})
|
||||||
|
}
|
||||||
@@ -76,6 +76,7 @@ type menuPayload struct {
|
|||||||
Sort *int64 `json:"sort"`
|
Sort *int64 `json:"sort"`
|
||||||
Status *int8 `json:"status"`
|
Status *int8 `json:"status"`
|
||||||
IsVisible *int8 `json:"is_visible"`
|
IsVisible *int8 `json:"is_visible"`
|
||||||
|
RequireLogin *int8 `json:"require_login"` // 1=必须登录(默认) 0=可免登录(游客可用)
|
||||||
Views []int `json:"views"`
|
Views []int `json:"views"`
|
||||||
Platforms []string `json:"platforms"` // 展示端:["web"] ["app"] ["web","app"];空=继承所属模块
|
Platforms []string `json:"platforms"` // 展示端:["web"] ["app"] ["web","app"];空=继承所属模块
|
||||||
Type *int8 `json:"type"`
|
Type *int8 `json:"type"`
|
||||||
@@ -417,6 +418,7 @@ type menuNode struct {
|
|||||||
Sort int64 `json:"sort"`
|
Sort int64 `json:"sort"`
|
||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"`
|
||||||
IsVisible *int8 `json:"is_visible,omitempty"`
|
IsVisible *int8 `json:"is_visible,omitempty"`
|
||||||
|
RequireLogin int8 `json:"require_login"` // 1=必须登录 0=可免登录(游客)
|
||||||
Views []int `json:"views,omitempty"`
|
Views []int `json:"views,omitempty"`
|
||||||
Platforms []string `json:"platforms,omitempty"` // 展示端,空=继承所属模块
|
Platforms []string `json:"platforms,omitempty"` // 展示端,空=继承所属模块
|
||||||
Type int8 `json:"type"`
|
Type int8 `json:"type"`
|
||||||
@@ -436,6 +438,7 @@ func buildMenuTree(menus []models.SystemMenu, pid int64) []*menuNode {
|
|||||||
Sort: m.Sort,
|
Sort: m.Sort,
|
||||||
Status: m.Status,
|
Status: m.Status,
|
||||||
IsVisible: m.IsVisible,
|
IsVisible: m.IsVisible,
|
||||||
|
RequireLogin: m.RequireLogin,
|
||||||
Views: parseViews(m.Views),
|
Views: parseViews(m.Views),
|
||||||
Platforms: services.ParsePlatforms(m.Platforms),
|
Platforms: services.ParsePlatforms(m.Platforms),
|
||||||
Type: m.Type,
|
Type: m.Type,
|
||||||
@@ -514,6 +517,7 @@ func (c *BackendMenuController) CreateMenu() {
|
|||||||
Sort: valueInt64(payload.Sort, 0),
|
Sort: valueInt64(payload.Sort, 0),
|
||||||
Status: valueInt8(payload.Status, 1),
|
Status: valueInt8(payload.Status, 1),
|
||||||
IsVisible: ptrInt8(valueInt8(payload.IsVisible, 1)),
|
IsVisible: ptrInt8(valueInt8(payload.IsVisible, 1)),
|
||||||
|
RequireLogin: valueInt8(payload.RequireLogin, 1),
|
||||||
Views: &viewsStr,
|
Views: &viewsStr,
|
||||||
Type: valueInt8(payload.Type, 1),
|
Type: valueInt8(payload.Type, 1),
|
||||||
Path: ptrString(valueString(payload.Path, "")),
|
Path: ptrString(valueString(payload.Path, "")),
|
||||||
@@ -565,6 +569,7 @@ func (c *BackendMenuController) UpdateMenu() {
|
|||||||
"sort": valueInt64(payload.Sort, 0),
|
"sort": valueInt64(payload.Sort, 0),
|
||||||
"status": valueInt8(payload.Status, 1),
|
"status": valueInt8(payload.Status, 1),
|
||||||
"is_visible": valueInt8(payload.IsVisible, 1),
|
"is_visible": valueInt8(payload.IsVisible, 1),
|
||||||
|
"require_login": valueInt8(payload.RequireLogin, 1),
|
||||||
"views": string(viewsBytes),
|
"views": string(viewsBytes),
|
||||||
"platforms": services.NormalizePlatforms(payload.Platforms),
|
"platforms": services.NormalizePlatforms(payload.Platforms),
|
||||||
"type": valueInt8(payload.Type, 1),
|
"type": valueInt8(payload.Type, 1),
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -52,7 +52,6 @@ func (c *PlatformModulesController) modulesClaims() (*jwtutil.Claims, error) {
|
|||||||
return identity.ClaimsByPath(c.Ctx.Request.Header.Get("Authorization"), c.Ctx.Request.URL.Path)
|
return identity.ClaimsByPath(c.Ctx.Request.Header.Get("Authorization"), c.Ctx.Request.URL.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (c *PlatformModulesController) jsonErr(httpStatus, bizCode int, msg string) {
|
func (c *PlatformModulesController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||||
c.Ctx.Output.SetStatus(httpStatus)
|
c.Ctx.Output.SetStatus(httpStatus)
|
||||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||||
@@ -90,6 +89,7 @@ func (c *PlatformModulesController) GetList() {
|
|||||||
"sort": rows[i].Sort,
|
"sort": rows[i].Sort,
|
||||||
"status": rows[i].Status,
|
"status": rows[i].Status,
|
||||||
"is_show": rows[i].IsShow,
|
"is_show": rows[i].IsShow,
|
||||||
|
"require_login": rows[i].RequireLogin,
|
||||||
"platforms": platformList(&rows[i]),
|
"platforms": platformList(&rows[i]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,27 @@ func (c *PlatformModulesController) GetDetail() {
|
|||||||
c.jsonErr(404, 404, "模块不存在")
|
c.jsonErr(404, 404, "模块不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "获取成功", "data": row}
|
// 与 GetList 保持一致的规范化输出:platforms 统一为数组、带上 require_login。
|
||||||
|
// 否则详情回填时 platforms 是原始 JSON 字符串,编辑提交会因 []string 反序列化失败报 400。
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "获取成功",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"id": row.ID,
|
||||||
|
"mid": row.Mid,
|
||||||
|
"name": row.Name,
|
||||||
|
"code": row.Code,
|
||||||
|
"path": row.Path,
|
||||||
|
"icon": row.Icon,
|
||||||
|
"description": row.Description,
|
||||||
|
"type": row.Type,
|
||||||
|
"sort": row.Sort,
|
||||||
|
"status": row.Status,
|
||||||
|
"is_show": row.IsShow,
|
||||||
|
"require_login": row.RequireLogin,
|
||||||
|
"platforms": platformList(&row),
|
||||||
|
},
|
||||||
|
}
|
||||||
_ = c.ServeJSON()
|
_ = c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +192,18 @@ type modulePayload struct {
|
|||||||
Status int8 `json:"status"`
|
Status int8 `json:"status"`
|
||||||
IsShow int8 `json:"is_show"`
|
IsShow int8 `json:"is_show"`
|
||||||
Platforms []string `json:"platforms"` // 展示端:["web"] ["app"] ["web","app"];空=全端
|
Platforms []string `json:"platforms"` // 展示端:["web"] ["app"] ["web","app"];空=全端
|
||||||
|
RequireLogin *int8 `json:"require_login"` // 是否必须登录:1=必须 0=可免登录;指针以区分「未传」(默认 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveRequireLogin 未传该字段时回落为 1(必须登录),保持既有行为不变。
|
||||||
|
func resolveRequireLogin(v *int8) int8 {
|
||||||
|
if v == nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if *v != 0 && *v != 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return *v
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add POST /platform/modules
|
// Add POST /platform/modules
|
||||||
@@ -223,6 +255,7 @@ func (c *PlatformModulesController) Add() {
|
|||||||
Sort: p.Sort,
|
Sort: p.Sort,
|
||||||
Status: p.Status,
|
Status: p.Status,
|
||||||
IsShow: p.IsShow,
|
IsShow: p.IsShow,
|
||||||
|
RequireLogin: resolveRequireLogin(p.RequireLogin),
|
||||||
CreateTime: &now,
|
CreateTime: &now,
|
||||||
UpdateTime: &now,
|
UpdateTime: &now,
|
||||||
}
|
}
|
||||||
@@ -294,6 +327,7 @@ func (c *PlatformModulesController) Edit() {
|
|||||||
"sort": p.Sort,
|
"sort": p.Sort,
|
||||||
"status": p.Status,
|
"status": p.Status,
|
||||||
"is_show": p.IsShow,
|
"is_show": p.IsShow,
|
||||||
|
"require_login": resolveRequireLogin(p.RequireLogin),
|
||||||
"platforms": services.NormalizePlatforms(p.Platforms),
|
"platforms": services.NormalizePlatforms(p.Platforms),
|
||||||
"update_time": now,
|
"update_time": now,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- =============================================================
|
||||||
|
-- yz_system_menu 增加「是否必须登录」字段 require_login
|
||||||
|
--
|
||||||
|
-- 用途:免登录控制从模块级下沉到菜单级——Platform 菜单管理逐条配置
|
||||||
|
-- 某菜单(功能入口)是否允许移动端游客(持游客令牌)免登录使用。
|
||||||
|
-- 1 = 必须登录(默认,保持既有行为)
|
||||||
|
-- 0 = 可免登录使用(进入 nav 公开菜单 /api/menus/public)
|
||||||
|
--
|
||||||
|
-- 关联:yz_system_modules.require_login 保留但移动端不再作为判定依据,
|
||||||
|
-- 以菜单级开关为准(模块宫格是否可点 = 该模块下存在免登录菜单)。
|
||||||
|
--
|
||||||
|
-- 幂等:MySQL 的 ALTER TABLE ADD COLUMN 不支持 IF NOT EXISTS,
|
||||||
|
-- 重复执行会报 1060 Duplicate column name。这里用 information_schema
|
||||||
|
-- 判断列是否存在,缺失才拼 ADD COLUMN,再通过 PREPARE 执行。
|
||||||
|
-- 约定:本脚本在 mysql 客户端手动执行(同一会话保持 PREPARE 生效)。
|
||||||
|
-- =============================================================
|
||||||
|
|
||||||
|
SET @sql := CONCAT(
|
||||||
|
'ALTER TABLE `yz_system_menu` ',
|
||||||
|
IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'yz_system_menu'
|
||||||
|
AND COLUMN_NAME = 'require_login') = 0,
|
||||||
|
'ADD COLUMN `require_login` TINYINT NOT NULL DEFAULT 1 COMMENT ''1=必须登录 0=可免登录使用'' AFTER `is_visible`',
|
||||||
|
''
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 列已存在时 @sql 仅剩 'ALTER TABLE `yz_system_menu` ',替换为无害语句
|
||||||
|
SET @sql := IF(RIGHT(TRIM(@sql), 1) = '`', 'SELECT 1 AS column_already_exists', @sql);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- =============================================================
|
||||||
|
-- 初始放开「便捷工具」的三个免登录功能(确认菜单标题后再执行):
|
||||||
|
--
|
||||||
|
-- SELECT id, title, path FROM `yz_system_menu`
|
||||||
|
-- WHERE title IN ('记事本', '日程提醒', '账号管理') AND delete_time IS NULL;
|
||||||
|
--
|
||||||
|
-- UPDATE `yz_system_menu`
|
||||||
|
-- SET `require_login` = 0
|
||||||
|
-- WHERE title IN ('记事本', '日程提醒', '账号管理');
|
||||||
|
--
|
||||||
|
-- 说明:父级目录菜单如需游客可见,也要一并置 0;
|
||||||
|
-- 后续新模块按需放开,全部走 Platform 菜单管理界面配置。
|
||||||
|
-- =============================================================
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
-- =============================================================
|
||||||
|
-- yz_system_modules 增加「是否必须登录」字段 require_login
|
||||||
|
--
|
||||||
|
-- 用途:Platform 模块管理可配置某业务模块是否允许移动端免登录访问。
|
||||||
|
-- 1 = 必须登录(默认,保持既有行为)
|
||||||
|
-- 0 = 可免登录使用
|
||||||
|
--
|
||||||
|
-- 幂等:MySQL 的 ALTER TABLE ADD COLUMN 不支持 IF NOT EXISTS,
|
||||||
|
-- 重复执行会报 1060 Duplicate column name。这里用 information_schema
|
||||||
|
-- 判断列是否存在,缺失才拼 ADD COLUMN,再通过 PREPARE 执行。
|
||||||
|
-- 约定:本脚本在 mysql 客户端手动执行(同一会话保持 PREPARE 生效)。
|
||||||
|
-- =============================================================
|
||||||
|
|
||||||
|
SET @sql := CONCAT(
|
||||||
|
'ALTER TABLE `yz_system_modules` ',
|
||||||
|
IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'yz_system_modules'
|
||||||
|
AND COLUMN_NAME = 'require_login') = 0,
|
||||||
|
'ADD COLUMN `require_login` TINYINT NOT NULL DEFAULT 1 COMMENT ''1=必须登录 0=可免登录使用'' AFTER `is_show`',
|
||||||
|
''
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 列已存在时 @sql 仅剩 'ALTER TABLE `yz_system_modules` ',替换为无害语句
|
||||||
|
SET @sql := IF(RIGHT(TRIM(@sql), 1) = '`', 'SELECT 1 AS column_already_exists', @sql);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- =============================================================
|
||||||
|
-- 方案 B(兜底):不支持会话变量 / PREPARE 的执行器(工单系统等),
|
||||||
|
-- 改用下面的纯静态语句。注意:本条不幂等,列已存在会报
|
||||||
|
-- 1060 Duplicate column,执行前先用 information_schema 自查:
|
||||||
|
--
|
||||||
|
-- SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
-- WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
-- AND TABLE_NAME = 'yz_system_modules'
|
||||||
|
-- AND COLUMN_NAME = 'require_login'; -- 返回 0 才执行 ALTER
|
||||||
|
--
|
||||||
|
-- ALTER TABLE `yz_system_modules`
|
||||||
|
-- ADD COLUMN `require_login` TINYINT NOT NULL DEFAULT 1
|
||||||
|
-- COMMENT '1=必须登录 0=可免登录使用' AFTER `is_show`;
|
||||||
|
-- =============================================================
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- =============================================================
|
||||||
|
-- 游客(免登录模块)身份与通用数据表
|
||||||
|
-- 对应代码:models.GuestUser / models.GuestData、services/gueststore
|
||||||
|
-- 幂等:CREATE TABLE IF NOT EXISTS,可重复执行
|
||||||
|
-- 用途:小程序用手机号授权换取“游客轻身份”,免登录模块产生的数据
|
||||||
|
-- 按 guest_id + module 存入 yz_guest_data(文档式,永久保留不清理);
|
||||||
|
-- 正式登录后按手机号把数据合并进正式账号(merged_uid 标记,行置 is_deleted=2)。
|
||||||
|
-- =============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `yz_guest_user` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '微信验证手机号(游客唯一标识)',
|
||||||
|
`openid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '小程序 openid(可选溯源)',
|
||||||
|
`unionid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'unionid(可为空)',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '首次授权时间',
|
||||||
|
`last_active_time` DATETIME NULL COMMENT '最近一次签发/活动时间',
|
||||||
|
`merged_uid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '转正并入的 AuthIdentity.ID,0=未合并',
|
||||||
|
`merged_time` DATETIME NULL COMMENT '合并时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_phone` (`phone`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='游客轻身份(微信手机号授权签发)';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `yz_guest_data` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`guest_id` BIGINT UNSIGNED NOT NULL COMMENT 'yz_guest_user.id',
|
||||||
|
`module` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '所属业务模块:notebook / schedule / passwordStore / …',
|
||||||
|
`payload` TEXT NOT NULL COMMENT '业务条目 JSON 序列化(结构由各模块自定义)',
|
||||||
|
`is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1已删除(保留不清理) 2已合并进正式账号',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` DATETIME NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_guest_module` (`guest_id`, `module`, `is_deleted`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='游客通用数据(按模块文档式存储,永久保留)';
|
||||||
@@ -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" }
|
||||||
@@ -77,6 +77,9 @@ func Init(_ string) {
|
|||||||
new(SystemDomainPool),
|
new(SystemDomainPool),
|
||||||
new(SystemTenantDomain),
|
new(SystemTenantDomain),
|
||||||
new(SystemModules),
|
new(SystemModules),
|
||||||
|
// 游客轻身份与通用数据(docs/sql/create_guest_tables.sql):免登录模块的临时存储
|
||||||
|
new(GuestUser),
|
||||||
|
new(GuestData),
|
||||||
new(StorageConfig),
|
new(StorageConfig),
|
||||||
new(TenantSiteSetting),
|
new(TenantSiteSetting),
|
||||||
new(PlatformSiteSetting),
|
new(PlatformSiteSetting),
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ type SystemMenu struct {
|
|||||||
Sort int64 `orm:"column(sort);default(0)" json:"sort"` // 排序号
|
Sort int64 `orm:"column(sort);default(0)" json:"sort"` // 排序号
|
||||||
Status int8 `orm:"column(status);default(0)" json:"status"` // 状态:1-启用,0-禁用
|
Status int8 `orm:"column(status);default(0)" json:"status"` // 状态:1-启用,0-禁用
|
||||||
IsVisible *int8 `orm:"column(is_visible);null" json:"isVisible"` // 是否显示:1-显示 0-不显示
|
IsVisible *int8 `orm:"column(is_visible);null" json:"isVisible"` // 是否显示:1-显示 0-不显示
|
||||||
|
// 是否必须登录才能使用:1=必须登录(默认) 0=可免登录(游客持游客令牌可用)。
|
||||||
|
// 移动端免登录控制从模块级下沉到菜单级:公开菜单接口 /api/menus/public 只下发 0 的菜单。
|
||||||
|
RequireLogin int8 `orm:"column(require_login);default(1)" json:"require_login"`
|
||||||
Views *string `orm:"column(views);size(255);null" json:"views"` // 菜单显示端(JSON数组字符串):[1]=平台端 [2]=租户端 [1,2]=双端
|
Views *string `orm:"column(views);size(255);null" json:"views"` // 菜单显示端(JSON数组字符串):[1]=平台端 [2]=租户端 [1,2]=双端
|
||||||
Platforms *string `orm:"column(platforms);size(64);null" json:"platforms"` // 展示端(JSON数组字符串):["web"] ["app"] ["web","app"];空/NULL=全端
|
Platforms *string `orm:"column(platforms);size(64);null" json:"platforms"` // 展示端(JSON数组字符串):["web"] ["app"] ["web","app"];空/NULL=全端
|
||||||
Type int8 `orm:"column(type)" json:"type"` // 菜单类型:1-目录,2-页面,3-接口
|
Type int8 `orm:"column(type)" json:"type"` // 菜单类型:1-目录,2-页面,3-接口
|
||||||
@@ -38,4 +41,3 @@ type SystemMenu struct {
|
|||||||
func (m *SystemMenu) TableName() string {
|
func (m *SystemMenu) TableName() string {
|
||||||
return "yz_system_menu"
|
return "yz_system_menu"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,6 +15,9 @@ type SystemModules struct {
|
|||||||
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
||||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 0禁用 1启用
|
Status int8 `orm:"column(status);default(1)" json:"status"` // 0禁用 1启用
|
||||||
IsShow int8 `orm:"column(is_show);default(1)" json:"is_show"` // 0否 1是
|
IsShow int8 `orm:"column(is_show);default(1)" json:"is_show"` // 0否 1是
|
||||||
|
// 是否必须登录才能进入该模块:1=必须登录(默认,保持既有行为) 0=可免登录使用。
|
||||||
|
// 移动端 nav 未登录时据此决定宫格卡片点击是「进登录引导」还是「直接进入」。
|
||||||
|
RequireLogin int8 `orm:"column(require_login);default(1)" json:"require_login"`
|
||||||
// 展示端(JSON 数组字符串):["web"] ["app"] ["web","app"];空/NULL=全端显示。
|
// 展示端(JSON 数组字符串):["web"] ["app"] ["web","app"];空/NULL=全端显示。
|
||||||
// 取值见 PlatformWeb / PlatformApp。菜单未单独配置时继承所属模块的展示端。
|
// 取值见 PlatformWeb / PlatformApp。菜单未单独配置时继承所属模块的展示端。
|
||||||
Platforms *string `orm:"column(platforms);size(64);null" json:"platforms"`
|
Platforms *string `orm:"column(platforms);size(64);null" json:"platforms"`
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const (
|
|||||||
EndPlatform End = "platform" // 平台端 /platform/**
|
EndPlatform End = "platform" // 平台端 /platform/**
|
||||||
EndBackend End = "backend" // 租户(用户)端 /backend/**
|
EndBackend End = "backend" // 租户(用户)端 /backend/**
|
||||||
EndApp End = "app" // 移动端与小程序 /api/**
|
EndApp End = "app" // 移动端与小程序 /api/**
|
||||||
|
// EndGuest 游客轻身份:小程序手机号授权签发的临时令牌(user_type=guest),
|
||||||
|
// 仅免登录业务控制器显式放行,数据落在 yz_guest_data,转正后按手机号合并。
|
||||||
|
EndGuest End = "guest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 错误分类直接决定 HTTP 状态码,因此必须与前端拦截器行为对齐:
|
// 错误分类直接决定 HTTP 状态码,因此必须与前端拦截器行为对齐:
|
||||||
@@ -93,6 +96,12 @@ func Claims(auth string, allowed ...End) (*jwtutil.Claims, error) {
|
|||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsGuest 判断解析结果是否为游客令牌(user_type=guest)。
|
||||||
|
// 控制器据此分流:游客请求不进业务表,走 gueststore 通用存储。
|
||||||
|
func IsGuest(claims *jwtutil.Claims) bool {
|
||||||
|
return claims != nil && claims.UserType == string(EndGuest)
|
||||||
|
}
|
||||||
|
|
||||||
// PathEnd 按路由前缀判定接口属于哪个端;前缀不属于任何端时 ok=false。
|
// PathEnd 按路由前缀判定接口属于哪个端;前缀不属于任何端时 ok=false。
|
||||||
func PathEnd(path string) (End, bool) {
|
func PathEnd(path string) (End, bool) {
|
||||||
p := strings.ToLower(path)
|
p := strings.ToLower(path)
|
||||||
|
|||||||
@@ -170,6 +170,19 @@ func Register() {
|
|||||||
|
|
||||||
// 工作台聚合(ERP/OA/CRM/CMS 应用门户首屏数据)
|
// 工作台聚合(ERP/OA/CRM/CMS 应用门户首屏数据)
|
||||||
beego.Router("/api/home", &controllers.AppAggregateController{}, "get:Home")
|
beego.Router("/api/home", &controllers.AppAggregateController{}, "get:Home")
|
||||||
|
// 移动端未登录时的模块宫格(公开:返回模块列表 + require_login 标记,路径不在鉴权前缀内)
|
||||||
|
beego.Router("/api/modules/public", &controllers.AppAggregateController{}, "get:PublicModules")
|
||||||
|
// 移动端免登录菜单(公开:菜单级 require_login=0 的集合,游客可用功能的唯一入口清单)
|
||||||
|
beego.Router("/api/menus/public", &controllers.AppAggregateController{}, "get:PublicMenus")
|
||||||
|
|
||||||
|
// ===== 游客(免登录模块)=====
|
||||||
|
// 微信手机号授权签发游客令牌(公开接口,不在鉴权前缀内,凭证由控制器校验)
|
||||||
|
beego.Router("/api/guest/mini-login", &controllers.GuestController{}, "post:MiniLogin")
|
||||||
|
// 正式登录后把游客数据并入正式账号(需正式令牌)
|
||||||
|
beego.Router("/api/guest/merge", &controllers.GuestController{}, "post:Merge")
|
||||||
|
// 通用游客数据接口(需游客令牌,供后续免登录模块直接复用)
|
||||||
|
beego.Router("/api/guest/data", &controllers.GuestController{}, "get:DataList;post:DataCreate")
|
||||||
|
beego.Router("/api/guest/data/:id", &controllers.GuestController{}, "put:DataUpdate;delete:DataDelete")
|
||||||
// 移动端用户自定义「常用功能」保存(读取随 /api/home 下发 quick_menu_ids)
|
// 移动端用户自定义「常用功能」保存(读取随 /api/home 下发 quick_menu_ids)
|
||||||
beego.Router("/api/quick-menus", &controllers.AppAggregateController{}, "post:SaveQuickMenus")
|
beego.Router("/api/quick-menus", &controllers.AppAggregateController{}, "post:SaveQuickMenus")
|
||||||
}
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -98,6 +98,17 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="免登录访问" prop="require_login">
|
||||||
|
<el-switch
|
||||||
|
v-model="currentMenu.require_login"
|
||||||
|
:active-value="0"
|
||||||
|
:inactive-value="1"
|
||||||
|
/>
|
||||||
|
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary);">
|
||||||
|
开启后移动端游客(微信手机号授权)可免登录使用此功能;登录后游客数据自动并入账号。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="菜单显示" prop="views">
|
<el-form-item label="菜单显示" prop="views">
|
||||||
<el-checkbox-group v-model="currentMenu.views" class="views-inline">
|
<el-checkbox-group v-model="currentMenu.views" class="views-inline">
|
||||||
<el-checkbox :value="1" label="平台端" />
|
<el-checkbox :value="1" label="平台端" />
|
||||||
@@ -137,6 +148,7 @@ interface Menu {
|
|||||||
sort: number;
|
sort: number;
|
||||||
status: 0 | 1;
|
status: 0 | 1;
|
||||||
is_visible: 0 | 1;
|
is_visible: 0 | 1;
|
||||||
|
require_login?: 0 | 1; // 1=必须登录(默认) 0=可免登录(移动端游客)
|
||||||
views: number[];
|
views: number[];
|
||||||
type: 1 | 2 | 3;
|
type: 1 | 2 | 3;
|
||||||
permission: string;
|
permission: string;
|
||||||
@@ -182,6 +194,7 @@ const currentMenu = ref<Partial<Menu>>({
|
|||||||
sort: 0,
|
sort: 0,
|
||||||
status: 1,
|
status: 1,
|
||||||
is_visible: 1,
|
is_visible: 1,
|
||||||
|
require_login: 1,
|
||||||
views: [1],
|
views: [1],
|
||||||
type: 1,
|
type: 1,
|
||||||
permission: '',
|
permission: '',
|
||||||
@@ -252,6 +265,7 @@ watch(() => props.menu, (newMenu) => {
|
|||||||
sort: 0,
|
sort: 0,
|
||||||
status: 1,
|
status: 1,
|
||||||
is_visible: 1,
|
is_visible: 1,
|
||||||
|
require_login: 1,
|
||||||
views: [1],
|
views: [1],
|
||||||
type: 1,
|
type: 1,
|
||||||
permission: '',
|
permission: '',
|
||||||
@@ -290,6 +304,7 @@ watch(() => props.visible, (newVisible) => {
|
|||||||
sort: 0,
|
sort: 0,
|
||||||
status: 1,
|
status: 1,
|
||||||
is_visible: 1,
|
is_visible: 1,
|
||||||
|
require_login: 1,
|
||||||
views: [1],
|
views: [1],
|
||||||
type: 1,
|
type: 1,
|
||||||
permission: '',
|
permission: '',
|
||||||
|
|||||||
@@ -95,6 +95,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="require_login" label="免登录" width="90" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.require_login === 0" type="success">游客可用</el-tag>
|
||||||
|
<span v-else style="color: var(--el-text-color-secondary)">需登录</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column prop="views" label="菜单显示" width="180" align="center">
|
<el-table-column prop="views" label="菜单显示" width="180" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="Array.isArray(scope.row.views) && scope.row.views.includes(1)" type="primary" style="margin-right:6px;">
|
<el-tag v-if="Array.isArray(scope.row.views) && scope.row.views.includes(1)" type="primary" style="margin-right:6px;">
|
||||||
|
|||||||
@@ -198,6 +198,7 @@
|
|||||||
:inactive-value="0"
|
:inactive-value="0"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<!-- 免登录控制已下沉到菜单级(系统菜单 · 免登录访问),模块级不再提供配置入口 -->
|
||||||
<el-form-item label="状态" prop="status">
|
<el-form-item label="状态" prop="status">
|
||||||
<el-radio-group v-model="formData.status">
|
<el-radio-group v-model="formData.status">
|
||||||
<el-radio :label="1">启用</el-radio>
|
<el-radio :label="1">启用</el-radio>
|
||||||
@@ -246,6 +247,7 @@ const formData = ref({
|
|||||||
sort: 0,
|
sort: 0,
|
||||||
is_show: 1,
|
is_show: 1,
|
||||||
status: 1,
|
status: 1,
|
||||||
|
require_login: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -292,6 +294,7 @@ function handleAdd() {
|
|||||||
sort: 0,
|
sort: 0,
|
||||||
is_show: 1,
|
is_show: 1,
|
||||||
status: 1,
|
status: 1,
|
||||||
|
require_login: 1,
|
||||||
};
|
};
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -1,10 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getBaseURL } from '@/api/request.js'
|
import { getBaseURL } from '@/api/request.js'
|
||||||
import { isLoggedIn } from '@/utils/auth.js'
|
import { isLoggedIn } from '@/utils/auth.js'
|
||||||
|
import { getGuestToken } from '@/utils/guest.js'
|
||||||
|
|
||||||
const LOGIN_PAGE = '/pages/login/login'
|
const LOGIN_PAGE = '/pages/login/login'
|
||||||
// 不需要登录即可停留的页面:登录 / 注册 / 忘记密码 / 人机验证 / 小程序码确认页 / 启动跳板页
|
// 不需要登录即可停留的页面:门户首页(默认落地)/ 登录 / 注册 / 忘记密码 / 人机验证 / 小程序码确认页 / 启动跳板页
|
||||||
const PUBLIC_ROUTES = [
|
const PUBLIC_ROUTES = [
|
||||||
|
'pages/nav/nav',
|
||||||
'pages/login/login',
|
'pages/login/login',
|
||||||
'pages/login/register',
|
'pages/login/register',
|
||||||
'pages/login/forget',
|
'pages/login/forget',
|
||||||
@@ -25,11 +27,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 未登录一律回登录页:覆盖「切后台回来」「从其他入口直接进内页」等场景 */
|
/** 未登录一律回登录页:覆盖「切后台回来」「从其他入口直接进内页」等场景。
|
||||||
|
* 游客令牌(手机号授权签发)可停留任意页面:免登录范围由菜单级配置决定,
|
||||||
|
* 能进哪些页、能调哪些接口由后端公开菜单与控制器 guest 分支兜底;
|
||||||
|
* 无令牌时回门户 nav 触发授权引导,而不是直接甩进登录页。 */
|
||||||
function ensureLogin() {
|
function ensureLogin() {
|
||||||
const route = currentRoute()
|
const route = currentRoute()
|
||||||
if (!route || PUBLIC_ROUTES.indexOf(route) !== -1) return
|
if (!route || PUBLIC_ROUTES.indexOf(route) !== -1) return
|
||||||
if (isLoggedIn()) return
|
if (isLoggedIn()) return
|
||||||
|
if (getGuestToken()) return
|
||||||
uni.reLaunch({ url: LOGIN_PAGE })
|
uni.reLaunch({ url: LOGIN_PAGE })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 游客轻身份接口(后端 controllers/guest.go)
|
||||||
|
* 失败均由调用方静默处理,不弹全局 toast。
|
||||||
|
*/
|
||||||
|
import { request } from '@/api/request.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游客登录:微信手机号动态凭证换游客令牌
|
||||||
|
* @param {{ code: string, phoneCode: string, appid?: string }} params code=uni.login 的登录凭证
|
||||||
|
* @returns {Promise<{ token: string, guest_id: number, phone_mask: string, expires_in: number }>}
|
||||||
|
*/
|
||||||
|
export function guestMiniLogin({ code = '', phoneCode = '', appid = '' } = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/api/guest/mini-login',
|
||||||
|
method: 'POST',
|
||||||
|
data: { code, phone_code: phoneCode, appid },
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转正合并:正式登录令牌调用,把该手机号下的游客数据并入账号
|
||||||
|
* @returns {Promise<object>} 各模块合并条数,如 { notebook: 3, schedule: 1, passwordStore: 2 }
|
||||||
|
*/
|
||||||
|
export function guestMerge() {
|
||||||
|
return request({
|
||||||
|
url: '/api/guest/merge',
|
||||||
|
method: 'POST',
|
||||||
|
data: {},
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
}
|
||||||
+45
-10
@@ -2,7 +2,8 @@
|
|||||||
* 统一请求封装:baseURL、Bearer Token、业务 code 处理。
|
* 统一请求封装:baseURL、Bearer Token、业务 code 处理。
|
||||||
* 接口地址统一从 .env 的 VITE_API_BASE_URL 读取(见 api/config.js)。
|
* 接口地址统一从 .env 的 VITE_API_BASE_URL 读取(见 api/config.js)。
|
||||||
*/
|
*/
|
||||||
import { getToken, logout, isSSOEnabled, getRefreshToken, setAuthTokens } from '@/utils/auth.js'
|
import { getToken, logout, isSSOEnabled, getRefreshToken, setAuthTokens, isLoggedIn } from '@/utils/auth.js'
|
||||||
|
import { getGuestToken, clearGuestToken } from '@/utils/guest.js'
|
||||||
import { resolveBaseURL, getBaseURL } from '@/api/config.js'
|
import { resolveBaseURL, getBaseURL } from '@/api/config.js'
|
||||||
|
|
||||||
export { getBaseURL }
|
export { getBaseURL }
|
||||||
@@ -74,14 +75,11 @@ async function replayRequest(options) {
|
|||||||
*/
|
*/
|
||||||
export function request(options = {}) {
|
export function request(options = {}) {
|
||||||
const { url, method = 'GET', data, header = {}, silent = false, timeout = 30000 } = options
|
const { url, method = 'GET', data, header = {}, silent = false, timeout = 30000 } = options
|
||||||
const token = getToken()
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...header
|
...header
|
||||||
}
|
}
|
||||||
if (token) {
|
applyAuth(headers)
|
||||||
headers.Authorization = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullUrl = buildFullUrl(url)
|
const fullUrl = buildFullUrl(url)
|
||||||
|
|
||||||
@@ -97,6 +95,19 @@ export function request(options = {}) {
|
|||||||
const body = res.data || {}
|
const body = res.data || {}
|
||||||
|
|
||||||
if (status === 401 || body.code === 401) {
|
if (status === 401 || body.code === 401) {
|
||||||
|
// 游客令牌过期/被吊销:只清游客态并回门户重新授权,
|
||||||
|
// 不进登录引导(免登录模块对游客令牌报 403 端不匹配,不会走到这里)。
|
||||||
|
if (!isLoggedIn() && getGuestToken()) {
|
||||||
|
clearGuestToken()
|
||||||
|
if (!silent) {
|
||||||
|
uni.showToast({ title: '免登录授权已过期,请重新授权', icon: 'none' })
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({ url: '/pages/nav/nav' })
|
||||||
|
}, 400)
|
||||||
|
reject(new Error(body.msg || '未授权'))
|
||||||
|
return
|
||||||
|
}
|
||||||
// 统一认证模式:先用 refresh_token 静默续期并重放请求,
|
// 统一认证模式:先用 refresh_token 静默续期并重放请求,
|
||||||
// 续期失败(令牌过期/被吊销/被踢下线)才真正登出。
|
// 续期失败(令牌过期/被吊销/被踢下线)才真正登出。
|
||||||
if (isSSOEnabled()) {
|
if (isSSOEnabled()) {
|
||||||
@@ -111,8 +122,19 @@ export function request(options = {}) {
|
|||||||
if (!silent) {
|
if (!silent) {
|
||||||
uni.showToast({ title: body.msg || '请重新登录', icon: 'none' })
|
uni.showToast({ title: body.msg || '请重新登录', icon: 'none' })
|
||||||
}
|
}
|
||||||
|
// 门户首页 nav 是默认落地页,允许游客停留。
|
||||||
|
// 若 401 发生在 nav(多为残留/过期令牌触发的 /api/home),
|
||||||
|
// 登出后回 nav 以游客态重载,而不是强跳统一认证登录页;
|
||||||
|
// 业务内页(需登录)仍照常引导到登录。
|
||||||
|
const onNav = (() => {
|
||||||
|
const ps = (typeof getCurrentPages === 'function' && getCurrentPages()) || []
|
||||||
|
const cur = ps.length ? ps[ps.length - 1] : null
|
||||||
|
if (!cur) return false
|
||||||
|
const route = cur.route || (cur.$page && cur.$page.path) || ''
|
||||||
|
return String(route).replace(/^\//, '') === 'pages/nav/nav'
|
||||||
|
})()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
uni.reLaunch({ url: onNav ? '/pages/nav/nav' : '/pages/login/login' })
|
||||||
}, 400)
|
}, 400)
|
||||||
reject(new Error(body.msg || '未授权'))
|
reject(new Error(body.msg || '未授权'))
|
||||||
return
|
return
|
||||||
@@ -157,17 +179,30 @@ export function request(options = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入 Authorization:正式令牌优先;未登录但持有游客令牌(手机号授权签发)时带游客令牌,
|
||||||
|
* 由后端各控制器自行决定是否放行 guest 端(不放行者报 403,不影响游客态浏览)。
|
||||||
|
*/
|
||||||
|
function applyAuth(headers) {
|
||||||
|
const token = getToken()
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = `Bearer ${token}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const guestToken = getGuestToken()
|
||||||
|
if (guestToken) {
|
||||||
|
headers.Authorization = `Bearer ${guestToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 返回完整响应体(含 code/msg),用于需要自行判断 code 的场景 */
|
/** 返回完整响应体(含 code/msg),用于需要自行判断 code 的场景 */
|
||||||
export function requestRaw(options = {}) {
|
export function requestRaw(options = {}) {
|
||||||
const { url, method = 'GET', data, header = {} } = options
|
const { url, method = 'GET', data, header = {} } = options
|
||||||
const token = getToken()
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...header
|
...header
|
||||||
}
|
}
|
||||||
if (token) {
|
applyAuth(headers)
|
||||||
headers.Authorization = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullUrl = buildFullUrl(url)
|
const fullUrl = buildFullUrl(url)
|
||||||
|
|
||||||
|
|||||||
@@ -33,3 +33,32 @@ export function clearHomeCache() {
|
|||||||
cache = null
|
cache = null
|
||||||
cacheAt = 0
|
cacheAt = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 免登录模块宫格:GET /api/modules/public(公开接口,无需 token)。
|
||||||
|
* 返回 [{ id, code, name, icon, path, description, require_login }]。
|
||||||
|
* 供未登录用户渲染 nav 首页,按 require_login 决定点击是「进登录引导」还是「直接进入」。
|
||||||
|
*/
|
||||||
|
export function fetchPublicModules() {
|
||||||
|
return request({
|
||||||
|
url: '/api/modules/public',
|
||||||
|
method: 'GET',
|
||||||
|
silent: true,
|
||||||
|
timeout: 15000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 免登录菜单:GET /api/menus/public(公开接口,无需 token)。
|
||||||
|
* 返回 [{ id, pid, title, path, icon, type, sort, module, leaf }],
|
||||||
|
* 菜单级 require_login=0 的集合:游客(持游客令牌)可用功能的唯一入口清单,
|
||||||
|
* nav 宫格据此判定模块是否可免登录进入,子系统据此渲染游客功能列表。
|
||||||
|
*/
|
||||||
|
export function fetchPublicMenus() {
|
||||||
|
return request({
|
||||||
|
url: '/api/menus/public',
|
||||||
|
method: 'GET',
|
||||||
|
silent: true,
|
||||||
|
timeout: 15000
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -62,7 +62,6 @@ import { mpHeadPad } from '@/utils/mpHead.js'
|
|||||||
|
|
||||||
const headPad = mpHeadPad()
|
const headPad = mpHeadPad()
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { isLoggedIn } from '@/utils/auth.js'
|
|
||||||
import { openMenu, WEB_RULES } from '@/utils/nav.js'
|
import { openMenu, WEB_RULES } from '@/utils/nav.js'
|
||||||
import { useSubsystem } from '@/utils/subsystem.js'
|
import { useSubsystem } from '@/utils/subsystem.js'
|
||||||
import AppTabbar from '@/components/AppTabbar.vue'
|
import AppTabbar from '@/components/AppTabbar.vue'
|
||||||
@@ -173,15 +172,13 @@ function goBackNav() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isLoggedIn()) {
|
// 菜单:登录走 /api/home,游客走公开免登录菜单(菜单级 require_login 控制),
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
// 无免登录菜单的子系统游客看到空列表,不强制跳登录
|
||||||
return
|
|
||||||
}
|
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (isLoggedIn() && !loading.value) load()
|
if (!loading.value) load()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ import { mpHeadPad } from '@/utils/mpHead.js'
|
|||||||
const headPad = mpHeadPad()
|
const headPad = mpHeadPad()
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { isLoggedIn } from '@/utils/auth.js'
|
import { isLoggedIn } from '@/utils/auth.js'
|
||||||
|
import { getGuestToken } from '@/utils/guest.js'
|
||||||
import { request } from '@/api/request.js'
|
import { request } from '@/api/request.js'
|
||||||
import { clearHomeCache } from '@/api/workspace.js'
|
import { clearHomeCache } from '@/api/workspace.js'
|
||||||
import { openMenu } from '@/utils/nav.js'
|
import { openMenu } from '@/utils/nav.js'
|
||||||
@@ -406,18 +407,17 @@ function goNoticeDetail(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isLoggedIn()) {
|
// 菜单:登录走 /api/home,游客走公开免登录菜单(菜单级 require_login 控制);
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
// 待办等受保护数据需登录或持游客令牌(手机号授权)才拉,失败不阻断外壳
|
||||||
return
|
|
||||||
}
|
|
||||||
load()
|
load()
|
||||||
|
if (!isLoggedIn() && !getGuestToken()) return
|
||||||
loadTodo()
|
loadTodo()
|
||||||
loadNotices()
|
loadNotices()
|
||||||
loadDashboard()
|
loadDashboard()
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (isLoggedIn() && !loading.value) {
|
if ((isLoggedIn() || getGuestToken()) && !loading.value) {
|
||||||
loadTodo()
|
loadTodo()
|
||||||
loadNotices()
|
loadNotices()
|
||||||
loadDashboard()
|
loadDashboard()
|
||||||
|
|||||||
@@ -206,10 +206,8 @@ function handleLogout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isLoggedIn()) {
|
// 游客进入免登录模块:停留外壳,不拉受保护数据,也不强制跳登录
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
if (!isLoggedIn()) return
|
||||||
return
|
|
||||||
}
|
|
||||||
load()
|
load()
|
||||||
loadStats()
|
loadStats()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -116,10 +116,8 @@ function goSchedules() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isLoggedIn()) {
|
// 游客进入免登录模块:停留外壳,不拉受保护数据,也不强制跳登录
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
if (!isLoggedIn()) return
|
||||||
return
|
|
||||||
}
|
|
||||||
load()
|
load()
|
||||||
loadTodo()
|
loadTodo()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -64,6 +64,12 @@
|
|||||||
<FaIcon name="chevron-right" color="#94a3b8" :size="12" />
|
<FaIcon name="chevron-right" color="#94a3b8" :size="12" />
|
||||||
<view class="alt-line" />
|
<view class="alt-line" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 返回门户:不登录也能先用免登录功能 -->
|
||||||
|
<view class="back-portal" @tap="goPortal">
|
||||||
|
<FaIcon name="chevron-left" color="#94a3b8" :size="12" />
|
||||||
|
<text class="back-portal-text">返回门户,先使用免登录功能</text>
|
||||||
|
</view>
|
||||||
</block>
|
</block>
|
||||||
|
|
||||||
<!-- ============ 账号密码登录(独立视图) ============ -->
|
<!-- ============ 账号密码登录(独立视图) ============ -->
|
||||||
@@ -133,6 +139,12 @@
|
|||||||
<view class="back-row">
|
<view class="back-row">
|
||||||
<text class="tip-text">没有账号?请联系企业管理员开通</text>
|
<text class="tip-text">没有账号?请联系企业管理员开通</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 返回门户:不登录也能先用免登录功能 -->
|
||||||
|
<view class="back-portal" @tap="goPortal">
|
||||||
|
<FaIcon name="chevron-left" color="#94a3b8" :size="12" />
|
||||||
|
<text class="back-portal-text">返回门户,先使用免登录功能</text>
|
||||||
|
</view>
|
||||||
</block>
|
</block>
|
||||||
|
|
||||||
<!-- ============ 选择企业(一人多企业) ============ -->
|
<!-- ============ 选择企业(一人多企业) ============ -->
|
||||||
@@ -221,6 +233,8 @@ import {
|
|||||||
getToken
|
getToken
|
||||||
} from '@/utils/auth.js'
|
} from '@/utils/auth.js'
|
||||||
import { showGeetest4 } from '@/utils/geetest.js'
|
import { showGeetest4 } from '@/utils/geetest.js'
|
||||||
|
import { getGuestToken, clearGuestToken } from '@/utils/guest.js'
|
||||||
|
import { guestMerge } from '@/api/guest.js'
|
||||||
import FaIcon from '@/components/FaIcon.vue'
|
import FaIcon from '@/components/FaIcon.vue'
|
||||||
|
|
||||||
/** 运行端检测:h5 / apk(app-plus) / mp-weixin,决定登录方式优先级 */
|
/** 运行端检测:h5 / apk(app-plus) / mp-weixin,决定登录方式优先级 */
|
||||||
@@ -286,10 +300,10 @@ const brandPadTop = computed(() => {
|
|||||||
return `${statusBarHeight.value + uni.upx2px(88)}px`
|
return `${statusBarHeight.value + uni.upx2px(88)}px`
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 是否存在可返回的上级视图(账号密码视图仅在小程序端可退回主视图) */
|
/** 是否显示可返回的上级箭头(账号视图无论能否退回主视图都可见:退不了就回门户) */
|
||||||
const canGoBack = computed(() => {
|
const canGoBack = computed(() => {
|
||||||
if (step.value === 'tenant') return true
|
if (step.value === 'tenant') return true
|
||||||
if (step.value === 'account') return showMiniLogin.value
|
if (step.value === 'account') return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -308,13 +322,30 @@ function goMain() {
|
|||||||
errorMsg.value = ''
|
errorMsg.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 顶部导航返回 */
|
/** 顶部导航返回:登录内部层级优先逐级退;无可退上级时返回门户(免登录页) */
|
||||||
function onNavBack() {
|
function onNavBack() {
|
||||||
if (step.value === 'tenant') {
|
if (step.value === 'tenant') {
|
||||||
backToAccount()
|
backToAccount()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (step.value === 'account' && showMiniLogin.value) {
|
||||||
goMain()
|
goMain()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 无登录内部上级 → 返回门户,继续用免登录功能
|
||||||
|
goPortal()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回门户首页(nav)。
|
||||||
|
* 多数从 nav 点「需登录」模块进来,直接 navigateBack 回上一页(保留游客宫格);
|
||||||
|
* 若是被 401 兑回登录页等无栈场景,回落到 reLaunch nav 以游客态重载。
|
||||||
|
*/
|
||||||
|
function goPortal() {
|
||||||
|
if (submitting.value) return
|
||||||
|
const pages = (typeof getCurrentPages === 'function' && getCurrentPages()) || []
|
||||||
|
if (pages.length > 1) uni.navigateBack()
|
||||||
|
else uni.reLaunch({ url: '/pages/nav/nav' })
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -508,6 +539,17 @@ async function finishLogin(tokens, tenantList, tid, fallbackAccount = '') {
|
|||||||
// 微信端:登录成功后静默绑定 openid,认证中/无手机号能力时一键登录靠它认人
|
// 微信端:登录成功后静默绑定 openid,认证中/无手机号能力时一键登录靠它认人
|
||||||
await silentBindMiniapp()
|
await silentBindMiniapp()
|
||||||
|
|
||||||
|
// 游客数据转正:持游客令牌(手机号授权)时静默并入账号;
|
||||||
|
// 失败不阻断跳转(令牌保留,下次登录自动重试;令牌失效则由 401 自动清理)
|
||||||
|
if (getGuestToken()) {
|
||||||
|
try {
|
||||||
|
await guestMerge()
|
||||||
|
clearGuestToken()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[login] guest merge failed:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1082,6 +1124,25 @@ function goForget() {
|
|||||||
margin-top: 32rpx;
|
margin-top: 32rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 返回门户:不登录也能先浏览免登录模块 */
|
||||||
|
.back-portal {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-portal:active {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-portal-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
|
|||||||
+241
-13
@@ -7,14 +7,15 @@
|
|||||||
<text class="head-eyebrow">YUNZE WORKSPACE</text>
|
<text class="head-eyebrow">YUNZE WORKSPACE</text>
|
||||||
<text class="head-title">工作台</text>
|
<text class="head-title">工作台</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="account-chip"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.account || '我的账号' }}</text></view>
|
<view v-if="!guest" class="account-chip"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.account || '我的账号' }}</text></view>
|
||||||
|
<view v-else class="account-chip" @tap="goLogin"><FaIcon name="right-to-bracket" color="#3478f6" :size="15" /><text>登录</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="tenant-switch" @tap="chooseTenant">
|
<view v-if="!guest" class="tenant-switch" @tap="chooseTenant">
|
||||||
<view class="tenant-mark"><FaIcon name="building" color="#ffffff" :size="19" /></view>
|
<view class="tenant-mark"><FaIcon name="building" color="#ffffff" :size="19" /></view>
|
||||||
<view class="tenant-copy"><text class="tenant-label">当前租户</text><text class="tenant-name">{{ user.tenant || '云泽平台' }}</text></view>
|
<view class="tenant-copy"><text class="tenant-label">当前租户</text><text class="tenant-name">{{ user.tenant || '云泽平台' }}</text></view>
|
||||||
<view class="tenant-action"><text>切换</text><FaIcon name="chevron-down" color="#3478f6" :size="12" /></view>
|
<view class="tenant-action"><text>切换</text><FaIcon name="chevron-down" color="#3478f6" :size="12" /></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="head-sub">选择一个业务系统,开始高效工作</text>
|
<text class="head-sub">{{ guest ? '免登录可逛部分功能,登录后使用完整工作台' : '选择一个业务系统,开始高效工作' }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="body">
|
<view class="body">
|
||||||
@@ -46,12 +47,27 @@
|
|||||||
<FaIcon :name="card.icon" color="#ffffff" :size="26" />
|
<FaIcon :name="card.icon" color="#ffffff" :size="26" />
|
||||||
</view>
|
</view>
|
||||||
<text class="portal-name">{{ card.name }}</text>
|
<text class="portal-name">{{ card.name }}</text>
|
||||||
|
<text v-if="guest" class="portal-tag" :class="card.needLogin ? 'need' : 'free'">{{ card.needLogin ? '需登录' : '免登录' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="bottom-space" />
|
<view class="bottom-space" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<!-- 游客冷启动授权引导:手机号授权后签发游客令牌,免登录模块数据才能保存 -->
|
||||||
|
<view v-if="guestAuthVisible" class="guest-auth-mask">
|
||||||
|
<view class="guest-auth-card">
|
||||||
|
<text class="guest-auth-title">开启免登录保存</text>
|
||||||
|
<text class="guest-auth-desc">授权手机号后,记事本、日程提醒、密码库等免登录功能的数据即可保存;正式登录后会自动并入账号,不会丢失。</text>
|
||||||
|
<button class="guest-auth-btn" open-type="getPhoneNumber" :disabled="guestAuthLoading" @getphonenumber="onGuestPhone">
|
||||||
|
{{ guestAuthLoading ? '授权中…' : '立即授权' }}
|
||||||
|
</button>
|
||||||
|
<text class="guest-auth-skip" @tap="skipGuestAuth">暂不授权(使用将无法保存数据)</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -63,9 +79,80 @@ const headPad = mpHeadPad()
|
|||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { isLoggedIn, getTenants, getTid, setAuthTokens, setTid, setTenantName, getUser, setUser } from '@/utils/auth.js'
|
import { isLoggedIn, getTenants, getTid, setAuthTokens, setTid, setTenantName, getUser, setUser } from '@/utils/auth.js'
|
||||||
import { switchTenant } from '@/api/auth.js'
|
import { switchTenant } from '@/api/auth.js'
|
||||||
import { fetchHome } from '@/api/workspace.js'
|
import { fetchHome, fetchPublicModules, fetchPublicMenus } from '@/api/workspace.js'
|
||||||
import { openMenu } from '@/utils/nav.js'
|
import { openMenu } from '@/utils/nav.js'
|
||||||
import { getSubsystemByModule, goSubsystem } from '@/config/subsystems.js'
|
import { getSubsystemByModule, goSubsystem } from '@/config/subsystems.js'
|
||||||
|
import { getGuestToken, setGuestToken } from '@/utils/guest.js'
|
||||||
|
import { guestMiniLogin } from '@/api/guest.js'
|
||||||
|
|
||||||
|
/* ==================== 游客手机号授权(免登录保存) ==================== */
|
||||||
|
|
||||||
|
const guestAuthVisible = ref(false)
|
||||||
|
const guestAuthLoading = ref(false)
|
||||||
|
// 本次停留内跳过:关闭后不在 nav 反复弹窗,下次冷启动再引导
|
||||||
|
const guestAuthDismissed = ref(false)
|
||||||
|
|
||||||
|
/** 仅微信小程序可拿手机号动态凭证 */
|
||||||
|
function canRequestGuestPhone() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
return true
|
||||||
|
// #endif
|
||||||
|
// eslint-disable-next-line no-unreachable
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeAskGuestAuth() {
|
||||||
|
if (!guest.value || getGuestToken() || guestAuthDismissed.value || !canRequestGuestPhone()) {
|
||||||
|
guestAuthVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guestAuthVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取 uni.login 临时登录凭证(配合手机号动态 code 换游客令牌) */
|
||||||
|
function navUniLoginCode() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: (res) => (res && res.code ? resolve(res.code) : reject(new Error('未获取到微信登录凭证'))),
|
||||||
|
fail: () => reject(new Error('获取微信登录凭证失败'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onGuestPhone(e) {
|
||||||
|
const detail = (e && e.detail) || {}
|
||||||
|
if (String(detail.errMsg || '').indexOf('ok') === -1 || !detail.code) {
|
||||||
|
uni.showToast({ title: '未获取到手机号授权', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (guestAuthLoading.value) return
|
||||||
|
guestAuthLoading.value = true
|
||||||
|
try {
|
||||||
|
const code = await navUniLoginCode()
|
||||||
|
let appid = ''
|
||||||
|
try {
|
||||||
|
appid = (uni.getAccountInfoSync().miniProgram || {}).appId || ''
|
||||||
|
} catch (err) {
|
||||||
|
appid = ''
|
||||||
|
}
|
||||||
|
const res = await guestMiniLogin({ code, phoneCode: detail.code, appid })
|
||||||
|
if (!res || !res.token) throw new Error('未返回游客令牌')
|
||||||
|
setGuestToken(res.token)
|
||||||
|
guestAuthVisible.value = false
|
||||||
|
uni.showToast({ title: '授权成功,数据可保存了', icon: 'none' })
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[nav] guest mini-login failed:', err)
|
||||||
|
uni.showToast({ title: (err && err.message) || '授权失败,请重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
guestAuthLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipGuestAuth() {
|
||||||
|
guestAuthDismissed.value = true
|
||||||
|
guestAuthVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
/** 模块 code → 子系统 slug → 进入其概览页;未注册返回 false */
|
/** 模块 code → 子系统 slug → 进入其概览页;未注册返回 false */
|
||||||
function goSubsystemByModule(code) {
|
function goSubsystemByModule(code) {
|
||||||
@@ -91,12 +178,43 @@ const modules = ref([])
|
|||||||
const menus = ref([])
|
const menus = ref([])
|
||||||
const tenants = ref(getTenants())
|
const tenants = ref(getTenants())
|
||||||
const switchingTenant = ref(false)
|
const switchingTenant = ref(false)
|
||||||
|
// 游客态:未登录时默认落在 nav,渲染公开模块宫格,按菜单级免登录配置分流。
|
||||||
|
const guest = ref(!isLoggedIn())
|
||||||
|
const publicModules = ref([])
|
||||||
|
// 免登录菜单(/api/menus/public,require_login=0):模块宫格是否可免登录进入的唯一依据
|
||||||
|
const publicMenus = ref([])
|
||||||
|
|
||||||
|
/** 含免登录菜单的模块 code 集合(小写) */
|
||||||
|
const guestMenuModuleSet = computed(
|
||||||
|
() => new Set(publicMenus.value.map((m) => String(m.module || '').toLowerCase()).filter(Boolean))
|
||||||
|
)
|
||||||
|
|
||||||
/** 叶子菜单(可直接跳转的功能页),已锁定的过滤掉 */
|
/** 叶子菜单(可直接跳转的功能页),已锁定的过滤掉 */
|
||||||
const leafMenus = computed(() => menus.value.filter((m) => m.leaf !== false && !m.locked))
|
const leafMenus = computed(() => menus.value.filter((m) => m.leaf !== false && !m.locked))
|
||||||
|
|
||||||
/** 子系统卡片:每个业务模块一张卡 */
|
/** 子系统卡片:每个业务模块一张卡 */
|
||||||
const cards = computed(() => {
|
const cards = computed(() => {
|
||||||
|
// 游客:模块可免登录 = 该模块下存在免登录菜单(菜单级 require_login 控制)
|
||||||
|
if (guest.value) {
|
||||||
|
return publicModules.value.map((mod, index) => {
|
||||||
|
const code = String(mod.code || '').toLowerCase()
|
||||||
|
const needLogin = !guestMenuModuleSet.value.has(code)
|
||||||
|
return {
|
||||||
|
id: mod.id,
|
||||||
|
code: mod.code,
|
||||||
|
path: mod.path || '',
|
||||||
|
name: mod.name || '未命名系统',
|
||||||
|
desc: mod.description || (needLogin ? '需登录使用' : '免登录'),
|
||||||
|
icon: MODULE_ICONS[code] || 'cube',
|
||||||
|
tint: 'tint-' + (index % 8),
|
||||||
|
locked: false,
|
||||||
|
needLogin,
|
||||||
|
items: [],
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const byCode = {}
|
const byCode = {}
|
||||||
leafMenus.value.forEach((m) => {
|
leafMenus.value.forEach((m) => {
|
||||||
if (!m.module) return
|
if (!m.module) return
|
||||||
@@ -132,6 +250,25 @@ const cards = computed(() => {
|
|||||||
|
|
||||||
async function load(force = false) {
|
async function load(force = false) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
guest.value = !isLoggedIn()
|
||||||
|
// 游客:公开模块宫格 + 免登录菜单;模块可点的判定下沉到菜单级
|
||||||
|
// (存在 require_login=0 的菜单即免登录,模块级 require_login 不再作为依据)
|
||||||
|
if (guest.value) {
|
||||||
|
try {
|
||||||
|
const [list, menuList] = await Promise.all([fetchPublicModules(), fetchPublicMenus()])
|
||||||
|
publicModules.value = Array.isArray(list) ? list : []
|
||||||
|
publicMenus.value = Array.isArray(menuList) ? menuList : []
|
||||||
|
loadError.value = ''
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[nav] public modules load failed:', e)
|
||||||
|
loadError.value = (e && e.message) || '接口请求失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
// 进小程序即引导授权:游客无令牌时弹手机号授权窗
|
||||||
|
maybeAskGuestAuth()
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = await fetchHome(force)
|
const data = await fetchHome(force)
|
||||||
user.value = data.user || {}
|
user.value = data.user || {}
|
||||||
@@ -188,12 +325,17 @@ function chooseTenant() {
|
|||||||
/**
|
/**
|
||||||
* 点击子系统卡片 → 进入该子系统独立页面组(带自己的 tabbar)。
|
* 点击子系统卡片 → 进入该子系统独立页面组(带自己的 tabbar)。
|
||||||
* 未在 config/subsystems.js 注册的模块,降级为直接打开 Web 端该模块首页。
|
* 未在 config/subsystems.js 注册的模块,降级为直接打开 Web 端该模块首页。
|
||||||
|
* 游客:命中「必须登录」模块时不直接进,先引导登录(登录后回 nav)。
|
||||||
*/
|
*/
|
||||||
function enter(card) {
|
function enter(card) {
|
||||||
if (card.locked) {
|
if (card.locked) {
|
||||||
openMenu({ locked: true, title: card.name })
|
openMenu({ locked: true, title: card.name })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (guest.value && card.needLogin) {
|
||||||
|
goLogin(card.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (goSubsystemByModule(card.code)) return
|
if (goSubsystemByModule(card.code)) return
|
||||||
if (card.items.length) {
|
if (card.items.length) {
|
||||||
openMenu(card.items[0], card.name)
|
openMenu(card.items[0], card.name)
|
||||||
@@ -206,6 +348,14 @@ function enter(card) {
|
|||||||
uni.showToast({ title: card.name + '暂未开放', icon: 'none' })
|
uni.showToast({ title: card.name + '暂未开放', icon: 'none' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 引导登录:登录页登录成功后会回 nav(见 login.vue finishLogin) */
|
||||||
|
function goLogin(name = '') {
|
||||||
|
if (name) {
|
||||||
|
uni.showToast({ title: '「' + name + '」需登录后使用', icon: 'none' })
|
||||||
|
}
|
||||||
|
uni.navigateTo({ url: '/pages/login/login' })
|
||||||
|
}
|
||||||
|
|
||||||
function openTenantPicker() {
|
function openTenantPicker() {
|
||||||
if (tenants.value.length < 2) return
|
if (tenants.value.length < 2) return
|
||||||
tenantPickerVisible.value = true
|
tenantPickerVisible.value = true
|
||||||
@@ -246,19 +396,12 @@ async function changeTenant(item) {
|
|||||||
/* ==================== 生命周期 ==================== */
|
/* ==================== 生命周期 ==================== */
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isLoggedIn()) {
|
// 默认落地页:未登录不再强制回登录页,而是以游客态渲染公开模块宫格
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
load()
|
load()
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
// 未登录(含后台切回来时令牌被清)直接回登录页,不再停留在首页
|
// 登录态可能在登录页/其他入口发生变化,每次展示重新判定并加载
|
||||||
if (!isLoggedIn()) {
|
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!loading.value) load()
|
if (!loading.value) load()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -458,7 +601,92 @@ onShow(() => {
|
|||||||
color: $color-text-muted;
|
color: $color-text-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 游客态:卡片左上角标出免登录 / 需登录 */
|
||||||
|
.portal-card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.portal-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 14rpx;
|
||||||
|
right: 14rpx;
|
||||||
|
padding: 2rpx 12rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 18rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.portal-tag.free {
|
||||||
|
color: #16a34a;
|
||||||
|
background: rgba(22, 163, 74, 0.12);
|
||||||
|
}
|
||||||
|
.portal-tag.need {
|
||||||
|
color: #94a3b8;
|
||||||
|
background: rgba(148, 163, 184, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
.bottom-space {
|
.bottom-space {
|
||||||
height: 48rpx;
|
height: 48rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============ 游客授权引导弹窗 ============ */
|
||||||
|
.guest-auth-mask {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 99;
|
||||||
|
background: rgba(15, 23, 42, 0.55);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 56rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guest-auth-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 560rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 44rpx 36rpx 32rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
box-shadow: 0 24rpx 64rpx rgba(15, 23, 42, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guest-auth-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #17345f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guest-auth-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: #64748b;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guest-auth-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
background: linear-gradient(145deg, #69a5ff, #3478f6);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
line-height: 2.6;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.guest-auth-skip {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -69,6 +69,7 @@ import { onShow } from '@dcloudio/uni-app'
|
|||||||
import { fetchNotes, deleteNote, toggleNotePin } from '@/api/note.js'
|
import { fetchNotes, deleteNote, toggleNotePin } from '@/api/note.js'
|
||||||
import { formatRelativeTime } from '@/utils/date.js'
|
import { formatRelativeTime } from '@/utils/date.js'
|
||||||
import { isLoggedIn } from '@/utils/auth.js'
|
import { isLoggedIn } from '@/utils/auth.js'
|
||||||
|
import { getGuestToken } from '@/utils/guest.js'
|
||||||
|
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const notes = ref([])
|
const notes = ref([])
|
||||||
@@ -78,15 +79,16 @@ const loading = ref(false)
|
|||||||
const pinnedCount = computed(() => notes.value.filter(item => item.pinned).length)
|
const pinnedCount = computed(() => notes.value.filter(item => item.pinned).length)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isLoggedIn()) {
|
// 免登录页面:登录态或游客令牌均可用;两者皆无回门户触发授权/登录
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
if (!isLoggedIn() && !getGuestToken()) {
|
||||||
|
uni.reLaunch({ url: '/pages/nav/nav' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadNotes()
|
loadNotes()
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (isLoggedIn()) loadNotes()
|
if (isLoggedIn() || getGuestToken()) loadNotes()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadNotes() {
|
async function loadNotes() {
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ import { onShow } from '@dcloudio/uni-app'
|
|||||||
import { fetchSchedules, getRemindChannelLabel, getRemindMinutesLabel } from '@/api/schedule.js'
|
import { fetchSchedules, getRemindChannelLabel, getRemindMinutesLabel } from '@/api/schedule.js'
|
||||||
import { pad, startOfDay, isToday } from '@/utils/date.js'
|
import { pad, startOfDay, isToday } from '@/utils/date.js'
|
||||||
import { isLoggedIn } from '@/utils/auth.js'
|
import { isLoggedIn } from '@/utils/auth.js'
|
||||||
|
import { getGuestToken } from '@/utils/guest.js'
|
||||||
|
|
||||||
/** 周一为一周第一天 */
|
/** 周一为一周第一天 */
|
||||||
const WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日']
|
const WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日']
|
||||||
@@ -282,8 +283,9 @@ function openCreate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isLoggedIn()) {
|
// 免登录页面:登录态或游客令牌均可用;两者皆无回门户触发授权/登录
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
if (!isLoggedIn() && !getGuestToken()) {
|
||||||
|
uni.reLaunch({ url: '/pages/nav/nav' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadReminders()
|
loadReminders()
|
||||||
@@ -291,8 +293,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
// 新增/编辑返回后刷新列表
|
// 新增/编辑返回后刷新列表
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (!isLoggedIn()) return
|
if (isLoggedIn() || getGuestToken()) loadReminders()
|
||||||
if (schedules.value.length || !loading.value) loadReminders()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadReminders() {
|
async function loadReminders() {
|
||||||
|
|||||||
@@ -12,7 +12,15 @@
|
|||||||
"disablePlugins": [],
|
"disablePlugins": [],
|
||||||
"outputPath": ""
|
"outputPath": ""
|
||||||
},
|
},
|
||||||
"useCompilerPlugins": false
|
"useCompilerPlugins": false,
|
||||||
|
"compileWorklet": false,
|
||||||
|
"uploadWithSourceMap": true,
|
||||||
|
"packNpmManually": false,
|
||||||
|
"minifyWXSS": true,
|
||||||
|
"minifyWXML": true,
|
||||||
|
"localPlugins": false,
|
||||||
|
"disableUseStrict": false,
|
||||||
|
"condition": false
|
||||||
},
|
},
|
||||||
"compileType": "miniprogram",
|
"compileType": "miniprogram",
|
||||||
"simulatorPluginLibVersion": {},
|
"simulatorPluginLibVersion": {},
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
"autoAudits": false,
|
"autoAudits": false,
|
||||||
"showShadowRootInWxmlPanel": true,
|
"showShadowRootInWxmlPanel": true,
|
||||||
"compileHotReLoad": true,
|
"compileHotReLoad": true,
|
||||||
"useGlassEaselForWxml": false
|
"useGlassEaselForWxml": false,
|
||||||
|
"useApiHook": true,
|
||||||
|
"useStaticServer": false,
|
||||||
|
"useLanDebug": false,
|
||||||
|
"showES6CompileOption": false,
|
||||||
|
"checkInvalidKey": true,
|
||||||
|
"ignoreDevUnusedFiles": true,
|
||||||
|
"bigPackageSizeSupport": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 游客轻身份(免登录模块的数据临时存储)
|
||||||
|
*
|
||||||
|
* 小程序冷启动进入 nav 游客态时,引导用户授权微信手机号,
|
||||||
|
* 由 /api/guest/mini-login 签发游客令牌(30 天有效)。
|
||||||
|
* 携带该令牌可直接使用记事本 / 日程提醒 / 密码库等免登录功能,
|
||||||
|
* 数据落在后端 yz_guest_data;正式登录后调 /api/guest/merge 并入账号。
|
||||||
|
*/
|
||||||
|
import { getToken } from '@/utils/auth.js'
|
||||||
|
|
||||||
|
const GUEST_TOKEN_KEY = 'app_guest_token'
|
||||||
|
|
||||||
|
export function setGuestToken(token) {
|
||||||
|
uni.setStorageSync(GUEST_TOKEN_KEY, token || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGuestToken() {
|
||||||
|
return uni.getStorageSync(GUEST_TOKEN_KEY) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearGuestToken() {
|
||||||
|
uni.removeStorageSync(GUEST_TOKEN_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 游客模式:未登录但持有手机号授权签发的游客令牌 */
|
||||||
|
export function isGuestMode() {
|
||||||
|
return !getToken() && !!getGuestToken()
|
||||||
|
}
|
||||||
@@ -7,9 +7,29 @@
|
|||||||
* 这里统一封装,避免三个页面各写一遍。
|
* 这里统一封装,避免三个页面各写一遍。
|
||||||
*/
|
*/
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { fetchHome } from '@/api/workspace.js'
|
import { fetchHome, fetchPublicMenus } from '@/api/workspace.js'
|
||||||
|
import { isLoggedIn } from '@/utils/auth.js'
|
||||||
import { getSubsystem } from '@/config/subsystems.js'
|
import { getSubsystem } from '@/config/subsystems.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游客(未登录)菜单源:公开接口 /api/menus/public(菜单级 require_login=0)。
|
||||||
|
* 免登录控制按菜单粒度在 Platform 菜单管理配置,后端下发什么游客就能用什么;
|
||||||
|
* 多个 Sub 组件共享一次拉取(会话级缓存,失败不缓存以便重试)。
|
||||||
|
*/
|
||||||
|
let guestMenusPromise = null
|
||||||
|
function loadGuestMenus() {
|
||||||
|
if (!guestMenusPromise) {
|
||||||
|
guestMenusPromise = fetchPublicMenus()
|
||||||
|
.then((list) => (Array.isArray(list) ? list : []))
|
||||||
|
.catch((e) => {
|
||||||
|
guestMenusPromise = null
|
||||||
|
console.error('[subsystem] public menus failed:', e)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return guestMenusPromise
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} slug 子系统目录名(crm / erp / oa / cms / tools)
|
* @param {string} slug 子系统目录名(crm / erp / oa / cms / tools)
|
||||||
* @param {import('vue').Ref<string>} currentTab 当前 tab,用于按需拉数据
|
* @param {import('vue').Ref<string>} currentTab 当前 tab,用于按需拉数据
|
||||||
@@ -47,6 +67,17 @@ export function useSubsystem(slug) {
|
|||||||
|
|
||||||
async function load(force = false) {
|
async function load(force = false) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
// 游客(未登录)不拉需鉴权的 /api/home,避免 401 触发登出回环;
|
||||||
|
// 改用公开菜单接口,按 module 归组后渲染免登录入口(菜单级 require_login 控制)。
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
try {
|
||||||
|
menus.value = await loadGuestMenus()
|
||||||
|
} catch (e) {
|
||||||
|
menus.value = []
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = await fetchHome(force)
|
const data = await fetchHome(force)
|
||||||
user.value = data.user || {}
|
user.value = data.user || {}
|
||||||
|
|||||||
Reference in new issue
Block a user