Files
yunzerwebsiteallinone/go/controllers/backend_oa_scheduling_plan.go
2026-11-05 18:00:50 +08:00

883 lines
27 KiB
Go

package controllers
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"server/models"
"github.com/beego/beego/v2/client/orm"
)
// scPlanMaxEmployees / scPlanMaxEntries 限制单次方案规模,避免误选整个公司
// 再叠加长周期导致一次写入数万条明细。
const (
scPlanMaxEmployees = 500
scPlanMaxEntries = 10000
)
// scPlanInput 排班方案的入参(新建 / 编辑 / 预览共用)。
//
// 两种排班方式:
// - 按星期:Weekdays + ShiftID,每天固定一个班次;
// - 按模板:Pattern + AlignMode,规则来自平台模板(可在租户端微调),
// 此时 ShiftID 允许为 0。
type scPlanInput struct {
PlanName string
PlanType int8
ScopeType int8
OrgID uint64
IncludeChild bool
EmployeeIDs []uint64
StartDate string
EndDate string
Weekdays map[int]bool
ShiftID uint64
Note string
TemplateID uint64
TemplateCode string
TemplateName string
AlignMode int8
CycleDays int
WeekOffset int8
MonthRestDays int8
MonthRestWeekday int8
Pattern []scPatternDay
}
func (c *BackendOaSchedulingController) scPlanInputFromBody(body map[string]interface{}, requireName bool) (*scPlanInput, string) {
input := &scPlanInput{
PlanName: scStringValue(body, "plan_name"),
ScopeType: models.OaScopeOrg,
IncludeChild: true,
StartDate: scStringValue(body, "start_date"),
EndDate: scStringValue(body, "end_date"),
Weekdays: scWeekdaySet(body),
Note: scStringValue(body, "note"),
ShiftID: scUintValue(body, "shift_id"),
EmployeeIDs: scUintSlice(body, "employee_ids"),
TemplateID: scUintValue(body, "template_id"),
TemplateCode: scStringValue(body, "template_code"),
TemplateName: scStringValue(body, "template_name"),
Pattern: scPatternFromBody(body),
}
if requireName && input.PlanName == "" {
return nil, "方案名称不能为空"
}
if len([]rune(input.PlanName)) > 100 {
return nil, "方案名称不能超过 100 个字符"
}
if value, ok := scIntValue(body, "plan_type"); ok {
input.PlanType = int8(value)
} else {
input.PlanType = models.OaPlanTypeRegular
}
if input.PlanType != models.OaPlanTypeRegular && input.PlanType != models.OaPlanTypeTemp {
return nil, "方案类型不合法"
}
if value, ok := scIntValue(body, "scope_type"); ok {
input.ScopeType = int8(value)
}
if input.ScopeType < models.OaScopeCompany || input.ScopeType > models.OaScopeEmployees {
return nil, "排班范围不合法"
}
input.OrgID = scUintValue(body, "org_id")
if input.ScopeType == models.OaScopeOrg && input.OrgID == 0 {
return nil, "请选择要排班的部门或小组"
}
if input.ScopeType == models.OaScopeEmployees && len(input.EmployeeIDs) == 0 {
return nil, "请选择需要排班的员工"
}
if _, has := body["include_child"]; has {
input.IncludeChild = scBoolValue(body, "include_child")
}
// 无期限方案:结束日期允许为空,落库时存哨兵值,发布时按滚动窗口分批生成
if scIsNoEnd(input.EndDate) {
if _, ok := scParseDate(input.StartDate); !ok {
return nil, "开始日期格式错误"
}
input.EndDate = scPlanNoEndDate
} else if _, msg := scDateSpan(input.StartDate, input.EndDate); msg != "" {
return nil, msg
}
if len(input.Pattern) > 0 {
// 模板模式:校验周期规则,班次信息随规则一起保存
if value, ok := scIntValue(body, "align_mode"); ok {
input.AlignMode = int8(value)
}
if value, ok := scIntValue(body, "cycle_days"); ok {
input.CycleDays = value
}
if value, ok := scIntValue(body, "week_offset"); ok {
input.WeekOffset = int8(value)
}
if value, ok := scIntValue(body, "month_rest_days"); ok {
input.MonthRestDays = int8(value)
}
if value, ok := scIntValue(body, "month_rest_weekday"); ok {
input.MonthRestWeekday = int8(value)
}
days, msg := scValidatePattern(input.Pattern, input.AlignMode, input.CycleDays)
if msg != "" {
return nil, msg
}
input.Pattern = days
if input.TemplateName == "" {
input.TemplateName = "自定义规则"
}
} else {
if input.ShiftID == 0 {
return nil, "请选择班次"
}
input.AlignMode = 0
}
if len(input.EmployeeIDs) > scPlanMaxEmployees {
return nil, fmt.Sprintf("单次最多为 %d 名员工排班,请缩小范围", scPlanMaxEmployees)
}
return input, ""
}
func (c *BackendOaSchedulingController) scLoadShift(tid, id uint64) (*models.OaShift, bool) {
shift, ok := c.shiftByID(tid, id)
if !ok || shift.Status != 1 {
return nil, false
}
return shift, true
}
func scWeekdayList(set map[int]bool) []int {
list := make([]int, 0, 7)
for day := 1; day <= 7; day++ {
if set[day] {
list = append(list, day)
}
}
return list
}
// scPlanView 把方案转成前端友好的结构:employee_ids / weekdays 直接给数组,
// 避免前端再解析 JSON 字符串。
func scPlanView(plan *models.OaSchedulePlan) map[string]interface{} {
employeeIDs := []uint64{}
if strings.TrimSpace(plan.EmployeeIDs) != "" {
_ = json.Unmarshal([]byte(plan.EmployeeIDs), &employeeIDs)
}
weekdays := []int{}
if strings.TrimSpace(plan.Weekdays) != "" {
_ = json.Unmarshal([]byte(plan.Weekdays), &weekdays)
}
return map[string]interface{}{
"id": plan.ID,
"plan_name": plan.PlanName,
"plan_type": plan.PlanType,
"scope_type": plan.ScopeType,
"org_id": plan.OrgID,
"org_name": plan.OrgName,
"include_child": plan.IncludeChild,
"employee_ids": employeeIDs,
"employee_names": plan.EmployeeNames,
"start_date": normalizeOaDate(plan.StartDate),
"end_date": normalizeOaDate(plan.EndDate),
"no_end": scIsNoEnd(plan.EndDate),
"weekdays": weekdays,
"shift_id": plan.ShiftID,
"shift_name": plan.ShiftName,
"color": plan.Color,
"template_id": plan.TemplateID,
"template_code": plan.TemplateCode,
"template_name": plan.TemplateName,
"align_mode": plan.AlignMode,
"cycle_days": plan.CycleDays,
"week_offset": plan.WeekOffset,
"month_rest_days": plan.MonthRestDays,
"month_rest_weekday": plan.MonthRestWeekday,
"pattern": scPlanPatternDays(plan),
"note": plan.Note,
"status": plan.Status,
"publish_status": plan.PublishStatus,
"entry_count": plan.EntryCount,
"created_by": plan.CreatedBy,
"created_by_name": plan.CreatedByName,
"create_time": plan.CreateTime.Format("2006-01-02 15:04:05"),
"update_time": plan.UpdateTime,
}
}
func (c *BackendOaSchedulingController) scPlanQuery(tid uint64) orm.QuerySeter {
return models.Orm.QueryTable(new(models.OaSchedulePlan)).
Filter("tid", tid).
Filter("is_deleted", 0)
}
// PlanList 排班方案列表。
// GET /backend/oa/scheduling/plans
func (c *BackendOaSchedulingController) PlanList() {
tid, _, _, ok := c.scOperator()
if !ok {
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("page_size", 20)
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 200 {
pageSize = 20
}
qs := c.scPlanQuery(tid)
if keyword := strings.TrimSpace(c.GetString("keyword")); keyword != "" {
qs = qs.Filter("plan_name__icontains", keyword)
}
if raw := strings.TrimSpace(c.GetString("status")); raw != "" {
if value, err := strconv.Atoi(raw); err == nil {
qs = qs.Filter("status", value)
}
}
if raw := strings.TrimSpace(c.GetString("plan_type")); raw != "" {
if value, err := strconv.Atoi(raw); err == nil {
qs = qs.Filter("plan_type", value)
}
}
total, _ := qs.Count()
var rows []models.OaSchedulePlan
_, err := qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&rows)
if err != nil && err != orm.ErrNoRows {
c.scFail(500, 500, "查询排班方案失败: "+err.Error())
return
}
list := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
list = append(list, scPlanView(&rows[i]))
}
c.scOK(map[string]interface{}{
"list": list,
"total": total,
"page": page,
})
}
// PlanDetail 排班方案详情。
// GET /backend/oa/scheduling/plans/detail/:id
func (c *BackendOaSchedulingController) PlanDetail() {
tid, _, _, ok := c.scOperator()
if !ok {
return
}
id, valid := c.scPathID(":id")
if !valid {
c.scFail(400, 400, "无效的方案ID")
return
}
var plan models.OaSchedulePlan
if err := c.scPlanQuery(tid).Filter("id", id).One(&plan); err != nil {
c.scFail(404, 404, "排班方案不存在")
return
}
c.scOK(scPlanView(&plan))
}
// PlanPreview 预览方案覆盖的员工与班次数,不写入任何数据。
// POST /backend/oa/scheduling/plans/preview
func (c *BackendOaSchedulingController) PlanPreview() {
tid, _, _, ok := c.scOperator()
if !ok {
return
}
body := c.scBody()
input, msg := c.scPlanInputFromBody(body, false)
if msg != "" {
c.scFail(400, 400, msg)
return
}
// 模板模式下由 pattern 携带每天班次,不再要求预先选择班次
var shift *models.OaShift
if len(input.Pattern) == 0 {
loaded, found := c.scLoadShift(tid, input.ShiftID)
if !found {
c.scFail(400, 400, "班次不存在或已停用")
return
}
shift = loaded
}
employees, err := c.scResolveEmployees(tid, input.ScopeType, input.OrgID, input.IncludeChild, input.EmployeeIDs)
if err != nil {
c.scFail(500, 500, "展开排班范围失败: "+err.Error())
return
}
if len(employees) == 0 {
c.scOK(map[string]interface{}{
"employee_count": 0,
"employee_list": []schedulingEmployee{},
"dates": []string{},
"entry_count": 0,
"rest_count": 0,
"shift": shift,
"scope_name": c.scScopeName(tid, input),
"template_name": input.TemplateName,
"pattern": input.Pattern,
})
return
}
if len(employees) > scPlanMaxEmployees {
c.scFail(400, 400, fmt.Sprintf("范围覆盖 %d 名员工,超过单次 %d 人上限,请缩小范围", len(employees), scPlanMaxEmployees))
return
}
dates, msg := scPlanGenWindow(input.StartDate, input.EndDate)
if msg != "" {
c.scFail(400, 400, msg)
return
}
previewPlan := &models.OaSchedulePlan{
StartDate: input.StartDate,
EndDate: input.EndDate,
Weekdays: scWeekdaysJSON(input.Weekdays),
AlignMode: input.AlignMode,
CycleDays: input.CycleDays,
WeekOffset: input.WeekOffset,
MonthRestDays: input.MonthRestDays,
MonthRestWeekday: input.MonthRestWeekday,
Pattern: scPatternToJSON(input.Pattern),
}
_, workdays, msg := scPlanWorkRules(previewPlan, shift, dates)
if msg != "" {
c.scFail(400, 400, msg)
return
}
if len(workdays) == 0 {
c.scFail(400, 400, "所选日期区间内没有需要上班的日期")
return
}
entryCount := len(employees) * len(workdays)
if entryCount > scPlanMaxEntries {
c.scFail(400, 400, fmt.Sprintf("本次将生成 %d 条排班,超过单次 %d 条上限,请缩短日期或缩小范围", entryCount, scPlanMaxEntries))
return
}
list := c.scEmployeeDTOs(tid, employees)
c.scOK(map[string]interface{}{
"employee_count": len(employees),
"employee_list": list,
"dates": workdays,
"workday_count": len(workdays),
"rest_count": len(dates) - len(workdays),
"entry_count": entryCount,
"no_end": scIsNoEnd(input.EndDate),
"gen_start": dates[0],
"gen_end": dates[len(dates)-1],
"shift": shift,
"scope_name": c.scScopeName(tid, input),
"template_name": input.TemplateName,
"pattern": input.Pattern,
})
}
// scWeekdaysJSON 把星期集合转成与方案存储一致的 JSON 数组。
func scWeekdaysJSON(set map[int]bool) string {
list := scWeekdayList(set)
if len(list) == 0 {
return ""
}
buf, err := json.Marshal(list)
if err != nil {
return ""
}
return string(buf)
}
// scScopeName 给方案生成可读的范围描述。
func (c *BackendOaSchedulingController) scScopeName(tid uint64, input *scPlanInput) string {
switch input.ScopeType {
case models.OaScopeCompany:
return "全公司"
case models.OaScopeEmployees:
employees, err := c.scEmployeesByIDs(tid, input.EmployeeIDs)
if err != nil || len(employees) == 0 {
return fmt.Sprintf("指定员工(%d 人)", len(input.EmployeeIDs))
}
names := make([]string, 0, len(employees))
for _, employee := range employees {
names = append(names, employee.Name)
}
if len(names) > 3 {
return fmt.Sprintf("%s 等 %d 人", strings.Join(names[:3], "、"), len(names))
}
return strings.Join(names, "、")
default:
name := c.scLoadOrgIndex(tid).name(input.OrgID)
if name == "" {
name = "部门"
}
if input.IncludeChild {
return name + "(含下级)"
}
return name
}
}
// PlanCreate 新建排班方案。
// POST /backend/oa/scheduling/plans/create
func (c *BackendOaSchedulingController) PlanCreate() {
tid, uid, username, ok := c.scOperator()
if !ok {
return
}
body := c.scBody()
input, msg := c.scPlanInputFromBody(body, true)
if msg != "" {
c.scFail(400, 400, msg)
return
}
// 模板模式下由 pattern 携带每天班次,不再要求预先选择班次
var shift *models.OaShift
if len(input.Pattern) == 0 {
loaded, found := c.scLoadShift(tid, input.ShiftID)
if !found {
c.scFail(400, 400, "班次不存在或已停用")
return
}
shift = loaded
}
weekdays := scWeekdayList(input.Weekdays)
weekdayJSON, _ := json.Marshal(weekdays)
employeeJSON, _ := json.Marshal(input.EmployeeIDs)
plan := &models.OaSchedulePlan{
Tid: int(tid),
PlanName: input.PlanName,
PlanType: input.PlanType,
ScopeType: input.ScopeType,
OrgID: input.OrgID,
OrgName: c.scScopeName(tid, input),
IncludeChild: scBoolToInt8(input.IncludeChild),
EmployeeIDs: string(employeeJSON),
EmployeeNames: c.scEmployeeNames(tid, input.EmployeeIDs),
StartDate: input.StartDate,
EndDate: input.EndDate,
Weekdays: string(weekdayJSON),
Note: input.Note,
Status: 1,
PublishStatus: 0,
CreatedBy: uid,
CreatedByName: username,
}
scApplyPlanRules(plan, input, shift)
if _, err := models.Orm.Insert(plan); err != nil {
c.scFail(500, 500, "保存排班方案失败: "+err.Error())
return
}
// publish_now=true 时创建后立即下发,减少“建完再点发布”的一步操作。
if scBoolValue(body, "publish_now") {
stats, publishErr := c.scPublishPlan(tid, uid, username, plan, true)
if publishErr != nil {
c.scFail(500, 500, "方案已保存,但发布失败: "+publishErr.Error())
return
}
c.scOK(map[string]interface{}{"plan": scPlanView(plan), "publish": stats})
return
}
c.scOK(map[string]interface{}{"plan": scPlanView(plan)})
}
func scBoolToInt8(value bool) int8 {
if value {
return 1
}
return 0
}
// scApplyPlanRules 把入参的排班规则写入方案:
// - 带 pattern:保存模板快照与周期参数,方案的展示班次取第一个上班日;
// - 不带 pattern:回到按星期模式,清空模板字段。
func scApplyPlanRules(plan *models.OaSchedulePlan, input *scPlanInput, shift *models.OaShift) {
if len(input.Pattern) > 0 {
plan.TemplateID = input.TemplateID
plan.TemplateCode = input.TemplateCode
plan.TemplateName = input.TemplateName
plan.AlignMode = input.AlignMode
plan.CycleDays = input.CycleDays
if plan.AlignMode == models.SchedTplAlignMonth {
plan.CycleDays = 0
}
plan.WeekOffset = input.WeekOffset
plan.MonthRestDays = input.MonthRestDays
plan.MonthRestWeekday = input.MonthRestWeekday
plan.Pattern = scPatternToJSON(input.Pattern)
if primary, ok := scPrimaryPatternShift(input.Pattern); ok {
plan.ShiftID = primary.ShiftID
plan.ShiftName = primary.ShiftName
plan.Color = primary.Color
}
return
}
plan.TemplateID = 0
plan.TemplateCode = ""
plan.TemplateName = ""
plan.AlignMode = 0
plan.CycleDays = 0
plan.WeekOffset = 0
plan.MonthRestDays = 0
plan.MonthRestWeekday = 0
plan.Pattern = ""
if shift != nil {
plan.ShiftID = shift.ID
plan.ShiftName = shift.Name
plan.Color = shift.Color
}
}
func (c *BackendOaSchedulingController) scEmployeeNames(tid uint64, ids []uint64) string {
if len(ids) == 0 {
return ""
}
employees, err := c.scEmployeesByIDs(tid, ids)
if err != nil {
return ""
}
names := make([]string, 0, len(employees))
for _, employee := range employees {
names = append(names, employee.Name)
}
return strings.Join(names, "、")
}
// PlanUpdate 更新排班方案。已发布的方案会回到“未发布”状态,
// 需要重新发布后新规则才会写入明细(已有明细不会被自动清除)。
// POST /backend/oa/scheduling/plans/update/:id
func (c *BackendOaSchedulingController) PlanUpdate() {
tid, _, _, ok := c.scOperator()
if !ok {
return
}
id, valid := c.scPathID(":id")
if !valid {
c.scFail(400, 400, "无效的方案ID")
return
}
var plan models.OaSchedulePlan
if err := c.scPlanQuery(tid).Filter("id", id).One(&plan); err != nil {
c.scFail(404, 404, "排班方案不存在")
return
}
body := c.scBody()
input, msg := c.scPlanInputFromBody(body, true)
if msg != "" {
c.scFail(400, 400, msg)
return
}
// 模板模式下由 pattern 携带每天班次,不再要求预先选择班次
var shift *models.OaShift
if len(input.Pattern) == 0 {
loaded, found := c.scLoadShift(tid, input.ShiftID)
if !found {
c.scFail(400, 400, "班次不存在或已停用")
return
}
shift = loaded
}
weekdays := scWeekdayList(input.Weekdays)
weekdayJSON, _ := json.Marshal(weekdays)
employeeJSON, _ := json.Marshal(input.EmployeeIDs)
plan.PlanName = input.PlanName
plan.PlanType = input.PlanType
plan.ScopeType = input.ScopeType
plan.OrgID = input.OrgID
plan.OrgName = c.scScopeName(tid, input)
plan.IncludeChild = scBoolToInt8(input.IncludeChild)
plan.EmployeeIDs = string(employeeJSON)
plan.EmployeeNames = c.scEmployeeNames(tid, input.EmployeeIDs)
plan.StartDate = input.StartDate
plan.EndDate = input.EndDate
plan.Weekdays = string(weekdayJSON)
plan.Note = input.Note
plan.Status = 1
plan.PublishStatus = 0
scApplyPlanRules(&plan, input, shift)
if _, err := models.Orm.Update(&plan); err != nil {
c.scFail(500, 500, "更新排班方案失败: "+err.Error())
return
}
c.scOK(map[string]interface{}{"plan": scPlanView(&plan)})
}
// PlanPublish 发布方案:把规则展开成每人每天的排班明细。
// POST /backend/oa/scheduling/plans/publish/:id
// body: { overwrite: true } 覆盖同一员工同一天已有排班,默认保留原排班。
func (c *BackendOaSchedulingController) PlanPublish() {
tid, uid, username, ok := c.scOperator()
if !ok {
return
}
id, valid := c.scPathID(":id")
if !valid {
c.scFail(400, 400, "无效的方案ID")
return
}
var plan models.OaSchedulePlan
if err := c.scPlanQuery(tid).Filter("id", id).One(&plan); err != nil {
c.scFail(404, 404, "排班方案不存在")
return
}
if plan.Status != 1 {
c.scFail(400, 400, "方案已停用,请先启用")
return
}
body := c.scBody()
stats, err := c.scPublishPlan(tid, uid, username, &plan, scBoolValue(body, "overwrite"))
if err != nil {
c.scFail(500, 500, "发布排班失败: "+err.Error())
return
}
c.scOK(map[string]interface{}{"plan": scPlanView(&plan), "publish": stats})
}
// scPublishPlan 方案展开与写入。同一员工同一天只保留一条有效明细:
// - 不存在 -> 新增
// - 已存在且 overwrite=false -> 跳过(保留手工调整结果)
// - 已存在且 overwrite=true -> 覆盖为新班次并记录变更
func (c *BackendOaSchedulingController) scPublishPlan(tid, uid uint64, username string, plan *models.OaSchedulePlan, overwrite bool) (map[string]int, error) {
// 模板模式(AlignMode=1/2/3)由 pattern 携带每天班次;按星期模式回退到方案绑定的班次
var fallback *models.OaShift
if plan.AlignMode == 0 {
loaded, found := c.scLoadShift(tid, plan.ShiftID)
if !found {
return nil, errors.New("方案绑定的班次不存在或已停用")
}
fallback = loaded
}
employeeIDs := []uint64{}
if strings.TrimSpace(plan.EmployeeIDs) != "" {
_ = json.Unmarshal([]byte(plan.EmployeeIDs), &employeeIDs)
}
employees, err := c.scResolveEmployees(tid, plan.ScopeType, plan.OrgID, plan.IncludeChild == 1, employeeIDs)
if err != nil {
return nil, err
}
if len(employees) == 0 {
return nil, errors.New("方案范围内没有可排班的员工")
}
if len(employees) > scPlanMaxEmployees {
return nil, fmt.Errorf("范围覆盖 %d 名员工,超过单次 %d 人上限", len(employees), scPlanMaxEmployees)
}
dates, msg := scPlanGenWindow(normalizeOaDate(plan.StartDate), normalizeOaDate(plan.EndDate))
if msg != "" {
return nil, errors.New(msg)
}
rules, workdays, msg := scPlanWorkRules(plan, fallback, dates)
if msg != "" {
return nil, errors.New(msg)
}
if len(workdays) == 0 {
return nil, errors.New("方案日期区间内没有需要上班的日期")
}
if len(employees)*len(workdays) > scPlanMaxEntries {
return nil, fmt.Errorf("本次将生成 %d 条排班,超过单次 %d 条上限", len(employees)*len(workdays), scPlanMaxEntries)
}
index := c.scLoadOrgIndex(tid)
empIDs := make([]uint64, 0, len(employees))
for _, employee := range employees {
empIDs = append(empIDs, uint64(employee.ID))
}
var existing []models.OaScheduleEntry
_, err = models.Orm.QueryTable(new(models.OaScheduleEntry)).
Filter("tid", tid).
Filter("is_deleted", 0).
Filter("employee_id__in", empIDs).
Filter("schedule_date__gte", workdays[0]).
Filter("schedule_date__lte", workdays[len(workdays)-1]).
All(&existing)
if err != nil && err != orm.ErrNoRows {
return nil, err
}
existingMap := map[string]*models.OaScheduleEntry{}
for i := range existing {
key := fmt.Sprintf("%d|%s", existing[i].EmployeeID, normalizeOaDate(existing[i].ScheduleDate))
existingMap[key] = &existing[i]
}
stats := map[string]int{"created": 0, "updated": 0, "skipped": 0}
tx, err := orm.NewOrm().Begin()
if err != nil {
return nil, err
}
for _, employee := range employees {
orgID := scEmployeeDepartmentID(employee)
position := ""
if employee.Position != nil {
position = *employee.Position
}
for _, date := range workdays {
rule := rules[date]
key := fmt.Sprintf("%d|%s", employee.ID, date)
if row, hit := existingMap[key]; hit {
if !overwrite {
stats["skipped"]++
continue
}
if row.ShiftID == rule.ShiftID && row.ShiftName == rule.ShiftName && row.Source == models.OaEntrySourcePlan && row.PlanID == plan.ID {
stats["skipped"]++
continue
}
oldShiftID, oldShiftName := row.ShiftID, row.ShiftName
row.PlanID = plan.ID
row.ShiftID = rule.ShiftID
row.ShiftName = rule.ShiftName
row.ShortName = rule.ShortName
row.Color = rule.Color
row.WorkStart = rule.WorkStart
row.WorkEnd = rule.WorkEnd
row.Source = models.OaEntrySourcePlan
if _, updateErr := tx.Update(row); updateErr != nil {
_ = tx.Rollback()
return nil, updateErr
}
c.scWriteChange(tx, tid, row.ID, row.EmployeeID, row.EmployeeName, date,
models.OaChangePublish, oldShiftID, oldShiftName, rule.ShiftID, rule.ShiftName, "方案重新发布覆盖", uid, username)
stats["updated"]++
continue
}
entry := &models.OaScheduleEntry{
Tid: int(tid),
PlanID: plan.ID,
EmployeeID: employee.ID,
EmployeeName: employee.Name,
EmployeeAccount: employee.Account,
OrgID: orgID,
OrgName: index.name(orgID),
Position: position,
ScheduleDate: date,
ShiftID: rule.ShiftID,
ShiftName: rule.ShiftName,
ShortName: rule.ShortName,
Color: rule.Color,
WorkStart: rule.WorkStart,
WorkEnd: rule.WorkEnd,
Source: models.OaEntrySourcePlan,
Note: plan.Note,
CreatedBy: uid,
CreatedByName: username,
}
if _, insertErr := tx.Insert(entry); insertErr != nil {
_ = tx.Rollback()
return nil, insertErr
}
existingMap[key] = entry
stats["created"]++
}
}
now := time.Now()
plan.PublishStatus = 1
plan.EntryCount = stats["created"] + stats["updated"]
plan.UpdateTime = &now
if _, err := tx.Update(plan, "publish_status", "entry_count", "update_time"); err != nil {
_ = tx.Rollback()
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
stats["employee_count"] = len(employees)
stats["workday_count"] = len(workdays)
return stats, nil
}
// scChangeInserter 抽取 Insert,使普通 Ormer 与事务 TxOrmer 都能写变更记录。
type scChangeInserter interface {
Insert(interface{}) (int64, error)
}
// scWriteChange 写一条排班变更记录;失败不阻断主流程。
func (c *BackendOaSchedulingController) scWriteChange(tx scChangeInserter, tid uint64, entryID uint64, employeeID uint, employeeName, date string,
changeType int8, oldShiftID uint64, oldShiftName string, newShiftID uint64, newShiftName, reason string, operatorID uint64, operatorName string) {
if tx == nil || (oldShiftID == newShiftID && oldShiftName == newShiftName) {
return
}
_, _ = tx.Insert(&models.OaScheduleChange{
Tid: int(tid),
EntryID: entryID,
EmployeeID: employeeID,
EmployeeName: employeeName,
ScheduleDate: date,
ChangeType: changeType,
OldShiftID: oldShiftID,
OldShiftName: oldShiftName,
NewShiftID: newShiftID,
NewShiftName: newShiftName,
Reason: reason,
OperatorID: operatorID,
OperatorName: operatorName,
})
}
// PlanDelete 删除排班方案;默认同时清除该方案生成且仍未被调整的排班明细
// (source=1)。被临时调配过的明细 source 已变为 2,会保留下来,
// 避免把人工调整过的排班一起删掉。
// DELETE /backend/oa/scheduling/plans/delete/:id?delete_entries=1
func (c *BackendOaSchedulingController) PlanDelete() {
tid, _, _, ok := c.scOperator()
if !ok {
return
}
id, valid := c.scPathID(":id")
if !valid {
c.scFail(400, 400, "无效的方案ID")
return
}
var plan models.OaSchedulePlan
if err := c.scPlanQuery(tid).Filter("id", id).One(&plan); err != nil {
c.scFail(404, 404, "排班方案不存在")
return
}
now := time.Now()
plan.IsDeleted = 1
plan.DeleteTime = &now
if _, err := models.Orm.Update(&plan, "is_deleted", "delete_time"); err != nil {
c.scFail(500, 500, "删除方案失败: "+err.Error())
return
}
removed := int64(0)
deleteEntries := c.GetString("delete_entries") != "0"
if deleteEntries {
result, err := models.Orm.Raw(
"UPDATE yz_backend_oa_schedule_entry SET is_deleted=id, delete_time=? "+
"WHERE tid=? AND plan_id=? AND source=1 AND is_deleted=0", now, tid, plan.ID,
).Exec()
if err == nil {
removed, _ = result.RowsAffected()
}
}
c.scOK(map[string]interface{}{"id": plan.ID, "deleted_entries": removed})
}