更新若干事项

This commit is contained in:
hero920103 committed 2026-10-26 08:54:34 +08:00
1 parent 68cdaf38fa
commit 9898962836
30 files changed
+7715

No files matched your search

@@ -0,0 +1,635 @@
package controllers
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/identity"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformSchedTemplateController 平台端排班模板管理(全局模板,只有平台可维护)。
//
// 表:yz_platform_sched_tpl / yz_platform_sched_tpl_day
// 建表与预置数据:docs/sql/create_platform_sched_template.sql
//
// 规则:
// - 系统预置模板(is_system=1)由平台锁定:禁止编辑、删除、停用;
// - 平台可新建「自定义模板」,并编辑 / 停用 / 删除自定义模板;
// - 租户端只读取启用模板(/backend/oa/scheduling/templates),
// 调用后把规则快照进自己的排班方案,模板改动不影响已保存方案。
type PlatformSchedTemplateController struct {
beego.Controller
}
func (c *PlatformSchedTemplateController) tplClaims() (*jwtutil.Claims, error) {
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndPlatform)
}
func (c *PlatformSchedTemplateController) tplErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *PlatformSchedTemplateController) tplOK(data interface{}, msg string) {
if msg == "" {
msg = "success"
}
resp := map[string]interface{}{"code": 200, "msg": msg}
if data != nil {
resp["data"] = data
}
c.Data["json"] = resp
_ = c.ServeJSON()
}
// ---------------------------------------------------------------------------
// 参数结构
// ---------------------------------------------------------------------------
type schedTplDayPayload struct {
DayIndex int `json:"day_index"`
DayType int8 `json:"day_type"`
ShiftName string `json:"shift_name"`
ShiftShort string `json:"shift_short"`
Color string `json:"color"`
WorkStart string `json:"work_start"`
WorkEnd string `json:"work_end"`
RestStart string `json:"rest_start"`
RestEnd string `json:"rest_end"`
WorkHours float64 `json:"work_hours"`
Remark string `json:"remark"`
}
type schedTplPayload struct {
Name string `json:"name"`
Code string `json:"code"`
TemplateType int8 `json:"template_type"`
AlignMode int8 `json:"align_mode"`
CycleDays int `json:"cycle_days"`
WeekOffset int8 `json:"week_offset"`
MonthRestDays int8 `json:"month_rest_days"`
MonthRestWeekday int8 `json:"month_rest_weekday"`
Summary string `json:"summary"`
Status int8 `json:"status"`
Sort int `json:"sort"`
Days []schedTplDayPayload `json:"days"`
}
// ---------------------------------------------------------------------------
// 列表 / 详情
// ---------------------------------------------------------------------------
func (c *PlatformSchedTemplateController) tplQuery() orm.QuerySeter {
return models.Orm.QueryTable(new(models.PlatformSchedTpl)).Filter("delete_time__isnull", true)
}
func (c *PlatformSchedTemplateController) tplByID(id uint64) (*models.PlatformSchedTpl, bool) {
var row models.PlatformSchedTpl
if err := c.tplQuery().Filter("id", id).One(&row); err != nil {
return nil, false
}
return &row, true
}
func (c *PlatformSchedTemplateController) tplDays(templateID uint64) []models.PlatformSchedTplDay {
rows := []models.PlatformSchedTplDay{}
_, err := models.Orm.QueryTable(new(models.PlatformSchedTplDay)).
Filter("template_id", templateID).
OrderBy("day_index").All(&rows)
if err != nil && err != orm.ErrNoRows {
return []models.PlatformSchedTplDay{}
}
return rows
}
func (c *PlatformSchedTemplateController) tplView(row *models.PlatformSchedTpl, withDays bool) map[string]interface{} {
view := map[string]interface{}{
"id": row.ID,
"code": row.Code,
"name": row.Name,
"template_type": row.TemplateType,
"template_type_text": schedTplTypeText(row.TemplateType),
"align_mode": row.AlignMode,
"cycle_days": row.CycleDays,
"week_offset": row.WeekOffset,
"month_rest_days": row.MonthRestDays,
"month_rest_weekday": row.MonthRestWeekday,
"summary": row.Summary,
"is_system": row.IsSystem,
"editable": row.IsSystem != 1,
"status": row.Status,
"sort": row.Sort,
"creater": row.Creater,
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
}
if row.UpdateTime != nil {
view["update_time"] = row.UpdateTime.Format("2006-01-02 15:04:05")
}
if withDays {
view["days"] = c.tplDays(row.ID)
}
return view
}
func schedTplTypeText(value int8) string {
switch value {
case models.SchedTplTypeDoubleWeekend:
return "双休"
case models.SchedTplTypeSingleWeekend:
return "单休"
case models.SchedTplTypeBigSmallWeek:
return "大小周"
case models.SchedTplTypeMonthRest:
return "月休"
case models.SchedTplTypeThreeShift:
return "三班倒"
case models.SchedTplTypeWorkRestOne:
return "做一休一"
default:
return "自定义"
}
}
// List 模板列表。
// GET /platform/schedTemplates?page=1&pageSize=20&keyword=&status=&template_type=
func (c *PlatformSchedTemplateController) List() {
if _, err := c.tplClaims(); err != nil {
c.tplErr(401, 401, "未登录或登录已过期")
return
}
page, pageSize, offset := dictPageSize(&c.Controller)
qs := c.tplQuery()
if keyword := strings.TrimSpace(c.GetString("keyword")); keyword != "" {
qs = qs.Filter("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("template_type")); raw != "" {
if value, err := strconv.Atoi(raw); err == nil {
qs = qs.Filter("template_type", value)
}
}
total, _ := qs.Count()
var rows []models.PlatformSchedTpl
if _, err := qs.OrderBy("sort", "id").Limit(pageSize).Offset(offset).All(&rows); err != nil && err != orm.ErrNoRows {
c.tplErr(500, 500, "查询模板失败: "+err.Error())
return
}
list := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
list = append(list, c.tplView(&rows[i], false))
}
c.tplOK(map[string]interface{}{
"list": list,
"total": total,
"page": page,
"page_size": pageSize,
}, "")
}
// Detail 模板详情(含周期每日规则)。
// GET /platform/schedTemplates/:id
func (c *PlatformSchedTemplateController) Detail() {
if _, err := c.tplClaims(); err != nil {
c.tplErr(401, 401, "未登录或登录已过期")
return
}
id, err := strconv.ParseUint(strings.TrimSpace(c.Ctx.Input.Param(":id")), 10, 64)
if err != nil || id == 0 {
c.tplErr(400, 400, "无效的模板ID")
return
}
row, found := c.tplByID(id)
if !found {
c.tplErr(404, 404, "模板不存在")
return
}
c.tplOK(c.tplView(row, true), "")
}
// ---------------------------------------------------------------------------
// 新建 / 编辑 / 删除 / 启停
// ---------------------------------------------------------------------------
func (c *PlatformSchedTemplateController) parsePayload() (*schedTplPayload, bool) {
var payload schedTplPayload
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
c.tplErr(400, 400, "参数错误")
return nil, false
}
return &payload, true
}
// buildDays 校验并构造周期每日规则;返回非空 msg 表示参数错误。
func buildSchedTplDays(payload *schedTplPayload) ([]models.PlatformSchedTplDay, string) {
if payload.AlignMode < models.SchedTplAlignWeek || payload.AlignMode > models.SchedTplAlignMonth {
return nil, "排班方式不合法"
}
if len(payload.Days) == 0 {
return nil, "请至少配置一天的规则"
}
cycle := payload.CycleDays
if payload.AlignMode == models.SchedTplAlignMonth {
// 月休按自然月计算,周期天数无意义
cycle = 0
if payload.MonthRestDays < 1 || payload.MonthRestDays > 15 {
return nil, "月休天数需在 1~15 天之间"
}
if payload.MonthRestWeekday < 0 || payload.MonthRestWeekday > 7 {
return nil, "月休休息周几不合法"
}
} else {
if cycle < 2 || cycle > 31 {
return nil, "周期天数需在 2~31 天之间"
}
if payload.AlignMode == models.SchedTplAlignWeek && cycle%7 != 0 {
return nil, "按自然周排班时,周期天数必须是 7 的倍数(7 / 14 / 21 / 28)"
}
if len(payload.Days) != cycle {
return nil, fmt.Sprintf("周期为 %d 天,需要配置 %d 天的规则", cycle, cycle)
}
}
seen := map[int]bool{}
workCount := 0
days := make([]models.PlatformSchedTplDay, 0, len(payload.Days))
for _, item := range payload.Days {
index := item.DayIndex
if payload.AlignMode == models.SchedTplAlignMonth {
index = len(days)
}
if index < 0 || (payload.AlignMode != models.SchedTplAlignMonth && index >= cycle) {
return nil, "每日规则的天序号不合法"
}
if seen[index] {
return nil, "每日规则的天序号重复"
}
seen[index] = true
dayType := item.DayType
if dayType != models.SchedTplDayWork && dayType != models.SchedTplDayRest {
return nil, "每日规则类型不合法(1=上班 2=休息)"
}
row := models.PlatformSchedTplDay{
DayIndex: index,
DayType: dayType,
Remark: strings.TrimSpace(item.Remark),
}
if dayType == models.SchedTplDayWork {
workCount++
row.ShiftName = strings.TrimSpace(item.ShiftName)
if row.ShiftName == "" {
return nil, fmt.Sprintf("第 %d 天选择上班,需填写班次名称", index+1)
}
if len([]rune(row.ShiftName)) > 50 {
return nil, "班次名称不能超过 50 个字符"
}
row.WorkStart = strings.TrimSpace(item.WorkStart)
row.WorkEnd = strings.TrimSpace(item.WorkEnd)
if !isValidOaTime(row.WorkStart) || !isValidOaTime(row.WorkEnd) {
return nil, fmt.Sprintf("第 %d 天的上下班时间格式应为 HH:mm", index+1)
}
if row.WorkStart == row.WorkEnd {
return nil, fmt.Sprintf("第 %d 天的上下班时间不能相同", index+1)
}
row.RestStart = strings.TrimSpace(item.RestStart)
row.RestEnd = strings.TrimSpace(item.RestEnd)
if (row.RestStart == "") != (row.RestEnd == "") {
return nil, fmt.Sprintf("第 %d 天的休息开始与结束时间需要同时填写", index+1)
}
if row.RestStart != "" && (!isValidOaTime(row.RestStart) || !isValidOaTime(row.RestEnd)) {
return nil, fmt.Sprintf("第 %d 天的休息时间格式应为 HH:mm", index+1)
}
row.ShiftShort = strings.TrimSpace(item.ShiftShort)
if row.ShiftShort == "" {
row.ShiftShort = row.ShiftName
}
row.Color = strings.TrimSpace(item.Color)
if row.Color == "" {
row.Color = "#409EFF"
}
row.WorkHours = scCalcWorkHours(row.WorkStart, row.WorkEnd, row.RestStart, row.RestEnd)
}
days = append(days, row)
}
if workCount == 0 {
return nil, "排班模板至少需要一天上班"
}
// 按天序号排序,避免前端乱序提交
for i := 0; i < len(days); i++ {
for j := i + 1; j < len(days); j++ {
if days[j].DayIndex < days[i].DayIndex {
days[i], days[j] = days[j], days[i]
}
}
}
return days, ""
}
// autoSummary 生成规则摘要(前端未填写时使用)。
func autoSummary(payload *schedTplPayload, days []models.PlatformSchedTplDay) string {
work, rest := 0, 0
shiftNames := []string{}
seenShift := map[string]bool{}
for _, day := range days {
if day.DayType == models.SchedTplDayWork {
work++
if !seenShift[day.ShiftName] {
seenShift[day.ShiftName] = true
shiftNames = append(shiftNames, fmt.Sprintf("%s %s-%s", day.ShiftName, day.WorkStart, day.WorkEnd))
}
} else {
rest++
}
}
switch payload.AlignMode {
case models.SchedTplAlignWeek:
weeks := payload.CycleDays / 7
if weeks <= 1 {
return fmt.Sprintf("每周上班 %d 天、休息 %d 天;%s", work, rest, strings.Join(shiftNames, "、"))
}
return fmt.Sprintf("%d 周循环,共上班 %d 天、休息 %d 天;%s", weeks, work, rest, strings.Join(shiftNames, "、"))
case models.SchedTplAlignMonth:
return fmt.Sprintf("每月固定休 %d 天;上班日 %s", payload.MonthRestDays, strings.Join(shiftNames, "、"))
default:
return fmt.Sprintf("每 %d 天循环,上班 %d 天、休息 %d 天;%s", payload.CycleDays, work, rest, strings.Join(shiftNames, "、"))
}
}
// Create 新建自定义模板。
// POST /platform/schedTemplates
func (c *PlatformSchedTemplateController) Create() {
claims, err := c.tplClaims()
if err != nil {
c.tplErr(401, 401, "未登录或登录已过期")
return
}
payload, ok := c.parsePayload()
if !ok {
return
}
payload.Name = strings.TrimSpace(payload.Name)
if payload.Name == "" {
c.tplErr(400, 400, "模板名称不能为空")
return
}
if len([]rune(payload.Name)) > 50 {
c.tplErr(400, 400, "模板名称不能超过 50 个字符")
return
}
if payload.TemplateType <= 0 || (payload.TemplateType > 6 && payload.TemplateType != models.SchedTplTypeCustom) {
payload.TemplateType = models.SchedTplTypeCustom
}
days, msg := buildSchedTplDays(payload)
if msg != "" {
c.tplErr(400, 400, msg)
return
}
if payload.Status != 0 {
payload.Status = 1
}
code := strings.TrimSpace(payload.Code)
if code == "" {
code = fmt.Sprintf("custom_%d", time.Now().UnixNano()/1e6)
}
exist, _ := c.tplQuery().Filter("code", code).Count()
if exist > 0 {
c.tplErr(400, 400, "模板编码已存在")
return
}
summary := strings.TrimSpace(payload.Summary)
if summary == "" {
summary = autoSummary(payload, days)
}
row := &models.PlatformSchedTpl{
Code: code,
Name: payload.Name,
TemplateType: payload.TemplateType,
AlignMode: payload.AlignMode,
CycleDays: payload.CycleDays,
WeekOffset: payload.WeekOffset,
MonthRestDays: payload.MonthRestDays,
MonthRestWeekday: payload.MonthRestWeekday,
Summary: summary,
IsSystem: 0,
Status: payload.Status,
Sort: payload.Sort,
Creater: claims.Username,
}
if row.AlignMode == models.SchedTplAlignMonth {
row.CycleDays = 0
}
tx, beginErr := orm.NewOrm().Begin()
if beginErr != nil {
c.tplErr(500, 500, "保存模板失败: "+beginErr.Error())
return
}
id, insertErr := tx.Insert(row)
if insertErr != nil {
_ = tx.Rollback()
c.tplErr(500, 500, "保存模板失败: "+insertErr.Error())
return
}
for i := range days {
days[i].TemplateID = row.ID
if _, err := tx.Insert(&days[i]); err != nil {
_ = tx.Rollback()
c.tplErr(500, 500, "保存模板规则失败: "+err.Error())
return
}
}
if err := tx.Commit(); err != nil {
c.tplErr(500, 500, "保存模板失败: "+err.Error())
return
}
_ = id
c.tplOK(c.tplView(row, true), "模板已创建")
}
// Update 编辑自定义模板;系统预置模板只读。
// POST /platform/schedTemplates/:id
func (c *PlatformSchedTemplateController) Update() {
if _, err := c.tplClaims(); err != nil {
c.tplErr(401, 401, "未登录或登录已过期")
return
}
id, err := strconv.ParseUint(strings.TrimSpace(c.Ctx.Input.Param(":id")), 10, 64)
if err != nil || id == 0 {
c.tplErr(400, 400, "无效的模板ID")
return
}
row, found := c.tplByID(id)
if !found {
c.tplErr(404, 404, "模板不存在")
return
}
if row.IsSystem == 1 {
c.tplErr(400, 400, "系统预置模板只读,不能编辑")
return
}
payload, ok := c.parsePayload()
if !ok {
return
}
payload.Name = strings.TrimSpace(payload.Name)
if payload.Name == "" {
c.tplErr(400, 400, "模板名称不能为空")
return
}
if len([]rune(payload.Name)) > 50 {
c.tplErr(400, 400, "模板名称不能超过 50 个字符")
return
}
if payload.TemplateType <= 0 || (payload.TemplateType > 6 && payload.TemplateType != models.SchedTplTypeCustom) {
payload.TemplateType = models.SchedTplTypeCustom
}
days, msg := buildSchedTplDays(payload)
if msg != "" {
c.tplErr(400, 400, msg)
return
}
if payload.Status != 0 {
payload.Status = 1
}
summary := strings.TrimSpace(payload.Summary)
if summary == "" {
summary = autoSummary(payload, days)
}
row.Name = payload.Name
row.TemplateType = payload.TemplateType
row.AlignMode = payload.AlignMode
row.CycleDays = payload.CycleDays
if row.AlignMode == models.SchedTplAlignMonth {
row.CycleDays = 0
}
row.WeekOffset = payload.WeekOffset
row.MonthRestDays = payload.MonthRestDays
row.MonthRestWeekday = payload.MonthRestWeekday
row.Summary = summary
row.Status = payload.Status
row.Sort = payload.Sort
tx, beginErr := orm.NewOrm().Begin()
if beginErr != nil {
c.tplErr(500, 500, "保存模板失败: "+beginErr.Error())
return
}
if _, err := tx.Update(row); err != nil {
_ = tx.Rollback()
c.tplErr(500, 500, "保存模板失败: "+err.Error())
return
}
// 规则整体替换:先清空旧规则再写入新规则,避免残留天数
if _, err := tx.QueryTable(new(models.PlatformSchedTplDay)).Filter("template_id", row.ID).Delete(); err != nil {
_ = tx.Rollback()
c.tplErr(500, 500, "保存模板规则失败: "+err.Error())
return
}
for i := range days {
days[i].TemplateID = row.ID
if _, err := tx.Insert(&days[i]); err != nil {
_ = tx.Rollback()
c.tplErr(500, 500, "保存模板规则失败: "+err.Error())
return
}
}
if err := tx.Commit(); err != nil {
c.tplErr(500, 500, "保存模板失败: "+err.Error())
return
}
c.tplOK(c.tplView(row, true), "模板已更新")
}
// Delete 删除自定义模板(软删除);系统预置模板禁止删除。
// DELETE /platform/schedTemplates/:id
func (c *PlatformSchedTemplateController) Delete() {
if _, err := c.tplClaims(); err != nil {
c.tplErr(401, 401, "未登录或登录已过期")
return
}
id, err := strconv.ParseUint(strings.TrimSpace(c.Ctx.Input.Param(":id")), 10, 64)
if err != nil || id == 0 {
c.tplErr(400, 400, "无效的模板ID")
return
}
row, found := c.tplByID(id)
if !found {
c.tplErr(404, 404, "模板不存在")
return
}
if row.IsSystem == 1 {
c.tplErr(400, 400, "系统预置模板不能删除")
return
}
now := time.Now()
row.DeleteTime = &now
if _, err := models.Orm.Update(row, "delete_time"); err != nil {
c.tplErr(500, 500, "删除模板失败: "+err.Error())
return
}
c.tplOK(map[string]interface{}{"id": row.ID}, "模板已删除")
}
// ToggleStatus 启用 / 停用自定义模板;系统预置模板不可停用。
// POST /platform/schedTemplates/:id/status
func (c *PlatformSchedTemplateController) ToggleStatus() {
if _, err := c.tplClaims(); err != nil {
c.tplErr(401, 401, "未登录或登录已过期")
return
}
id, err := strconv.ParseUint(strings.TrimSpace(c.Ctx.Input.Param(":id")), 10, 64)
if err != nil || id == 0 {
c.tplErr(400, 400, "无效的模板ID")
return
}
row, found := c.tplByID(id)
if !found {
c.tplErr(404, 404, "模板不存在")
return
}
if row.IsSystem == 1 {
c.tplErr(400, 400, "系统预置模板固定启用,不能停用")
return
}
status, _ := c.GetInt("status", -1)
if status != 0 && status != 1 {
var body struct {
Status int8 `json:"status"`
}
_ = json.Unmarshal(c.Ctx.Input.RequestBody, &body)
if body.Status != 0 && body.Status != 1 {
c.tplErr(400, 400, "状态只能是 0 或 1")
return
}
status = int(body.Status)
}
row.Status = int8(status)
if _, err := models.Orm.Update(row, "status"); err != nil {
c.tplErr(500, 500, "更新状态失败: "+err.Error())
return
}
c.tplOK(map[string]interface{}{"id": row.ID, "status": row.Status}, "状态已更新")
}