Explorar el Código

chore(docs): 移除 superpowers 开发文档

malk hace 1 día
padre
commit
222ea7a336

+ 0 - 392
docs/superpowers/plans/2026-07-15-workhours-data-cleanup.md

@@ -1,392 +0,0 @@
-# Workhours Data Cleanup 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:** Safely identify and delete duplicate required-hours records plus records after an employee's offline date, while preserving all other empty or unmatched data.
-
-**Architecture:** Add a package-private pure resolver that deterministically selects one keeper per employee/date key. `WorkHoursCalcService` will scan required-hours records by month, exclude post-offline records from duplicate grouping so the two deletion sets are disjoint, and expose a dry-run-first controller endpoint. Production deletion remains blocked until online dry-run counts are reported and the user confirms a second time.
-
-**Tech Stack:** Java 8, Spring Boot 2.1, JUnit 4, Mockito, DingTalk YiDa form APIs, Maven.
-
-## 2026-07-15 实际执行修订
-
-用户确认实际顺序调整为:隔离分支本地启动服务处理正式宜搭数据 → 更新文档归档 → 最后整理提交并部署。没有为了 dry-run 先部署生产服务。
-
-当前结果:
-
-- 重复记录删除 35 条,失败 0。
-- 离职日后记录删除 796 条,失败 0。
-- 删除后重复、离职后、未来日期三项 dry-run 的 `toDelete` 均为 0。
-- 两份业务文档已更新;最终生产部署仍待执行。
-
----
-
-### Task 1: Deterministic Duplicate Resolver
-
-**Files:**
-- Create: `mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursDuplicateResolver.java`
-- Create: `mjava-akdsbeisen/src/test/java/com/malk/service/workhours/WorkHoursDuplicateResolverTest.java`
-
-- [ ] **Step 1: Write the failing resolver tests**
-
-Create tests for completeness, modified-time tie-breaking, and invalid keys:
-
-```java
-@Test
-public void resolveShouldKeepMostCompleteCandidate() {
-    Candidate sparse = candidate("old", "u1|2026-07-15", 2, 200L, 200L);
-    Candidate complete = candidate("complete", "u1|2026-07-15", 5, 100L, 100L);
-
-    Resolution result = WorkHoursDuplicateResolver.resolve(Arrays.asList(sparse, complete));
-
-    assertEquals(Collections.singletonList("old"), result.getDeleteInstanceIds());
-    assertEquals("complete", result.getGroups().get(0).getKeepInstanceId());
-}
-
-@Test
-public void resolveShouldKeepMostRecentlyModifiedWhenCompletenessMatches() {
-    Candidate older = candidate("older", "u1|2026-07-15", 5, 100L, 100L);
-    Candidate newer = candidate("newer", "u1|2026-07-15", 5, 200L, 100L);
-
-    Resolution result = WorkHoursDuplicateResolver.resolve(Arrays.asList(older, newer));
-
-    assertEquals(Collections.singletonList("older"), result.getDeleteInstanceIds());
-}
-
-@Test
-public void resolveShouldPreserveCandidatesWithoutReliableKey() {
-    Resolution result = WorkHoursDuplicateResolver.resolve(Collections.singletonList(
-            candidate("invalid", null, 5, 200L, 100L)));
-
-    assertEquals(1, result.getSkippedInvalidKey());
-    assertTrue(result.getDeleteInstanceIds().isEmpty());
-}
-```
-
-- [ ] **Step 2: Run the resolver tests and verify RED**
-
-Run:
-
-```bash
-mvn -pl mjava-akdsbeisen -am \
-  -Dmaven.test.skip=false -DskipTests=false \
-  -Dsurefire.failIfNoSpecifiedTests=false \
-  -Dtest=WorkHoursDuplicateResolverTest test
-```
-
-Expected: test compilation fails because `WorkHoursDuplicateResolver` does not exist.
-
-- [ ] **Step 3: Implement the minimal resolver**
-
-Create package-private typed classes:
-
-```java
-final class WorkHoursDuplicateResolver {
-    private WorkHoursDuplicateResolver() {
-    }
-
-    static Resolution resolve(List<Candidate> candidates) {
-        Map<String, List<Candidate>> grouped = new LinkedHashMap<>();
-        int skippedInvalidKey = 0;
-        for (Candidate candidate : candidates) {
-            if (candidate.getKey() == null || candidate.getKey().isEmpty()) {
-                skippedInvalidKey++;
-                continue;
-            }
-            grouped.computeIfAbsent(candidate.getKey(), key -> new ArrayList<>()).add(candidate);
-        }
-
-        List<DuplicateGroup> groups = new ArrayList<>();
-        List<String> deleteIds = new ArrayList<>();
-        for (Map.Entry<String, List<Candidate>> entry : grouped.entrySet()) {
-            List<Candidate> group = entry.getValue();
-            if (group.size() <= 1) continue;
-            group.sort(KEEPER_ORDER);
-            Candidate keeper = group.get(0);
-            List<String> groupDeleteIds = group.subList(1, group.size()).stream()
-                    .map(Candidate::getInstanceId)
-                    .collect(Collectors.toList());
-            deleteIds.addAll(groupDeleteIds);
-            groups.add(new DuplicateGroup(entry.getKey(), keeper.getInstanceId(), groupDeleteIds));
-        }
-        return new Resolution(grouped.size(), skippedInvalidKey, groups, deleteIds);
-    }
-}
-```
-
-`KEEPER_ORDER` sorts completeness, modified time, and created time descending, then instance ID ascending. Add explicit constructors and typed getters for `Candidate`, `DuplicateGroup`, and `Resolution` because the project targets Java 8.
-
-- [ ] **Step 4: Run the resolver tests and verify GREEN**
-
-Run the command from Step 2.
-
-Expected: 3 tests, 0 failures, 0 errors.
-
-- [ ] **Step 5: Commit the resolver**
-
-```bash
-git add mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursDuplicateResolver.java \
-        mjava-akdsbeisen/src/test/java/com/malk/service/workhours/WorkHoursDuplicateResolverTest.java
-git commit -m "feat(workhours): 增加重复记录保留规则"
-```
-
-### Task 2: Lock the Offline-Date Boundary
-
-**Files:**
-- Modify: `mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursCalcService.java`
-- Modify: `mjava-akdsbeisen/src/test/java/com/malk/service/workhours/WorkHoursCalcServiceTest.java`
-
-- [ ] **Step 1: Write a failing boundary test**
-
-```java
-@Test
-public void isAfterOfflineDateShouldKeepOfflineDayAndRejectFollowingDay() {
-    LocalDate offlineDate = LocalDate.of(2026, 7, 15);
-
-    assertFalse(WorkHoursCalcService.isAfterOfflineDate(offlineDate, offlineDate));
-    assertTrue(WorkHoursCalcService.isAfterOfflineDate(offlineDate.plusDays(1), offlineDate));
-    assertFalse(WorkHoursCalcService.isAfterOfflineDate(offlineDate.plusDays(1), null));
-}
-```
-
-- [ ] **Step 2: Run the service test and verify RED**
-
-Run:
-
-```bash
-mvn -pl mjava-akdsbeisen -am \
-  -Dmaven.test.skip=false -DskipTests=false \
-  -Dsurefire.failIfNoSpecifiedTests=false \
-  -Dtest=WorkHoursCalcServiceTest test
-```
-
-Expected: test compilation fails because `isAfterOfflineDate` does not exist.
-
-- [ ] **Step 3: Implement and reuse the boundary helper**
-
-```java
-static boolean isAfterOfflineDate(LocalDate workDay, LocalDate offlineDate) {
-    return workDay != null && offlineDate != null && workDay.isAfter(offlineDate);
-}
-```
-
-Replace the three direct `workDay.isAfter(offlineDate)` checks in single-day sync, concurrent upsert, and offline cleanup with this helper.
-
-- [ ] **Step 4: Run the service test and verify GREEN**
-
-Run the command from Step 2.
-
-Expected: all `WorkHoursCalcServiceTest` tests pass.
-
-- [ ] **Step 5: Commit the boundary test and refactor**
-
-```bash
-git add mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursCalcService.java \
-        mjava-akdsbeisen/src/test/java/com/malk/service/workhours/WorkHoursCalcServiceTest.java
-git commit -m "test(workhours): 锁定离职日期过滤边界"
-```
-
-### Task 3: Dry-Run-First Duplicate Cleanup Endpoint
-
-**Files:**
-- Modify: `mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursCalcService.java`
-- Modify: `mjava-akdsbeisen/src/main/java/com/malk/controller/WorkHoursController.java`
-- Modify: `mjava-akdsbeisen/src/test/java/com/malk/service/workhours/WorkHoursCalcServiceTest.java`
-
-- [ ] **Step 1: Write a failing dry-run integration test**
-
-Mock two required-hours records with the same employee/date. Make one candidate more complete, and verify dry-run reports one deletion without invoking YiDa delete:
-
-```java
-@Test
-public void cleanupDuplicateHoursDryRunShouldReportOneDeletionWithoutDeleting() {
-    YDClient ydClient = mock(YDClient.class);
-    WHConf conf = requiredHoursConf();
-    DDR_New<Object> page = pageOf(
-            requiredHoursRecord("sparse", "employee-1", LocalDate.of(2026, 7, 15)),
-            completeRequiredHoursRecord("complete", "employee-1", LocalDate.of(2026, 7, 15)));
-    when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
-            .thenReturn(personnelPageWithoutOfflineDate(), page, emptyPage());
-    WorkHoursCalcService service = serviceWith(ydClient, conf);
-
-    Map<String, Object> stats = service.cleanupDuplicateHours(true);
-
-    assertEquals(1, stats.get("duplicateGroups"));
-    assertEquals(1, stats.get("toDelete"));
-    assertEquals(0, stats.get("deleted"));
-    verify(ydClient, never()).operateData(any(YDParam.class), eq(YDConf.FORM_OPERATION.delete_batch));
-}
-```
-
-- [ ] **Step 2: Run the service test and verify RED**
-
-Run the Task 2 test command.
-
-Expected: compilation fails because `cleanupDuplicateHours` does not exist.
-
-- [ ] **Step 3: Implement monthly scan and candidate mapping**
-
-Add:
-
-```java
-public Map<String, Object> cleanupDuplicateHours(boolean dryRun)
-```
-
-Implementation requirements:
-
-1. Load all personnel details and build `userId -> offlineDate`.
-2. Scan required-hours records from 2026-04 through the current month using the existing monthly date-range pattern.
-3. Build candidate key only when employee and work day are both present.
-4. Exclude `isAfterOfflineDate(workDay, offlineDate)` records from duplicate grouping and count them in `excludedAfterOffline`; these are handled by `cleanupAfterOffline`.
-5. Score completeness using hours, Manager, employee number, attribute, department, company, and CF fields.
-6. Normalize `gmtModified` and `gmtCreate` to epoch milliseconds when numeric; use 0 for unavailable values.
-7. Resolve duplicates with `WorkHoursDuplicateResolver`.
-8. In dry-run mode, return stats and up to five group samples without calling delete.
-9. In formal mode, delete resolver IDs in batches of at most 100 and report `deleted` and `fail`.
-
-- [ ] **Step 4: Add the controller endpoint**
-
-```java
-@GetMapping("/cleanup-duplicates")
-public Map<String, Object> cleanupDuplicates(
-        @RequestParam(defaultValue = "true") boolean dryRun) {
-    Map<String, Object> result = new LinkedHashMap<>();
-    try {
-        long start = System.currentTimeMillis();
-        Map<String, Object> stats = workHoursCalcService.cleanupDuplicateHours(dryRun);
-        result.put("success", true);
-        result.put("message", dryRun ? "重复工时清理预览完成(未删除)" : "重复工时清理完成");
-        result.put("stats", stats);
-        result.put("costMs", System.currentTimeMillis() - start);
-    } catch (Exception e) {
-        log.error("重复工时清理失败", e);
-        result.put("success", false);
-        result.put("message", e.getMessage());
-    }
-    return result;
-}
-```
-
-Default `dryRun=true` is intentional so an omitted parameter cannot delete production data.
-
-- [ ] **Step 5: Run focused tests and verify GREEN**
-
-Run:
-
-```bash
-mvn -pl mjava-akdsbeisen -am \
-  -Dmaven.test.skip=false -DskipTests=false \
-  -Dsurefire.failIfNoSpecifiedTests=false \
-  -Dtest=WorkHoursDuplicateResolverTest,WorkHoursCalcServiceTest,WorkHoursTimerScheduleTest test
-```
-
-Expected: all focused tests pass with 0 failures and 0 errors.
-
-- [ ] **Step 6: Commit the endpoint**
-
-```bash
-git add mjava-akdsbeisen/src/main/java/com/malk/controller/WorkHoursController.java \
-        mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursCalcService.java \
-        mjava-akdsbeisen/src/test/java/com/malk/service/workhours/WorkHoursCalcServiceTest.java
-git commit -m "feat(workhours): 增加重复数据安全清理接口"
-```
-
-### Task 4: Verify, Deploy the Dry-Run Capability, and Audit Production
-
-**Files:**
-- Verify only; no additional source files.
-
-- [ ] **Step 1: Run the final build**
-
-```bash
-mvn -q -pl mjava-akdsbeisen -am clean package \
-  -Dmaven.test.skip=false -DskipTests=false \
-  -Dsurefire.failIfNoSpecifiedTests=false \
-  -Dtest=WorkHoursDuplicateResolverTest,WorkHoursCalcServiceTest,WorkHoursTimerScheduleTest
-```
-
-Expected: exit code 0; all test report XML files show zero failures and errors.
-
-- [ ] **Step 2: Verify source and artifact**
-
-```bash
-git diff --check HEAD~3 HEAD
-jar tf mjava-akdsbeisen/target/mjava-akdsbeisen.jar | rg '(^|/)h2-[^/]*\.jar$' || true
-shasum -a 256 mjava-akdsbeisen/target/mjava-akdsbeisen.jar
-```
-
-Expected: no diff errors, no H2 dependency, and a SHA-256 value.
-
-- [ ] **Step 3: Deploy using the already approved protected workflow**
-
-Use the isolated JAR, preserve the original dirty repository's local JAR, and run:
-
-```bash
-/Users/malk/.agents/skills/mcli/deploy/bin/deploy.sh akds --skip-build --backup --yes
-```
-
-Expected: remote backup created, upload completes, and `./server.sh status` reports RUNNING with prod profile.
-
-- [ ] **Step 4: Run online read-only audits**
-
-```bash
-curl -sS 'http://127.0.0.1:9055/api/akds/workhours/cleanup-duplicates?dryRun=true'
-curl -sS 'http://127.0.0.1:9055/api/akds/workhours/cleanup-after-offline?dryRun=true'
-```
-
-Expected: duplicate audit returns exact disjoint duplicate counts; offline audit continues to return 796 before deletion.
-
-- [ ] **Step 5: Stop before deletion and request confirmation**
-
-Report target form, duplicate groups, duplicate delete count, offline delete count, combined delete count, keeper rule, and samples. Do not call either formal endpoint until the user explicitly confirms.
-
-### Task 5: Execute Confirmed Deletion and Verify Idempotency
-
-**Files:**
-- Modify after successful deletion: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时月度计算.md`
-- Modify after successful deletion: `/Users/malk/Desktop/Tech/claude/临时/阿科德斯-应报工时修复-2026-07-15.md`
-
-- [x] **Step 1: Delete valid-record duplicates after confirmation**
-
-```bash
-curl -sS 'http://127.0.0.1:9055/api/akds/workhours/cleanup-duplicates?dryRun=false'
-```
-
-Expected: `deleted == toDelete` and `fail == 0`.
-
-- [x] **Step 2: Delete post-offline records**
-
-```bash
-curl -sS 'http://127.0.0.1:9055/api/akds/workhours/cleanup-after-offline?dryRun=false'
-```
-
-Expected: `deleted == toDelete` and `fail == 0`.
-
-- [x] **Step 3: Re-run all read-only audits**
-
-```bash
-curl -sS 'http://127.0.0.1:9055/api/akds/workhours/cleanup-duplicates?dryRun=true'
-curl -sS 'http://127.0.0.1:9055/api/akds/workhours/cleanup-after-offline?dryRun=true'
-curl -sS 'http://127.0.0.1:9055/api/akds/workhours/cleanup-future?dryRun=true'
-```
-
-Expected: every response has `toDelete=0` and `fail=0`.
-
-- [x] **Step 4: Update both workhours documents**
-
-Record actual duplicate groups, duplicate deletions, offline deletions, verification results, commit IDs, and deployment state. Keep unmatched personnel and empty-source preservation rules explicit.
-
-- [ ] **Step 5: Final repository and remote verification**
-
-```bash
-git status --short
-ssh root@120.55.113.155 'cd /home/server/akds && ./server.sh status'
-```
-
-Expected: code worktree clean and remote service RUNNING.
-
-### 2026-07-16 部署暂缓说明
-
-- 离职链路安全补强完成后先提交代码,不在本轮部署。
-- 等后续工时异常处理全部完成,再统一构建候选 JAR、执行只读审计并申请生产部署确认。
-- 部署前必须再次验证人员同步不会误标离职且会清除恢复在职人员的旧离职日期,并确认三类工时清理 dry-run 均符合预期。

+ 0 - 379
docs/superpowers/plans/2026-07-15-workhours-docs-restructure.md

@@ -1,379 +0,0 @@
-# 应填报工时文档重构 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:** 把应填报工时的业务规则、程序逻辑、运维治理和变更记录收敛到一个项目目录,同时保留原月度文档路径作为兼容入口,并清理临时目录中的交接文件。
-
-**Architecture:** 使用一个总入口 README 和四份单一职责专题文档。两个原始长文档通过 `mv` 放入项目内归档目录,当前有效内容按职责提取到专题文档;根目录原路径只保留简短索引,临时目录原路径不再保留。
-
-**Tech Stack:** Markdown、CommonMark、Shell `mkdir`/`mv`/`test`、Codex `apply_patch`
-
----
-
-## 文件结构与职责
-
-**创建:**
-
-- `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/README.md`:唯一项目入口、状态摘要和导航。
-- `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/01-业务规则与同步流程.md`:唯一业务口径与程序主流程。
-- `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/02-字段接口与程序实现.md`:字段、类、方法、接口和数据格式。
-- `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/03-运维验证与数据治理.md`:本地运行、dry-run、删除确认、复查、测试和部署。
-- `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/04-变更记录.md`:功能历史、故障修复和数据治理结果。
-- `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/归档/2026-07-15-单文件原稿.md`:月度计算原始长文档。
-- `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/归档/2026-07-15-Claude交接与修复原稿.md`:临时交接原稿。
-
-**重建:**
-
-- `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时月度计算.md`:原路径兼容索引,不再维护正文。
-
-**移除原路径:**
-
-- `/Users/malk/Desktop/Tech/claude/临时/阿科德斯-应报工时修复-2026-07-15.md`:使用 `mv` 归档后原路径自然消失。
-
-### Task 1: 建立项目目录并归档两个原稿
-
-**Files:**
-
-- Create directory: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/归档`
-- Move: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时月度计算.md`
-- Move: `/Users/malk/Desktop/Tech/claude/临时/阿科德斯-应报工时修复-2026-07-15.md`
-
-- [x] **Step 1: 确认两个源文件存在且目标文件不存在**
-
-Run:
-
-```bash
-test -f '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时月度计算.md'
-test -f '/Users/malk/Desktop/Tech/claude/临时/阿科德斯-应报工时修复-2026-07-15.md'
-test ! -e '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/归档/2026-07-15-单文件原稿.md'
-test ! -e '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/归档/2026-07-15-Claude交接与修复原稿.md'
-```
-
-Expected: 四条命令退出码均为 0。
-
-- [x] **Step 2: 创建项目与归档目录**
-
-Run:
-
-```bash
-mkdir -p '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/归档'
-```
-
-Expected: 目录存在,不影响阿科德斯其他业务文档。
-
-- [x] **Step 3: 使用 mv 归档月度计算原稿**
-
-Run:
-
-```bash
-mv '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时月度计算.md' '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/归档/2026-07-15-单文件原稿.md'
-```
-
-Expected: 原稿完整保存在归档目录。
-
-- [x] **Step 4: 使用 mv 归档临时交接原稿**
-
-Run:
-
-```bash
-mv '/Users/malk/Desktop/Tech/claude/临时/阿科德斯-应报工时修复-2026-07-15.md' '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/归档/2026-07-15-Claude交接与修复原稿.md'
-```
-
-Expected: 临时目录原路径不存在,归档副本存在。
-
-### Task 2: 创建项目总入口和兼容入口
-
-**Files:**
-
-- Create: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/README.md`
-- Create: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时月度计算.md`
-
-- [x] **Step 1: 创建 README 项目总入口**
-
-Use `apply_patch`。README 必须包含以下确定内容:
-
-```markdown
-# 阿科德斯 · 应填报工时
-
-> 更新日期:2026-07-15
-> 唯一维护入口:本目录
-> 当前状态:数据治理完成,本次重复与离职逻辑待生产部署
-
-## 当前结论
-
-- 月度全量只生成 `workDay <= today` 的记录。
-- 每日 03:30、12:45 执行近 3 天工作日补漏,每次重新读取全量人员档案和离职日期。
-- 离职当天保留,离职日之后不再写入。
-- 重复键为员工+日期,清理时保留业务字段最完整的一条。
-- 2026-07-15 已删除重复 35 条、离职后 796 条;重复、离职后、未来日期复查均为 0。
-- 项目变更同步开关保持关闭。
-
-## 文档导航
-
-- [业务规则与同步流程](01-业务规则与同步流程.md)
-- [字段接口与程序实现](02-字段接口与程序实现.md)
-- [运维验证与数据治理](03-运维验证与数据治理.md)
-- [变更记录](04-变更记录.md)
-- [历史原稿](归档/)
-
-## 代码状态
-
-- 仓库:`/Users/malk/server/cur/akds-codex-workhours-20260715`
-- 分支:`codex/akds-workhours-20260715`
-- 文档重构前最新提交:`4c1e037`
-- 未 push;本次重复与离职逻辑尚未部署。
-```
-
-- [x] **Step 2: 重建原路径兼容入口**
-
-Use `apply_patch`,内容保持一页内:
-
-```markdown
-# 应填报工时月度计算
-
-本文档已拆分,后续统一维护在 [应填报工时项目目录](应填报工时/README.md)。
-
-## 快速入口
-
-- [业务规则与同步流程](应填报工时/01-业务规则与同步流程.md)
-- [字段接口与程序实现](应填报工时/02-字段接口与程序实现.md)
-- [运维验证与数据治理](应填报工时/03-运维验证与数据治理.md)
-- [变更记录](应填报工时/04-变更记录.md)
-
-> 当前状态:2026-07-15 数据治理完成;重复与离职逻辑待生产部署。历史原稿仅用于追溯,不再作为操作依据。
-```
-
-### Task 3: 创建业务规则与同步流程文档
-
-**Files:**
-
-- Create: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/01-业务规则与同步流程.md`
-- Source: `归档/2026-07-15-单文件原稿.md`
-
-- [x] **Step 1: 按程序调用链整理当前业务规则**
-
-Use `apply_patch`,文档章节固定为:
-
-```markdown
-# 业务规则与同步流程
-
-## 1. 业务目标
-## 2. 当前有效规则
-## 3. 月度全量同步
-## 4. 每日增量补漏
-## 5. 工作日计算
-## 6. Manager 计算
-## 7. 未来日期与离职日期过滤
-## 8. 重复记录治理
-## 9. 规则边界与保留逻辑
-```
-
-必须明确:
-
-- `calculateAndSyncMonthlyHours` 和 `incrementalSync` 都调用 `queryAllPersonnelDetails`。
-- `incrementalSync(3)` 是近 3 个自然日内的工作日补漏,已有键直接跳过。
-- `workDay > today` 跳过。
-- `workDay > offlineDate` 跳过,离职当天保留。
-- 状态离职但离职日期为空时整人防御跳过;当前没有此类数据。
-- 外部员工当天无有效 PM 时不生成记录。
-- 重复清理排除离职后记录,无法形成可靠键的记录保留。
-
-- [x] **Step 2: 对照 Java 源码核验方法名和边界**
-
-Run:
-
-```bash
-rg -n 'calculateAndSyncMonthlyHours|incrementalSync|queryAllPersonnelDetails|isAfterOfflineDate|cleanupDuplicateHours|cleanupAfterOffline|computeDailyPms' '/Users/malk/server/cur/akds-codex-workhours-20260715/mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursCalcService.java'
-```
-
-Expected: 文档使用的方法名均能在源码中命中,离职边界为严格大于。
-
-### Task 4: 创建字段接口与程序实现文档
-
-**Files:**
-
-- Create: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/02-字段接口与程序实现.md`
-- Source: `归档/2026-07-15-单文件原稿.md`
-
-- [x] **Step 1: 整理表单与字段映射**
-
-Use `apply_patch`,只保留当前有效字段:人员、人员属性、员工编号、部门、归属公司、是否 CF、离职日期、在职状态、应填报日期、应填报工时和 Manager。系统令牌只写“通过配置或环境提供”,不得记录值。
-
-- [x] **Step 2: 按模块整理类与方法职责**
-
-文档必须覆盖:
-
-- `WHConf`
-- `WorkHoursCalcService`
-- `WorkHoursDuplicateResolver`
-- `WorkHoursController`
-- `WorkHoursCalcTimer`
-- `ProjectChangeSyncTimer`
-
-- [x] **Step 3: 整理 HTTP 接口与安全默认值**
-
-接口表至少包含:
-
-- `/workhours/sync`
-- `/workhours/sync-one`
-- `/workhours/sync-batch`
-- `/workhours/backfill-cf`
-- `/workhours/backfill-company-attr`
-- `/workhours/cleanup-future`
-- `/workhours/cleanup-after-offline`
-- `/workhours/cleanup-duplicates`
-- `/workhours/sync-project-changes`
-
-必须注明 `/cleanup-duplicates` 默认 `dryRun=true`,其他删除接口正式执行时必须显式写 `dryRun=false`,文档示例不使用省略参数的危险写法。
-
-- [x] **Step 4: 整理数据格式与性能实现**
-
-保留员工字段数组、部门 ID 数组、日期 epoch 毫秒、日期查询区间格式;记录分月扫描、100 条批量删除、10 线程、20 QPS 和重试退避。
-
-### Task 5: 创建运维验证与数据治理文档
-
-**Files:**
-
-- Create: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/03-运维验证与数据治理.md`
-- Source: 两份归档原稿
-
-- [x] **Step 1: 写本地隔离服务操作流程**
-
-必须使用独立端口,明确关闭:
-
-```text
---enable.scheduling=false
---workhours.projectChangeSyncEnabled=false
---spring.task.scheduling.enabled=false
-```
-
-说明旧定时器未全部受统一开关控制,因此操作完成后立即停止本地服务。
-
-- [x] **Step 2: 写数据治理安全流程**
-
-顺序固定为:
-
-1. dry-run 获取精确数量和样本。
-2. 复述目标表、保留规则和待删除数量。
-3. 用户明确确认。
-4. 显式 `dryRun=false` 正式执行。
-5. 重复、离职后、未来日期三项 dry-run 复查。
-
-- [x] **Step 3: 写 2026-07-15 实际结果**
-
-记录:
-
-- 重复 dry-run:43,851 条、35 组、70 条重复、待删 35、离职后排除 796。
-- 重复正式删除:35,失败 0。
-- 离职后正式删除:796,失败 0。
-- 复查:现存 43,020、唯一键 43,020、三项 `toDelete=0`。
-- 未来日期复查截止 2026-07-15,扫描 6,908。
-
-- [x] **Step 4: 写测试、打包和部署流程**
-
-说明 16 项聚焦测试通过、JAR 不含 H2、部署必须备份旧 JAR并重启生产服务;当前状态仍为待部署,不能写成已上线。
-
-### Task 6: 创建变更记录
-
-**Files:**
-
-- Create: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/04-变更记录.md`
-- Source: 两份归档原稿
-
-- [x] **Step 1: 迁移功能变更历史**
-
-按日期保留 2026-04-08 至 2026-07-15 的重要变更。每行只写变更摘要、验证结果和关联提交,不复制操作步骤。
-
-- [x] **Step 2: 迁移重要故障修复**
-
-只保留仍有诊断价值的问题:日期查询数组格式、部门 ID、组件扫描、分月绕过 30,000、QPS 限流、未来数据、空源值、定时开关、重复和离职后治理。
-
-- [x] **Step 3: 标明历史与当前状态边界**
-
-文档顶部写明:“历史记录只用于追溯,当前规则以 `01-业务规则与同步流程.md` 为准,当前操作以 `03-运维验证与数据治理.md` 为准。”
-
-### Task 7: 验证目录、链接、内容和敏感信息
-
-**Files:**
-
-- Verify all files under: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时`
-- Verify compatibility index: `/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时月度计算.md`
-
-- [x] **Step 1: 检查目录结构和原路径状态**
-
-Run:
-
-```bash
-find '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时' -maxdepth 2 -type f -print | sort
-test -f '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时月度计算.md'
-test ! -e '/Users/malk/Desktop/Tech/claude/临时/阿科德斯-应报工时修复-2026-07-15.md'
-```
-
-Expected: 2 个归档文件、README、4 个专题文档和兼容入口存在;临时原路径不存在。
-
-- [x] **Step 2: 检查 Markdown 相对链接目标**
-
-逐个核对兼容入口和 README 的 9 个链接目标均存在,不使用 `file://` 或失效的临时路径。
-
-- [x] **Step 3: 检查当前状态与关键业务规则**
-
-Run:
-
-```bash
-rg -n '离职当天保留|次日起不再写入|dryRun=true|待部署|43020|35|796' '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时'
-```
-
-Expected: 当前规则、数据结果和待部署状态在对应单一职责文档中存在。
-
-- [x] **Step 4: 检查敏感信息**
-
-Run:
-
-```bash
-rg -n -i 'systemToken\s*[:=]\s*[A-Za-z0-9]|access[_-]?token\s*[:=]\s*[A-Za-z0-9]|password\s*[:=]\s*[^$]' '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时' -g '*.md' -g '!归档/**'
-```
-
-Expected: 当前维护文档不命中真实凭据;归档原稿不做内容输出,并在 README 中标记为仅限追溯。
-
-- [x] **Step 5: 检查文件长度与职责边界**
-
-Run:
-
-```bash
-wc -l '/Users/malk/Desktop/Tech/claude/后端/阿科德斯/应填报工时/'*.md
-```
-
-Expected: README 和兼容入口简短;四份专题文档不再把全部职责集中到单文件。
-
-### Task 8: 更新设计状态并提交计划与状态
-
-**Files:**
-
-- Modify: `docs/superpowers/specs/2026-07-15-workhours-docs-restructure-design.md`
-- Modify: `docs/superpowers/plans/2026-07-15-workhours-docs-restructure.md`
-
-- [x] **Step 1: 更新设计状态**
-
-把设计文档状态改为“文档拆分完成,生产部署仍暂停”,并记录临时原路径已清理、归档目标存在。
-
-- [x] **Step 2: 勾选实际完成步骤并执行差异检查**
-
-Run:
-
-```bash
-git diff --check
-git status --short
-```
-
-Expected: 仅设计与计划状态发生仓库内变更,业务代码无变化。
-
-- [x] **Step 3: 提交仓库内状态更新**
-
-Run:
-
-```bash
-git add docs/superpowers/specs/2026-07-15-workhours-docs-restructure-design.md docs/superpowers/plans/2026-07-15-workhours-docs-restructure.md
-git commit -m "docs(workhours): 完成应填报工时文档收敛"
-```
-
-Expected: 提交成功,不 push,不部署。

+ 0 - 130
docs/superpowers/specs/2026-07-15-workhours-data-cleanup-design.md

@@ -1,130 +0,0 @@
-# 应报工时重复与离职后数据清理设计
-
-> 日期:2026-07-15
-> 状态:数据清理与删除后复查已完成;2026-07-16 离职链路安全补强完成,等待工时异常处理结束后部署
-
-## 目标
-
-清理「应填报工时」表中的两类无效数据:
-
-1. 同一员工同一应填报日期存在多条记录时,只保留一条。
-2. 员工已有离职日期时,删除应填报日期晚于离职日期的记录。
-
-离职当天及之前的数据保留。无法匹配人员档案、人员档案公司或属性为空等其他情况继续沿用现有保留逻辑,不在本次清理范围内。
-
-## 已确认现状
-
-- 每日增量同步在 03:30、12:45 执行,每次重新读取全量人员档案。
-- 人员档案的离职日期字段为 `dateField_mh8xhqc7`。
-- 写入前已有 `workDay > offlineDate` 过滤,离职日后不再新增应报工时。
-- 状态为离职但离职日期为空时,代码会整人防御跳过;当前数据不存在这种情况。
-- 本地隔离服务连接正式宜搭执行 dry-run:扫描 43,851 条,识别 74 名有离职日期的员工、796 条离职日后记录。
-
-## 重复记录判定
-
-重复键为:
-
-```text
-employeeId + "|" + workDay
-```
-
-- `employeeId` 从 `employeeField_mmd8onl4` 提取。
-- `workDay` 从 `dateField_mmd8onl5` 解析。
-- 员工或日期缺失时无法形成可靠重复键,该记录保留并计入 `skippedInvalidKey`。
-- 仅键完全相同且数量大于 1 的记录视为重复组。
-
-## 重复组保留规则
-
-每个重复组只保留一条,按以下顺序选择:
-
-1. 保留业务字段非空数量最多的记录。
-2. 完整度相同时,保留 `gmtModified` 最近的记录。
-3. 修改时间仍相同时,保留 `gmtCreate` 最近的记录。
-4. 仍相同时,按 `formInstanceId` 做稳定排序,保证重复执行结果一致。
-
-完整度仅统计已有应报工时业务字段:工时、Manager、员工编号、属性、部门、归属公司、是否 CF 员工。核心键字段不计分。
-
-不合并不同记录的字段,避免构造从未真实存在过的组合数据。此前公司和属性已完成统一回填,因此选择最完整记录即可。
-
-## 接口与执行流程
-
-新增一次性接口:
-
-```text
-GET /workhours/cleanup-duplicates?dryRun=true
-GET /workhours/cleanup-duplicates
-```
-
-执行分为四步:
-
-1. 按月份扫描从 2026-04 至当前月的应报工时,绕过宜搭搜索结果 30,000 条上限。
-2. 在内存中按重复键分组并选择保留记录,收集其余实例 ID。
-3. dry-run 返回统计与前 5 个重复组样本,不调用删除接口。
-4. 正式模式按每批最多 100 条调用 `delete_batch`,返回实际删除数与失败数。
-
-返回统计至少包含:
-
-- `total`
-- `uniqueKeys`
-- `duplicateGroups`
-- `duplicateRecords`
-- `toDelete`
-- `skippedInvalidKey`
-- `deleted`
-- `fail`
-- `samples`
-
-## 删除安全门槛
-
-正式删除前必须把以下范围复述给用户并取得二次确认:
-
-- 目标表单:应填报工时表。
-- 重复组数量、重复记录总数和待删除实例数。
-- 离职后待删除记录数,当前 dry-run 为 796。
-- 重复记录保留规则。
-
-正式删除后必须再次执行:
-
-1. `/workhours/cleanup-duplicates?dryRun=true`,期望 `toDelete=0`。
-2. `/workhours/cleanup-after-offline?dryRun=true`,期望 `toDelete=0`。
-3. `/workhours/cleanup-future?dryRun=true`,期望 `toDelete=0`。
-
-## 实际执行结果
-
-- 重复数据:35 个重复组、70 条重复记录,按保留规则删除 35 条,失败 0。
-- 离职后数据:删除 796 条,失败 0;离职当天及之前记录保留。
-- 删除后现存 43,020 条,员工+日期唯一键 43,020 个。
-- 重复复查:`duplicateGroups=0`、`toDelete=0`、`skippedInvalidKey=0`。
-- 离职后复查:`toDelete=0`。
-- 未来日期复查:截止 2026-07-15,`toDelete=0`。
-- 本地服务完成后已停止;本次代码尚未部署到生产服务。
-
-## 测试
-
-采用测试驱动实现,至少覆盖:
-
-1. 同组中保留业务字段最完整的记录。
-2. 完整度相同时保留最近修改的记录。
-3. 员工或日期缺失的记录不进入删除集合。
-4. dry-run 只返回待删除数量,不调用 `delete_batch`。
-5. 离职边界保持 `workDay > offlineDate`:离职当天保留,次日删除或跳过写入。
-6. 外部部门人员源抓取失败时中止整轮同步,不生成误标离职动作。
-7. 员工恢复在职时由人员同步清除旧离职日期,应报工时只读取人员档案离职日期。
-8. `/cleanup-after-offline` 省略 `dryRun` 时默认仅预演。
-
-最终执行模块测试、完整打包、差异检查,再提交和部署。
-
-## 2026-07-16 离职链路安全补强
-
-- 人员全量同步依赖的外部部门抓取失败时直接终止,不再用不完整人员集合计算离职差异。
-- 钉钉仍可查询到的人员统一恢复为在职;若宜搭残留旧离职日期,同步时显式清空。
-- 应报工时与人员同步解耦:不调用人员同步服务、不触发人员数据修改,只读取人员档案离职日期作为过滤边界。
-- 离职后工时清理继续保持独立操作,不与人员同步自动耦合,正式删除仍需显式 `dryRun=false`。
-- 本轮只完成代码、测试和文档归档,不执行生产部署。
-
-## 不在本次范围
-
-- 不删除无法匹配人员档案的记录。
-- 不清空人员档案公司或属性为空的历史字段。
-- 不处理项目变更审批 Manager 回算。
-- 不启用 `projectChangeSyncEnabled`。

+ 0 - 159
docs/superpowers/specs/2026-07-15-workhours-docs-restructure-design.md

@@ -1,159 +0,0 @@
-# 应填报工时文档重构设计
-
-> 日期:2026-07-15
-> 状态:文档拆分完成,生产部署仍暂停
-
-## 目标
-
-把分散、重复且同时承载多种职责的应填报工时文档,收敛为一个项目目录。后续业务规则、程序逻辑、数据治理和变更记录统一在该目录维护。
-
-本次只整理文档,不修改程序代码,不执行部署。
-
-执行结果:两份原稿已移动到项目归档目录,原月度文档路径已重建为兼容入口,临时交接文件原路径已清理。
-
-## 当前问题
-
-`应填报工时月度计算.md` 已超过 480 行,同时包含:
-
-- 当前业务规则与同步流程
-- 宜搭字段、接口和程序实现
-- 本地运行、数据删除和验证操作
-- 多阶段变更历史与故障修复记录
-- 一次性数据修复结果
-
-临时目录中的 `阿科德斯-应报工时修复-2026-07-15.md` 又重复记录了执行结果、交接过程和过期恢复步骤。两份文档存在重复和历史口径混入当前流程的风险。
-
-## 目标目录
-
-```text
-后端/阿科德斯/
-├── 应填报工时月度计算.md
-└── 应填报工时/
-    ├── README.md
-    ├── 01-业务规则与同步流程.md
-    ├── 02-字段接口与程序实现.md
-    ├── 03-运维验证与数据治理.md
-    ├── 04-变更记录.md
-    └── 归档/
-        ├── 2026-07-15-单文件原稿.md
-        └── 2026-07-15-Claude交接与修复原稿.md
-```
-
-## 文件职责
-
-### 兼容入口:`应填报工时月度计算.md`
-
-保留原路径,改为不超过一页的兼容索引:
-
-- 标明唯一维护入口为 `应填报工时/README.md`
-- 展示当前数据与部署状态
-- 链接各专题文档
-- 不再复制字段表、接口表和历史记录
-
-### `README.md`
-
-作为项目总入口,仅保留:
-
-- 项目目标和当前有效结论
-- 当前代码分支、提交与部署状态
-- 数据修复最终结果
-- 文档导航
-- 尚未启用的项目变更同步等后续项
-
-### `01-业务规则与同步流程.md`
-
-维护唯一的业务口径和程序主流程:
-
-- 月度全量同步
-- 每日增量补漏
-- 工作日计算
-- 内部/外部员工 Manager 规则
-- 未来日期过滤
-- 离职日期边界
-- 重复记录判定与保留规则
-
-### `02-字段接口与程序实现.md`
-
-维护实现细节:
-
-- 表单与字段映射
-- Java 模块、类和方法职责
-- Controller 接口
-- 宜搭请求和字段格式
-- 分页、限流、并发与重试策略
-
-### `03-运维验证与数据治理.md`
-
-维护操作流程和安全门槛:
-
-- 本地隔离服务启动与停止
-- dry-run 优先规则
-- 回填、重复、离职后和未来数据清理
-- 正式删除前确认要求
-- 删除后复查清单
-- 测试、打包和生产部署流程
-- 敏感日志与定时任务注意事项
-
-### `04-变更记录.md`
-
-维护按日期排序的变化与结果:
-
-- 功能变更历史
-- 重要故障及修复
-- 2026-07-15 数据治理实际数量
-- 关联提交和部署状态
-
-历史记录只描述当时情况,不再承担当前操作指引。
-
-## 原文件处理
-
-### 月度计算单文件
-
-1. 使用 `mv` 把当前完整原稿移动到 `应填报工时/归档/2026-07-15-单文件原稿.md`。
-2. 在原路径新建简短兼容索引。
-3. 从归档原稿提取内容,按职责写入 4 份专题文档。
-
-### 临时修复交接文件
-
-目标文件:
-
-```text
-/Users/malk/Desktop/Tech/claude/临时/阿科德斯-应报工时修复-2026-07-15.md
-```
-
-处理方式:
-
-1. 把当前有效的执行结果、代码状态和待部署状态收敛到 `README.md`、`03-运维验证与数据治理.md`、`04-变更记录.md`。
-2. 历史 Claude 交接、过期恢复步骤和原始疑问不进入当前流程文档。
-3. 使用 `mv` 将完整原稿移到 `应填报工时/归档/2026-07-15-Claude交接与修复原稿.md`。
-4. 原临时路径不再保留文件,后续不从临时目录维护应填报工时逻辑。
-
-采用移动归档而非不可恢复删除,既满足原路径清理,也保留必要追溯能力。
-
-## 内容去重规则
-
-- 当前规则只在 `01` 维护,其他文档通过链接引用。
-- 字段和接口只在 `02` 维护。
-- 可执行操作和安全要求只在 `03` 维护。
-- 时间线和历史数据只在 `04` 维护。
-- README 和兼容入口只写摘要,不复制大段正文。
-- 归档文件只用于追溯,不作为当前操作依据。
-
-## 验证标准
-
-整理完成后检查:
-
-1. 原兼容入口存在且所有链接有效。
-2. `应填报工时/` 下每份专题文档职责单一。
-3. 临时目录中的原交接文件已不存在,归档副本存在。
-4. 当前有效规则与 Java 程序逻辑一致:每日读取离职日期、离职当天保留、次日起不写、重复清理默认 dry-run。
-5. 数据结果一致:重复删除 35、离职后删除 796,三项复查 `toDelete=0`。
-6. 本次部署状态明确为“待部署”,不误写为已上线。
-7. 文档不记录系统令牌、访问令牌、Cookie 或密码。
-
-## 不在本次范围
-
-- 不修改 Java 程序和配置。
-- 不执行生产部署。
-- 不处理项目变更审批 Manager 回算功能。
-- 不重组阿科德斯目录下其他业务文档。