@@ -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 } )
}