Files
yunzerwebsiteallinone/go/controllers/backend_oa_schedule.go
T
2026-08-28 16:58:17 +08:00

439 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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})
}