feat(schedule): remove sourceType from schedule models and payloads

This commit is contained in:
mlogclub
2026-04-29 15:09:35 +08:00
parent e56fe82b3a
commit 6331ce32c4
12 changed files with 81 additions and 119 deletions
+1 -1
Submodule docs updated: 83ab315da6...a9c8cacac4
@@ -111,12 +111,11 @@ func (c *AgentTeamScheduleController) PostDelete() *web.JsonResult {
func buildAgentTeamScheduleResponse(item *models.AgentTeamSchedule) response.AgentTeamScheduleResponse {
ret := response.AgentTeamScheduleResponse{
ID: item.ID,
TeamID: item.TeamID,
StartAt: item.StartAt.Format("2006-01-02 15:04:05"),
EndAt: item.EndAt.Format("2006-01-02 15:04:05"),
SourceType: item.SourceType,
Remark: item.Remark,
ID: item.ID,
TeamID: item.TeamID,
StartAt: item.StartAt.Format("2006-01-02 15:04:05"),
EndAt: item.EndAt.Format("2006-01-02 15:04:05"),
Remark: item.Remark,
}
if team := services.AgentTeamService.Get(item.TeamID); team != nil {
ret.TeamName = team.Name
+6 -7
View File
@@ -742,13 +742,12 @@ type AgentTeam struct {
// AgentTeamSchedule 客服组排班。
type AgentTeamSchedule struct {
ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为组排班主键。
TeamID int64 `gorm:"type:bigint;not null;index"` // TeamID 为被排班的客服组ID。
StartAt time.Time `gorm:"type:datetime;not null;index"` // StartAt 为班次开始时间。
EndAt time.Time `gorm:"type:datetime;not null;index"` // EndAt 为班次结束时间。
SourceType string `gorm:"type:varchar(30);not null;default:'';index"` // SourceType 表示排班来源,如 manual、batch_import、template_generate
Remark string `gorm:"type:varchar(255);not null;default:''"` // Remark 记录排班备注
Status enums.Status `gorm:"type:int;not null;default:0;index"` // Status 表示组排班记录状态。
ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为组排班主键。
TeamID int64 `gorm:"type:bigint;not null;index"` // TeamID 为被排班的客服组ID。
StartAt time.Time `gorm:"type:datetime;not null;index"` // StartAt 为班次开始时间。
EndAt time.Time `gorm:"type:datetime;not null;index"` // EndAt 为班次结束时间。
Remark string `gorm:"type:varchar(255);not null;default:''"` // Remark 记录排班备注
Status enums.Status `gorm:"type:int;not null;default:0;index"` // Status 表示组排班记录状态
AuditFields
}
+4 -5
View File
@@ -47,11 +47,10 @@ type DeleteAgentTeamRequest struct {
}
type CreateAgentTeamScheduleRequest struct {
TeamID int64 `json:"teamId"`
StartAt string `json:"startAt"`
EndAt string `json:"endAt"`
SourceType string `json:"sourceType"`
Remark string `json:"remark"`
TeamID int64 `json:"teamId"`
StartAt string `json:"startAt"`
EndAt string `json:"endAt"`
Remark string `json:"remark"`
}
type UpdateAgentTeamScheduleRequest struct {
+6 -7
View File
@@ -34,11 +34,10 @@ type AgentTeamResponse struct {
}
type AgentTeamScheduleResponse struct {
ID int64 `json:"id"`
TeamID int64 `json:"teamId"`
TeamName string `json:"teamName,omitempty"`
StartAt string `json:"startAt"`
EndAt string `json:"endAt"`
SourceType string `json:"sourceType"`
Remark string `json:"remark"`
ID int64 `json:"id"`
TeamID int64 `json:"teamId"`
TeamName string `json:"teamName,omitempty"`
StartAt string `json:"startAt"`
EndAt string `json:"endAt"`
Remark string `json:"remark"`
}
@@ -0,0 +1,27 @@
package response
import (
"encoding/json"
"testing"
)
func TestAgentTeamScheduleResponseOmitsSourceType(t *testing.T) {
payload, err := json.Marshal(AgentTeamScheduleResponse{
ID: 1,
TeamID: 2,
StartAt: "2026-04-29 09:00:00",
EndAt: "2026-04-29 18:00:00",
Remark: "test",
})
if err != nil {
t.Fatalf("marshal response error = %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("unmarshal response error = %v", err)
}
if _, ok := decoded["sourceType"]; ok {
t.Fatalf("sourceType should not be exposed: %s", payload)
}
}
@@ -92,7 +92,7 @@ func (s *agentTeamScheduleService) CreateAgentTeamSchedule(req request.CreateAge
if operator == nil {
return nil, errorsx.Unauthorized("未登录或登录已过期")
}
item, err := s.buildScheduleModel(0, req.TeamID, req.StartAt, req.EndAt, req.SourceType, req.Remark)
item, err := s.buildScheduleModel(0, req.TeamID, req.StartAt, req.EndAt, req.Remark)
if err != nil {
return nil, err
}
@@ -111,7 +111,7 @@ func (s *agentTeamScheduleService) UpdateAgentTeamSchedule(req request.UpdateAge
if s.Get(req.ID) == nil {
return errorsx.InvalidParam("客服组排班不存在")
}
item, err := s.buildScheduleModel(req.ID, req.TeamID, req.StartAt, req.EndAt, req.SourceType, req.Remark)
item, err := s.buildScheduleModel(req.ID, req.TeamID, req.StartAt, req.EndAt, req.Remark)
if err != nil {
return err
}
@@ -119,7 +119,6 @@ func (s *agentTeamScheduleService) UpdateAgentTeamSchedule(req request.UpdateAge
"team_id": item.TeamID,
"start_at": item.StartAt,
"end_at": item.EndAt,
"source_type": item.SourceType,
"remark": item.Remark,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
@@ -139,7 +138,7 @@ func (s *agentTeamScheduleService) DeleteAgentTeamSchedule(id int64) error {
return nil
}
func (s *agentTeamScheduleService) buildScheduleModel(id, teamID int64, startAt, endAt, sourceType, remark string) (*models.AgentTeamSchedule, error) {
func (s *agentTeamScheduleService) buildScheduleModel(id, teamID int64, startAt, endAt, remark string) (*models.AgentTeamSchedule, error) {
if teamID <= 0 {
return nil, errorsx.InvalidParam("请选择客服组")
}
@@ -150,10 +149,6 @@ func (s *agentTeamScheduleService) buildScheduleModel(id, teamID int64, startAt,
if !slices.Contains(enums.StatusValues, team.Status) {
return nil, errorsx.InvalidParam("客服组状态不合法")
}
sourceType = strings.TrimSpace(sourceType)
if sourceType == "" {
return nil, errorsx.InvalidParam("排班来源不能为空")
}
startAtValue, err := parseRequiredDateTime(startAt, "开始时间格式错误")
if err != nil {
return nil, err
@@ -179,11 +174,10 @@ func (s *agentTeamScheduleService) buildScheduleModel(id, teamID int64, startAt,
return nil, errorsx.InvalidParam("该客服组在所选时间段已存在排班")
}
return &models.AgentTeamSchedule{
TeamID: teamID,
StartAt: startAtValue,
EndAt: endAtValue,
SourceType: sourceType,
Remark: strings.TrimSpace(remark),
TeamID: teamID,
StartAt: startAtValue,
EndAt: endAtValue,
Remark: strings.TrimSpace(remark),
}, nil
}
@@ -83,10 +83,9 @@ func TestAgentTeamScheduleServiceCreateRejectsCrossDaySchedule(t *testing.T) {
tomorrow := time.Now().AddDate(0, 0, 1)
_, err := services.AgentTeamScheduleService.CreateAgentTeamSchedule(request.CreateAgentTeamScheduleRequest{
TeamID: 1,
StartAt: formatTestDateTime(tomorrow, "22:00:00"),
EndAt: formatTestDateTime(tomorrow.AddDate(0, 0, 1), "08:00:00"),
SourceType: "manual",
TeamID: 1,
StartAt: formatTestDateTime(tomorrow, "22:00:00"),
EndAt: formatTestDateTime(tomorrow.AddDate(0, 0, 1), "08:00:00"),
}, testOperator())
if err == nil {
t.Fatalf("expected cross-day schedule to fail")
@@ -102,10 +101,9 @@ func TestAgentTeamScheduleServiceCreateRejectsHistoricalScheduleByDay(t *testing
yesterday := time.Now().AddDate(0, 0, -1)
_, err := services.AgentTeamScheduleService.CreateAgentTeamSchedule(request.CreateAgentTeamScheduleRequest{
TeamID: 1,
StartAt: formatTestDateTime(yesterday, "09:00:00"),
EndAt: formatTestDateTime(yesterday, "18:00:00"),
SourceType: "manual",
TeamID: 1,
StartAt: formatTestDateTime(yesterday, "09:00:00"),
EndAt: formatTestDateTime(yesterday, "18:00:00"),
}, testOperator())
if err == nil {
t.Fatalf("expected historical schedule to fail")
@@ -121,10 +119,9 @@ func TestAgentTeamScheduleServiceCreateAllowsTodayEarlierThanCurrentTime(t *test
today := time.Now()
item, err := services.AgentTeamScheduleService.CreateAgentTeamSchedule(request.CreateAgentTeamScheduleRequest{
TeamID: 1,
StartAt: formatTestDateTime(today, "00:00:00"),
EndAt: formatTestDateTime(today, "01:00:00"),
SourceType: "manual",
TeamID: 1,
StartAt: formatTestDateTime(today, "00:00:00"),
EndAt: formatTestDateTime(today, "01:00:00"),
}, testOperator())
if err != nil {
t.Fatalf("expected today's schedule to pass, got %v", err)
@@ -143,10 +140,9 @@ func TestAgentTeamScheduleServiceUpdateRejectsCrossDaySchedule(t *testing.T) {
err := services.AgentTeamScheduleService.UpdateAgentTeamSchedule(request.UpdateAgentTeamScheduleRequest{
ID: existingID,
CreateAgentTeamScheduleRequest: request.CreateAgentTeamScheduleRequest{
TeamID: 1,
StartAt: formatTestDateTime(tomorrow, "22:00:00"),
EndAt: formatTestDateTime(tomorrow.AddDate(0, 0, 1), "08:00:00"),
SourceType: "manual",
TeamID: 1,
StartAt: formatTestDateTime(tomorrow, "22:00:00"),
EndAt: formatTestDateTime(tomorrow.AddDate(0, 0, 1), "08:00:00"),
},
}, testOperator())
if err == nil {
@@ -166,10 +162,9 @@ func TestAgentTeamScheduleServiceUpdateRejectsHistoricalScheduleByDay(t *testing
err := services.AgentTeamScheduleService.UpdateAgentTeamSchedule(request.UpdateAgentTeamScheduleRequest{
ID: existingID,
CreateAgentTeamScheduleRequest: request.CreateAgentTeamScheduleRequest{
TeamID: 1,
StartAt: formatTestDateTime(yesterday, "09:00:00"),
EndAt: formatTestDateTime(yesterday, "18:00:00"),
SourceType: "manual",
TeamID: 1,
StartAt: formatTestDateTime(yesterday, "09:00:00"),
EndAt: formatTestDateTime(yesterday, "18:00:00"),
},
}, testOperator())
if err == nil {
@@ -220,11 +215,11 @@ func createAgentTeamScheduleTestData(t *testing.T, db *gorm.DB) {
return ret
}
schedules := []models.AgentTeamSchedule{
{ID: 1, TeamID: 1, StartAt: parse("2026-04-26 20:00:00"), EndAt: parse("2026-04-27 10:00:00"), SourceType: "manual", Status: enums.StatusOk},
{ID: 2, TeamID: 1, StartAt: parse("2026-04-28 09:00:00"), EndAt: parse("2026-04-28 18:00:00"), SourceType: "manual", Status: enums.StatusOk},
{ID: 3, TeamID: 2, StartAt: parse("2026-05-03 20:00:00"), EndAt: parse("2026-05-04 08:00:00"), SourceType: "manual", Status: enums.StatusOk},
{ID: 4, TeamID: 1, StartAt: parse("2026-04-20 09:00:00"), EndAt: parse("2026-04-20 18:00:00"), SourceType: "manual", Status: enums.StatusOk},
{ID: 5, TeamID: 2, StartAt: parse("2026-05-04 09:00:00"), EndAt: parse("2026-05-04 18:00:00"), SourceType: "manual", Status: enums.StatusOk},
{ID: 1, TeamID: 1, StartAt: parse("2026-04-26 20:00:00"), EndAt: parse("2026-04-27 10:00:00"), Status: enums.StatusOk},
{ID: 2, TeamID: 1, StartAt: parse("2026-04-28 09:00:00"), EndAt: parse("2026-04-28 18:00:00"), Status: enums.StatusOk},
{ID: 3, TeamID: 2, StartAt: parse("2026-05-03 20:00:00"), EndAt: parse("2026-05-04 08:00:00"), Status: enums.StatusOk},
{ID: 4, TeamID: 1, StartAt: parse("2026-04-20 09:00:00"), EndAt: parse("2026-04-20 18:00:00"), Status: enums.StatusOk},
{ID: 5, TeamID: 2, StartAt: parse("2026-05-04 09:00:00"), EndAt: parse("2026-05-04 18:00:00"), Status: enums.StatusOk},
}
if err := db.Create(&schedules).Error; err != nil {
t.Fatalf("create schedules error = %v", err)
@@ -250,11 +245,10 @@ func createFutureAgentTeamSchedule(t *testing.T, db *gorm.DB) int64 {
t.Helper()
tomorrow := time.Now().AddDate(0, 0, 1)
item := models.AgentTeamSchedule{
TeamID: 1,
StartAt: parseTestDateTime(t, formatTestDateTime(tomorrow, "09:00:00")),
EndAt: parseTestDateTime(t, formatTestDateTime(tomorrow, "18:00:00")),
SourceType: "manual",
Status: enums.StatusOk,
TeamID: 1,
StartAt: parseTestDateTime(t, formatTestDateTime(tomorrow, "09:00:00")),
EndAt: parseTestDateTime(t, formatTestDateTime(tomorrow, "18:00:00")),
Status: enums.StatusOk,
}
if err := db.Create(&item).Error; err != nil {
t.Fatalf("create future schedule error = %v", err)
@@ -146,7 +146,6 @@ function buildMovePayload(item: AdminAgentTeamSchedule, date: string): UpdateAdm
teamId: item.teamId,
startAt: formatDateTimeValue(nextStart),
endAt: formatDateTimeValue(nextEnd),
sourceType: item.sourceType,
remark: item.remark,
}
}
@@ -177,7 +176,6 @@ function buildResizePayload(
teamId: item.teamId,
startAt: formatDateTimeValue(startAt),
endAt: formatDateTimeValue(endAt),
sourceType: item.sourceType,
remark: item.remark,
}
}
@@ -242,7 +240,6 @@ export function ScheduleCalendar({
teamId: defaultTeamID || undefined,
startAt: formatDateTimeValue(startAt),
endAt: formatDateTimeValue(endAt),
sourceType: "manual",
remark: "",
})
}
@@ -21,7 +21,6 @@ import {
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { OptionCombobox } from "@/components/option-combobox"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import {
type AdminAgentTeam,
@@ -41,17 +40,10 @@ type ScheduleEditDialogProps = {
onDelete?: (id: number) => Promise<void>
}
const sourceTypeOptions = [
{ value: "manual", label: "手工录入" },
{ value: "batch_import", label: "批量导入" },
{ value: "template_generate", label: "模板生成" },
] as const
const emptyForm: EditForm = {
teamId: "",
startAt: "",
endAt: "",
sourceType: "manual",
remark: "",
}
@@ -59,7 +51,6 @@ const editFormSchema = z.object({
teamId: z.string().trim().regex(/^\d+$/, "请选择客服组"),
startAt: z.string().trim().min(1, "开始时间不能为空"),
endAt: z.string().trim().min(1, "结束时间不能为空"),
sourceType: z.enum(["manual", "batch_import", "template_generate"], { message: "请选择排班来源" }),
remark: z.string().trim(),
}).superRefine((value, ctx) => {
const startAt = parseDateTimeLocal(value.startAt)
@@ -133,7 +124,6 @@ function buildForm(item: AdminAgentTeamSchedule | null, defaultValues?: Partial<
teamId: defaultValues?.teamId ? String(defaultValues.teamId) : emptyForm.teamId,
startAt: toDateTimeLocal(defaultValues?.startAt),
endAt: toDateTimeLocal(defaultValues?.endAt),
sourceType: (defaultValues?.sourceType as EditForm["sourceType"] | undefined) ?? emptyForm.sourceType,
remark: defaultValues?.remark ?? emptyForm.remark,
}
}
@@ -141,7 +131,6 @@ function buildForm(item: AdminAgentTeamSchedule | null, defaultValues?: Partial<
teamId: String(item.teamId),
startAt: toDateTimeLocal(item.startAt),
endAt: toDateTimeLocal(item.endAt),
sourceType: item.sourceType as EditForm["sourceType"],
remark: item.remark || "",
}
}
@@ -151,7 +140,6 @@ function buildPayload(form: EditForm): CreateAdminAgentTeamSchedulePayload {
teamId: Number(form.teamId),
startAt: form.startAt.trim(),
endAt: form.endAt.trim(),
sourceType: form.sourceType,
remark: form.remark.trim(),
}
}
@@ -298,34 +286,6 @@ function ScheduleEditDialogBody({
</FieldContent>
</Field>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field data-invalid={!!errors.sourceType}>
<FieldLabel></FieldLabel>
<FieldContent>
<Controller
control={control}
name="sourceType"
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange} modal={false}>
<SelectTrigger className="w-full">
<SelectValue>
{sourceTypeOptions.find((item) => item.value === field.value)?.label ?? "请选择来源"}
</SelectValue>
</SelectTrigger>
<SelectContent>
{sourceTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<FieldError errors={[errors.sourceType]} />
</FieldContent>
</Field>
</div>
<Field>
<FieldLabel htmlFor="agent-team-schedule-remark"></FieldLabel>
<FieldContent>
@@ -406,7 +406,6 @@ export default function DashboardAgentTeamSchedulesPage() {
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-[92px] text-right"></TableHead>
</TableRow>
</TableHeader>
@@ -428,9 +427,6 @@ export default function DashboardAgentTeamSchedulesPage() {
<div className="text-sm">{formatDateTime(item.startAt)}</div>
<div className="text-sm text-muted-foreground">{formatDateTime(item.endAt)}</div>
</TableCell>
<TableCell>
<div className="text-sm">{item.sourceType}</div>
</TableCell>
<TableCell className="text-right">
<ButtonGroup className="ml-auto">
<Button variant="outline" size="sm" onClick={() => openEditDialog(item)} disabled={isHistoricalSchedule(item)}>
@@ -459,7 +455,7 @@ export default function DashboardAgentTeamSchedulesPage() {
))}
{!loading && result.results.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="py-12 text-center text-muted-foreground">
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
</TableCell>
</TableRow>
-2
View File
@@ -510,7 +510,6 @@ export type AdminAgentTeamSchedule = {
teamName?: string
startAt: string
endAt: string
sourceType: string
remark: string
}
@@ -518,7 +517,6 @@ export type CreateAdminAgentTeamSchedulePayload = {
teamId: number
startAt: string
endAt: string
sourceType: string
remark: string
}