diff --git a/backend/components.d.ts b/backend/components.d.ts index 46b841b..96bb377 100644 --- a/backend/components.d.ts +++ b/backend/components.d.ts @@ -68,6 +68,7 @@ declare module 'vue' { ElTag: typeof import('element-plus/es')['ElTag'] ElTimeline: typeof import('element-plus/es')['ElTimeline'] ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] + ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTree: typeof import('element-plus/es')['ElTree'] ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] diff --git a/backend/src/api/oaSchedule.js b/backend/src/api/oaSchedule.js new file mode 100644 index 0000000..c46dab0 --- /dev/null +++ b/backend/src/api/oaSchedule.js @@ -0,0 +1,49 @@ +import request from '@/utils/request' + +// OA 日程管理(日历待办) +// 响应拦截器只返回 {code, data, msg} 包装,调用方需自行取 res.data + +export function getScheduleList(params) { + return request({ + url: '/backend/oa/schedule/list', + method: 'get', + params + }) +} + +export function getScheduleStats() { + return request({ + url: '/backend/oa/schedule/stats', + method: 'get' + }) +} + +export function createSchedule(data) { + return request({ + url: '/backend/oa/schedule/create', + method: 'post', + data + }) +} + +export function updateSchedule(id, data) { + return request({ + url: `/backend/oa/schedule/update/${id}`, + method: 'post', + data + }) +} + +export function deleteSchedule(id) { + return request({ + url: `/backend/oa/schedule/delete/${id}`, + method: 'delete' + }) +} + +export function finishSchedule(id) { + return request({ + url: `/backend/oa/schedule/finish/${id}`, + method: 'post' + }) +} diff --git a/backend/src/router/index.js b/backend/src/router/index.js index 2a7953c..06535b4 100644 --- a/backend/src/router/index.js +++ b/backend/src/router/index.js @@ -110,6 +110,12 @@ const staticMainChildren = [ props: { module: "oa" }, meta: { requiresAuth: true, title: "职位管理", modulePath: "/apps/oa" } }, + { + path: "/apps/oa/schedule", + name: "OaSchedule", + component: () => import("@/views/apps/oa/schedule/index.vue"), + meta: { requiresAuth: true, title: "日程管理", modulePath: "/apps/oa" } + }, { path: "/tools/passwordStore", name: "BackendPasswordStore", diff --git a/backend/src/views/apps/oa/dashboard/index.vue b/backend/src/views/apps/oa/dashboard/index.vue index 5e73906..dadc7ac 100644 --- a/backend/src/views/apps/oa/dashboard/index.vue +++ b/backend/src/views/apps/oa/dashboard/index.vue @@ -35,30 +35,54 @@ -
-
状态分布
-
-
+
+ 工作日程提醒 + 查看全部 - {{ - item.name - }} -
+
+
+
+ +
+ class="schedule-item-title" + :class="{ done: item.status === 1 }" + > + {{ item.title }} + + {{ item.priority === 2 ? "紧急" : "重要" }} + +
+
+ {{ + scheduleDateTag(item) + }} + {{ scheduleTimeText(item) }} +
- {{ item.count }} 单 / ¥ {{ money(item.amount) }}
@@ -66,6 +90,36 @@ + +
+
状态分布
+
+
+ {{ + item.name + }} +
+
+
+ {{ item.count }} 单 / ¥ {{ money(item.amount) }} +
+ +
+
+
报销类型分析
@@ -94,9 +148,12 @@ + + diff --git a/backend/src/views/apps/oa/schedule/components/edit.vue b/backend/src/views/apps/oa/schedule/components/edit.vue new file mode 100644 index 0000000..1a64d93 --- /dev/null +++ b/backend/src/views/apps/oa/schedule/components/edit.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/backend/src/views/apps/oa/schedule/index.vue b/backend/src/views/apps/oa/schedule/index.vue new file mode 100644 index 0000000..a90b4a6 --- /dev/null +++ b/backend/src/views/apps/oa/schedule/index.vue @@ -0,0 +1,991 @@ + + + + + diff --git a/go/conf/app.conf b/go/conf/app.conf index 5d6b20a..8d09288 100644 --- a/go/conf/app.conf +++ b/go/conf/app.conf @@ -1,4 +1,4 @@ -appname = server +appname = server httpport = 9000 runmode = dev diff --git a/go/controllers/backend_oa_schedule.go b/go/controllers/backend_oa_schedule.go new file mode 100644 index 0000000..77d4ab3 --- /dev/null +++ b/go/controllers/backend_oa_schedule.go @@ -0,0 +1,438 @@ +package controllers + +import ( + "encoding/json" + "io" + "regexp" + "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" +) + +// BackendOaScheduleController OA日程管理(日历待办)。 +// 日程数据按 JWT 中的租户与用户双重隔离,仅创建者本人可见与操作。 +type BackendOaScheduleController struct { + beego.Controller +} + +func (c *BackendOaScheduleController) oaScheduleClaims() (*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 *BackendOaScheduleController) oasJsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *BackendOaScheduleController) oasOk(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +var ( + oasDatePattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) + oasTimePattern = regexp.MustCompile(`^\d{2}:\d{2}$`) +) + +// isValidOaDate 校验 "YYYY-MM-DD" 日期字符串(含真实日历合法性,如 2 月 30 日)。 +func isValidOaDate(s string) bool { + if !oasDatePattern.MatchString(s) { + return false + } + _, err := time.ParseInLocation("2006-01-02", s, time.Local) + return err == nil +} + +// isValidOaTime 校验 "HH:mm" 时间字符串;HH:mm 定长数字串的字典序即时间序。 +func isValidOaTime(s string) bool { + if !oasTimePattern.MatchString(s) { + return false + } + _, err := time.ParseInLocation("15:04", s, time.Local) + return err == nil +} + +// normalizeOaDate 将 ORM 读出的日期值统一为 "YYYY-MM-DD"。 +// DSN 开启 parseTime=True 后 DATE 列会被驱动解析为 time.Time, +// 赋给 string 字段后是 Go 默认格式("2026-08-29 00:00:00 +0800 CST"),取前 10 位。 +func normalizeOaDate(s string) string { + s = strings.TrimSpace(s) + if len(s) >= 10 { + return s[:10] + } + return s +} + +// List GET /backend/oa/schedule/list +// 分页查询当前用户的日程,支持 keyword/status/priority/start_date/end_date 筛选。 +// 日历视图传 start_date/end_date 且 pageSize 足够大时一次拉取区间全量数据。 +func (c *BackendOaScheduleController) List() { + claims, err := c.oaScheduleClaims() + if err != nil { + c.oasJsonErr(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")) + priority := strings.TrimSpace(c.GetString("priority")) + startDate := strings.TrimSpace(c.GetString("start_date")) + endDate := strings.TrimSpace(c.GetString("end_date")) + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 500 { + pageSize = 20 + } + + qs := models.Orm.QueryTable(new(models.OaSchedule)). + Filter("is_deleted", 0). + Filter("tid", claims.TenantId). + Filter("user_id", claims.UserID) + + if keyword != "" { + qs = qs.Filter("title__icontains", keyword) + } + if status == "0" || status == "1" { + statusVal, _ := strconv.Atoi(status) + qs = qs.Filter("status", int8(statusVal)) + } + if priority == "0" || priority == "1" || priority == "2" { + priorityVal, _ := strconv.Atoi(priority) + qs = qs.Filter("priority", int8(priorityVal)) + } + if isValidOaDate(startDate) { + qs = qs.Filter("schedule_date__gte", startDate) + } + if isValidOaDate(endDate) { + qs = qs.Filter("schedule_date__lte", endDate) + } + + total, _ := qs.Count() + + var list []models.OaSchedule + _, err = qs.OrderBy("schedule_date", "start_time", "-id"). + Limit(pageSize).Offset((page - 1) * pageSize). + All(&list) + if err != nil && err != orm.ErrNoRows { + c.oasJsonErr(500, 500, "查询失败") + return + } + if list == nil { + list = []models.OaSchedule{} + } + + // 统一日期格式,前端日历分组与日期标签均依赖 "YYYY-MM-DD"。 + for i := range list { + list[i].ScheduleDate = normalizeOaDate(list[i].ScheduleDate) + } + + c.oasOk(map[string]interface{}{"list": list, "total": total}) +} + +// Stats GET /backend/oa/schedule/stats +// 返回今日/本周/本月的日程统计与逾期数,供页面顶部统计卡片展示。 +func (c *BackendOaScheduleController) Stats() { + claims, err := c.oaScheduleClaims() + if err != nil { + c.oasJsonErr(401, 401, "未登录或无权限") + return + } + + now := time.Now() + today := now.Format("2006-01-02") + // 本周从周一开始 + weekday := int(now.Weekday()) + if weekday == 0 { + weekday = 7 + } + weekStart := now.AddDate(0, 0, -(weekday - 1)).Format("2006-01-02") + weekEnd := now.AddDate(0, 0, 7-weekday).Format("2006-01-02") + monthStart := now.Format("2006-01") + "-01" + + base := func() orm.QuerySeter { + return models.Orm.QueryTable(new(models.OaSchedule)). + Filter("is_deleted", 0). + Filter("tid", claims.TenantId). + Filter("user_id", claims.UserID) + } + + todayTotal, _ := base().Filter("schedule_date", today).Count() + todayDone, _ := base().Filter("schedule_date", today).Filter("status", 1).Count() + weekTotal, _ := base().Filter("schedule_date__gte", weekStart).Filter("schedule_date__lte", weekEnd).Count() + monthTotal, _ := base().Filter("schedule_date__gte", monthStart).Count() + monthDone, _ := base().Filter("schedule_date__gte", monthStart).Filter("status", 1).Count() + overdue, _ := base().Filter("schedule_date__lt", today).Filter("status", 0).Count() + + c.oasOk(map[string]interface{}{ + "today_total": todayTotal, + "today_done": todayDone, + "week_total": weekTotal, + "month_total": monthTotal, + "month_done": monthDone, + "overdue": overdue, + }) +} + +type oaSchedulePayload struct { + Title string `json:"title"` + Content string `json:"content"` + ScheduleDate string `json:"schedule_date"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + AllDay int8 `json:"all_day"` + Color string `json:"color"` + Priority int8 `json:"priority"` +} + +// parseOaSchedulePayload 读取并校验新建/编辑日程的请求体;校验失败时直接输出错误响应。 +func (c *BackendOaScheduleController) parseOaSchedulePayload() (oaSchedulePayload, bool) { + var payload oaSchedulePayload + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil || json.Unmarshal(raw, &payload) != nil { + c.oasJsonErr(400, 400, "参数错误") + return payload, false + } + + payload.Title = strings.TrimSpace(payload.Title) + if payload.Title == "" { + c.oasJsonErr(400, 400, "请输入日程标题") + return payload, false + } + if utf8.RuneCountInString(payload.Title) > 80 { + c.oasJsonErr(400, 400, "日程标题不能超过80字") + return payload, false + } + if !isValidOaDate(payload.ScheduleDate) { + c.oasJsonErr(400, 400, "日程日期无效") + return payload, false + } + if payload.AllDay != 0 { + payload.AllDay = 1 + payload.StartTime = "" + payload.EndTime = "" + } else { + if !isValidOaTime(payload.StartTime) { + c.oasJsonErr(400, 400, "开始时间无效") + return payload, false + } + if payload.EndTime != "" { + if !isValidOaTime(payload.EndTime) { + c.oasJsonErr(400, 400, "结束时间无效") + return payload, false + } + if payload.EndTime < payload.StartTime { + c.oasJsonErr(400, 400, "结束时间不能早于开始时间") + return payload, false + } + } + } + if payload.Priority < 0 || payload.Priority > 2 { + payload.Priority = 0 + } + payload.Color = strings.TrimSpace(payload.Color) + if payload.Color == "" { + payload.Color = "#409EFF" + } + return payload, true +} + +// Create POST /backend/oa/schedule/create +func (c *BackendOaScheduleController) Create() { + claims, err := c.oaScheduleClaims() + if err != nil { + c.oasJsonErr(401, 401, "未登录或无权限") + return + } + + payload, ok := c.parseOaSchedulePayload() + if !ok { + return + } + + now := time.Now() + item := &models.OaSchedule{ + Tid: claims.TenantId, + UserID: uint64(claims.UserID), + UserName: claims.Username, + Title: payload.Title, + Content: payload.Content, + ScheduleDate: payload.ScheduleDate, + StartTime: payload.StartTime, + EndTime: payload.EndTime, + AllDay: payload.AllDay, + Color: payload.Color, + Priority: payload.Priority, + Status: 0, + IsDeleted: 0, + CreateTime: now, + UpdateTime: &now, + } + if _, err := models.Orm.Insert(item); err != nil { + c.oasJsonErr(500, 500, "保存失败") + return + } + + c.oasOk(item) +} + +// Update POST /backend/oa/schedule/update/:id +func (c *BackendOaScheduleController) Update() { + claims, err := c.oaScheduleClaims() + if err != nil { + c.oasJsonErr(401, 401, "未登录或无权限") + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.oasJsonErr(400, 400, "无效ID") + return + } + + payload, ok := c.parseOaSchedulePayload() + if !ok { + return + } + + var item models.OaSchedule + err = models.Orm.QueryTable(new(models.OaSchedule)). + Filter("id", id). + Filter("is_deleted", 0). + Filter("tid", claims.TenantId). + Filter("user_id", claims.UserID). + One(&item) + if err != nil { + c.oasJsonErr(404, 404, "日程不存在") + return + } + + now := time.Now() + item.Title = payload.Title + item.Content = payload.Content + item.ScheduleDate = payload.ScheduleDate + item.StartTime = payload.StartTime + item.EndTime = payload.EndTime + item.AllDay = payload.AllDay + item.Color = payload.Color + item.Priority = payload.Priority + item.UpdateTime = &now + + if _, err := models.Orm.Update(&item, + "Title", "Content", "ScheduleDate", "StartTime", "EndTime", + "AllDay", "Color", "Priority", "UpdateTime"); err != nil { + c.oasJsonErr(500, 500, "保存失败") + return + } + + c.oasOk(item) +} + +// Delete DELETE /backend/oa/schedule/delete/:id 软删除日程。 +func (c *BackendOaScheduleController) Delete() { + claims, err := c.oaScheduleClaims() + if err != nil { + c.oasJsonErr(401, 401, "未登录或无权限") + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.oasJsonErr(400, 400, "无效ID") + return + } + + now := time.Now() + n, err := models.Orm.QueryTable(new(models.OaSchedule)). + Filter("id", id). + Filter("is_deleted", 0). + Filter("tid", claims.TenantId). + Filter("user_id", claims.UserID). + Update(map[string]interface{}{ + "IsDeleted": int8(1), + "DeleteTime": now, + "UpdateTime": now, + }) + if err != nil || n == 0 { + c.oasJsonErr(404, 404, "日程不存在或已删除") + return + } + + c.oasOk(nil) +} + +// FinishSchedule POST /backend/oa/schedule/finish/:id +// 切换完成状态:待办 -> 完成(记录完成时间);完成 -> 待办(清除完成时间)。 +// 注意:方法名不能叫 Finish——Finish 是 beego Controller 的生命周期钩子, +// 同名会导致路由调用后再被框架自动调用一次,toggle 逻辑被执行两遍(状态来回切)。 +func (c *BackendOaScheduleController) FinishSchedule() { + claims, err := c.oaScheduleClaims() + if err != nil { + c.oasJsonErr(401, 401, "未登录或无权限") + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.oasJsonErr(400, 400, "无效ID") + return + } + + var item models.OaSchedule + err = models.Orm.QueryTable(new(models.OaSchedule)). + Filter("id", id). + Filter("is_deleted", 0). + Filter("tid", claims.TenantId). + Filter("user_id", claims.UserID). + One(&item) + if err != nil { + c.oasJsonErr(404, 404, "日程不存在") + return + } + + now := time.Now() + newStatus := int8(1) + var finishTime *time.Time + if item.Status == 1 { + newStatus = 0 + } else { + finishTime = &now + } + + item.Status = newStatus + item.FinishTime = finishTime + item.UpdateTime = &now + + if _, err := models.Orm.Update(&item, "Status", "FinishTime", "UpdateTime"); err != nil { + c.oasJsonErr(500, 500, "操作失败") + return + } + + c.oasOk(map[string]interface{}{"status": newStatus}) +} diff --git a/go/models/init.go b/go/models/init.go index 7d513ba..a937b21 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -99,6 +99,8 @@ func Init(_ string) { new(BackendSchedule), new(BackendScheduleReminder), new(BackendScheduleReminderSendLog), + + new(OaSchedule), ) // 创建全局 Ormer diff --git a/go/models/oa_schedule.go b/go/models/oa_schedule.go new file mode 100644 index 0000000..9609224 --- /dev/null +++ b/go/models/oa_schedule.go @@ -0,0 +1,32 @@ +package models + +import "time" + +// OaSchedule OA日程(日历待办): yz_backend_oa_schedule +// 日程按租户 tid + 用户 user_id 双重隔离,供 backend 日程管理页面的 +// 月历视图与待办列表读写。schedule_date/start_time/end_time 均以 +// "YYYY-MM-DD"、"HH:mm" 字符串存取,前端无需再做时区换算。 +type OaSchedule struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid int `orm:"column(tid)" json:"tid"` + UserID uint64 `orm:"column(user_id)" json:"user_id"` + UserName string `orm:"column(user_name);size(100);default()" json:"user_name"` + Title string `orm:"column(title);size(255)" json:"title"` + Content string `orm:"column(content);type(text);null" json:"content"` + ScheduleDate string `orm:"column(schedule_date);type(date)" json:"schedule_date"` + StartTime string `orm:"column(start_time);size(8);default()" json:"start_time"` + EndTime string `orm:"column(end_time);size(8);default()" json:"end_time"` + AllDay int8 `orm:"column(all_day);default(1)" json:"all_day"` + Color string `orm:"column(color);size(20);default(#409EFF)" json:"color"` + Priority int8 `orm:"column(priority);default(0)" json:"priority"` + Status int8 `orm:"column(status);default(0)" json:"status"` + FinishTime *time.Time `orm:"column(finish_time);type(datetime);null" json:"finish_time"` + 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 *OaSchedule) TableName() string { + return "yz_backend_oa_schedule" +} diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 833937d..3d83e67 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -227,6 +227,14 @@ func RegisterAuthRoutes() { beego.Router("/backend/reminder", &controllers.BackendReminderController{}, "post:CreateReminder") beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder") beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder") + + // OA日程管理(日历待办) + beego.Router("/backend/oa/schedule/list", &controllers.BackendOaScheduleController{}, "get:List") + beego.Router("/backend/oa/schedule/stats", &controllers.BackendOaScheduleController{}, "get:Stats") + beego.Router("/backend/oa/schedule/create", &controllers.BackendOaScheduleController{}, "post:Create") + beego.Router("/backend/oa/schedule/update/:id", &controllers.BackendOaScheduleController{}, "post:Update") + beego.Router("/backend/oa/schedule/delete/:id", &controllers.BackendOaScheduleController{}, "delete:Delete") + beego.Router("/backend/oa/schedule/finish/:id", &controllers.BackendOaScheduleController{}, "post:FinishSchedule") } // registerOrganizationRoutes 为指定模块前缀注册组织架构路由。 diff --git a/sql/yz_backend_oa_schedule.sql b/sql/yz_backend_oa_schedule.sql new file mode 100644 index 0000000..85a6535 --- /dev/null +++ b/sql/yz_backend_oa_schedule.sql @@ -0,0 +1,27 @@ +-- OA 日程管理表(日历待办) +-- 在租户业务库执行以下语句创建表结构。 +-- 数据按租户 tid + 用户 user_id 双重隔离,仅创建者本人可见与操作。 + +CREATE TABLE `yz_backend_oa_schedule` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tid` int NOT NULL DEFAULT '0' COMMENT '租户ID', + `user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '所属用户ID', + `user_name` varchar(100) NOT NULL DEFAULT '' COMMENT '所属用户名', + `title` varchar(255) NOT NULL DEFAULT '' COMMENT '日程标题', + `content` text NULL COMMENT '日程内容/备注', + `schedule_date` date NOT NULL COMMENT '日程日期(YYYY-MM-DD)', + `start_time` varchar(8) NOT NULL DEFAULT '' COMMENT '开始时间 HH:mm(全天日程为空)', + `end_time` varchar(8) NOT NULL DEFAULT '' COMMENT '结束时间 HH:mm(全天日程为空)', + `all_day` tinyint NOT NULL DEFAULT '1' COMMENT '是否全天 0-否 1-是', + `color` varchar(20) NOT NULL DEFAULT '#409EFF' COMMENT '标签颜色', + `priority` tinyint NOT NULL DEFAULT '0' COMMENT '优先级 0-普通 1-重要 2-紧急', + `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0-待办 1-完成', + `finish_time` datetime DEFAULT NULL 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_user_date` (`tid`,`user_id`,`schedule_date`), + KEY `idx_schedule_date` (`schedule_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA日程管理表(日历待办)';