优化日程提醒模块

This commit is contained in:
2026-09-08 22:18:36 +08:00
parent 3d53f03246
commit 7f3de60423
17 changed files with 1516 additions and 270 deletions
+309 -18
View File
@@ -52,6 +52,11 @@ func (c *BackendOaScheduleController) oasOk(data interface{}) {
_ = c.ServeJSON()
}
// Prepare 所有日程接口执行前补齐顺延所需字段,避免历史库缺列导致查询报错。
func (c *BackendOaScheduleController) Prepare() {
_ = models.EnsureOaScheduleCarryColumns()
}
var (
oasDatePattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
oasTimePattern = regexp.MustCompile(`^\d{2}:\d{2}$`)
@@ -86,8 +91,61 @@ func normalizeOaDate(s string) string {
return s
}
// oaScheduleFilter 日程列表筛选条件,用于构造 ORM 查询条件。
// 指针式的 Has*/On* 标记用于区分"未传参"与"传了默认值 0/空串"。
type oaScheduleFilter struct {
Keyword string
Priority int8
HasPriority bool
StartDate string
EndDate string
OnDate string
Status int8
HasStatus bool
Ongoing bool
}
// oaScheduleCond 构造日程查询条件,始终按租户 + 用户双重隔离。
// Ongoing 表示"进行中":跨天顺延中的任务,即被顺延出去的源日程(carry_over=1)
// 或由前序日程顺延而来的续做日程(source_id>0)。
func oaScheduleCond(tid int, userID int, f oaScheduleFilter) *orm.Condition {
cond := orm.NewCondition()
cond = cond.And("is_deleted", 0).And("tid", tid).And("user_id", userID)
if f.Keyword != "" {
cond = cond.And("title__icontains", f.Keyword)
}
if f.HasPriority {
cond = cond.And("priority", f.Priority)
}
if f.StartDate != "" {
cond = cond.And("schedule_date__gte", f.StartDate)
}
if f.EndDate != "" {
cond = cond.And("schedule_date__lte", f.EndDate)
}
if f.OnDate != "" {
cond = cond.And("schedule_date", f.OnDate)
}
if f.HasStatus {
cond = cond.And("status", f.Status)
}
if f.Ongoing {
sub := orm.NewCondition()
sub = sub.Or("source_id__gt", 0).Or("carry_over", 1)
cond = cond.AndCond(sub)
}
return cond
}
func oaScheduleQS(tid int, userID int, f oaScheduleFilter) orm.QuerySeter {
return models.Orm.QueryTable(new(models.OaSchedule)).
SetCond(oaScheduleCond(tid, userID, f))
}
// List GET /backend/oa/schedule/list
// 分页查询当前用户的日程,支持 keyword/status/priority/start_date/end_date 筛选。
// 分页查询当前用户的日程,支持 keyword/status/priority/start_date/end_date 筛选,
// 以及 category 待办分类:all-全部 today-今日 pending-未完成 ongoing-进行中 done-已完成。
// 返回值额外带 counts(各分类数量),供页面 tabs 展示角标。
// 日历视图传 start_date/end_date 且 pageSize 足够大时一次拉取区间全量数据。
func (c *BackendOaScheduleController) List() {
claims, err := c.oaScheduleClaims()
@@ -103,6 +161,7 @@ func (c *BackendOaScheduleController) List() {
priority := strings.TrimSpace(c.GetString("priority"))
startDate := strings.TrimSpace(c.GetString("start_date"))
endDate := strings.TrimSpace(c.GetString("end_date"))
category := strings.TrimSpace(c.GetString("category"))
if page < 1 {
page = 1
@@ -111,29 +170,54 @@ func (c *BackendOaScheduleController) List() {
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)
}
base := oaScheduleFilter{Keyword: keyword}
if status == "0" || status == "1" {
statusVal, _ := strconv.Atoi(status)
qs = qs.Filter("status", int8(statusVal))
base.HasStatus = true
base.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)
base.HasPriority = true
base.Priority = int8(priorityVal)
}
// 分类角标按全量统计(只受关键词/优先级影响),不随日历翻月或区间筛选跳动
countOf := func(f oaScheduleFilter) int64 {
n, _ := oaScheduleQS(claims.TenantId, claims.UserID, f).Count()
return n
}
today := time.Now().Format("2006-01-02")
counts := map[string]interface{}{
"all": countOf(base),
"today": countOf(oaScheduleFilter{Keyword: base.Keyword, Priority: base.Priority, HasPriority: base.HasPriority, OnDate: today}),
"pending": countOf(oaScheduleFilter{Keyword: base.Keyword, Priority: base.Priority, HasPriority: base.HasPriority, HasStatus: true, Status: 0}),
"ongoing": countOf(oaScheduleFilter{Keyword: base.Keyword, Priority: base.Priority, HasPriority: base.HasPriority, Ongoing: true}),
"done": countOf(oaScheduleFilter{Keyword: base.Keyword, Priority: base.Priority, HasPriority: base.HasPriority, HasStatus: true, Status: 1}),
}
// 列表数据 = 基础筛选 + 日期区间 + 当前分类
listFilter := base
if isValidOaDate(startDate) {
listFilter.StartDate = startDate
}
if isValidOaDate(endDate) {
listFilter.EndDate = endDate
}
switch category {
case "today":
listFilter.OnDate = today
case "pending":
listFilter.HasStatus = true
listFilter.Status = 0
case "ongoing":
listFilter.Ongoing = true
case "done":
listFilter.HasStatus = true
listFilter.Status = 1
}
qs := oaScheduleQS(claims.TenantId, claims.UserID, listFilter)
total, _ := qs.Count()
var list []models.OaSchedule
@@ -153,7 +237,11 @@ func (c *BackendOaScheduleController) List() {
list[i].ScheduleDate = normalizeOaDate(list[i].ScheduleDate)
}
c.oasOk(map[string]interface{}{"list": list, "total": total})
c.oasOk(map[string]interface{}{
"list": list,
"total": total,
"counts": counts,
})
}
// Stats GET /backend/oa/schedule/stats
@@ -436,3 +524,206 @@ func (c *BackendOaScheduleController) FinishSchedule() {
c.oasOk(map[string]interface{}{"status": newStatus})
}
// nextOaDate 返回 dateStr 之后的第 days 天,格式仍为 "YYYY-MM-DD"。
func nextOaDate(dateStr string, days int) string {
t, err := time.ParseInLocation("2006-01-02", dateStr, time.Local)
if err != nil {
return ""
}
return t.AddDate(0, 0, days).Format("2006-01-02")
}
// oaScheduleRootID 取顺延链的源头日程ID:本身就是顺延出来的则沿用其来源,否则为自身。
func oaScheduleRootID(item *models.OaSchedule) uint64 {
if item.SourceID > 0 {
return item.SourceID
}
return item.ID
}
// carryOaSchedule 把一条待办日程顺延到目标日期:
// 1) 原日程标记完成(status=1, carry_over=1, carry_to_date=目标日期);
// 2) 在目标日期生成一条同内容的待办,source_id 指向顺延链源头。
// 返回新生成的日程与失败原因(空串表示成功)。
func carryOaSchedule(item *models.OaSchedule, targetDate string, claims *jwtutil.Claims, now time.Time) (*models.OaSchedule, string) {
if item.Status == 1 {
return nil, "该日程已完成,无需顺延"
}
if !isValidOaDate(targetDate) {
return nil, "顺延日期无效"
}
if targetDate <= normalizeOaDate(item.ScheduleDate) {
return nil, "顺延日期需晚于当前日程日期"
}
rootID := oaScheduleRootID(item)
// 同一条顺延链在目标日期只允许存在一条,避免重复顺延刷出多份待办。
exist := models.Orm.QueryTable(new(models.OaSchedule)).
Filter("is_deleted", 0).
Filter("tid", claims.TenantId).
Filter("user_id", claims.UserID).
Filter("source_id", rootID).
Filter("schedule_date", targetDate).
Exist()
if exist {
return nil, "该日期已存在顺延的日程"
}
next := &models.OaSchedule{
Tid: item.Tid,
UserID: item.UserID,
UserName: item.UserName,
Title: item.Title,
Content: item.Content,
ScheduleDate: targetDate,
StartTime: item.StartTime,
EndTime: item.EndTime,
AllDay: item.AllDay,
Color: item.Color,
Priority: item.Priority,
Status: 0,
SourceID: rootID,
CarryCount: item.CarryCount + 1,
IsDeleted: 0,
CreateTime: now,
UpdateTime: &now,
}
if _, err := models.Orm.Insert(next); err != nil {
return nil, "生成顺延日程失败"
}
item.Status = 1
item.FinishTime = &now
item.CarryOver = 1
item.CarryToDate = targetDate
item.UpdateTime = &now
if _, err := models.Orm.Update(item,
"Status", "FinishTime", "CarryOver", "CarryToDate", "UpdateTime"); err != nil {
return nil, "标记原日程完成失败"
}
return next, ""
}
// CarrySchedule POST /backend/oa/schedule/carry/:id
// 将单条待办顺延到指定日期(默认下一天):原日程标记完成,目标日期自动生成新待办。
func (c *BackendOaScheduleController) CarrySchedule() {
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 payload struct {
TargetDate string `json:"target_date"`
}
if raw, e := io.ReadAll(c.Ctx.Request.Body); e == nil && len(raw) > 0 {
_ = json.Unmarshal(raw, &payload)
}
payload.TargetDate = strings.TrimSpace(payload.TargetDate)
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
}
targetDate := payload.TargetDate
if targetDate == "" {
targetDate = nextOaDate(normalizeOaDate(item.ScheduleDate), 1)
}
now := time.Now()
next, msg := carryOaSchedule(&item, targetDate, claims, now)
if msg != "" {
c.oasJsonErr(400, 400, msg)
return
}
c.oasOk(map[string]interface{}{
"new_id": next.ID,
"schedule_date": next.ScheduleDate,
"carry_count": next.CarryCount,
})
}
// CarryPending POST /backend/oa/schedule/carry-pending
// 批量顺延:把指定日期(默认今天)全部未完成的日程一次性顺延到目标日期(默认下一天)。
// 已在目标日期存在顺延记录的会被跳过,不会重复生成。
func (c *BackendOaScheduleController) CarryPending() {
claims, err := c.oaScheduleClaims()
if err != nil {
c.oasJsonErr(401, 401, "未登录或无权限")
return
}
var payload struct {
Date string `json:"date"`
TargetDate string `json:"target_date"`
}
if raw, e := io.ReadAll(c.Ctx.Request.Body); e == nil && len(raw) > 0 {
_ = json.Unmarshal(raw, &payload)
}
payload.Date = strings.TrimSpace(payload.Date)
payload.TargetDate = strings.TrimSpace(payload.TargetDate)
now := time.Now()
date := payload.Date
if !isValidOaDate(date) {
date = now.Format("2006-01-02")
}
targetDate := payload.TargetDate
if !isValidOaDate(targetDate) {
targetDate = nextOaDate(date, 1)
}
if !isValidOaDate(targetDate) {
c.oasJsonErr(400, 400, "顺延日期无效")
return
}
var list []models.OaSchedule
_, err = models.Orm.QueryTable(new(models.OaSchedule)).
Filter("is_deleted", 0).
Filter("tid", claims.TenantId).
Filter("user_id", claims.UserID).
Filter("schedule_date", date).
Filter("status", 0).
OrderBy("start_time", "id").
All(&list)
if err != nil && err != orm.ErrNoRows {
c.oasJsonErr(500, 500, "查询失败")
return
}
success, skipped := 0, 0
lastDate := targetDate
for i := range list {
if _, msg := carryOaSchedule(&list[i], targetDate, claims, now); msg != "" {
skipped++
continue
}
success++
lastDate = targetDate
}
c.oasOk(map[string]interface{}{
"success": success,
"skipped": skipped,
"schedule_date": lastDate,
})
}