This commit is contained in:
hero920103 committed 2026-11-03 11:13:59 +08:00
1 parent 012f8edf8b
commit 36ff960fbd
33 files changed
+1644 -344

No files matched your search

+111
View File
@@ -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 列表(有序)。
// 未设置或解析失败一律返回空数组,前端据此回落到默认取前 N 个叶子菜单。
func (c *AppAggregateController) appQuickMenuIds(tid, uid uint64) []uint64 {
+146 -2
View File
@@ -7,6 +7,8 @@ import (
"server/models"
"server/pkg/identity"
"server/pkg/jwtutil"
"server/services/gueststore"
"sort"
"strconv"
"strings"
"time"
@@ -19,9 +21,9 @@ type AppNotebookController struct {
beego.Controller
}
// 只放行移动端与租户端令牌:/api/** 仅被 uniapp 调用,平台令牌不该能打移动端接口。
// 放行移动端、租户端与游客令牌:游客调用时不进业务表,分流到 gueststore。
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) {
@@ -45,6 +47,11 @@ func (c *AppNotebookController) GetList() {
keyword := strings.TrimSpace(c.GetString("keyword"))
if identity.IsGuest(claims) {
c.guestList(claims, keyword)
return
}
qs := models.Orm.QueryTable(new(models.BackendNotebook)).
Filter("is_deleted", 0).
Filter("user_id", claims.UserID)
@@ -109,6 +116,11 @@ func (c *AppNotebookController) GetDetail() {
return
}
if identity.IsGuest(claims) {
c.guestDetail(claims, id)
return
}
var note models.BackendNotebook
err = models.Orm.QueryTable(new(models.BackendNotebook)).
Filter("id", id).
@@ -164,6 +176,11 @@ func (c *AppNotebookController) Create() {
title = "无标题"
}
if identity.IsGuest(claims) {
c.guestCreateItem(claims, title, payload.Content, payload.Pinned)
return
}
userID := uint64(claims.UserID)
pinned := int8(0)
if payload.Pinned {
@@ -224,6 +241,11 @@ func (c *AppNotebookController) Update() {
return
}
if identity.IsGuest(claims) {
c.guestUpdateItem(claims, id, payload.Title, payload.Content, payload.Pinned)
return
}
var note models.BackendNotebook
err = models.Orm.QueryTable(new(models.BackendNotebook)).
Filter("id", id).
@@ -286,6 +308,19 @@ func (c *AppNotebookController) Delete() {
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
err = models.Orm.QueryTable(new(models.BackendNotebook)).
Filter("id", id).
@@ -327,6 +362,11 @@ func (c *AppNotebookController) TogglePin() {
return
}
if identity.IsGuest(claims) {
c.guestTogglePin(claims, id)
return
}
var note models.BackendNotebook
err = models.Orm.QueryTable(new(models.BackendNotebook)).
Filter("id", id).
@@ -361,3 +401,107 @@ func (c *AppNotebookController) TogglePin() {
"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})
}
+161 -2
View File
@@ -1,13 +1,16 @@
package controllers
import (
"encoding/json"
"strconv"
"strings"
"time"
"server/middleware"
"server/models"
"server/pkg/identity"
"server/pkg/jwtutil"
"server/services/gueststore"
"github.com/beego/beego/v2/client/orm"
"github.com/beego/beego/v2/core/logs"
@@ -24,12 +27,18 @@ type AppPasswordStoreController struct {
beego.Controller
}
// 只放行移动端与租户端令牌:/api/** 仅被 uniapp 调用,平台令牌不该能打移动端接口。
// 放行移动端、租户端与游客令牌:游客 tid=0 合法,数据进 gueststore 而非业务表。
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 {
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 {
return nil, orm.ErrNoRows
}
@@ -43,6 +52,10 @@ func (c *AppPasswordStoreController) GetList() {
passwordReply(&c.Controller, 401, "未登录或无权限", nil)
return
}
if identity.IsGuest(cl) {
c.guestList(cl, strings.TrimSpace(c.GetString("keyword")))
return
}
qs := models.Orm.QueryTable(new(models.BackendPasswordStore)).
Filter("is_deleted", 0).Filter("tid", cl.TenantId).Filter("user_id", cl.UserID)
if k := strings.TrimSpace(c.GetString("keyword")); k != "" {
@@ -82,6 +95,10 @@ func (c *AppPasswordStoreController) GetDetail() {
passwordReply(&c.Controller, 400, "参数错误", nil)
return
}
if identity.IsGuest(cl) {
c.guestDetail(cl, uint64(id))
return
}
var m 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)
@@ -121,6 +138,10 @@ func (c *AppPasswordStoreController) Create() {
passwordReply(&c.Controller, 400, e.Error(), nil)
return
}
if identity.IsGuest(cl) {
c.guestCreate(cl, p)
return
}
qs := models.Orm.QueryTable(new(models.BackendPasswordStore)).
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 {
@@ -172,6 +193,10 @@ func (c *AppPasswordStoreController) Update() {
passwordReply(&c.Controller, 400, e.Error(), nil)
return
}
if identity.IsGuest(cl) {
c.guestUpdate(cl, uint64(id), p)
return
}
var m 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)
@@ -221,6 +246,18 @@ func (c *AppPasswordStoreController) Delete() {
passwordReply(&c.Controller, 400, "参数错误", nil)
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
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)
@@ -241,3 +278,125 @@ func (c *AppPasswordStoreController) Delete() {
middleware.WriteDeleteLog(uint64(cl.UserID), cl.TenantId, "passwordStore", m.Platform)
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)
}
+205 -2
View File
@@ -13,6 +13,7 @@ import (
"server/models"
"server/pkg/identity"
"server/pkg/jwtutil"
"server/services/gueststore"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
@@ -22,9 +23,9 @@ type AppReminderController struct {
beego.Controller
}
// 只放行移动端与租户端令牌:/api/** 仅被 uniapp 调用,平台令牌不该能打移动端接口。
// 放行移动端、租户端与游客令牌:游客调用时不进业务表,分流到 gueststore。
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) {
@@ -75,6 +76,11 @@ func (c *AppReminderController) GetList() {
pageSize = 20
}
if identity.IsGuest(claims) {
c.guestList(claims, page, pageSize, keyword, status)
return
}
qs := models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId)
@@ -171,6 +177,11 @@ func (c *AppReminderController) GetDetail() {
return
}
if identity.IsGuest(claims) {
c.guestDetail(claims, id)
return
}
var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
@@ -272,6 +283,11 @@ func (c *AppReminderController) Create() {
title = "日程提醒"
}
if identity.IsGuest(claims) {
c.guestCreate(claims, p, content, schedTime)
return
}
schedule := models.BackendSchedule{
Tid: claims.TenantId,
Title: title,
@@ -363,6 +379,11 @@ func (c *AppReminderController) Update() {
return
}
if identity.IsGuest(claims) {
c.guestUpdate(claims, id, p)
return
}
var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
@@ -482,6 +503,11 @@ func (c *AppReminderController) Delete() {
return
}
if identity.IsGuest(claims) {
c.guestDelete(claims, id)
return
}
var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
@@ -543,6 +569,11 @@ func (c *AppReminderController) ToggleComplete() {
return
}
if identity.IsGuest(claims) {
c.guestToggle(claims, id)
return
}
var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
@@ -587,3 +618,175 @@ func (c *AppReminderController) ToggleComplete() {
"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})
}
+14 -9
View File
@@ -76,6 +76,7 @@ type menuPayload struct {
Sort *int64 `json:"sort"`
Status *int8 `json:"status"`
IsVisible *int8 `json:"is_visible"`
RequireLogin *int8 `json:"require_login"` // 1=必须登录(默认) 0=可免登录(游客可用)
Views []int `json:"views"`
Platforms []string `json:"platforms"` // 展示端:["web"] ["app"] ["web","app"];空=继承所属模块
Type *int8 `json:"type"`
@@ -417,6 +418,7 @@ type menuNode struct {
Sort int64 `json:"sort"`
Status int8 `json:"status"`
IsVisible *int8 `json:"is_visible,omitempty"`
RequireLogin int8 `json:"require_login"` // 1=必须登录 0=可免登录(游客)
Views []int `json:"views,omitempty"`
Platforms []string `json:"platforms,omitempty"` // 展示端,空=继承所属模块
Type int8 `json:"type"`
@@ -430,15 +432,16 @@ func buildMenuTree(menus []models.SystemMenu, pid int64) []*menuNode {
for _, m := range menus {
if m.Pid == pid {
node := &menuNode{
ID: m.ID,
Pid: m.Pid,
Title: m.Title,
Sort: m.Sort,
Status: m.Status,
IsVisible: m.IsVisible,
Views: parseViews(m.Views),
Platforms: services.ParsePlatforms(m.Platforms),
Type: m.Type,
ID: m.ID,
Pid: m.Pid,
Title: m.Title,
Sort: m.Sort,
Status: m.Status,
IsVisible: m.IsVisible,
RequireLogin: m.RequireLogin,
Views: parseViews(m.Views),
Platforms: services.ParsePlatforms(m.Platforms),
Type: m.Type,
}
if m.Path != nil {
node.Path = *m.Path
@@ -514,6 +517,7 @@ func (c *BackendMenuController) CreateMenu() {
Sort: valueInt64(payload.Sort, 0),
Status: valueInt8(payload.Status, 1),
IsVisible: ptrInt8(valueInt8(payload.IsVisible, 1)),
RequireLogin: valueInt8(payload.RequireLogin, 1),
Views: &viewsStr,
Type: valueInt8(payload.Type, 1),
Path: ptrString(valueString(payload.Path, "")),
@@ -565,6 +569,7 @@ func (c *BackendMenuController) UpdateMenu() {
"sort": valueInt64(payload.Sort, 0),
"status": valueInt8(payload.Status, 1),
"is_visible": valueInt8(payload.IsVisible, 1),
"require_login": valueInt8(payload.RequireLogin, 1),
"views": string(viewsBytes),
"platforms": services.NormalizePlatforms(payload.Platforms),
"type": valueInt8(payload.Type, 1),
+83 -49
View File
@@ -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)
}
func (c *PlatformModulesController) jsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
@@ -79,18 +78,19 @@ func (c *PlatformModulesController) GetList() {
list := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
list = append(list, map[string]interface{}{
"id": rows[i].ID,
"mid": rows[i].Mid,
"name": rows[i].Name,
"code": rows[i].Code,
"path": rows[i].Path,
"icon": rows[i].Icon,
"description": rows[i].Description,
"type": rows[i].Type,
"sort": rows[i].Sort,
"status": rows[i].Status,
"is_show": rows[i].IsShow,
"platforms": platformList(&rows[i]),
"id": rows[i].ID,
"mid": rows[i].Mid,
"name": rows[i].Name,
"code": rows[i].Code,
"path": rows[i].Path,
"icon": rows[i].Icon,
"description": rows[i].Description,
"type": rows[i].Type,
"sort": rows[i].Sort,
"status": rows[i].Status,
"is_show": rows[i].IsShow,
"require_login": rows[i].RequireLogin,
"platforms": platformList(&rows[i]),
})
}
c.Data["json"] = map[string]interface{}{
@@ -156,22 +156,54 @@ func (c *PlatformModulesController) GetDetail() {
c.jsonErr(404, 404, "模块不存在")
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()
}
type modulePayload struct {
Mid *uint64 `json:"mid"`
Name string `json:"name"`
Code string `json:"code"`
Path string `json:"path"`
Icon string `json:"icon"`
Description string `json:"description"`
Type int `json:"type"`
Sort int `json:"sort"`
Status int8 `json:"status"`
IsShow int8 `json:"is_show"`
Platforms []string `json:"platforms"` // 展示端:["web"] ["app"] ["web","app"];空=全端
Mid *uint64 `json:"mid"`
Name string `json:"name"`
Code string `json:"code"`
Path string `json:"path"`
Icon string `json:"icon"`
Description string `json:"description"`
Type int `json:"type"`
Sort int `json:"sort"`
Status int8 `json:"status"`
IsShow int8 `json:"is_show"`
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
@@ -213,18 +245,19 @@ func (c *PlatformModulesController) Add() {
now := time.Now()
platforms := services.NormalizePlatforms(p.Platforms)
row := &models.SystemModules{
Mid: p.Mid,
Name: p.Name,
Code: p.Code,
Path: strings.TrimSpace(p.Path),
Icon: strings.TrimSpace(p.Icon),
Description: strings.TrimSpace(p.Description),
Type: p.Type,
Sort: p.Sort,
Status: p.Status,
IsShow: p.IsShow,
CreateTime: &now,
UpdateTime: &now,
Mid: p.Mid,
Name: p.Name,
Code: p.Code,
Path: strings.TrimSpace(p.Path),
Icon: strings.TrimSpace(p.Icon),
Description: strings.TrimSpace(p.Description),
Type: p.Type,
Sort: p.Sort,
Status: p.Status,
IsShow: p.IsShow,
RequireLogin: resolveRequireLogin(p.RequireLogin),
CreateTime: &now,
UpdateTime: &now,
}
if platforms != "" {
row.Platforms = &platforms
@@ -284,18 +317,19 @@ func (c *PlatformModulesController) Edit() {
now := time.Now()
// 展示端为空串表示「全端」,需显式落库以覆盖历史值
up := map[string]interface{}{
"mid": p.Mid,
"name": p.Name,
"code": p.Code,
"path": strings.TrimSpace(p.Path),
"icon": strings.TrimSpace(p.Icon),
"description": strings.TrimSpace(p.Description),
"type": p.Type,
"sort": p.Sort,
"status": p.Status,
"is_show": p.IsShow,
"platforms": services.NormalizePlatforms(p.Platforms),
"update_time": now,
"mid": p.Mid,
"name": p.Name,
"code": p.Code,
"path": strings.TrimSpace(p.Path),
"icon": strings.TrimSpace(p.Icon),
"description": strings.TrimSpace(p.Description),
"type": p.Type,
"sort": p.Sort,
"status": p.Status,
"is_show": p.IsShow,
"require_login": resolveRequireLogin(p.RequireLogin),
"platforms": services.NormalizePlatforms(p.Platforms),
"update_time": now,
}
n, err := models.Orm.QueryTable(new(models.SystemModules)).
Filter("id", id).