优化日程提醒模块
This commit is contained in:
@@ -211,8 +211,6 @@ type employeeFileDTO struct {
|
||||
Position string `json:"position"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
Education string `json:"education"`
|
||||
Nation string `json:"nation"`
|
||||
HomeAddress string `json:"home_address"`
|
||||
AccountStatus int8 `json:"account_status"`
|
||||
HireDate string `json:"hire_date"`
|
||||
@@ -226,6 +224,8 @@ type employeeFileDTO struct {
|
||||
func efFileCompleteness(file models.BackendEmployeeFile) int {
|
||||
items := []bool{
|
||||
strings.TrimSpace(file.IDCard) != "",
|
||||
strings.TrimSpace(file.Education) != "",
|
||||
strings.TrimSpace(file.Nation) != "",
|
||||
strings.TrimSpace(file.PoliticalStatus) != "",
|
||||
file.MaritalStatus > 0,
|
||||
strings.TrimSpace(file.NativePlace) != "",
|
||||
@@ -305,8 +305,6 @@ func (c *BackendEmployeeFileController) efAssembleDTOList(tid int, files []model
|
||||
dto.Position = derefString(emp.Position)
|
||||
dto.Phone = derefString(emp.Phone)
|
||||
dto.Email = derefString(emp.Email)
|
||||
dto.Education = derefString(emp.Education)
|
||||
dto.Nation = derefString(emp.Nation)
|
||||
dto.HomeAddress = derefString(emp.HomeAddress)
|
||||
dto.AccountStatus = emp.AccountStatus
|
||||
}
|
||||
@@ -452,6 +450,8 @@ type employeeFilePayload struct {
|
||||
IDCardFront string `json:"id_card_front"`
|
||||
IDCardBack string `json:"id_card_back"`
|
||||
EducationPhoto string `json:"education_photo"`
|
||||
Education string `json:"education"`
|
||||
Nation string `json:"nation"`
|
||||
PoliticalStatus string `json:"political_status"`
|
||||
MaritalStatus int8 `json:"marital_status"`
|
||||
NativePlace string `json:"native_place"`
|
||||
@@ -479,6 +479,8 @@ func (c *BackendEmployeeFileController) efParsePayload() (employeeFilePayload, b
|
||||
|
||||
payload.FileNo = strings.TrimSpace(payload.FileNo)
|
||||
payload.IDCard = strings.TrimSpace(payload.IDCard)
|
||||
payload.Education = strings.TrimSpace(payload.Education)
|
||||
payload.Nation = strings.TrimSpace(payload.Nation)
|
||||
payload.WorkEmail = strings.TrimSpace(payload.WorkEmail)
|
||||
if payload.IDCard != "" && len(payload.IDCard) > 30 {
|
||||
c.efErr(400, 400, "身份证号过长")
|
||||
@@ -570,6 +572,8 @@ func (c *BackendEmployeeFileController) Create() {
|
||||
IDCardFront: payload.IDCardFront,
|
||||
IDCardBack: payload.IDCardBack,
|
||||
EducationPhoto: payload.EducationPhoto,
|
||||
Education: payload.Education,
|
||||
Nation: payload.Nation,
|
||||
PoliticalStatus: payload.PoliticalStatus,
|
||||
MaritalStatus: payload.MaritalStatus,
|
||||
NativePlace: payload.NativePlace,
|
||||
@@ -629,6 +633,8 @@ func (c *BackendEmployeeFileController) Update() {
|
||||
item.IDCard = payload.IDCard
|
||||
// 证照字段由 UpdateCertificates 独立维护,基础资料保存不得覆盖已有附件。
|
||||
item.PoliticalStatus = payload.PoliticalStatus
|
||||
item.Education = payload.Education
|
||||
item.Nation = payload.Nation
|
||||
item.MaritalStatus = payload.MaritalStatus
|
||||
item.NativePlace = payload.NativePlace
|
||||
item.HouseholdAddress = payload.HouseholdAddress
|
||||
@@ -645,7 +651,7 @@ func (c *BackendEmployeeFileController) Update() {
|
||||
item.UpdateTime = &now
|
||||
|
||||
if _, err := models.Orm.Update(&item,
|
||||
"FileNo", "IDCard", "PoliticalStatus", "MaritalStatus", "NativePlace",
|
||||
"FileNo", "IDCard", "PoliticalStatus", "Education", "Nation", "MaritalStatus", "NativePlace",
|
||||
"HouseholdAddress", "CurrentAddress", "EmergencyContact", "EmergencyPhone",
|
||||
"EmergencyRelation", "WorkEmail", "HireDate", "RegularDate", "LeaveDate",
|
||||
"EmploymentStatus", "Remark", "UpdateTime"); err != nil {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user