feat(oa): add confirmation date month-add helper for 转正套件
Also add the confirmation kit implementation plan. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
c31ef0e809
commit
b1b15da624
5 files changed
+598
No files matched your search
@@ -0,0 +1,29 @@
|
||||
"""转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。"""
|
||||
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}"
|
||||
@@ -0,0 +1,20 @@
|
||||
from app.services.oa_confirmation_date import add_months_ymd
|
||||
|
||||
|
||||
def test_simple_month_add():
|
||||
assert add_months_ymd("2026-01-15", 3) == "2026-04-15"
|
||||
|
||||
|
||||
def test_clamp_end_of_month():
|
||||
assert add_months_ymd("2026-01-31", 1) == "2026-02-28"
|
||||
|
||||
|
||||
def test_zero_months_returns_same_day():
|
||||
assert add_months_ymd("2026-07-30", 0) == "2026-07-30"
|
||||
|
||||
|
||||
def test_invalid_returns_none():
|
||||
assert add_months_ymd("", 3) is None
|
||||
assert add_months_ymd("2026-01-15", -1) is None
|
||||
assert add_months_ymd("2026-01-15", True) is None # type: ignore[arg-type]
|
||||
assert add_months_ymd("2026-02-30", 1) is None
|
||||
@@ -0,0 +1,513 @@
|
||||
# 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.
|
||||
|
||||
**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: 写前端失败测试**
|
||||
|
||||
```ts
|
||||
// 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**
|
||||
|
||||
```bash
|
||||
cd web-admin && pnpm exec tsx --test src/utils/oa-confirmation-date.test.ts
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 实现前端**
|
||||
|
||||
```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: 后端同测 + 实现**
|
||||
|
||||
```python
|
||||
# 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}"
|
||||
```
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_oa_confirmation_date.py -v
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**(仅当用户要求时)
|
||||
|
||||
```bash
|
||||
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 增量**
|
||||
|
||||
```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` 增加:
|
||||
|
||||
```python
|
||||
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` 增加:
|
||||
|
||||
```python
|
||||
"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`:
|
||||
|
||||
```python
|
||||
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` 至少增加:
|
||||
|
||||
```python
|
||||
"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**(仅当用户要求时)
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```ts
|
||||
// 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**
|
||||
|
||||
导出:
|
||||
|
||||
```ts
|
||||
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**
|
||||
|
||||
```python
|
||||
# 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: 挂钩**
|
||||
|
||||
```bash
|
||||
rg -n 'status = "completed"' backend/app/services/oa_engine.py
|
||||
```
|
||||
|
||||
每一处赋值 `completed` 后:
|
||||
|
||||
```python
|
||||
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**(仅当用户要求时)
|
||||
|
||||
```bash
|
||||
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:**
|
||||
|
||||
```bash
|
||||
./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?
|
||||
@@ -0,0 +1,22 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
/** 入职日 + 试用期月数 → 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')}`
|
||||
}
|
||||
Reference in new issue
Block a user