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
| 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 状态 → 实现中/已实现 |
addMonthsYmd 算法(TDD,前后端)Files:
web-admin/src/utils/oa-confirmation-date.tsweb-admin/src/utils/oa-confirmation-date.test.tsbackend/app/services/oa_confirmation_date.pyCreate: 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)
})
})
cd web-admin && pnpm exec tsx --test src/utils/oa-confirmation-date.test.ts
// 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
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 转正套件"
sys_user 入职日期 / 试用期 + 用户管理 + userInfoFiles:
docs/sql/sys_user_hire_probation.sqlbackend/app/models/models.py(SysUser 在 phone_no 后增列)backend/app/crud/user.pybackend/app/api/admin.pybackend/app/api/public.pyModify: 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`;
在本地/测试库执行该脚本后再跑后端。
在 SysUser 增加:
hire_date = Column(Date, nullable=True, comment="入职日期")
probation_months = Column(Integer, nullable=True, comment="试用期月数")
(确认 Date / Integer 已从 sqlalchemy 导入。)
_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)。
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。
_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 |
|
| phone | phone |
| dept | regionId + 部门树全路径(复用 resolveCurrentOwnerDept) |
| hireDate | hireDate |
| probationMonths | probationMonths |
在 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"
Files:
web-admin/src/components/oa-approval/OaConfirmationKit.vueweb-admin/src/utils/oa-confirmation-kit-rule.tsweb-admin/src/utils/oa-form-create-confirmation-kit.tsweb-admin/src/main.ts、web-admin/src/oa-fc-host/main.tsModify: backend/app/services/oa_publish.py
[ ] Step 1: 容器组件
对齐 OaLeaveKit.vue:设计器显示虚线框 +「转正套件」标题 + <slot />;运行时(非 designer)仅渲染 slot、无边框。
oa-confirmation-kit-rule.ts 要点:
menu: 'kit',label: '转正',name: 'confirmationKit',input: false,inside: trueuniqueId() 与请假相同props._fc_kit_field 为语义 keyprops 含 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 |
| input | ||
| dept | dept | multiple=false, $required |
| phone | input |
设计器容器属性:9 个「显示:xxx」开关;文案提示:
隐藏字段仍会写入默认值;审批通过后可能回写用户主数据。
显隐 source of truth: props.visibleFields;runtime ensureConfirmationKitVisibility 同步到各 child hidden。
// 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(...)。
oa_publish.py 的 LAYOUT_TYPES 增加 "confirmationKit"。
[ ] Step 5: 手测 设计器套件 Tab 出现「转正」,拖入可见 9 字段;切换显隐后 schema 中 visibleFields / child hidden 正确。
[ ] Step 6: Commit(仅当用户要求时)
Files:
web-admin/src/utils/oa-confirmation-kit-runtime.tsweb-admin/src/views/oa-approval/start/index.vueModify: 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 空且可算则写入 addMonthsYmdformData[CONFIRMATION_MANUAL_PREFIX + confirmationDateField] 为真则不联动__manual_)部门默认:复用 resolveCurrentOwnerDept(user)(oa-owner-field-defaults.ts)。
在 applyOwnerFieldDefaults 之后:
ensureConfirmationKitVisibility(schema)formData = applyConfirmationKitDefaults(...)formData = applyConfirmationKitDateLinkage(...)watch formData:hire/probation 变化 → linkage;confirmationDate 手改 → mark manual。
「恢复自动计算」最小实现:发起页检测到任一 __manual_<confirmationDateField> 时,在表单上方显示链接「恢复转正日期自动计算」。
[ ] Step 3: detail.vue 加载 schema 后调用 ensureConfirmationKitVisibility;不调用 defaults/linkage。
[ ] Step 4: 手测
Files:
backend/app/services/oa_confirmation_kit_sync.pybackend/tests/test_oa_confirmation_kit_sync.pyModify: backend/app/services/oa_engine.py
[ ] Step 1: 写失败测试
覆盖:有值更新六列;空跳过;非法手机号 skip+不抛;无 kit noop。
# 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 / 详情序列化已有代码)。
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"
[ ] Step 1: 打印路径核对:若 confirmationKit 被跳过导致子字段丢失,对照 leaveKit 打印 unwrap 补一层。
[ ] Step 2: Spec 状态改为「已实现」,链接本 plan。
[ ] Step 3:
./scripts/update-graph.sh all
| Spec 项 | Task |
|---|---|
| hire_date / probation_months + 用户管理 | T2 |
| userInfo 带出 | T2 |
| confirmationKit + 9 子字段 | T3 |
| 显示/隐藏 + 隐藏仍默认/回写 | T3 + T4 + T5 |
| 发起默认(职位不默认) | T4 |
| 转正日自动建议 + 手改断开 + 恢复 | T1 + T4 |
| 通过后回写六项、空跳过、失败不回滚 | T5 |
| 职位多选 / 部门 {id,name} | T3 |
| 发布白名单 | T3 |
| 列表不加列 | T2 明确不做 |
__manual_。Plan complete and saved to docs/superpowers/plans/2026-07-30-oa-confirmation-kit.md.
Two execution options:
Which approach?