2026-07-30-oa-confirmation-kit.md 18 KB

OA Confirmation Kit(转正套件)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.

Status: Tasks 1–6 implemented on master (2026-07-30).

Goal: 补齐用户入职日期/试用期主数据;在审批套件 Tab 增加转正套件(9 子字段、可显隐、发起默认、转正日联动);审批全部通过后回写发起人六项主数据。

Architecture: 与请假套件相同:FormCreate confirmationKit 布局容器 + 子字段拼装;语义用 props._fc_kit_field。转正日用纯函数 addMonthsYmd(TS/Python 对齐),手改标记 formData['__manual_' + confirmationDateField]。实例 status 变为 completed 时调用 apply_confirmation_kit_user_sync。

Tech Stack: Vue 3 · @form-create/designer · Element Plus · FastAPI · SQLAlchemy · pytest · Node node:test

Spec: docs/superpowers/specs/2026-07-30-oa-confirmation-kit-design.md
参考: docs/superpowers/plans/2026-07-30-oa-leave-kit.md、oa-owner-fields


File map

Path Responsibility
Create: docs/sql/sys_user_hire_probation.sql ALTER TABLE 增列
Modify: backend/app/models/models.py SysUser.hire_date / probation_months
Modify: backend/app/crud/user.py _row_to_dict / create / update
Modify: backend/app/api/admin.py UserCreateBody / UserUpdateBody + create/update 传参
Modify: backend/app/api/public.py _build_user_info_payload 带出 hireDate / probationMonths
Create: web-admin/src/utils/oa-confirmation-date.ts addMonthsYmd
Create: web-admin/src/utils/oa-confirmation-date.test.ts 前端算法单测
Create: backend/app/services/oa_confirmation_date.py 同算法
Create: backend/tests/test_oa_confirmation_date.py pytest
Create: backend/app/services/oa_confirmation_kit_sync.py 通过后回写
Create: backend/tests/test_oa_confirmation_kit_sync.py 回写单测
Modify: backend/app/services/oa_publish.py LAYOUT_TYPES + confirmationKit
Modify: backend/app/services/oa_engine.py completed 时调用 sync
Create: web-admin/src/components/oa-approval/OaConfirmationKit.vue 容器(设计器虚线框;运行时无框)
Create: web-admin/src/utils/oa-confirmation-kit-rule.ts DragRule + 9 子字段
Create: web-admin/src/utils/oa-form-create-confirmation-kit.ts 注册(kit menu 已存在则勿重复 addMenu)
Create: web-admin/src/utils/oa-confirmation-kit-runtime.ts 默认值、联动、显隐归一、manual 标记
Modify: web-admin/src/main.ts / oa-fc-host/main.ts setupOaConfirmationKit
Modify: web-admin/src/views/oa-approval/start/index.vue 默认 + 联动
Modify: web-admin/src/views/system/user/index.vue 编辑入职/试用期
Modify: Spec 状态 → 实现中/已实现

Task 1: addMonthsYmd 算法(TDD,前后端)

Files:

  • Create: web-admin/src/utils/oa-confirmation-date.ts
  • Create: web-admin/src/utils/oa-confirmation-date.test.ts
  • Create: backend/app/services/oa_confirmation_date.py
  • Create: backend/tests/test_oa_confirmation_date.py

  • [ ] Step 1: 写前端失败测试

// web-admin/src/utils/oa-confirmation-date.test.ts
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { addMonthsYmd } from './oa-confirmation-date'

describe('addMonthsYmd', () => {
  it('simple month add', () => {
    assert.equal(addMonthsYmd('2026-01-15', 3), '2026-04-15')
  })

  it('clamp end of month', () => {
    assert.equal(addMonthsYmd('2026-01-31', 1), '2026-02-28')
  })

  it('zero months returns same day', () => {
    assert.equal(addMonthsYmd('2026-07-30', 0), '2026-07-30')
  })

  it('invalid => null', () => {
    assert.equal(addMonthsYmd('', 3), null)
    assert.equal(addMonthsYmd('2026-01-15', -1), null)
  })
})
  • Step 2: 运行 — expect FAIL
cd web-admin && pnpm exec tsx --test src/utils/oa-confirmation-date.test.ts
  • Step 3: 实现前端
// web-admin/src/utils/oa-confirmation-date.ts
/** 入职日 + 试用期月数 → YYYY-MM-DD;非法返回 null。按自然月加,日溢出钳到目标月最后一天。 */
export function addMonthsYmd(hireYmd: string, months: number): string | null {
  const s = String(hireYmd || '').trim()
  if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null
  if (!Number.isInteger(months) || months < 0) return null
  const [y, m, d] = s.split('-').map((x) => Number(x))
  if (!y || !m || !d) return null
  const total = y * 12 + (m - 1) + months
  const ny = Math.floor(total / 12)
  const nm = (total % 12) + 1
  const lastDay = new Date(ny, nm, 0).getDate()
  const nd = Math.min(d, lastDay)
  return `${ny}-${String(nm).padStart(2, '0')}-${String(nd).padStart(2, '0')}`
}
  • [ ] Step 4: 前端测试 PASS

  • [ ] Step 5: 后端同测 + 实现

# backend/app/services/oa_confirmation_date.py
from __future__ import annotations
import calendar
import re
from datetime import date
from typing import Optional

_YMD = re.compile(r"^(\d{4})-(\d{2})-(\d{2})$")

def add_months_ymd(hire_ymd: str, months: int) -> Optional[str]:
    s = (hire_ymd or "").strip()
    m = _YMD.match(s)
    if not m:
        return None
    if not isinstance(months, int) or isinstance(months, bool) or months < 0:
        return None
    y, mo, d = int(m.group(1)), int(m.group(2)), int(m.group(3))
    try:
        date(y, mo, d)
    except ValueError:
        return None
    total = y * 12 + (mo - 1) + months
    ny, nm = divmod(total, 12)
    nm += 1
    last = calendar.monthrange(ny, nm)[1]
    nd = min(d, last)
    return f"{ny:04d}-{nm:02d}-{nd:02d}"
cd backend && source venv/bin/activate && pytest tests/test_oa_confirmation_date.py -v
  • Step 6: Commit(仅当用户要求时)
git add web-admin/src/utils/oa-confirmation-date.ts \
  web-admin/src/utils/oa-confirmation-date.test.ts \
  backend/app/services/oa_confirmation_date.py \
  backend/tests/test_oa_confirmation_date.py
git commit -m "feat(oa): add confirmation date month-add helper for 转正套件"

Task 2: sys_user 入职日期 / 试用期 + 用户管理 + userInfo

Files:

  • Create: docs/sql/sys_user_hire_probation.sql
  • Modify: backend/app/models/models.py(SysUser 在 phone_no 后增列)
  • Modify: backend/app/crud/user.py
  • Modify: backend/app/api/admin.py
  • Modify: backend/app/api/public.py
  • Modify: web-admin/src/views/system/user/index.vue

  • [ ] Step 1: SQL 增量

-- docs/sql/sys_user_hire_probation.sql
ALTER TABLE `sys_user`
  ADD COLUMN `hire_date` date DEFAULT NULL COMMENT '入职日期' AFTER `phone_no`,
  ADD COLUMN `probation_months` int(11) DEFAULT NULL COMMENT '试用期月数' AFTER `hire_date`;

在本地/测试库执行该脚本后再跑后端。

  • Step 2: Model

在 SysUser 增加:

hire_date = Column(Date, nullable=True, comment="入职日期")
probation_months = Column(Integer, nullable=True, comment="试用期月数")

(确认 Date / Integer 已从 sqlalchemy 导入。)

  • Step 3: _row_to_dict + create/update

_row_to_dict 增加:

"hireDate": u.hire_date.strftime("%Y-%m-%d") if getattr(u, "hire_date", None) else "",
"probationMonths": u.probation_months if getattr(u, "probation_months", None) is not None else None,

create_user / update_user 增加可选参数 hire_date、probation_months 并写入列。
update_user 约定:未传不改;传入 null/空字符串清空;传入整数更新(含 0)。

  • Step 4: API Body

UserCreateBody / UserUpdateBody:

hire_date: Optional[str] = Field(None, alias="hireDate")  # YYYY-MM-DD
probation_months: Optional[int] = Field(None, alias="probationMonths")

create/update 路由把字段传入 crud;解析日期字符串为 date 或 None。

  • Step 5: userInfo

_build_user_info_payload 至少增加:

"hireDate": user.hire_date.strftime("%Y-%m-%d") if getattr(user, "hire_date", None) else None,
"probationMonths": getattr(user, "probation_months", None),

Runtime 映射约定(锁定):

kit 语义 userInfo 字段
name realName
email email
phone phone
dept regionId + 部门树全路径(复用 resolveCurrentOwnerDept)
hireDate hireDate
probationMonths probationMonths
  • Step 6: 用户管理 UI

在 web-admin/src/views/system/user/index.vue 新增/编辑弹窗(form-grid-2)增加:

  • 入职日期:el-date-picker,value-format="YYYY-MM-DD",v-model="formData.hireDate"
  • 试用期(月):el-input-number,:min="0",:precision="0",v-model="formData.probationMonths"

formData 初始化 / reset / 打开编辑赋值 / submit payload 带上两字段。
查看详情增加两项(试用期展示为 N 个月 或 -)。

列表列:首期不加(符合 spec §4.2 最小范围)。

  • [ ] Step 7: 手测 用户管理保存后再登录,确认 userInfo 有新字段

  • [ ] Step 8: Commit(仅当用户要求时)

git commit -m "feat(user): add hire_date and probation_months to sys_user and user admin"

Task 3: FormCreate 转正套件(容器 + DragRule + 注册 + 发布白名单)

Files:

  • Create: web-admin/src/components/oa-approval/OaConfirmationKit.vue
  • Create: web-admin/src/utils/oa-confirmation-kit-rule.ts
  • Create: web-admin/src/utils/oa-form-create-confirmation-kit.ts
  • Modify: web-admin/src/main.ts、web-admin/src/oa-fc-host/main.ts
  • Modify: backend/app/services/oa_publish.py

  • [ ] Step 1: 容器组件

对齐 OaLeaveKit.vue:设计器显示虚线框 +「转正套件」标题 + <slot />;运行时(非 designer)仅渲染 slot、无边框。

  • Step 2: DragRule

oa-confirmation-kit-rule.ts 要点:

  • menu: 'kit',label: '转正',name: 'confirmationKit',input: false,inside: true
  • uniqueId() 与请假相同
  • 每个 child:props._fc_kit_field 为语义 key
  • 容器 props 含 visibleFields(九项默认 true)以及各 *Field id 便于 runtime

子字段类型:

_fc_kit_field type 关键 props
hireDate datePicker type=date, valueFormat=YYYY-MM-DD, $required
probationMonths inputNumber min=0, precision=0, $required
confirmationDate datePicker type=date, valueFormat=YYYY-MM-DD
post post multiple=true
jobLevel input
name input $required
email input
dept dept multiple=false, $required
phone input

设计器容器属性:9 个「显示:xxx」开关;文案提示:

隐藏字段仍会写入默认值;审批通过后可能回写用户主数据。

显隐 source of truth: props.visibleFields;runtime ensureConfirmationKitVisibility 同步到各 child hidden。

  • Step 3: setup
// oa-form-create-confirmation-kit.ts
export function setupOaConfirmationKit(formCreate?: { component?: (n: string, c: unknown) => void }) {
  try {
    FcDesigner.addMenu({ name: 'kit', title: '套件', list: [] })
  } catch {
    /* kit menu may already exist from leave kit */
  }
  FcDesigner.component('confirmationKit', OaConfirmationKit)
  formCreate?.component?.('confirmationKit', OaConfirmationKit)
  FcDesigner.addDragRule(confirmationKitDragRule)
}

在 main.ts / oa-fc-host/main.ts 于 setupOaLeaveKit 后调用 setupOaConfirmationKit(...)。

  • Step 4: 发布白名单

oa_publish.py 的 LAYOUT_TYPES 增加 "confirmationKit"。

  • [ ] Step 5: 手测 设计器套件 Tab 出现「转正」,拖入可见 9 字段;切换显隐后 schema 中 visibleFields / child hidden 正确。

  • [ ] Step 6: Commit(仅当用户要求时)


Task 4: 发起页默认值 + 转正日联动

Files:

  • Create: web-admin/src/utils/oa-confirmation-kit-runtime.ts
  • Modify: web-admin/src/views/oa-approval/start/index.vue
  • Modify: web-admin/src/views/oa-approval/instances/detail.vue(仅 ensureConfirmationKitVisibility,不写默认)

  • [ ] Step 1: Runtime API

导出:

export const CONFIRMATION_MANUAL_PREFIX = '__manual_'

export type ConfirmationKitBinding = {
  fields: Record<string, string> // semantic -> field id
  visibleFields: Record<string, boolean>
}

export function listConfirmationKitBindings(schema: unknown): ConfirmationKitBinding[]
export function ensureConfirmationKitVisibility(schema: unknown): void
export function applyConfirmationKitDefaults(
  schema: unknown,
  formData: Record<string, unknown>,
  user: Record<string, unknown>,
  deptValue: { id: string; name: string } | null
): Record<string, unknown>
export function applyConfirmationKitDateLinkage(
  schema: unknown,
  formData: Record<string, unknown>
): Record<string, unknown>
export function markConfirmationDateManual(
  formData: Record<string, unknown>,
  confirmationDateField: string
): void
export function clearConfirmationDateManualAndRelink(
  schema: unknown,
  formData: Record<string, unknown>,
  confirmationDateField: string
): Record<string, unknown>

行为要点:

  • 默认仅填空;post / jobLevel 不填;隐藏字段也填默认
  • 若 confirmationDate 空且可算则写入 addMonthsYmd
  • 若 formData[CONFIRMATION_MANUAL_PREFIX + confirmationDateField] 为真则不联动
  • 手改转正日:值与当前自动结果不同时打 manual 标记(对齐请假 __manual_)

部门默认:复用 resolveCurrentOwnerDept(user)(oa-owner-field-defaults.ts)。

  • Step 2: 接入 start/index.vue

在 applyOwnerFieldDefaults 之后:

  1. ensureConfirmationKitVisibility(schema)
  2. formData = applyConfirmationKitDefaults(...)
  3. formData = applyConfirmationKitDateLinkage(...)

watch formData:hire/probation 变化 → linkage;confirmationDate 手改 → mark manual。

「恢复自动计算」最小实现:发起页检测到任一 __manual_<confirmationDateField> 时,在表单上方显示链接「恢复转正日期自动计算」。

  • [ ] Step 3: detail.vue 加载 schema 后调用 ensureConfirmationKitVisibility;不调用 defaults/linkage。

  • [ ] Step 4: 手测

  1. 有入职/试用期 → 自动填六项 + 建议转正日;职位空
  2. 改试用期 → 转正日变;手改转正日 → 再改试用期不变
  3. 恢复自动 → 再联动
  • Step 5: Commit(仅当用户要求时)

Task 5: 审批通过后回写发起人

Files:

  • Create: backend/app/services/oa_confirmation_kit_sync.py
  • Create: backend/tests/test_oa_confirmation_kit_sync.py
  • Modify: backend/app/services/oa_engine.py

  • [ ] Step 1: 写失败测试

覆盖:有值更新六列;空跳过;非法手机号 skip+不抛;无 kit noop。

  • Step 2: 实现 sync
# backend/app/services/oa_confirmation_kit_sync.py
WRITEBACK_KEYS = {
    "hireDate": "hire_date",
    "probationMonths": "probation_months",
    "name": "nick_name",
    "email": "email",
    "dept": "dept_id",
    "phone": "phone_no",
}

def apply_confirmation_kit_user_sync(db: Session, inst) -> None:
    """扫描 confirmationKit,有值则更新发起人 sys_user;异常只打日志。"""
    # 1. schema + form_data(对照 OaInstance / OaAppVersion 实际字段名)
    # 2. walk type==confirmationKit,children props._fc_kit_field → field
    # 3. 发起人:tenant_no + initiator id,del_flag=0
    # 4. dept 取 dict['id'];hire_date 解析为 date;probation_months int
    # 5. phone: ^1[3-9]\\d{9}$;email 含 @;失败 skip 该列 + logger.warning
    # 6. db.add(user);不自行 commit
    # 7. try/except 整函数,不向上抛

实现前用模型核对:OaInstance 的发起人字段、form_data、关联 version schema 取法(可对照 start_instance / 详情序列化已有代码)。

  • Step 3: 挂钩
rg -n 'status = "completed"' backend/app/services/oa_engine.py

每一处赋值 completed 后:

try:
    from app.services.oa_confirmation_kit_sync import apply_confirmation_kit_user_sync
    apply_confirmation_kit_user_sync(db, inst)
except Exception:
    logger.exception("confirmation kit user sync failed instance=%s", getattr(inst, "id", None))
  • [ ] Step 4: pytest PASS

  • [ ] Step 5: 手测 发起→通过→用户管理六项已更新;驳回不更新。

  • [ ] Step 6: Commit(仅当用户要求时)

git commit -m "feat(oa): sync initiator user from confirmation kit on approval complete"

Task 6: 收尾(打印、图谱、spec)

  • [ ] Step 1: 打印路径核对:若 confirmationKit 被跳过导致子字段丢失,对照 leaveKit 打印 unwrap 补一层。

  • [ ] Step 2: Spec 状态改为「已实现」,链接本 plan。

  • [ ] Step 3:

./scripts/update-graph.sh all
  • Step 4: 按 spec §9 验收清单手测勾选。

Spec coverage

Spec 项 Task
hire_date / probation_months + 用户管理 T2
userInfo 带出 T2
confirmationKit + 9 子字段 T3
显示/隐藏 + 隐藏仍默认/回写 T3 + T4 + T5
发起默认(职位不默认) T4
转正日自动建议 + 手改断开 + 恢复 T1 + T4
通过后回写六项、空跳过、失败不回滚 T5
职位多选 / 部门 {id,name} T3
发布白名单 T3
列表不加列 T2 明确不做

Self-review

  • Spec 各节均有对应 Task;无 TBD/“类似 Task N”。
  • 手改标记前缀与请假套件一致:__manual_。
  • Commit 步骤默认「用户要求时」执行。

Execution

Plan complete and saved to docs/superpowers/plans/2026-07-30-oa-confirmation-kit.md.

Two execution options:

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

Which approach?