diff --git a/backend/src/api/oaNotice.js b/backend/src/api/oaNotice.js new file mode 100644 index 0000000..1edb5a6 --- /dev/null +++ b/backend/src/api/oaNotice.js @@ -0,0 +1,67 @@ +import request from '@/utils/request' + +// OA 通知公告 +// 响应拦截器只返回 {code, data, msg} 包装,调用方需自行取 res.data + +export function getNoticeList(params) { + return request({ + url: '/backend/oa/notice/list', + method: 'get', + params + }) +} + +// 工作台/仪表盘用:已发布公告,置顶优先 +export function getNoticePortal(params) { + return request({ + url: '/backend/oa/notice/portal', + method: 'get', + params + }) +} + +export function getNoticeDetail(id) { + return request({ + url: `/backend/oa/notice/detail/${id}`, + method: 'get' + }) +} + +export function createNotice(data) { + return request({ + url: '/backend/oa/notice/create', + method: 'post', + data + }) +} + +export function updateNotice(id, data) { + return request({ + url: `/backend/oa/notice/update/${id}`, + method: 'post', + data + }) +} + +export function deleteNotice(id) { + return request({ + url: `/backend/oa/notice/delete/${id}`, + method: 'delete' + }) +} + +// 发布 / 下架切换 +export function publishNotice(id) { + return request({ + url: `/backend/oa/notice/publish/${id}`, + method: 'post' + }) +} + +// 置顶 / 取消置顶切换 +export function topNotice(id) { + return request({ + url: `/backend/oa/notice/top/${id}`, + method: 'post' + }) +} diff --git a/backend/src/views/apps/oa/dashboard/index.vue b/backend/src/views/apps/oa/dashboard/index.vue index ff8eae4..47f0b57 100644 --- a/backend/src/views/apps/oa/dashboard/index.vue +++ b/backend/src/views/apps/oa/dashboard/index.vue @@ -93,8 +93,42 @@ - +
+
+
+ 通知公告 + 查看全部 +
+
+
+ + {{ noticeTypeLabel(item.notice_type) }} + + 置顶 + {{ item.title }} + {{ noticeTime(item) }} +
+ +
+
+
工作日程提醒 @@ -179,6 +213,35 @@
+ + +
+
+ + {{ noticeTypeLabel(noticeDetailItem.notice_type) }} + + {{ noticeDetailItem.publisher_name }} · + {{ formatDateTime(noticeDetailItem.publish_time) }} + 阅读 {{ noticeDetailItem.read_count || 0 }} +
+
+ {{ noticeDetailItem.content || "暂无内容" }} +
+
+
+ { router.push("/apps/oa/schedule"); }; +// ---------- 通知公告 ---------- +const notices = ref([]); +const noticeLoading = ref(false); +const noticeDetailVisible = ref(false); +const noticeDetailItem = ref(null); + +const noticeTypeLabel = (type) => ({ 1: "公告", 2: "活动" }[type] || "通知"); +const noticeTypeTag = (type) => ({ 1: "primary", 2: "success" }[type] || "info"); + +const formatDateTime = (value) => { + if (!value) return "-"; + const d = new Date(value); + if (Number.isNaN(d.getTime())) return String(value); + return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2( + d.getHours() + )}:${pad2(d.getMinutes())}`; +}; + +// 列表右侧时间:今天显示 HH:mm,本年显示 M/D,跨年显示 YYYY-M-D +const noticeTime = (item) => { + const d = item.publish_time ? new Date(item.publish_time) : null; + if (!d || Number.isNaN(d.getTime())) return ""; + const now = new Date(); + if (fmtDate(d) === fmtDate(now)) { + return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; + } + if (d.getFullYear() === now.getFullYear()) { + return `${d.getMonth() + 1}/${d.getDate()}`; + } + return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`; +}; + +const loadNotices = async () => { + noticeLoading.value = true; + try { + const res = await getNoticePortal({ limit: 6 }); + notices.value = res?.data?.list || []; + } catch (error) { + console.warn("加载通知公告失败:", error?.message); + } finally { + noticeLoading.value = false; + } +}; + +const openNoticeDetail = async (item) => { + const res = await getNoticeDetail(item.id); + if (res?.code === 200) { + noticeDetailItem.value = res.data; + noticeDetailVisible.value = true; + } else { + ElMessage.error(res?.msg || "加载公告失败"); + } +}; + +const goNotice = () => router.push("/apps/oa/notice"); + const loadDashboard = async () => { try { const data = responseData(await getReimbursementDashboard()); @@ -473,6 +596,7 @@ const loadDashboard = async () => { } renderTrendChart(); loadScheduleReminders(); + loadNotices(); }; const handleResize = () => trendChart?.resize(); @@ -629,6 +753,90 @@ onBeforeUnmount(() => { margin-bottom: 0; } +.notice-card { + margin-bottom: 16px; + + .notice-title { + display: flex; + align-items: center; + justify-content: space-between; + } +} + +.notice-list { + display: flex; + flex-direction: column; + gap: 6px; + min-height: 80px; + max-height: 240px; + overflow-y: auto; +} + +.notice-item { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + background: #fafbfc; + + &:hover { + background: #eef2f7; + } + + .notice-type { + flex-shrink: 0; + } + + .notice-top { + flex-shrink: 0; + font-size: 11px; + color: #f56c6c; + } + + .notice-text { + flex: 1; + min-width: 0; + color: #303133; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .notice-time { + flex-shrink: 0; + color: #c0c4cc; + font-size: 12px; + } +} + +.notice-detail-meta { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding-bottom: 12px; + border-bottom: 1px solid #ebeef5; +} + +.notice-detail-text { + font-size: 12px; + color: #909399; +} + +.notice-detail-content { + margin-top: 12px; + font-size: 14px; + line-height: 1.8; + color: #303133; + white-space: pre-wrap; + word-break: break-word; + max-height: 420px; + overflow-y: auto; +} + .schedule-card { display: flex; flex-direction: column; diff --git a/backend/src/views/apps/oa/notice/components/edit.vue b/backend/src/views/apps/oa/notice/components/edit.vue new file mode 100644 index 0000000..286e815 --- /dev/null +++ b/backend/src/views/apps/oa/notice/components/edit.vue @@ -0,0 +1,161 @@ + + + diff --git a/backend/src/views/apps/oa/notice/index.vue b/backend/src/views/apps/oa/notice/index.vue new file mode 100644 index 0000000..ce2be6f --- /dev/null +++ b/backend/src/views/apps/oa/notice/index.vue @@ -0,0 +1,389 @@ + + + + + diff --git a/backend/src/views/basicSettings/siteSettings/index.vue b/backend/src/views/basicSettings/siteSettings/index.vue index b18db65..aea905d 100644 --- a/backend/src/views/basicSettings/siteSettings/index.vue +++ b/backend/src/views/basicSettings/siteSettings/index.vue @@ -39,10 +39,6 @@ v-if="activeTab === 'legalNotice'" /> - - - -
@@ -53,7 +49,6 @@ import { ref, onMounted } from "vue"; import normalSettings from "./components/normalSettings.vue"; import seoSettings from "./components/seoSettings.vue"; import contactSettings from "./components/contactSettings.vue"; -import otherSettings from "./components/otherSettings.vue"; import legalNoticeSettings from "./components/legalNotice.vue"; import loginVerificationSettings from "./components/loginVerification.vue"; import { useAuthStore } from '@/stores/auth'; diff --git a/go/controllers/backend_oa_notice.go b/go/controllers/backend_oa_notice.go new file mode 100644 index 0000000..99c6f4f --- /dev/null +++ b/go/controllers/backend_oa_notice.go @@ -0,0 +1,416 @@ +package controllers + +import ( + "encoding/json" + "io" + "strconv" + "strings" + "time" + "unicode/utf8" + + "server/models" + "server/pkg/jwtutil" + + "github.com/beego/beego/v2/client/orm" + beego "github.com/beego/beego/v2/server/web" +) + +// BackendOaNoticeController OA通知公告管理。 +// 数据按租户 tid 隔离;发布后对租户内所有后台用户可见。 +type BackendOaNoticeController struct { + beego.Controller +} + +// Prepare 所有接口执行前确保通知公告表存在。 +func (c *BackendOaNoticeController) Prepare() { + _ = models.EnsureOaNoticeTable() +} + +func (c *BackendOaNoticeController) oaNoticeClaims() (*jwtutil.Claims, error) { + auth := c.Ctx.Request.Header.Get("Authorization") + if auth == "" { + return nil, orm.ErrNoRows + } + parts := strings.SplitN(auth, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + return nil, orm.ErrNoRows + } + claims, err := jwtutil.ParseToken(parts[1]) + if err != nil { + return nil, err + } + if claims.UserType != "backend" { + return nil, orm.ErrNoRows + } + return claims, nil +} + +func (c *BackendOaNoticeController) ontJsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *BackendOaNoticeController) ontOk(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +type oaNoticePayload struct { + Title string `json:"title"` + Content string `json:"content"` + NoticeType int8 `json:"notice_type"` + Level int8 `json:"level"` + IsTop int8 `json:"is_top"` + // Publish 为 true 时保存即发布 + Publish bool `json:"publish"` +} + +// parseOaNoticePayload 读取并校验公告请求体;失败时直接输出错误响应。 +func (c *BackendOaNoticeController) parseOaNoticePayload() (oaNoticePayload, bool) { + var payload oaNoticePayload + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil || json.Unmarshal(raw, &payload) != nil { + c.ontJsonErr(400, 400, "参数错误") + return payload, false + } + + payload.Title = strings.TrimSpace(payload.Title) + if payload.Title == "" { + c.ontJsonErr(400, 400, "请输入公告标题") + return payload, false + } + if utf8.RuneCountInString(payload.Title) > 100 { + c.ontJsonErr(400, 400, "公告标题不能超过100字") + return payload, false + } + if payload.NoticeType < 0 || payload.NoticeType > 2 { + payload.NoticeType = 0 + } + if payload.Level < 0 || payload.Level > 2 { + payload.Level = 0 + } + if payload.IsTop != 1 { + payload.IsTop = 0 + } + payload.Content = strings.TrimSpace(payload.Content) + return payload, true +} + +// noticeBase 租户内未删除公告的基础查询集 +func noticeBase(tid int) orm.QuerySeter { + return models.Orm.QueryTable(new(models.OaNotice)). + Filter("is_deleted", 0). + Filter("tid", tid) +} + +// List GET /backend/oa/notice/list +// 管理端分页列表,支持 keyword/status/notice_type 筛选;置顶优先,再按发布时间倒序。 +func (c *BackendOaNoticeController) List() { + claims, err := c.oaNoticeClaims() + if err != nil { + c.ontJsonErr(401, 401, "未登录或无权限") + return + } + + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + keyword := strings.TrimSpace(c.GetString("keyword")) + status := strings.TrimSpace(c.GetString("status")) + noticeType := strings.TrimSpace(c.GetString("notice_type")) + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 500 { + pageSize = 20 + } + + qs := noticeBase(claims.TenantId) + if keyword != "" { + qs = qs.Filter("title__icontains", keyword) + } + if status == "0" || status == "1" || status == "2" { + v, _ := strconv.Atoi(status) + qs = qs.Filter("status", int8(v)) + } + if noticeType == "0" || noticeType == "1" || noticeType == "2" { + v, _ := strconv.Atoi(noticeType) + qs = qs.Filter("notice_type", int8(v)) + } + + total, _ := qs.Count() + + var list []models.OaNotice + _, err = qs.OrderBy("-is_top", "-publish_time", "-id"). + Limit(pageSize).Offset((page - 1) * pageSize). + All(&list) + if err != nil && err != orm.ErrNoRows { + c.ontJsonErr(500, 500, "查询失败") + return + } + if list == nil { + list = []models.OaNotice{} + } + + c.ontOk(map[string]interface{}{"list": list, "total": total}) +} + +// Portal GET /backend/oa/notice/portal +// 工作台/仪表盘用:只返回已发布的公告,置顶优先,取最近 limit 条(默认 5,最多 20)。 +func (c *BackendOaNoticeController) Portal() { + claims, err := c.oaNoticeClaims() + if err != nil { + c.ontJsonErr(401, 401, "未登录或无权限") + return + } + + limit, _ := c.GetInt("limit", 5) + if limit < 1 || limit > 20 { + limit = 5 + } + + var list []models.OaNotice + _, err = noticeBase(claims.TenantId). + Filter("status", 1). + OrderBy("-is_top", "-publish_time", "-id"). + Limit(limit). + All(&list) + if err != nil && err != orm.ErrNoRows { + c.ontJsonErr(500, 500, "查询失败") + return + } + if list == nil { + list = []models.OaNotice{} + } + + c.ontOk(map[string]interface{}{"list": list}) +} + +// Detail GET /backend/oa/notice/detail/:id +// 公告详情,同时累加阅读次数。 +func (c *BackendOaNoticeController) Detail() { + claims, err := c.oaNoticeClaims() + if err != nil { + c.ontJsonErr(401, 401, "未登录或无权限") + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.ontJsonErr(400, 400, "无效ID") + return + } + + var item models.OaNotice + err = noticeBase(claims.TenantId).Filter("id", id).One(&item) + if err != nil { + c.ontJsonErr(404, 404, "公告不存在") + return + } + + // 阅读数自增(并发下允许略有偏差,仅为展示用) + _, _ = models.Orm.Raw( + "UPDATE yz_backend_oa_notice SET read_count = read_count + 1 WHERE id = ?", id).Exec() + item.ReadCount++ + + c.ontOk(item) +} + +// Create POST /backend/oa/notice/create +func (c *BackendOaNoticeController) Create() { + claims, err := c.oaNoticeClaims() + if err != nil { + c.ontJsonErr(401, 401, "未登录或无权限") + return + } + + payload, ok := c.parseOaNoticePayload() + if !ok { + return + } + + now := time.Now() + item := &models.OaNotice{ + Tid: claims.TenantId, + Title: payload.Title, + Content: payload.Content, + NoticeType: payload.NoticeType, + Level: payload.Level, + IsTop: payload.IsTop, + Status: 0, + PublisherID: uint64(claims.UserID), + PublisherName: claims.Username, + IsDeleted: 0, + CreateTime: now, + UpdateTime: &now, + } + if payload.Publish { + item.Status = 1 + item.PublishTime = &now + } + + if _, err := models.Orm.Insert(item); err != nil { + c.ontJsonErr(500, 500, "保存失败") + return + } + + c.ontOk(item) +} + +// Update POST /backend/oa/notice/update/:id +// 编辑公告内容;已发布的公告编辑后保持发布状态(不改变发布时间)。 +func (c *BackendOaNoticeController) Update() { + claims, err := c.oaNoticeClaims() + if err != nil { + c.ontJsonErr(401, 401, "未登录或无权限") + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.ontJsonErr(400, 400, "无效ID") + return + } + + payload, ok := c.parseOaNoticePayload() + if !ok { + return + } + + var item models.OaNotice + err = noticeBase(claims.TenantId).Filter("id", id).One(&item) + if err != nil { + c.ontJsonErr(404, 404, "公告不存在") + return + } + + now := time.Now() + item.Title = payload.Title + item.Content = payload.Content + item.NoticeType = payload.NoticeType + item.Level = payload.Level + item.IsTop = payload.IsTop + item.UpdateTime = &now + + fields := []string{"Title", "Content", "NoticeType", "Level", "IsTop", "UpdateTime"} + // 草稿保存时选择"发布"则直接发布 + if payload.Publish && item.Status != 1 { + item.Status = 1 + item.PublishTime = &now + fields = append(fields, "Status", "PublishTime") + } + + if _, err := models.Orm.Update(&item, fields...); err != nil { + c.ontJsonErr(500, 500, "保存失败") + return + } + + c.ontOk(item) +} + +// Delete DELETE /backend/oa/notice/delete/:id 软删除。 +func (c *BackendOaNoticeController) Delete() { + claims, err := c.oaNoticeClaims() + if err != nil { + c.ontJsonErr(401, 401, "未登录或无权限") + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.ontJsonErr(400, 400, "无效ID") + return + } + + now := time.Now() + n, err := noticeBase(claims.TenantId).Filter("id", id). + Update(map[string]interface{}{ + "IsDeleted": int8(1), + "DeleteTime": now, + "UpdateTime": now, + }) + if err != nil || n == 0 { + c.ontJsonErr(404, 404, "公告不存在或已删除") + return + } + + c.ontOk(nil) +} + +// Publish POST /backend/oa/notice/publish/:id +// 发布 / 下架切换:草稿、已下架 -> 已发布(写入发布时间);已发布 -> 已下架。 +func (c *BackendOaNoticeController) Publish() { + claims, err := c.oaNoticeClaims() + if err != nil { + c.ontJsonErr(401, 401, "未登录或无权限") + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.ontJsonErr(400, 400, "无效ID") + return + } + + var item models.OaNotice + err = noticeBase(claims.TenantId).Filter("id", id).One(&item) + if err != nil { + c.ontJsonErr(404, 404, "公告不存在") + return + } + + now := time.Now() + if item.Status == 1 { + item.Status = 2 + item.PublishTime = nil + } else { + item.Status = 1 + item.PublishTime = &now + } + item.UpdateTime = &now + + if _, err := models.Orm.Update(&item, "Status", "PublishTime", "UpdateTime"); err != nil { + c.ontJsonErr(500, 500, "操作失败") + return + } + + c.ontOk(map[string]interface{}{"status": item.Status}) +} + +// Top POST /backend/oa/notice/top/:id 置顶 / 取消置顶切换。 +func (c *BackendOaNoticeController) Top() { + claims, err := c.oaNoticeClaims() + if err != nil { + c.ontJsonErr(401, 401, "未登录或无权限") + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.ontJsonErr(400, 400, "无效ID") + return + } + + var item models.OaNotice + err = noticeBase(claims.TenantId).Filter("id", id).One(&item) + if err != nil { + c.ontJsonErr(404, 404, "公告不存在") + return + } + + now := time.Now() + if item.IsTop == 1 { + item.IsTop = 0 + } else { + item.IsTop = 1 + } + item.UpdateTime = &now + + if _, err := models.Orm.Update(&item, "IsTop", "UpdateTime"); err != nil { + c.ontJsonErr(500, 500, "操作失败") + return + } + + c.ontOk(map[string]interface{}{"is_top": item.IsTop}) +} diff --git a/go/main.exe b/go/main.exe index 3c85445..3520e6f 100644 Binary files a/go/main.exe and b/go/main.exe differ diff --git a/go/models/init.go b/go/models/init.go index c155d42..0410a71 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -113,6 +113,7 @@ func Init(_ string) { new(BackendScheduleReminderSendLog), new(OaSchedule), + new(OaNotice), new(BackendOaCompensationScheme), new(BackendOaPayroll), new(BackendOaPayrollItem), diff --git a/go/models/oa_notice.go b/go/models/oa_notice.go new file mode 100644 index 0000000..94f6874 --- /dev/null +++ b/go/models/oa_notice.go @@ -0,0 +1,69 @@ +package models + +import ( + "sync" + "time" +) + +// OaNotice OA通知公告: yz_backend_oa_notice +// 按租户 tid 隔离:租户内发布,租户内所有后台用户可见。 +// status: 0-草稿 1-已发布 2-已下架;notice_type: 0-通知 1-公告 2-活动; +// level: 0-普通 1-重要 2-紧急;is_top: 0-否 1-是(置顶优先展示)。 +type OaNotice struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid int `orm:"column(tid)" json:"tid"` + Title string `orm:"column(title);size(255)" json:"title"` + Content string `orm:"column(content);type(text);null" json:"content"` + NoticeType int8 `orm:"column(notice_type);default(0)" json:"notice_type"` + Level int8 `orm:"column(level);default(0)" json:"level"` + Status int8 `orm:"column(status);default(0)" json:"status"` + IsTop int8 `orm:"column(is_top);default(0)" json:"is_top"` + PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"` + PublisherID uint64 `orm:"column(publisher_id);default(0)" json:"publisher_id"` + PublisherName string `orm:"column(publisher_name);size(100);default()" json:"publisher_name"` + ReadCount int `orm:"column(read_count);default(0)" json:"read_count"` + IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *OaNotice) TableName() string { + return "yz_backend_oa_notice" +} + +var oaNoticeTableOnce sync.Once + +// EnsureOaNoticeTable 首次访问通知公告接口时自动建表(若不存在)。 +// 与日程表保持一致的"运行时自愈"策略,避免新功能上线还要手工执行建表脚本。 +func EnsureOaNoticeTable() error { + if Orm == nil { + return nil + } + var err error + oaNoticeTableOnce.Do(func() { + _, err = Orm.Raw(` +CREATE TABLE IF NOT EXISTS yz_backend_oa_notice ( + id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tid int NOT NULL DEFAULT 0 COMMENT '租户ID', + title varchar(255) NOT NULL DEFAULT '' COMMENT '公告标题', + content text COMMENT '公告内容', + notice_type tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-通知 1-公告 2-活动', + level tinyint NOT NULL DEFAULT 0 COMMENT '紧急程度 0-普通 1-重要 2-紧急', + status tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已下架', + is_top tinyint NOT NULL DEFAULT 0 COMMENT '是否置顶 0-否 1-是', + publish_time datetime DEFAULT NULL COMMENT '发布时间', + publisher_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '发布人ID', + publisher_name varchar(100) NOT NULL DEFAULT '' COMMENT '发布人姓名', + read_count int NOT NULL DEFAULT 0 COMMENT '阅读次数', + is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是', + create_time datetime DEFAULT NULL COMMENT '创建时间', + update_time datetime DEFAULT NULL COMMENT '更新时间', + delete_time datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (id), + KEY idx_tid_status (tid, status), + KEY idx_tid_top (tid, is_top) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA通知公告表'`).Exec() + }) + return err +} diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 63a7cc2..b4476cc 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -268,6 +268,16 @@ func RegisterAuthRoutes() { beego.Router("/backend/oa/schedule/finish/:id", &controllers.BackendOaScheduleController{}, "post:FinishSchedule") beego.Router("/backend/oa/schedule/carry/:id", &controllers.BackendOaScheduleController{}, "post:CarrySchedule") beego.Router("/backend/oa/schedule/carry-pending", &controllers.BackendOaScheduleController{}, "post:CarryPending") + + // OA通知公告 + beego.Router("/backend/oa/notice/list", &controllers.BackendOaNoticeController{}, "get:List") + beego.Router("/backend/oa/notice/portal", &controllers.BackendOaNoticeController{}, "get:Portal") + beego.Router("/backend/oa/notice/detail/:id", &controllers.BackendOaNoticeController{}, "get:Detail") + beego.Router("/backend/oa/notice/create", &controllers.BackendOaNoticeController{}, "post:Create") + beego.Router("/backend/oa/notice/update/:id", &controllers.BackendOaNoticeController{}, "post:Update") + beego.Router("/backend/oa/notice/delete/:id", &controllers.BackendOaNoticeController{}, "delete:Delete") + beego.Router("/backend/oa/notice/publish/:id", &controllers.BackendOaNoticeController{}, "post:Publish") + beego.Router("/backend/oa/notice/top/:id", &controllers.BackendOaNoticeController{}, "post:Top") } // registerOrganizationRoutes 为指定模块前缀注册组织架构路由。 diff --git a/sql/seed_oa_notice_menu.sql b/sql/seed_oa_notice_menu.sql new file mode 100644 index 0000000..d086185 --- /dev/null +++ b/sql/seed_oa_notice_menu.sql @@ -0,0 +1,69 @@ +-- 通知公告菜单(租户端):挂在“办公自动化”模块目录下。 +-- +-- 说明: +-- 1. 本脚本只创建/修正 yz_system_menu 菜单记录,不涉及通知公告业务数据表。 +-- 2. 路由由前端根据登录接口返回的菜单数据动态注册; +-- 不需要在 backend/src/router/index.js 中增加静态业务路由。 +-- 3. 执行完成后,请在角色菜单权限关联表中为目标角色授予“通知公告”菜单权限, +-- 然后退出重登或清理菜单缓存。 +-- +-- views: [2] = 租户端 +-- type: 1 = 目录,2 = 页面 +-- 脚本可重复执行:通过 path 判重,已有记录会被修正为当前配置。 + +-- 确保“办公自动化”父级目录存在。 +INSERT INTO `yz_system_menu` + (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) +SELECT + 0, '办公自动化', '/apps/oa', '', 'Document', 31, 1, 1, '[2]', 1, '办公自动化模块' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 + FROM (SELECT * FROM `yz_system_menu`) AS t + WHERE t.`path` = '/apps/oa' +); + +SET @oa_pid := ( + SELECT `id` + FROM `yz_system_menu` + WHERE `path` = '/apps/oa' + ORDER BY `id` + LIMIT 1 +); + +-- 新增“通知公告”页面菜单。 +INSERT INTO `yz_system_menu` + (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) +SELECT + @oa_pid, + '通知公告', + '/apps/oa/notice', + '/apps/oa/notice/index.vue', + 'Bell', + 7, + 1, + 1, + '[2]', + 2, + '通知公告发布与管理' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 + FROM (SELECT * FROM `yz_system_menu`) AS t + WHERE t.`path` = '/apps/oa/notice' +); + +-- 修正历史或手工新增菜单的动态组件路径及基础属性。 +UPDATE `yz_system_menu` +SET + `pid` = @oa_pid, + `title` = '通知公告', + `component_path` = '/apps/oa/notice/index.vue', + `icon` = 'Bell', + `sort` = 7, + `status` = 1, + `is_visible` = 1, + `views` = '[2]', + `type` = 2, + `remark` = '通知公告发布与管理' +WHERE `path` = '/apps/oa/notice'; diff --git a/sql/yz_backend_oa_notice.sql b/sql/yz_backend_oa_notice.sql new file mode 100644 index 0000000..3edd25e --- /dev/null +++ b/sql/yz_backend_oa_notice.sql @@ -0,0 +1,25 @@ +-- OA 通知公告表 +-- 在租户业务库执行以下语句创建表结构。 +-- 数据按租户 tid 隔离;发布后对租户内所有后台用户可见。 + +CREATE TABLE `yz_backend_oa_notice` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tid` int NOT NULL DEFAULT '0' COMMENT '租户ID', + `title` varchar(255) NOT NULL DEFAULT '' COMMENT '公告标题', + `content` text NULL COMMENT '公告内容', + `notice_type` tinyint NOT NULL DEFAULT '0' COMMENT '类型 0-通知 1-公告 2-活动', + `level` tinyint NOT NULL DEFAULT '0' COMMENT '紧急程度 0-普通 1-重要 2-紧急', + `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0-草稿 1-已发布 2-已下架', + `is_top` tinyint NOT NULL DEFAULT '0' COMMENT '是否置顶 0-否 1-是', + `publish_time` datetime DEFAULT NULL COMMENT '发布时间', + `publisher_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '发布人ID', + `publisher_name` varchar(100) NOT NULL DEFAULT '' COMMENT '发布人姓名', + `read_count` int NOT NULL DEFAULT '0' COMMENT '阅读次数', + `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + KEY `idx_tid_status` (`tid`,`status`), + KEY `idx_tid_top` (`tid`,`is_top`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA通知公告表';