backend增加租户简称登录

This commit is contained in:
2026-07-17 11:00:33 +08:00
parent dcf06db11b
commit 2232ef6f3c
14 changed files with 916 additions and 746 deletions
@@ -15,6 +15,16 @@
</div> </div>
</div> </div>
<div class="editor-toolbar">
<el-switch
v-model="pinned"
inline-prompt
active-text="置顶"
inactive-text="置顶"
:disabled="loading"
/>
</div>
<div class="editor-body"> <div class="editor-body">
<UmoEditor v-model="noteContent" /> <UmoEditor v-model="noteContent" />
</div> </div>
@@ -38,6 +48,7 @@ const emit = defineEmits(['save-success', 'create-success']);
const noteTitle = ref(''); const noteTitle = ref('');
const noteContent = ref(''); const noteContent = ref('');
const pinned = ref(false);
const loading = ref(false); const loading = ref(false);
const isNew = ref(false); const isNew = ref(false);
@@ -47,6 +58,7 @@ const loadNote = async () => {
isNew.value = true; isNew.value = true;
noteTitle.value = '新建笔记'; noteTitle.value = '新建笔记';
noteContent.value = ''; noteContent.value = '';
pinned.value = false;
return; return;
} }
@@ -58,6 +70,7 @@ const loadNote = async () => {
if (response.code === 200) { if (response.code === 200) {
noteTitle.value = response.data.title || '无标题'; noteTitle.value = response.data.title || '无标题';
noteContent.value = response.data.content || ''; noteContent.value = response.data.content || '';
pinned.value = Number(response.data.pinned) === 1;
} else { } else {
ElMessage.error('加载笔记失败'); ElMessage.error('加载笔记失败');
} }
@@ -84,6 +97,7 @@ const handleSave = async () => {
const response = await createNotebook({ const response = await createNotebook({
title: noteTitle.value, title: noteTitle.value,
content: noteContent.value, content: noteContent.value,
pinned: pinned.value ? 1 : 0,
}); });
if (response.code === 200) { if (response.code === 200) {
@@ -98,6 +112,7 @@ const handleSave = async () => {
const response = await updateNotebook(props.noteId, { const response = await updateNotebook(props.noteId, {
title: noteTitle.value, title: noteTitle.value,
content: noteContent.value, content: noteContent.value,
pinned: pinned.value ? 1 : 0,
}); });
if (response.code === 200) { if (response.code === 200) {
@@ -175,6 +190,15 @@ onMounted(() => {
} }
} }
.editor-toolbar {
min-height: 42px;
padding: 8px 20px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--el-border-color-lighter);
background: var(--el-bg-color);
}
.editor-body { .editor-body {
flex: 1; flex: 1;
padding: 20px; padding: 20px;
@@ -28,6 +28,15 @@
> >
<div class="note-item-header"> <div class="note-item-header">
<span class="note-title">{{ note.title || '无标题' }}</span> <span class="note-title">{{ note.title || '无标题' }}</span>
<el-tag
v-if="Number(note.pinned) === 1"
class="pinned-tag"
size="small"
type="warning"
effect="plain"
>
置顶
</el-tag>
<el-dropdown trigger="click" @command="(cmd) => handleNoteAction(cmd, note)"> <el-dropdown trigger="click" @command="(cmd) => handleNoteAction(cmd, note)">
<el-icon class="note-more"><MoreFilled /></el-icon> <el-icon class="note-more"><MoreFilled /></el-icon>
<template #dropdown> <template #dropdown>
@@ -305,6 +314,7 @@ onMounted(() => {
.note-title { .note-title {
flex: 1; flex: 1;
min-width: 0;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
@@ -313,6 +323,11 @@ onMounted(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.pinned-tag {
flex-shrink: 0;
margin-left: 8px;
}
.note-more { .note-more {
margin-left: 8px; margin-left: 8px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
+6 -1
View File
@@ -78,7 +78,7 @@ func (c *BackendNotebookController) List() {
total, _ := qs.Count() total, _ := qs.Count()
var list []models.BackendNotebook var list []models.BackendNotebook
_, err = qs.OrderBy("-update_time", "-create_time"). _, err = qs.OrderBy("-pinned", "-id").
Limit(pageSize).Offset((page - 1) * pageSize). Limit(pageSize).Offset((page - 1) * pageSize).
All(&list) All(&list)
if err != nil && err != orm.ErrNoRows { if err != nil && err != orm.ErrNoRows {
@@ -137,6 +137,7 @@ func (c *BackendNotebookController) Create() {
var payload struct { var payload struct {
Title string `json:"title"` Title string `json:"title"`
Content string `json:"content"` Content string `json:"content"`
Pinned int8 `json:"pinned"`
} }
if err := json.Unmarshal(raw, &payload); err != nil { if err := json.Unmarshal(raw, &payload); err != nil {
c.nbJsonErr(400, 400, "参数错误") c.nbJsonErr(400, 400, "参数错误")
@@ -153,6 +154,7 @@ func (c *BackendNotebookController) Create() {
Tid: claims.TenantId, Tid: claims.TenantId,
Title: payload.Title, Title: payload.Title,
Content: payload.Content, Content: payload.Content,
Pinned: payload.Pinned,
UserID: &userID, UserID: &userID,
UserName: &claims.Username, UserName: &claims.Username,
IsDeleted: 0, IsDeleted: 0,
@@ -190,6 +192,7 @@ func (c *BackendNotebookController) Update() {
var payload struct { var payload struct {
Title string `json:"title"` Title string `json:"title"`
Content string `json:"content"` Content string `json:"content"`
Pinned int8 `json:"pinned"`
} }
if err := json.Unmarshal(raw, &payload); err != nil { if err := json.Unmarshal(raw, &payload); err != nil {
c.nbJsonErr(400, 400, "参数错误") c.nbJsonErr(400, 400, "参数错误")
@@ -219,6 +222,7 @@ func (c *BackendNotebookController) Update() {
Update(map[string]interface{}{ Update(map[string]interface{}{
"title": payload.Title, "title": payload.Title,
"content": payload.Content, "content": payload.Content,
"pinned": payload.Pinned,
"update_time": now, "update_time": now,
}) })
if err != nil { if err != nil {
@@ -228,6 +232,7 @@ func (c *BackendNotebookController) Update() {
note.Title = payload.Title note.Title = payload.Title
note.Content = payload.Content note.Content = payload.Content
note.Pinned = payload.Pinned
note.UpdateTime = &now note.UpdateTime = &now
c.nbOk(note) c.nbOk(note)
} }
+317 -312
View File
@@ -1,312 +1,317 @@
package controllers package controllers
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"server/models" "server/models"
"server/pkg/jwtutil" "server/pkg/jwtutil"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"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"
) )
type PlatformNotebookController struct { type PlatformNotebookController struct {
beego.Controller beego.Controller
} }
// requireAuth 验证平台用户权限 // requireAuth 验证平台用户权限
func requireNotebookAuth(c *beego.Controller) (*jwtutil.Claims, error) { func requireNotebookAuth(c *beego.Controller) (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization") auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" { if auth == "" {
return nil, orm.ErrNoRows return nil, orm.ErrNoRows
} }
parts := strings.SplitN(auth, " ", 2) parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" { if len(parts) != 2 || parts[0] != "Bearer" {
return nil, orm.ErrNoRows return nil, orm.ErrNoRows
} }
claims, err := jwtutil.ParseToken(parts[1]) claims, err := jwtutil.ParseToken(parts[1])
if err != nil { if err != nil {
return nil, err return nil, err
} }
if claims.UserType != "platform" { if claims.UserType != "platform" {
return nil, orm.ErrNoRows return nil, orm.ErrNoRows
} }
return claims, nil return claims, nil
} }
// jsonResponse 统一JSON响应 // jsonResponse 统一JSON响应
func jsonResponse(c *beego.Controller, httpStatus, code int, msg string, data interface{}) { func jsonResponse(c *beego.Controller, httpStatus, code int, msg string, data interface{}) {
c.Ctx.Output.SetStatus(httpStatus) c.Ctx.Output.SetStatus(httpStatus)
resp := map[string]interface{}{ resp := map[string]interface{}{
"code": code, "code": code,
"msg": msg, "msg": msg,
} }
if data != nil { if data != nil {
resp["data"] = data resp["data"] = data
} }
c.Data["json"] = resp c.Data["json"] = resp
_ = c.ServeJSON() _ = c.ServeJSON()
} }
// List 获取笔记列表 // List 获取笔记列表
// GET /platform/notebook/list // GET /platform/notebook/list
func (c *PlatformNotebookController) List() { func (c *PlatformNotebookController) List() {
claims, err := requireNotebookAuth(&c.Controller) claims, err := requireNotebookAuth(&c.Controller)
if err != nil { if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil) jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return return
} }
page, _ := c.GetInt("page", 1) page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20) pageSize, _ := c.GetInt("pageSize", 20)
keyword := strings.TrimSpace(c.GetString("keyword")) keyword := strings.TrimSpace(c.GetString("keyword"))
if page < 1 { if page < 1 {
page = 1 page = 1
} }
if pageSize < 1 || pageSize > 100 { if pageSize < 1 || pageSize > 100 {
pageSize = 20 pageSize = 20
} }
qs := models.Orm.QueryTable(new(models.PlatformNotebook)). qs := models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("is_deleted", 0). Filter("is_deleted", 0).
Filter("user_id", claims.UserID) Filter("user_id", claims.UserID)
if keyword != "" { if keyword != "" {
qs = qs.Filter("title__icontains", keyword) qs = qs.Filter("title__icontains", keyword)
} }
total, err := qs.Count() total, err := qs.Count()
if err != nil { if err != nil {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil) jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
return return
} }
var list []models.PlatformNotebook var list []models.PlatformNotebook
_, err = qs.OrderBy("-update_time", "-create_time"). _, err = qs.OrderBy("-pinned", "-id").
Limit(pageSize). Limit(pageSize).
Offset((page - 1) * pageSize). Offset((page - 1) * pageSize).
All(&list) All(&list)
if err != nil && err != orm.ErrNoRows { if err != nil && err != orm.ErrNoRows {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil) jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
return return
} }
if list == nil { if list == nil {
list = []models.PlatformNotebook{} list = []models.PlatformNotebook{}
} }
jsonResponse(&c.Controller, 200, 200, "success", map[string]interface{}{ jsonResponse(&c.Controller, 200, 200, "success", map[string]interface{}{
"list": list, "list": list,
"total": total, "total": total,
}) })
} }
// Detail 获取笔记详情 // Detail 获取笔记详情
// GET /platform/notebook/detail/:id // GET /platform/notebook/detail/:id
func (c *PlatformNotebookController) Detail() { func (c *PlatformNotebookController) Detail() {
claims, err := requireNotebookAuth(&c.Controller) claims, err := requireNotebookAuth(&c.Controller)
if err != nil { if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil) jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return return
} }
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 { if err != nil || id == 0 {
jsonResponse(&c.Controller, 400, 400, "无效ID", nil) jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
return return
} }
var note models.PlatformNotebook var note models.PlatformNotebook
err = models.Orm.QueryTable(new(models.PlatformNotebook)). err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id). Filter("id", id).
Filter("is_deleted", 0). Filter("is_deleted", 0).
Filter("user_id", claims.UserID). Filter("user_id", claims.UserID).
One(&note) One(&note)
if err != nil { if err != nil {
if err == orm.ErrNoRows { if err == orm.ErrNoRows {
jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil) jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
} else { } else {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil) jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
} }
return return
} }
jsonResponse(&c.Controller, 200, 200, "success", note) jsonResponse(&c.Controller, 200, 200, "success", note)
} }
// Create 创建笔记 // Create 创建笔记
// POST /platform/notebook/create // POST /platform/notebook/create
func (c *PlatformNotebookController) Create() { func (c *PlatformNotebookController) Create() {
claims, err := requireNotebookAuth(&c.Controller) claims, err := requireNotebookAuth(&c.Controller)
if err != nil { if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil) jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return return
} }
raw, err := io.ReadAll(c.Ctx.Request.Body) raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil { if err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil) jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return return
} }
var payload struct { var payload struct {
Title string `json:"title"` Title string `json:"title"`
Content string `json:"content"` Content string `json:"content"`
} Pinned int8 `json:"pinned"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil) if err := json.Unmarshal(raw, &payload); err != nil {
return jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
} return
}
payload.Title = strings.TrimSpace(payload.Title)
if payload.Title == "" { payload.Title = strings.TrimSpace(payload.Title)
payload.Title = "无标题" if payload.Title == "" {
} payload.Title = "无标题"
}
userID := uint64(claims.UserID)
note := &models.PlatformNotebook{ userID := uint64(claims.UserID)
Title: payload.Title, note := &models.PlatformNotebook{
Content: payload.Content, Title: payload.Title,
UserID: &userID, Content: payload.Content,
UserName: &claims.Username, Pinned: payload.Pinned,
IsDeleted: 0, UserID: &userID,
} UserName: &claims.Username,
IsDeleted: 0,
id, err := models.Orm.Insert(note) }
if err != nil {
jsonResponse(&c.Controller, 500, 500, "创建失败", nil) id, err := models.Orm.Insert(note)
return if err != nil {
} jsonResponse(&c.Controller, 500, 500, "创建失败", nil)
return
note.ID = uint64(id) }
jsonResponse(&c.Controller, 200, 200, "创建成功", note)
} note.ID = uint64(id)
jsonResponse(&c.Controller, 200, 200, "创建成功", note)
// Update 更新笔记 }
// POST /platform/notebook/update/:id
func (c *PlatformNotebookController) Update() { // Update 更新笔记
claims, err := requireNotebookAuth(&c.Controller) // POST /platform/notebook/update/:id
if err != nil { func (c *PlatformNotebookController) Update() {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil) claims, err := requireNotebookAuth(&c.Controller)
return if err != nil {
} jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) }
if err != nil || id == 0 {
jsonResponse(&c.Controller, 400, 400, "无效ID", nil) id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
return if err != nil || id == 0 {
} jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
return
raw, err := io.ReadAll(c.Ctx.Request.Body) }
if err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil) raw, err := io.ReadAll(c.Ctx.Request.Body)
return if err != nil {
} jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return
var payload struct { }
Title string `json:"title"`
Content string `json:"content"` var payload struct {
} Title string `json:"title"`
Content string `json:"content"`
if err := json.Unmarshal(raw, &payload); err != nil { Pinned int8 `json:"pinned"`
jsonResponse(&c.Controller, 400, 400, "参数错误", nil) }
return
} if err := json.Unmarshal(raw, &payload); err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
payload.Title = strings.TrimSpace(payload.Title) return
if payload.Title == "" { }
payload.Title = "无标题"
} payload.Title = strings.TrimSpace(payload.Title)
if payload.Title == "" {
// 验证笔记是否存在且属于当前用户 payload.Title = "无标题"
var note models.PlatformNotebook }
err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id). // 验证笔记是否存在且属于当前用户
Filter("is_deleted", 0). var note models.PlatformNotebook
Filter("user_id", claims.UserID). err = models.Orm.QueryTable(new(models.PlatformNotebook)).
One(&note) Filter("id", id).
Filter("is_deleted", 0).
if err != nil { Filter("user_id", claims.UserID).
if err == orm.ErrNoRows { One(&note)
jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
} else { if err != nil {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil) if err == orm.ErrNoRows {
} jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
return } else {
} jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
}
now := time.Now() return
_, err = models.Orm.QueryTable(new(models.PlatformNotebook)). }
Filter("id", id).
Update(map[string]interface{}{ now := time.Now()
"title": payload.Title, _, err = models.Orm.QueryTable(new(models.PlatformNotebook)).
"content": payload.Content, Filter("id", id).
"update_time": now, Update(map[string]interface{}{
}) "title": payload.Title,
"content": payload.Content,
if err != nil { "pinned": payload.Pinned,
jsonResponse(&c.Controller, 500, 500, "更新失败", nil) "update_time": now,
return })
}
if err != nil {
note.Title = payload.Title jsonResponse(&c.Controller, 500, 500, "更新失败", nil)
note.Content = payload.Content return
note.UpdateTime = &now }
jsonResponse(&c.Controller, 200, 200, "更新成功", note) note.Title = payload.Title
} note.Content = payload.Content
note.Pinned = payload.Pinned
// Delete 删除笔记(软删除) note.UpdateTime = &now
// DELETE /platform/notebook/delete/:id
func (c *PlatformNotebookController) Delete() { jsonResponse(&c.Controller, 200, 200, "更新成功", note)
claims, err := requireNotebookAuth(&c.Controller) }
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil) // Delete 删除笔记(软删除)
return // DELETE /platform/notebook/delete/:id
} func (c *PlatformNotebookController) Delete() {
claims, err := requireNotebookAuth(&c.Controller)
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if err != nil {
if err != nil || id == 0 { jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
jsonResponse(&c.Controller, 400, 400, "无效ID", nil) return
return }
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
// 验证笔记是否存在且属于当前用户 if err != nil || id == 0 {
var note models.PlatformNotebook jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
err = models.Orm.QueryTable(new(models.PlatformNotebook)). return
Filter("id", id). }
Filter("is_deleted", 0).
Filter("user_id", claims.UserID). // 验证笔记是否存在且属于当前用户
One(&note) var note models.PlatformNotebook
err = models.Orm.QueryTable(new(models.PlatformNotebook)).
if err != nil { Filter("id", id).
if err == orm.ErrNoRows { Filter("is_deleted", 0).
jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil) Filter("user_id", claims.UserID).
} else { One(&note)
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
} if err != nil {
return if err == orm.ErrNoRows {
} jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
} else {
now := time.Now() jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
_, err = models.Orm.QueryTable(new(models.PlatformNotebook)). }
Filter("id", id). return
Update(map[string]interface{}{ }
"is_deleted": 1,
"delete_time": now, now := time.Now()
}) _, err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
if err != nil { Update(map[string]interface{}{
jsonResponse(&c.Controller, 500, 500, "删除失败", nil) "is_deleted": 1,
return "delete_time": now,
} })
jsonResponse(&c.Controller, 200, 200, "删除成功", nil) if err != nil {
} jsonResponse(&c.Controller, 500, 500, "删除失败", nil)
return
}
jsonResponse(&c.Controller, 200, 200, "删除成功", nil)
}
+21 -20
View File
@@ -1,20 +1,21 @@
package models package models
import "time" import "time"
// PlatformNotebook 平台记事本表: yz_platform_notebook // PlatformNotebook 平台记事本表: yz_platform_notebook
type PlatformNotebook struct { type PlatformNotebook struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"` ID uint64 `orm:"column(id);pk;auto" json:"id"`
Title string `orm:"column(title);size(255)" json:"title"` Title string `orm:"column(title);size(255)" json:"title"`
Content string `orm:"column(content);type(longtext);null" json:"content"` Content string `orm:"column(content);type(longtext);null" json:"content"`
UserID *uint64 `orm:"column(user_id);null" json:"user_id"` Pinned int8 `orm:"column(pinned);default(0)" json:"pinned"`
UserName *string `orm:"column(user_name);size(100);null" json:"user_name"` UserID *uint64 `orm:"column(user_id);null" json:"user_id"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` UserName *string `orm:"column(user_name);size(100);null" json:"user_name"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
} DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformNotebook) TableName() string {
return "yz_platform_notebook" func (m *PlatformNotebook) TableName() string {
} return "yz_platform_notebook"
}
+24 -23
View File
@@ -1,23 +1,24 @@
package models package models
import "time" import "time"
type SystemTenant struct { type SystemTenant struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"` ID uint64 `orm:"column(id);pk;auto" json:"id"`
TenantCode string `orm:"column(tenant_code);size(32)" json:"tenant_code"` TenantCode string `orm:"column(tenant_code);size(32)" json:"tenant_code"`
TenantName string `orm:"column(tenant_name);size(128)" json:"tenant_name"` TenantName string `orm:"column(tenant_name);size(128)" json:"tenant_name"`
ContactPerson *string `orm:"column(contact_person);size(64);null" json:"contact_person"` TenantShortName *string `orm:"column(tenant_short_name);size(128);null" json:"tenant_short_name"`
ContactPhone *string `orm:"column(contact_phone);size(20);null" json:"contact_phone"` ContactPerson *string `orm:"column(contact_person);size(64);null" json:"contact_person"`
ContactEmail *string `orm:"column(contact_email);size(128);null" json:"contact_email"` ContactPhone *string `orm:"column(contact_phone);size(20);null" json:"contact_phone"`
Address *string `orm:"column(address);size(255);null" json:"address"` ContactEmail *string `orm:"column(contact_email);size(128);null" json:"contact_email"`
Worktime *string `orm:"column(worktime);size(255);null" json:"worktime"` Address *string `orm:"column(address);size(255);null" json:"address"`
Status int8 `orm:"column(status);default(1)" json:"status"` Worktime *string `orm:"column(worktime);size(255);null" json:"worktime"`
Remark *string `orm:"column(remark);size(512);null" json:"remark"` Status int8 `orm:"column(status);default(1)" json:"status"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` Remark *string `orm:"column(remark);size(512);null" json:"remark"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
} DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *SystemTenant) TableName() string {
return "yz_system_tenant" func (m *SystemTenant) TableName() string {
} return "yz_system_tenant"
}
+232 -232
View File
@@ -1,232 +1,232 @@
package services package services
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
"time" "time"
"server/models" "server/models"
) )
type loginCodeItem struct { type loginCodeItem struct {
Code string Code string
Channel string Channel string
ExpiredAt time.Time ExpiredAt time.Time
} }
var loginCodeStore sync.Map var loginCodeStore sync.Map
func codeKey(account, channel string) string { func codeKey(account, channel string) string {
return strings.ToLower(strings.TrimSpace(account)) + "|" + strings.TrimSpace(channel) return strings.ToLower(strings.TrimSpace(account)) + "|" + strings.TrimSpace(channel)
} }
func SendPlatformLoginCode(account, channel string) error { func SendPlatformLoginCode(account, channel string) error {
account = strings.TrimSpace(account) account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel) channel = strings.TrimSpace(channel)
if account == "" { if account == "" {
return errors.New("账号不能为空") return errors.New("账号不能为空")
} }
if channel != "sms" && channel != "email" { if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码") return errors.New("仅支持短信或邮箱验证码")
} }
var u models.AdminUser var u models.AdminUser
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("account", account).One(&u); err != nil { if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("account", account).One(&u); err != nil {
return errors.New("用户不存在") return errors.New("用户不存在")
} }
if u.Status == 0 { if u.Status == 0 {
return errors.New("账号已禁用") return errors.New("账号已禁用")
} }
if channel == "sms" && (u.Phone == nil || strings.TrimSpace(*u.Phone) == "") { if channel == "sms" && (u.Phone == nil || strings.TrimSpace(*u.Phone) == "") {
return errors.New("该账号未绑定手机号") return errors.New("该账号未绑定手机号")
} }
if channel == "email" && (u.Email == nil || strings.TrimSpace(*u.Email) == "") { if channel == "email" && (u.Email == nil || strings.TrimSpace(*u.Email) == "") {
return errors.New("该账号未绑定邮箱") return errors.New("该账号未绑定邮箱")
} }
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000)) code := fmt.Sprintf("%06d", rand.Intn(1000000))
loginCodeStore.Store(codeKey(account, channel), loginCodeItem{ loginCodeStore.Store(codeKey(account, channel), loginCodeItem{
Code: code, Code: code,
Channel: channel, Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute), ExpiredAt: time.Now().Add(5 * time.Minute),
}) })
return nil return nil
} }
func VerifyPlatformLoginCode(account, channel, code string) error { func VerifyPlatformLoginCode(account, channel, code string) error {
account = strings.TrimSpace(account) account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel) channel = strings.TrimSpace(channel)
code = strings.TrimSpace(code) code = strings.TrimSpace(code)
if account == "" || code == "" { if account == "" || code == "" {
return errors.New("验证码不能为空") return errors.New("验证码不能为空")
} }
val, ok := loginCodeStore.Load(codeKey(account, channel)) val, ok := loginCodeStore.Load(codeKey(account, channel))
if !ok { if !ok {
return errors.New("验证码不存在或已失效") return errors.New("验证码不存在或已失效")
} }
item, ok := val.(loginCodeItem) item, ok := val.(loginCodeItem)
if !ok { if !ok {
return errors.New("验证码状态异常") return errors.New("验证码状态异常")
} }
if time.Now().After(item.ExpiredAt) { if time.Now().After(item.ExpiredAt) {
loginCodeStore.Delete(codeKey(account, channel)) loginCodeStore.Delete(codeKey(account, channel))
return errors.New("验证码已过期") return errors.New("验证码已过期")
} }
if item.Code != code { if item.Code != code {
return errors.New("验证码错误") return errors.New("验证码错误")
} }
loginCodeStore.Delete(codeKey(account, channel)) loginCodeStore.Delete(codeKey(account, channel))
return nil return nil
} }
func SendBackendLoginCode(tenantName, account, channel string) error { func SendBackendLoginCode(tenantName, account, channel string) error {
tenantName = strings.TrimSpace(tenantName) tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account) account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel) channel = strings.TrimSpace(channel)
if tenantName == "" || account == "" { if tenantName == "" || account == "" {
return errors.New("租户名称和账号不能为空") return errors.New("租户名称和账号不能为空")
} }
if channel != "sms" && channel != "email" { if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码") return errors.New("仅支持短信或邮箱验证码")
} }
var tenant models.SystemTenant tenant, err := findTenantByLoginName(tenantName)
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_name", tenantName).One(&tenant); err != nil { if err != nil {
return errors.New("租户不存在") return errors.New("租户不存在")
} }
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000)) code := fmt.Sprintf("%06d", rand.Intn(1000000))
switch channel { switch channel {
case "sms": case "sms":
phone := account phone := account
var user models.SystemTenantUser var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)). if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID). Filter("tid", tenant.ID).
Filter("phone", phone). Filter("phone", phone).
One(&user); err != nil { One(&user); err != nil {
return errors.New("该手机号非当前企业绑定号码,请重试") return errors.New("该手机号非当前企业绑定号码,请重试")
} }
if user.Status == 0 { if user.Status == 0 {
return errors.New("账号已禁用") return errors.New("账号已禁用")
} }
if user.Phone == nil || strings.TrimSpace(*user.Phone) == "" { if user.Phone == nil || strings.TrimSpace(*user.Phone) == "" {
return errors.New("该手机号非当前企业绑定号码,请重试") return errors.New("该手机号非当前企业绑定号码,请重试")
} }
content := "短信验证码:" + code content := "短信验证码:" + code
if err := enqueueSMSTaskForLogin(tenant.ID, phone, content, code); err != nil { if err := enqueueSMSTaskForLogin(tenant.ID, phone, content, code); err != nil {
return errors.New("短信发送失败,请重试") return errors.New("短信发送失败,请重试")
} }
case "email": case "email":
email := account email := account
var user models.SystemTenantUser var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)). if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID). Filter("tid", tenant.ID).
Filter("email", email). Filter("email", email).
One(&user); err != nil { One(&user); err != nil {
return errors.New("该账号未绑定邮箱") return errors.New("该账号未绑定邮箱")
} }
if user.Status == 0 { if user.Status == 0 {
return errors.New("账号已禁用") return errors.New("账号已禁用")
} }
if user.Email == nil || strings.TrimSpace(*user.Email) == "" { if user.Email == nil || strings.TrimSpace(*user.Email) == "" {
return errors.New("该账号未绑定邮箱") return errors.New("该账号未绑定邮箱")
} }
} }
loginCodeStore.Store(codeKey(tenantName+"#"+account, channel), loginCodeItem{ loginCodeStore.Store(codeKey(tenantName+"#"+account, channel), loginCodeItem{
Code: code, Code: code,
Channel: channel, Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute), ExpiredAt: time.Now().Add(5 * time.Minute),
}) })
return nil return nil
} }
func VerifyBackendLoginCode(tenantName, account, channel, code string) error { func VerifyBackendLoginCode(tenantName, account, channel, code string) error {
return VerifyPlatformLoginCode(tenantName+"#"+account, channel, code) return VerifyPlatformLoginCode(tenantName+"#"+account, channel, code)
} }
func getDefaultSystemSMSConfig() (backendURL string, apiKey string, err error) { func getDefaultSystemSMSConfig() (backendURL string, apiKey string, err error) {
backendURL = models.GetPlatformSettingValue("sms_custom_url", "") backendURL = models.GetPlatformSettingValue("sms_custom_url", "")
apiKey = models.GetPlatformSettingValue("sms_custom_key", "") apiKey = models.GetPlatformSettingValue("sms_custom_key", "")
if backendURL == "" || apiKey == "" { if backendURL == "" || apiKey == "" {
return "", "", fmt.Errorf("短信网关未配置") return "", "", fmt.Errorf("短信网关未配置")
} }
return backendURL, apiKey, nil return backendURL, apiKey, nil
} }
// enqueueSMSTaskForLogin 入队短信任务到网关,并写入 yz_system_sms_tasks // enqueueSMSTaskForLogin 入队短信任务到网关,并写入 yz_system_sms_tasks
func enqueueSMSTaskForLogin(tid uint64, phone, content, code string) error { func enqueueSMSTaskForLogin(tid uint64, phone, content, code string) error {
backendURL, apiKey, err := getDefaultSystemSMSConfig() backendURL, apiKey, err := getDefaultSystemSMSConfig()
if err != nil { if err != nil {
return err return err
} }
if backendURL == "" || apiKey == "" { if backendURL == "" || apiKey == "" {
return errors.New("短信网关未配置") return errors.New("短信网关未配置")
} }
enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks" enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
payload := map[string]interface{}{ payload := map[string]interface{}{
"phone": phone, "phone": phone,
"content": content, "content": content,
} }
bs, _ := json.Marshal(payload) bs, _ := json.Marshal(payload)
client := &http.Client{Timeout: 10 * time.Second} client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("POST", enqueueURL, bytes.NewReader(bs)) req, err := http.NewRequest("POST", enqueueURL, bytes.NewReader(bs))
if err != nil { if err != nil {
return err return err
} }
req.Header.Set("X-Api-Key", apiKey) req.Header.Set("X-Api-Key", apiKey)
req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body) bodyBytes, _ := io.ReadAll(resp.Body)
bodyStr := strings.TrimSpace(string(bodyBytes)) bodyStr := strings.TrimSpace(string(bodyBytes))
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("gateway http status: %d, body: %s", resp.StatusCode, bodyStr) return fmt.Errorf("gateway http status: %d, body: %s", resp.StatusCode, bodyStr)
} }
now := time.Now() now := time.Now()
tidCopy := tid tidCopy := tid
contentPtr := content contentPtr := content
var reportPtr *string var reportPtr *string
if bodyStr != "" { if bodyStr != "" {
reportPtr = &bodyStr reportPtr = &bodyStr
} }
task := &models.SystemSMSTask{ task := &models.SystemSMSTask{
Tid: &tidCopy, Tid: &tidCopy,
ApiKey: apiKey, ApiKey: apiKey,
Phone: phone, Phone: phone,
Content: &contentPtr, Content: &contentPtr,
Status: 3, Status: 3,
Code: code, Code: code,
ReportRaw: reportPtr, ReportRaw: reportPtr,
CreateTime: &now, CreateTime: &now,
UpdateTime: &now, UpdateTime: &now,
} }
_, insertErr := models.Orm.Insert(task) _, insertErr := models.Orm.Insert(task)
if insertErr != nil { if insertErr != nil {
return nil return nil
} }
return nil return nil
} }
+155 -158
View File
@@ -1,158 +1,155 @@
package services package services
import ( import (
"errors" "errors"
"strings" "strings"
"server/models" "server/models"
"server/pkg/jwtutil" "server/pkg/jwtutil"
"server/pkg/passwordutil" "server/pkg/passwordutil"
) )
type PlatformLoginUser struct { type PlatformLoginUser struct {
ID uint64 ID uint64
Account string Account string
Name string Name string
Tid uint64 Tid uint64
Rid uint64 Rid uint64
Avatar string Avatar string
RoleName string RoleName string
} }
func adminRoleNameByID(roleID uint64) string { func adminRoleNameByID(roleID uint64) string {
if roleID == 0 { if roleID == 0 {
return "" return ""
} }
var role models.AdminRole var role models.AdminRole
err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", roleID).One(&role) err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", roleID).One(&role)
if err != nil { if err != nil {
return "" return ""
} }
return role.Name return role.Name
} }
func toPlatformLoginUser(user *models.AdminUser) *PlatformLoginUser { func toPlatformLoginUser(user *models.AdminUser) *PlatformLoginUser {
name := "" name := ""
if user.Name != nil { if user.Name != nil {
name = *user.Name name = *user.Name
} }
avatar := "" avatar := ""
if user.Avatar != nil { if user.Avatar != nil {
avatar = *user.Avatar avatar = *user.Avatar
} }
return &PlatformLoginUser{ return &PlatformLoginUser{
ID: user.ID, ID: user.ID,
Account: user.Account, Account: user.Account,
Name: name, Name: name,
Tid: 0, Tid: 0,
Rid: user.RoleID, Rid: user.RoleID,
Avatar: avatar, Avatar: avatar,
RoleName: adminRoleNameByID(user.RoleID), RoleName: adminRoleNameByID(user.RoleID),
} }
} }
// PlatformAdminLogin 平台端登录:仅校验 yz_system_admin_user,不需要租户。 // PlatformAdminLogin 平台端登录:仅校验 yz_system_admin_user,不需要租户。
func PlatformAdminLogin(account, password string) (string, *PlatformLoginUser, error) { func PlatformAdminLogin(account, password string) (string, *PlatformLoginUser, error) {
account = strings.TrimSpace(account) account = strings.TrimSpace(account)
password = strings.TrimSpace(password) password = strings.TrimSpace(password)
if account == "" || password == "" { if account == "" || password == "" {
return "", nil, errors.New("用户名或密码不能为空") return "", nil, errors.New("用户名或密码不能为空")
} }
var user models.AdminUser var user models.AdminUser
err := models.Orm.QueryTable(new(models.AdminUser)). err := models.Orm.QueryTable(new(models.AdminUser)).
Filter("account", account). Filter("account", account).
One(&user) One(&user)
if err != nil { if err != nil {
return "", nil, errors.New("用户名或密码错误") return "", nil, errors.New("用户名或密码错误")
} }
if user.Status == 0 { if user.Status == 0 {
return "", nil, errors.New("账号已禁用") return "", nil, errors.New("账号已禁用")
} }
if !passwordutil.Verify(user.Password, password) { if !passwordutil.Verify(user.Password, password) {
return "", nil, errors.New("用户名或密码错误") return "", nil, errors.New("用户名或密码错误")
} }
const tenantID = 0 const tenantID = 0
const userType = "platform" const userType = "platform"
token, err := jwtutil.GenerateToken(int(user.ID), user.Account, tenantID, userType) token, err := jwtutil.GenerateToken(int(user.ID), user.Account, tenantID, userType)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
loginUser := toPlatformLoginUser(&user) loginUser := toPlatformLoginUser(&user)
return token, loginUser, nil return token, loginUser, nil
} }
// BackendLogin backend 登录:先校验租户,再校验租户下用户账号和密码。 // BackendLogin backend 登录:先校验租户,再校验租户下用户账号和密码。
func BackendLogin(tenantName, account, password string) (string, *PlatformLoginUser, error) { func BackendLogin(tenantName, account, password string) (string, *PlatformLoginUser, error) {
tenantName = strings.TrimSpace(tenantName) tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account) account = strings.TrimSpace(account)
password = strings.TrimSpace(password) password = strings.TrimSpace(password)
if tenantName == "" || account == "" || password == "" { if tenantName == "" || account == "" || password == "" {
return "", nil, errors.New("租户名称、用户名或密码不能为空") return "", nil, errors.New("租户名称、用户名或密码不能为空")
} }
var tenant models.SystemTenant tenant, err := findTenantByLoginName(tenantName)
err := models.Orm.QueryTable(new(models.SystemTenant)). if err != nil {
Filter("tenant_name", tenantName). return "", nil, errors.New("租户不存在")
One(&tenant) }
if err != nil { if tenant.Status != 1 {
return "", nil, errors.New("租户不存在") return "", nil, errors.New("租户已停用")
} }
if tenant.Status != 1 {
return "", nil, errors.New("租户已停用") var tenantUser models.SystemTenantUser
} err = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
var tenantUser models.SystemTenantUser Filter("account", account).
err = models.Orm.QueryTable(new(models.SystemTenantUser)). One(&tenantUser)
Filter("tid", tenant.ID). if err != nil {
Filter("account", account). return "", nil, errors.New("用户名或密码错误")
One(&tenantUser) }
if err != nil { if tenantUser.Status == 0 {
return "", nil, errors.New("用户名或密码错误") return "", nil, errors.New("账号已禁用")
} }
if tenantUser.Status == 0 { if tenantUser.Password == nil || !passwordutil.Verify(*tenantUser.Password, password) {
return "", nil, errors.New("账号已禁用") return "", nil, errors.New("用户名或密码错误")
} }
if tenantUser.Password == nil || !passwordutil.Verify(*tenantUser.Password, password) {
return "", nil, errors.New("用户名或密码错误") tenantID := int(tenant.ID)
} const userType = "backend"
token, err := jwtutil.GenerateToken(int(tenantUser.Uid), account, tenantID, userType)
tenantID := int(tenant.ID) if err != nil {
const userType = "backend" return "", nil, err
token, err := jwtutil.GenerateToken(int(tenantUser.Uid), account, tenantID, userType) }
if err != nil {
return "", nil, err loginUser := &PlatformLoginUser{
} ID: tenantUser.Uid,
Account: account,
loginUser := &PlatformLoginUser{ Name: "",
ID: tenantUser.Uid, Tid: tenant.ID,
Account: account, Rid: 0,
Name: "", Avatar: "",
Tid: tenant.ID, RoleName: "",
Rid: 0, }
Avatar: "", if tenantUser.Account != nil && strings.TrimSpace(*tenantUser.Account) != "" {
RoleName: "", loginUser.Account = strings.TrimSpace(*tenantUser.Account)
} }
if tenantUser.Account != nil && strings.TrimSpace(*tenantUser.Account) != "" { if tenantUser.Name != nil {
loginUser.Account = strings.TrimSpace(*tenantUser.Account) loginUser.Name = strings.TrimSpace(*tenantUser.Name)
} }
if tenantUser.Name != nil {
loginUser.Name = strings.TrimSpace(*tenantUser.Name) return token, loginUser, nil
} }
return token, loginUser, nil // PlatformGetCurrentUser 根据平台管理员用户 ID 返回登录用户信息(含角色名称)。
} func PlatformGetCurrentUser(uid uint64) (*PlatformLoginUser, error) {
u, err := GetAdminUserByID(uid)
// PlatformGetCurrentUser 根据平台管理员用户 ID 返回登录用户信息(含角色名称)。 if err != nil {
func PlatformGetCurrentUser(uid uint64) (*PlatformLoginUser, error) { return nil, errors.New("用户不存在")
u, err := GetAdminUserByID(uid) }
if err != nil { if u.Status == 0 {
return nil, errors.New("用户不存在") return nil, errors.New("账号已禁用")
} }
if u.Status == 0 { return toPlatformLoginUser(u), nil
return nil, errors.New("账号已禁用") }
}
return toPlatformLoginUser(u), nil
}
+43
View File
@@ -0,0 +1,43 @@
package services
import (
"strings"
"github.com/beego/beego/v2/client/orm"
"server/models"
)
/* findTenantByLoginName 根据租户简称短名或编码查找租户
*
* 当前表结构中 tenant_name 的业务含义就是租户简称同时兼容
* tenant_short_name tenant_code 两个历史/备用登录标识
*/
func findTenantByLoginName(loginName string) (*models.SystemTenant, error) {
loginName = strings.TrimSpace(loginName)
if loginName == "" {
return nil, orm.ErrNoRows
}
// tenant_name 的业务含义是租户简称。使用 TRIM 兼容历史数据中字段值
// 前后存在空格的情况;参数通过 SetArgs 绑定,避免 SQL 注入。
tenant := &models.SystemTenant{}
query := `
SELECT id, tenant_code, tenant_name, tenant_short_name,
contact_person, contact_phone, contact_email, address,
worktime, status, remark, create_time, update_time, delete_time
FROM yz_system_tenant
WHERE (TRIM(tenant_name) = TRIM(?) OR
TRIM(tenant_short_name) = TRIM(?) OR
TRIM(tenant_code) = TRIM(?))
AND status <> 0
ORDER BY id ASC
LIMIT 1`
err := models.Orm.Raw(query).
SetArgs(loginName, loginName, loginName).
QueryRow(tenant)
if err != nil {
return nil, err
}
return tenant, nil
}
@@ -15,6 +15,16 @@
</div> </div>
</div> </div>
<div class="editor-toolbar">
<el-switch
v-model="pinned"
inline-prompt
active-text="置顶"
inactive-text="置顶"
:disabled="loading"
/>
</div>
<div class="editor-body"> <div class="editor-body">
<UmoEditor v-model="noteContent" /> <UmoEditor v-model="noteContent" />
</div> </div>
@@ -38,6 +48,7 @@ const emit = defineEmits(['save-success', 'create-success']);
const noteTitle = ref(''); const noteTitle = ref('');
const noteContent = ref(''); const noteContent = ref('');
const pinned = ref(false);
const loading = ref(false); const loading = ref(false);
const isNew = ref(false); const isNew = ref(false);
@@ -47,6 +58,7 @@ const loadNote = async () => {
isNew.value = true; isNew.value = true;
noteTitle.value = '新建笔记'; noteTitle.value = '新建笔记';
noteContent.value = ''; noteContent.value = '';
pinned.value = false;
return; return;
} }
@@ -58,6 +70,7 @@ const loadNote = async () => {
if (response.code === 200) { if (response.code === 200) {
noteTitle.value = response.data.title || '无标题'; noteTitle.value = response.data.title || '无标题';
noteContent.value = response.data.content || ''; noteContent.value = response.data.content || '';
pinned.value = Number(response.data.pinned) === 1;
} else { } else {
ElMessage.error('加载笔记失败'); ElMessage.error('加载笔记失败');
} }
@@ -84,6 +97,7 @@ const handleSave = async () => {
const response = await createNotebook({ const response = await createNotebook({
title: noteTitle.value, title: noteTitle.value,
content: noteContent.value, content: noteContent.value,
pinned: pinned.value ? 1 : 0,
}); });
if (response.code === 200) { if (response.code === 200) {
@@ -98,6 +112,7 @@ const handleSave = async () => {
const response = await updateNotebook(props.noteId, { const response = await updateNotebook(props.noteId, {
title: noteTitle.value, title: noteTitle.value,
content: noteContent.value, content: noteContent.value,
pinned: pinned.value ? 1 : 0,
}); });
if (response.code === 200) { if (response.code === 200) {
@@ -175,6 +190,15 @@ onMounted(() => {
} }
} }
.editor-toolbar {
min-height: 42px;
padding: 8px 20px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--el-border-color-lighter);
background: var(--el-bg-color);
}
.editor-body { .editor-body {
flex: 1; flex: 1;
padding: 20px; padding: 20px;
@@ -28,6 +28,15 @@
> >
<div class="note-item-header"> <div class="note-item-header">
<span class="note-title">{{ note.title || '无标题' }}</span> <span class="note-title">{{ note.title || '无标题' }}</span>
<el-tag
v-if="Number(note.pinned) === 1"
class="pinned-tag"
size="small"
type="warning"
effect="plain"
>
置顶
</el-tag>
<el-dropdown trigger="click" @command="(cmd) => handleNoteAction(cmd, note)"> <el-dropdown trigger="click" @command="(cmd) => handleNoteAction(cmd, note)">
<el-icon class="note-more"><MoreFilled /></el-icon> <el-icon class="note-more"><MoreFilled /></el-icon>
<template #dropdown> <template #dropdown>
@@ -305,6 +314,7 @@ onMounted(() => {
.note-title { .note-title {
flex: 1; flex: 1;
min-width: 0;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
@@ -313,6 +323,11 @@ onMounted(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.pinned-tag {
flex-shrink: 0;
margin-left: 8px;
}
.note-more { .note-more {
margin-left: 8px; margin-left: 8px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
+14
View File
@@ -0,0 +1,14 @@
-- 记事本表增加置顶字段(兼容已有数据库)
-- 已存在字段时请跳过对应 ALTER 语句。
ALTER TABLE `yz_platform_notebook`
ADD COLUMN `pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0-否 1-是' AFTER `content`;
ALTER TABLE `yz_platform_notebook`
ADD KEY `idx_pinned` (`pinned`);
ALTER TABLE `yz_backend_notebook`
ADD COLUMN `pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0-否 1-是' AFTER `content`;
ALTER TABLE `yz_backend_notebook`
ADD KEY `idx_pinned` (`pinned`);
+2
View File
@@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS `yz_platform_notebook` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '笔记标题', `title` varchar(255) NOT NULL DEFAULT '' COMMENT '笔记标题',
`content` longtext COMMENT '笔记内容(HTML格式)', `content` longtext COMMENT '笔记内容(HTML格式)',
`pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0-否 1-是',
`user_id` bigint(20) unsigned DEFAULT NULL COMMENT '创建用户ID', `user_id` bigint(20) unsigned DEFAULT NULL COMMENT '创建用户ID',
`user_name` varchar(100) DEFAULT NULL COMMENT '创建用户名', `user_name` varchar(100) DEFAULT NULL COMMENT '创建用户名',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是',
@@ -11,6 +12,7 @@ CREATE TABLE IF NOT EXISTS `yz_platform_notebook` (
`delete_time` datetime DEFAULT NULL COMMENT '删除时间', `delete_time` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`), KEY `idx_user_id` (`user_id`),
KEY `idx_pinned` (`pinned`),
KEY `idx_create_time` (`create_time`), KEY `idx_create_time` (`create_time`),
KEY `idx_is_deleted` (`is_deleted`) KEY `idx_is_deleted` (`is_deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台记事本表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台记事本表';
+24
View File
@@ -0,0 +1,24 @@
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/controllers [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/middleware [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/models [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/jwtutil [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/tokenprobe [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/api [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/app [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/backend [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/index [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/platform [setup failed]
FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/services [setup failed]
FAIL backend [setup failed]
FAIL docs [setup failed]
FAIL frontend [setup failed]
FAIL go [setup failed]
FAIL platform [setup failed]
FAIL sql [setup failed]
FAIL uniapp [setup failed]
? _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/passwordutil [no test files]
? _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/versionutil [no test files]
? _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/version [no test files]
FAIL