1203 lines
35 KiB
Go
1203 lines
35 KiB
Go
package controllers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
"unicode/utf8"
|
||
|
||
"server/models"
|
||
"server/pkg/holiday"
|
||
"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
|
||
}
|
||
|
||
// 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 筛选,
|
||
// 以及 category 待办分类:all-全部 today-今日 pending-未完成 ongoing-进行中 done-已完成。
|
||
// 返回值额外带 counts(各分类数量),供页面 tabs 展示角标。
|
||
// 日历视图传 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"))
|
||
category := strings.TrimSpace(c.GetString("category"))
|
||
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 500 {
|
||
pageSize = 20
|
||
}
|
||
|
||
base := oaScheduleFilter{Keyword: keyword}
|
||
if status == "0" || status == "1" {
|
||
statusVal, _ := strconv.Atoi(status)
|
||
base.HasStatus = true
|
||
base.Status = int8(statusVal)
|
||
}
|
||
if priority == "0" || priority == "1" || priority == "2" {
|
||
priorityVal, _ := strconv.Atoi(priority)
|
||
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
|
||
_, 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,
|
||
"counts": counts,
|
||
})
|
||
}
|
||
|
||
// Holidays GET /backend/oa/holiday
|
||
// 返回指定日期区间(或指定年份)内的中国法定节假日/调休日,供日历控件标注
|
||
// 「休 / 班」角标与节日名称。
|
||
//
|
||
// 参数(二选一):
|
||
// - start_date + end_date:"YYYY-MM-DD",日历按 42 格区间查询;
|
||
// - year:整年查询。
|
||
//
|
||
// 数据获取顺序:内存缓存 → 内置数据(2024-2026,零网络)→ 外网接口(仅未收录年份)。
|
||
// 已收录年份不会发起任何网络请求,因此日历翻月是纯内存操作、无卡顿。
|
||
// 响应里的 builtin_years 字段告知前端内置数据覆盖到哪一年。
|
||
func (c *BackendOaScheduleController) Holidays() {
|
||
if _, err := c.oaScheduleClaims(); err != nil {
|
||
c.oasJsonErr(401, 401, "未登录或无权限")
|
||
return
|
||
}
|
||
|
||
startDate := strings.TrimSpace(c.GetString("start_date"))
|
||
endDate := strings.TrimSpace(c.GetString("end_date"))
|
||
yearStr := strings.TrimSpace(c.GetString("year"))
|
||
|
||
// 参数归一化:区间优先,其次按年
|
||
var start, end string
|
||
if isValidOaDate(startDate) && isValidOaDate(endDate) && startDate <= endDate {
|
||
start, end = startDate, endDate
|
||
} else if y, err := strconv.Atoi(yearStr); err == nil && y >= 2000 && y <= 2100 {
|
||
start = fmt.Sprintf("%04d-01-01", y)
|
||
end = fmt.Sprintf("%04d-12-31", y)
|
||
} else {
|
||
// 未传参时默认当前年份,保证前端任何情况下都能拿到可用数据
|
||
now := time.Now()
|
||
start = now.Format("2006-01-01")
|
||
end = now.Format("2006") + "-12-31"
|
||
}
|
||
|
||
list := ensureOaHolidays(start, end)
|
||
|
||
c.oasOk(map[string]interface{}{
|
||
"list": list,
|
||
"start_date": start,
|
||
"end_date": end,
|
||
"builtin_years": holiday.Years(),
|
||
})
|
||
}
|
||
|
||
// ensureOaHolidays 取区间内的节假日数据。
|
||
//
|
||
// 顺序上的取舍(性能关键):先调 holiday.FetchYear,而不是先查数据库。
|
||
// 原因是 FetchYear 对已收录年份(2024-2026)直接返回内置数据、不发网络请求,
|
||
// 也无需查库;日历每次翻月都会走到这里,先查库会白白多一次 SQL 往返。
|
||
// 只有 FetchYear 真的联网拉到了数据(即年份未收录),才回写数据库缓存,
|
||
// 供后续同一年份的请求直接命中。
|
||
func ensureOaHolidays(start, end string) []holiday.Day {
|
||
list := make([]holiday.Day, 0, 64)
|
||
|
||
startYear, endYear := holidayYear(start), holidayYear(end)
|
||
for y := startYear; y <= endYear; y++ {
|
||
if y == 0 {
|
||
continue
|
||
}
|
||
items, src, ferr := holiday.FetchYear(y)
|
||
if ferr != nil || len(items) == 0 {
|
||
continue
|
||
}
|
||
// 仅当数据来自外网时才落库:内置数据本身就在代码里,落库没有收益,
|
||
// 反而会让每次翻月都产生一批无谓的写库操作。
|
||
if src == holiday.SourceHolidayCN {
|
||
saveOaHolidays(items)
|
||
}
|
||
for _, d := range items {
|
||
if d.Date >= start && d.Date <= end {
|
||
list = append(list, d)
|
||
}
|
||
}
|
||
}
|
||
|
||
return mergeOaHolidays(list)
|
||
}
|
||
|
||
// holidayYear 从 "YYYY-MM-DD" 取年份,失败返回 0。
|
||
func holidayYear(date string) int {
|
||
if len(date) < 4 {
|
||
return 0
|
||
}
|
||
y, err := strconv.Atoi(date[:4])
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return y
|
||
}
|
||
|
||
// saveOaHolidays 把外网拉取到的数据写入缓存表。
|
||
// 使用 insert-or-update:已存在的日期更新其名称/类型/来源,保证外网数据
|
||
// 修正后能覆盖库里的旧值。写入失败静默忽略(缓存非强依赖)。
|
||
func saveOaHolidays(list []holiday.Day) {
|
||
now := time.Now()
|
||
for _, d := range list {
|
||
if !isValidOaDate(d.Date) {
|
||
continue
|
||
}
|
||
var exist models.OaHoliday
|
||
ferr := models.Orm.QueryTable(new(models.OaHoliday)).
|
||
Filter("date", d.Date).
|
||
One(&exist)
|
||
if ferr == nil {
|
||
// 已存在:仅当来源或名称有变化时才更新,减少无谓写库
|
||
if exist.Name == d.Name && exist.Type == d.Type && exist.Source == d.Source {
|
||
continue
|
||
}
|
||
exist.Name = d.Name
|
||
exist.Type = d.Type
|
||
exist.Year = d.Year
|
||
exist.Wage = d.Wage
|
||
exist.Source = d.Source
|
||
exist.UpdateTime = &now
|
||
_, _ = models.Orm.Update(&exist, "Name", "Type", "Year", "Wage", "Source", "UpdateTime")
|
||
continue
|
||
}
|
||
item := models.OaHoliday{
|
||
Date: d.Date,
|
||
Name: d.Name,
|
||
Type: d.Type,
|
||
Year: d.Year,
|
||
Wage: d.Wage,
|
||
Source: d.Source,
|
||
CreateTime: now,
|
||
UpdateTime: &now,
|
||
}
|
||
_, _ = models.Orm.Insert(&item)
|
||
}
|
||
}
|
||
|
||
// mergeOaHolidays 按日期去重并升序排列;同日重复时保留先出现的记录
|
||
// (数据库缓存排在前面,因此缓存优先于外网新拉取的数据)。
|
||
func mergeOaHolidays(list []holiday.Day) []holiday.Day {
|
||
seen := make(map[string]bool, len(list))
|
||
out := make([]holiday.Day, 0, len(list))
|
||
for _, d := range list {
|
||
if d.Date == "" || seen[d.Date] {
|
||
continue
|
||
}
|
||
seen[d.Date] = true
|
||
out = append(out, d)
|
||
}
|
||
for i := 1; i < len(out); i++ {
|
||
cur := out[i]
|
||
j := i - 1
|
||
for j >= 0 && out[j].Date > cur.Date {
|
||
out[j+1] = out[j]
|
||
j--
|
||
}
|
||
out[j+1] = cur
|
||
}
|
||
return out
|
||
}
|
||
|
||
// 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()
|
||
weekDone, _ := base().Filter("schedule_date__gte", weekStart).Filter("schedule_date__lte", weekEnd).Filter("status", 1).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,
|
||
"week_done": weekDone,
|
||
"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"`
|
||
// 提醒绑定:开启后会在提醒工具生成一条提醒任务,并把其 ID 回写到 ReminderID。
|
||
ReminderEnabled bool `json:"reminder_enabled"`
|
||
ReminderTime string `json:"reminder_time"`
|
||
RemindChannels []string `json:"remind_channels"`
|
||
AdvanceMinutes int `json:"advance_minutes"`
|
||
}
|
||
|
||
// oaReminderChannels 允许的提醒渠道集合。
|
||
var oaReminderChannels = map[string]bool{
|
||
"SMS": true, "EMAIL": true, "BARK": true, "SITE_MSG": true,
|
||
}
|
||
|
||
// createOaScheduleReminder 为 OA 日程创建一条绑定的提醒任务。
|
||
// 复用提醒工具的数据模型(yz_backend_schedule + yz_backend_schedule_reminder):
|
||
// 生成一条 BackendSchedule,再按渠道逐条生成提醒记录,返回 BackendSchedule.ID
|
||
// 作为 OA 日程的 reminder_id。EMAIL/BARK 渠道会附带确认回执令牌与重复发送配置。
|
||
func createOaScheduleReminder(tid int, userID uint64, title, content, reminderTime string, channels []string, advanceMinutes int) (uint64, error) {
|
||
if strings.TrimSpace(reminderTime) == "" {
|
||
return 0, fmt.Errorf("提醒时间不能为空")
|
||
}
|
||
schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", reminderTime, time.Local)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("提醒时间格式不合法")
|
||
}
|
||
if advanceMinutes < 0 {
|
||
advanceMinutes = 0
|
||
}
|
||
title = strings.TrimSpace(title)
|
||
if title == "" {
|
||
title = "日程提醒"
|
||
}
|
||
|
||
bs := models.BackendSchedule{
|
||
Tid: tid,
|
||
Title: title,
|
||
Content: content,
|
||
ScheduleTime: schedTime,
|
||
UserID: userID,
|
||
}
|
||
schedID, err := models.Orm.Insert(&bs)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("创建提醒任务失败: %v", err)
|
||
}
|
||
|
||
now := time.Now()
|
||
for _, ch := range channels {
|
||
ch = strings.ToUpper(strings.TrimSpace(ch))
|
||
if !oaReminderChannels[ch] {
|
||
continue
|
||
}
|
||
firstSend := schedTime.Add(-time.Duration(advanceMinutes) * time.Minute)
|
||
reminder := models.BackendScheduleReminder{
|
||
Tid: tid,
|
||
ScheduleID: uint64(schedID),
|
||
RemindChannel: ch,
|
||
AdvanceMinutes: advanceMinutes,
|
||
NextRemindTime: firstSend,
|
||
ReceiverUserID: userID,
|
||
RemindStatus: 0,
|
||
CreateTime: now,
|
||
UpdateTime: now,
|
||
}
|
||
if ch == "EMAIL" || ch == "BARK" {
|
||
token := backendGenerateToken()
|
||
reminder.AckToken = &token
|
||
reminder.RepeatIntervalMinutes = 10
|
||
reminder.MaxSendCount = 5
|
||
} else {
|
||
reminder.RepeatIntervalMinutes = 0
|
||
reminder.MaxSendCount = 1
|
||
}
|
||
if _, err := models.Orm.Insert(&reminder); err != nil {
|
||
return 0, fmt.Errorf("创建提醒失败: %v", err)
|
||
}
|
||
}
|
||
return uint64(schedID), nil
|
||
}
|
||
|
||
// deleteBoundReminder 软删除 OA 日程绑定的提醒任务及其提醒记录。
|
||
// 删除日程或关闭提醒时使用,避免提醒工具里残留孤立任务。
|
||
func deleteBoundReminder(scheduleID uint64) {
|
||
if scheduleID == 0 {
|
||
return
|
||
}
|
||
_, _ = models.Orm.QueryTable(new(models.BackendSchedule)).
|
||
Filter("id", scheduleID).Delete()
|
||
_, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
|
||
Filter("schedule_id", scheduleID).
|
||
Update(map[string]interface{}{"IsDeleted": 1, "UpdateTime": time.Now()})
|
||
}
|
||
|
||
// oaScheduleImage 日程相关图片(截图/照片),只存文件引用,不存二进制。
|
||
type oaScheduleImage struct {
|
||
ID uint64 `json:"id"`
|
||
URL string `json:"url"`
|
||
Name string `json:"name"`
|
||
}
|
||
|
||
const (
|
||
oaScheduleMaxImages = 20
|
||
oaScheduleMaxImageURL = 500
|
||
oaScheduleMaxImageName = 120
|
||
)
|
||
|
||
// normalizeOaScheduleImages 校验并序列化日程图片列表:
|
||
// 丢弃无 url 或超长的条目,条目数超限时返回 false。
|
||
func normalizeOaScheduleImages(list []oaScheduleImage) (string, bool) {
|
||
if len(list) > oaScheduleMaxImages {
|
||
return "", false
|
||
}
|
||
out := make([]oaScheduleImage, 0, len(list))
|
||
for _, img := range list {
|
||
img.URL = strings.TrimSpace(img.URL)
|
||
if img.URL == "" || len(img.URL) > oaScheduleMaxImageURL {
|
||
continue
|
||
}
|
||
img.Name = strings.TrimSpace(img.Name)
|
||
if utf8.RuneCountInString(img.Name) > oaScheduleMaxImageName {
|
||
img.Name = string([]rune(img.Name)[:oaScheduleMaxImageName])
|
||
}
|
||
out = append(out, img)
|
||
}
|
||
raw, err := json.Marshal(out)
|
||
if err != nil {
|
||
return "", false
|
||
}
|
||
return string(raw), true
|
||
}
|
||
|
||
// parseOaScheduleImages 解析 images 列;脏数据返回空数组,不阻断详情展示。
|
||
func parseOaScheduleImages(raw string) []oaScheduleImage {
|
||
list := make([]oaScheduleImage, 0)
|
||
_ = json.Unmarshal([]byte(raw), &list)
|
||
return list
|
||
}
|
||
|
||
// 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"
|
||
}
|
||
|
||
// 提醒绑定校验:开启提醒时必须选择渠道且提醒时间合法。
|
||
if payload.ReminderEnabled {
|
||
validChannels := make([]string, 0, len(payload.RemindChannels))
|
||
for _, ch := range payload.RemindChannels {
|
||
ch = strings.ToUpper(strings.TrimSpace(ch))
|
||
if oaReminderChannels[ch] {
|
||
validChannels = append(validChannels, ch)
|
||
}
|
||
}
|
||
payload.RemindChannels = validChannels
|
||
if len(validChannels) == 0 {
|
||
c.oasJsonErr(400, 400, "请至少选择一个提醒渠道")
|
||
return payload, false
|
||
}
|
||
if _, err := time.ParseInLocation("2006-01-02 15:04:05", payload.ReminderTime, time.Local); err != nil {
|
||
c.oasJsonErr(400, 400, "提醒时间格式不合法,支持 YYYY-MM-DD HH:mm:ss")
|
||
return payload, false
|
||
}
|
||
if payload.AdvanceMinutes < 0 {
|
||
payload.AdvanceMinutes = 0
|
||
}
|
||
}
|
||
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
|
||
}
|
||
|
||
// 开启提醒:生成绑定的提醒任务,并把其 ID 回写到日程。
|
||
if payload.ReminderEnabled {
|
||
rid, rerr := createOaScheduleReminder(
|
||
claims.TenantId,
|
||
uint64(claims.UserID),
|
||
payload.Title,
|
||
payload.Content,
|
||
payload.ReminderTime,
|
||
payload.RemindChannels,
|
||
payload.AdvanceMinutes,
|
||
)
|
||
if rerr != nil {
|
||
// 回滚已创建的日程,避免产生孤立日程
|
||
_, _ = models.Orm.QueryTable(new(models.OaSchedule)).Filter("id", item.ID).Delete()
|
||
c.oasJsonErr(500, 500, rerr.Error())
|
||
return
|
||
}
|
||
item.ReminderID = rid
|
||
if _, err := models.Orm.Update(item, "ReminderID"); 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 payload.ReminderEnabled {
|
||
rid, rerr := createOaScheduleReminder(
|
||
claims.TenantId,
|
||
uint64(claims.UserID),
|
||
payload.Title,
|
||
payload.Content,
|
||
payload.ReminderTime,
|
||
payload.RemindChannels,
|
||
payload.AdvanceMinutes,
|
||
)
|
||
if rerr != nil {
|
||
c.oasJsonErr(500, 500, rerr.Error())
|
||
return
|
||
}
|
||
if item.ReminderID > 0 {
|
||
deleteBoundReminder(item.ReminderID)
|
||
}
|
||
item.ReminderID = rid
|
||
} else if item.ReminderID > 0 {
|
||
deleteBoundReminder(item.ReminderID)
|
||
item.ReminderID = 0
|
||
}
|
||
|
||
if _, err := models.Orm.Update(&item,
|
||
"Title", "Content", "ScheduleDate", "StartTime", "EndTime",
|
||
"AllDay", "Color", "Priority", "ReminderID", "UpdateTime"); err != nil {
|
||
c.oasJsonErr(500, 500, "保存失败")
|
||
return
|
||
}
|
||
|
||
c.oasOk(item)
|
||
}
|
||
|
||
// SetImages POST /backend/oa/schedule/images/:id
|
||
// 单独维护日程的相关图片(文件已先上传到文件管理,这里只存引用)。
|
||
// 与 Update 分开,避免编辑标题/时间等基础信息时把图片清空。
|
||
func (c *BackendOaScheduleController) SetImages() {
|
||
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 {
|
||
Images []oaScheduleImage `json:"images"`
|
||
}
|
||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
if len(raw) > 0 && json.Unmarshal(raw, &payload) != nil {
|
||
c.oasJsonErr(400, 400, "参数错误")
|
||
return
|
||
}
|
||
imagesJSON, ok := normalizeOaScheduleImages(payload.Images)
|
||
if !ok {
|
||
c.oasJsonErr(400, 400, "图片数量过多")
|
||
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.Images = imagesJSON
|
||
item.UpdateTime = &now
|
||
if _, err := models.Orm.Update(&item, "Images", "UpdateTime"); err != nil {
|
||
c.oasJsonErr(500, 500, "保存失败")
|
||
return
|
||
}
|
||
|
||
c.oasOk(map[string]interface{}{"images": parseOaScheduleImages(imagesJSON)})
|
||
}
|
||
|
||
// UpdateContent POST /backend/oa/schedule/content/:id 仅更新备注内容。
|
||
// 与 SetImages 类似,只改写 Content 字段,不影响标题、日期、提醒等其他字段,
|
||
// 因此即便日程已完成、已绑定提醒,补充备注也不会破坏既有状态。
|
||
func (c *BackendOaScheduleController) UpdateContent() {
|
||
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 {
|
||
Content string `json:"content"`
|
||
}
|
||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
if len(raw) > 0 && json.Unmarshal(raw, &payload) != nil {
|
||
c.oasJsonErr(400, 400, "参数错误")
|
||
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.Content = payload.Content
|
||
item.UpdateTime = &now
|
||
if _, err := models.Orm.Update(&item, "Content", "UpdateTime"); err != nil {
|
||
c.oasJsonErr(500, 500, "保存失败")
|
||
return
|
||
}
|
||
|
||
c.oasOk(map[string]interface{}{"content": item.Content})
|
||
}
|
||
|
||
// 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()
|
||
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
|
||
}
|
||
|
||
// 先删除绑定的提醒任务,避免提醒工具里残留孤立任务
|
||
if item.ReminderID > 0 {
|
||
deleteBoundReminder(item.ReminderID)
|
||
}
|
||
|
||
item.IsDeleted = 1
|
||
item.DeleteTime = &now
|
||
item.UpdateTime = &now
|
||
if _, err := models.Orm.Update(&item, "IsDeleted", "DeleteTime", "UpdateTime"); err != nil {
|
||
c.oasJsonErr(500, 500, "删除失败")
|
||
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})
|
||
}
|
||
|
||
// 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,
|
||
Images: item.Images,
|
||
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,
|
||
})
|
||
}
|