Files
shanxia2.0/docs/superpowers/plans/2026-08-14-duty-schedule.md
T

23 KiB
Raw Blame History

值班排班(日历 + 公开链接抢班)Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在 web-admin 提供按日月历排班(预置早/晚/值班、名额、指定系统用户、复制/关闭按日公开链接);员工打开 /#/public/duty/{token} 免登录填姓名抢班,满员拒绝;支持历史翻月查看。

Architecture: 三表 duty_day / duty_slot / duty_assignment;管理 API 挂 /api/web/admin/duty;公开 API 挂 /api/public/duty(加密排除);公开页复用 publicPlGet/publicPlPost 与 isPublicMobile 静态路由。业务规则集中在 app/services/duty_schedule.py。

Tech Stack: FastAPI + SQLAlchemy + Pydantic v2;Vue 3 + Element Plus + SCSS(Fresh Mint 管理端 / 三墙 Token 公开 H5);pytest

Spec: docs/superpowers/specs/2026-08-14-duty-schedule-design.md
UI 原型: docs/demos/duty-schedule-v1-demo.html、docs/demos/duty-h5-grab-demo.html


文件结构

操作 文件 职责
Create backend/scripts/migrate_20260814_duty_schedule.sql 建表 + 菜单
Create backend/app/models/duty_models.py ORM
Create backend/app/schemas/duty.py 请求/响应 schema
Create backend/app/services/duty_schedule.py 保存日配置、抢班、关链、序列化
Create backend/app/api/duty.py 管理端路由
Create backend/app/api/public_duty.py 公开路由
Create backend/tests/test_duty_schedule.py 名额/关链/缩容单测
Modify backend/app/main.py include_router
Modify backend/app/core/config.py API_ENCRYPT_EXCLUDE_PATHS
Create web-admin/src/api/duty.ts 管理端 API
Create web-admin/src/api/public-duty.ts 公开 API
Create web-admin/src/views/duty/schedule/index.vue 月历 + 日配置弹窗
Create web-admin/src/views/public/duty/index.vue 公开 H5
Modify web-admin/src/router/routes/staticRoutes.ts 公开路由
Modify web-admin/.env.development / .env.production(若有独立排除列表) 排除公开 path
Sync docs/sql/ 将 migrate 表结构并入基线说明或追加脚本备注

Shift 常量(全栈统一): morning / evening / duty → 文案 早班 / 晚班 / 值班。


Task 1: SQL 迁移(三表 + 菜单)

Files:

  • Create: backend/scripts/migrate_20260814_duty_schedule.sql

  • Step 1: 写迁移 SQL

-- backend/scripts/migrate_20260814_duty_schedule.sql
-- 值班排班 V1:按日链接 + 班次名额 + 报名

CREATE TABLE IF NOT EXISTS `duty_day` (
  `id` varchar(64) NOT NULL COMMENT '主键ID(UUID)',
  `duty_date` date NOT NULL COMMENT '值班日期',
  `access_token` varchar(64) NOT NULL COMMENT '公开访问令牌',
  `link_status` varchar(16) NOT NULL DEFAULT 'open' COMMENT 'open/closed',
  `tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
  `order_num` int(11) DEFAULT NULL COMMENT '排序号',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
  `create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建部门',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标志',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_token` (`access_token`),
  KEY `idx_tenant_date` (`tenant_no`, `duty_date`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='值班日(一天一条公开链接)';

CREATE TABLE IF NOT EXISTS `duty_slot` (
  `id` varchar(64) NOT NULL COMMENT '主键ID(UUID)',
  `duty_day_id` varchar(64) NOT NULL COMMENT '值班日ID',
  `shift_type` varchar(32) NOT NULL COMMENT 'morning/evening/duty',
  `capacity` int(11) NOT NULL DEFAULT 1 COMMENT '名额',
  `tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
  `order_num` int(11) DEFAULT NULL COMMENT '排序号',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
  `create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建部门',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标志',
  PRIMARY KEY (`id`),
  KEY `idx_day` (`duty_day_id`, `del_flag`),
  KEY `idx_tenant` (`tenant_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='值班班次名额';

CREATE TABLE IF NOT EXISTS `duty_assignment` (
  `id` varchar(64) NOT NULL COMMENT '主键ID(UUID)',
  `duty_slot_id` varchar(64) NOT NULL COMMENT '班次名额ID',
  `source` varchar(16) NOT NULL COMMENT 'assign/grab',
  `user_id` varchar(64) DEFAULT NULL COMMENT '系统用户ID(指定时)',
  `person_name` varchar(64) NOT NULL COMMENT '展示姓名',
  `tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
  `order_num` int(11) DEFAULT NULL COMMENT '排序号',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
  `create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建部门',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标志',
  PRIMARY KEY (`id`),
  KEY `idx_slot` (`duty_slot_id`, `del_flag`),
  KEY `idx_tenant` (`tenant_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='值班占位(指定或抢班)';

-- 菜单:挂在系统管理或独立一级均可;此处挂 Web 虚拟根下「值班排班」
-- parent_id 请按环境库 Web 根菜单 id 调整(查询 parent_id=0 且面向 Web 的根)
INSERT INTO `sys_menu` (
  `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
  `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
  `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
) VALUES (
  'DUTY000000000000000000000000001',
  '值班排班',
  '0',
  'C',
  'duty-schedule',
  '/duty/schedule/index',
  'duty:schedule:list',
  1, '1', '1', 'shanxia2.0', 90,
  '日历排班与公开抢班链接',
  'system', '', NOW(), NULL, NOW(), 0
)
ON DUPLICATE KEY UPDATE
  `menu_name`=VALUES(`menu_name`),
  `component`=VALUES(`component`),
  `perms`=VALUES(`perms`),
  `path`=VALUES(`path`);

执行前:查出现网 Web 菜单父级,将 parent_id='0' 改为正确父菜单 id(勿做成孤儿叶子;若 0 为虚拟根且项目允许顶栏入口,按门户习惯调整)。

  • Step 2: 在本地库执行迁移
cd backend && source venv/bin/activate
# 用项目惯用方式执行 SQL,例如 mysql client 指向 .env.local
mysql ... < scripts/migrate_20260814_duty_schedule.sql

Expected: 三表创建成功;菜单可见(刷新权限后)。

  • Step 3: Commit
git add backend/scripts/migrate_20260814_duty_schedule.sql
git commit -m "$(cat <<'EOF'
chore(duty): add duty_day/slot/assignment tables and menu SQL

EOF
)"

Task 2: ORM 模型

Files:

  • Create: backend/app/models/duty_models.py

  • Modify: 若项目有 models/__init__.py 导出习惯则一并导出

  • Step 1: 创建模型(对齐 bx 公共字段风格)

"""值班排班模型 — docs/superpowers/specs/2026-08-14-duty-schedule-design.md"""
from datetime import datetime

from sqlalchemy import Column, Date, DateTime, Integer, SmallInteger, String

from app.core.database import Base

SHIFT_MORNING = "morning"
SHIFT_EVENING = "evening"
SHIFT_DUTY = "duty"
SHIFT_LABELS = {
    SHIFT_MORNING: "早班",
    SHIFT_EVENING: "晚班",
    SHIFT_DUTY: "值班",
}
LINK_OPEN = "open"
LINK_CLOSED = "closed"
SOURCE_ASSIGN = "assign"
SOURCE_GRAB = "grab"


class DutyDay(Base):
    __tablename__ = "duty_day"

    id = Column(String(64), primary_key=True)
    duty_date = Column(Date, nullable=False)
    access_token = Column(String(64), nullable=False)
    link_status = Column(String(16), nullable=False, default=LINK_OPEN)
    tenant_no = Column(String(64), nullable=False, default="shanxia2.0")
    order_num = Column(Integer, nullable=True)
    remark = Column(String(500), nullable=True)
    create_by = Column(String(64), nullable=True)
    create_dept_id = Column(String(64), nullable=False, default="")
    create_time = Column(DateTime, nullable=False, default=datetime.now)
    update_by = Column(String(64), nullable=True)
    update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
    del_flag = Column(SmallInteger, nullable=False, default=0)


class DutySlot(Base):
    __tablename__ = "duty_slot"

    id = Column(String(64), primary_key=True)
    duty_day_id = Column(String(64), nullable=False)
    shift_type = Column(String(32), nullable=False)
    capacity = Column(Integer, nullable=False, default=1)
    tenant_no = Column(String(64), nullable=False, default="shanxia2.0")
    order_num = Column(Integer, nullable=True)
    remark = Column(String(500), nullable=True)
    create_by = Column(String(64), nullable=True)
    create_dept_id = Column(String(64), nullable=False, default="")
    create_time = Column(DateTime, nullable=False, default=datetime.now)
    update_by = Column(String(64), nullable=True)
    update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
    del_flag = Column(SmallInteger, nullable=False, default=0)


class DutyAssignment(Base):
    __tablename__ = "duty_assignment"

    id = Column(String(64), primary_key=True)
    duty_slot_id = Column(String(64), nullable=False)
    source = Column(String(16), nullable=False)
    user_id = Column(String(64), nullable=True)
    person_name = Column(String(64), nullable=False)
    tenant_no = Column(String(64), nullable=False, default="shanxia2.0")
    order_num = Column(Integer, nullable=True)
    remark = Column(String(500), nullable=True)
    create_by = Column(String(64), nullable=True)
    create_dept_id = Column(String(64), nullable=False, default="")
    create_time = Column(DateTime, nullable=False, default=datetime.now)
    update_by = Column(String(64), nullable=True)
    update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
    del_flag = Column(SmallInteger, nullable=False, default=0)
  • Step 2: Commit
git add backend/app/models/duty_models.py
git commit -m "$(cat <<'EOF'
feat(duty): add SQLAlchemy models for duty schedule

EOF
)"

Task 3: Schemas + 领域服务(含 TDD)

Files:

  • Create: backend/app/schemas/duty.py

  • Create: backend/app/services/duty_schedule.py

  • Create: backend/tests/test_duty_schedule.py

  • Step 1: 写失败单测(纯函数/可测逻辑优先)

在 test_duty_schedule.py 先测容量校验与姓名规范化(不依赖 DB 的部分),以及用 SQLite/fixture 测 grab 满员(若项目测试多用 mock Session,则测 service 内 _assert_capacity / normalize_person_name)。

最小必测:

import pytest
from app.services.duty_schedule import normalize_person_name, SHIFT_LABELS
from app.models.duty_models import SHIFT_MORNING


def test_normalize_person_name_ok():
    assert normalize_person_name("  张三  ") == "张三"


def test_normalize_person_name_empty():
    with pytest.raises(ValueError, match="姓名"):
        normalize_person_name("   ")


def test_normalize_person_name_too_long():
    with pytest.raises(ValueError, match="姓名"):
        normalize_person_name("啊" * 21)


def test_shift_labels():
    assert SHIFT_LABELS[SHIFT_MORNING] == "早班"
  • Step 2: 跑测确认失败(函数尚未存在)
cd backend && source venv/bin/activate && pytest tests/test_duty_schedule.py -v

Expected: FAIL import / not defined

  • Step 3: 实现 schemas
# backend/app/schemas/duty.py
from __future__ import annotations
from datetime import date
from typing import List, Literal, Optional
from pydantic import BaseModel, Field

ShiftType = Literal["morning", "evening", "duty"]
LinkStatus = Literal["open", "closed"]
SourceType = Literal["assign", "grab"]


class DutyAssignmentIn(BaseModel):
    source: SourceType = "assign"
    userId: Optional[str] = None
    personName: str = ""


class DutySlotSaveIn(BaseModel):
    shiftType: ShiftType
    capacity: int = Field(ge=1, le=99)
    assignments: List[DutyAssignmentIn] = []


class DutyDaySaveBody(BaseModel):
    dutyDate: date
    slots: List[DutySlotSaveIn]
    linkStatus: Optional[LinkStatus] = None


class DutyGrabBody(BaseModel):
    shiftType: ShiftType
    personName: str
  • Step 4: 实现 service 核心

duty_schedule.py 必须提供:

  • normalize_person_name(name: str) -> str
  • new_access_token() -> str(secrets.token_urlsafe(24),截断到 ≤64)
  • build_public_url(token: str) -> str → f"{settings.PUBLIC_WEB_ORIGIN.rstrip('/')}/#/public/duty/{token}"
  • get_month_summary(db, tenant_no, year, month) -> list
  • get_day_detail(db, tenant_no, duty_date|day_id) -> dict
  • save_day(db, tenant_no, user, body: DutyDaySaveBody) -> dict
    • 校验:每个 slot len(assignments) <= capacity;停用已有报名的 shift 须先清空
    • upsert day(按 tenant+date);token 仅首次生成
    • soft-delete 去掉的 slots(无报名时)与多余 assignments
  • set_link_status(db, tenant_no, day_id, status)
  • soft_delete_assignment(db, tenant_no, assignment_id)
  • get_public_page(db, token) -> dict(无效 404 语义)
  • grab(db, token, shift_type, person_name) -> dict
    • 关链拒绝;姓名规范化;with_for_update 锁 slot 行或 day 行;count < capacity 再 insert source=grab

序列化字段用 camelCase,与前端一致:dutyDate, linkStatus, accessToken, publicUrl, shiftType, shiftLabel, capacity, taken, assignments: [{ id, source, userId, personName }]。

  • Step 5: 补充 DB 级单测(可用现有测试 DB fixture;若无 fixture,则 mock Session 测分支)

至少覆盖:

  1. grab 在 capacity=1 时第二次失败
  2. link_status=closed 时 grab 失败
  3. save_day 缩容小于已报名人数抛错
  • Step 6: 跑通测试
cd backend && source venv/bin/activate && pytest tests/test_duty_schedule.py -v

Expected: PASS

  • Step 7: Commit
git add backend/app/schemas/duty.py backend/app/services/duty_schedule.py backend/tests/test_duty_schedule.py
git commit -m "$(cat <<'EOF'
feat(duty): add schemas and duty_schedule service with capacity tests

EOF
)"

Task 4: 管理端 + 公开 API 路由注册

Files:

  • Create: backend/app/api/duty.py

  • Create: backend/app/api/public_duty.py

  • Modify: backend/app/main.py

  • Modify: backend/app/core/config.py

  • Step 1: 管理路由 prefix="/web/admin/duty"

# 关键端点(均 Depends 现有 admin 鉴权,如 require_admin_or_super_admin / 菜单守卫)
GET  ""?year=&month=          -> month summary
GET  "/day?dutyDate=YYYY-MM-DD" -> day detail(无则返回空骨架 slots=[])
POST "/day"                   -> save_day body
POST "/day/{dayId}/link"      -> { linkStatus }
DELETE "/assignments/{id}"    -> soft delete

鉴权用户取 tenant_no、user_id、create_dept_id(与 leave 一致)。指定用户时用现有用户查询解析姓名写入 person_name。

  • Step 2: 公开路由 prefix="/public/duty"
GET  "/{token}"      -> get_public_page;404 文案「链接无效或已失效」
POST "/{token}/grab" -> DutyGrabBody;业务错误用 error_response / HTTP 400
  • Step 3: main.py 注册
from app.api.duty import router as duty_router
from app.api.public_duty import router as public_duty_router
# ...
app.include_router(duty_router, prefix=settings.API_V1_PREFIX)
app.include_router(public_duty_router, prefix=settings.API_V1_PREFIX)
  • Step 4: 加密排除

在 API_ENCRYPT_EXCLUDE_PATHS 追加:

"/api/public/duty,"

检查 web-admin/.env.development 与 .env.production 的 VITE_API_ENCRYPT_EXCLUDE_PATHS,若存在则同样追加 /api/public/duty。

  • Step 5: 手工用 curl/Swagger 冒烟
# 登录后 POST /api/web/admin/duty/day 创建某日
# GET /api/public/duty/{token}
# POST /api/public/duty/{token}/grab  {"shiftType":"evening","personName":"测试"}
  • Step 6: Commit
git add backend/app/api/duty.py backend/app/api/public_duty.py backend/app/main.py backend/app/core/config.py web-admin/.env.development web-admin/.env.production
git commit -m "$(cat <<'EOF'
feat(duty): register admin and public duty APIs with encrypt exclude

EOF
)"

Task 5: web-admin 管理端月历页

Files:

  • Create: web-admin/src/api/duty.ts

  • Create: web-admin/src/views/duty/schedule/index.vue

  • 菜单 component 已指向 /duty/schedule/index(动态路由按现有菜单加载)

  • Step 1: API 模块

// web-admin/src/api/duty.ts
import request from '@/utils/http'

export type ShiftType = 'morning' | 'evening' | 'duty'

export const fetchDutyMonth = (year: number, month: number) =>
  request.get({ url: '/web/admin/duty', params: { year, month } })

export const fetchDutyDay = (dutyDate: string) =>
  request.get({ url: '/web/admin/duty/day', params: { dutyDate } })

export const saveDutyDay = (data: unknown) =>
  request.post({ url: '/web/admin/duty/day', data })

export const setDutyLinkStatus = (dayId: string, linkStatus: 'open' | 'closed') =>
  request.post({ url: `/web/admin/duty/day/${dayId}/link`, data: { linkStatus } })

export const deleteDutyAssignment = (id: string) =>
  request.del({ url: `/web/admin/duty/assignments/${id}` })

(若项目 request 方法名是 delete 而非 del,与邻近 API 文件保持一致。)

  • Step 2: 月历页

实现要求(对齐 docs/demos/duty-schedule-v1-demo.html + Fresh Mint):

  • panel-top + h2.title-bar-heading「值班排班」
  • 月切换、今天
  • 7 列日历网格(可用纯 CSS/table,不必上 FullCalendar)
  • 日格展示 chip:早班/晚班/值班颜色区分(管理端可用琥珀/蓝/红浅底,勿用小程序主色当 primary 按钮)
  • 点击日期打开 el-dialog:label-position="top",modal-class 无 blur,dialog-form-layout
  • 弹窗内三个可启用班次:capacity + 系统用户选择(复用现有用户远程搜索/选择组件,参考 system/user 或其它选人控件)+ 已安排 tags(指定/抢班)可删
  • 复制 publicUrl;开关链接按钮
  • 保存调用 saveDutyDay;缩容错误展示后端 message

按钮均带 <el-icon>。

  • Step 3: 本地 pnpm dev 打开菜单验证月历加载与保存

  • Step 4: Commit

git add web-admin/src/api/duty.ts web-admin/src/views/duty/schedule/index.vue
git commit -m "$(cat <<'EOF'
feat(duty): add web-admin calendar schedule page

EOF
)"

Task 6: 公开 H5 页

Files:

  • Create: web-admin/src/api/public-duty.ts

  • Create: web-admin/src/views/public/duty/index.vue

  • Modify: web-admin/src/router/routes/staticRoutes.ts

  • Step 1: 静态路由(插在其它 public 路由旁)

{
  path: '/public/duty/:token',
  name: 'PublicDutyGrab',
  component: () => import('@views/public/duty/index.vue'),
  meta: { title: '值班抢班', isHideTab: true, isPublicMobile: true }
},
  • Step 2: 公开 API
import { publicPlGet, publicPlPost } from '@/utils/http/public-pl'

export const publicDutyApi = {
  getPage: (token: string) => publicPlGet<any>(`/public/duty/${token}`),
  grab: (token: string, data: { shiftType: string; personName: string }) =>
    publicPlPost<any>(`/public/duty/${token}/grab`, data)
}
  • Step 3: H5 页面

对齐 docs/demos/duty-h5-grab-demo.html:

  • 三墙色:#0a9f6d / #087f57 / #e9f9f2 / #f5f8f7
  • 状态:加载中 / 无效 / 已关闭 / 可抢列表 / 成功摘要
  • 班次独立卡片;满员禁用;可抢输入姓名(max 20)提交
  • onMounted 读 route.params.token

参考 web-admin/src/views/public/pl/transport/index.vue 的公开页壳与错误处理。

  • Step 4: 端到端手测
  1. 管理端建日 → 复制链接 → 无痕打开 H5
  2. 抢晚班成功 → 再抢同班次满员失败
  3. 关链 → H5 显示关闭
  4. 管理端删人 → 名额释放可再抢
  • Step 5: Commit
git add web-admin/src/api/public-duty.ts web-admin/src/views/public/duty/index.vue web-admin/src/router/routes/staticRoutes.ts
git commit -m "$(cat <<'EOF'
feat(duty): add public H5 grab page for daily duty links

EOF
)"

Task 7: 收尾与图谱

Files:

  • 按需更新 docs/sql/ 备注或拷贝 migrate 说明

  • 跑 ./scripts/update-graph.sh backend 与 frontend(或 graphify update .)

  • Step 1: 对照 spec 验收清单勾选(设计文档「测试要点」7 条)

  • Step 2: 更新知识图谱

./scripts/update-graph.sh all
# 或
graphify update .
  • Step 3: 最终 commit(若有 docs/sql 或 graph 变更)
git add docs/sql backend/graphify-out web-admin/graphify-out graphify-out
git commit -m "$(cat <<'EOF'
chore(duty): sync sql notes and knowledge graph after duty schedule

EOF
)"

Spec 覆盖自检

Spec 项 Task
D1 web + 公开 H5 5, 6
D2 一天一链接 1–4 save_day token
D3 班次独立罗列 6
D4 只填姓名 3 grab / 6
D5 系统用户指定 4–5
D6/D7 员工不可取消、经理可改 4 delete/save/link
D8 无统计页 5 仅日历
D9 预置班次 models 常量
D10 按天建 5 点日配置
D11 同日多班次 grab 不按人跨 slot 限制
D12 token 不重置 save_day
D13 缩容校验 service + test
D14 历史可改 无冻结逻辑
D15 菜单权限 migrate menu
加密排除 Task 4
并发满员 grab + test

V1 限流: 若无现成中间件,可在 grab 内不做强限流,验收文档注明「后续可加」;不阻塞主路径(YAGNI)。若已有简易 IP 限流工具则挂上。


执行方式

Plan 已保存。可选:

  1. Subagent-Driven(推荐) — 每任务新开子代理,任务间审查
  2. Inline Execution — 本会话按 executing-plans 连续推进并设检查点

请选择一种,或指出计划中要先改的部分。