---
date: 2026-05-10
type: design-pattern
tags: [idempotency, gitea, automation, naming]
---

# 用滚动时间窗口里的"日期范围"做唯一键 → 跨天调用就不幂等

## 现象（real，PR #275 上线后第一次 dispatch 触发）

`weekly-retro-issue.py` 的 `derive_title` 直接用报告 H1 作 issue title：

```
"# Weekly Review · 2026-W19 (May 02 – May 09)"
  → "周复盘 2026-W19 (May 02 – May 09)"
```

`find_existing_issue` 用**精确 title 匹配**找已有 issue 决定 PATCH 还是 POST。

bug：`weekly-review.py` 的窗口是 `now - 7d`，**滚动**——

| 跑的时间 | 生成的 title |
|---|---|
| 5/9 23:30 | `周复盘 2026-W19 (May 02 – May 09)` |
| 5/10 10:22 | `周复盘 2026-W19 (May 03 – May 10)` |

**同一 ISO 周（W19）**，但 title 不同 → `find_existing_issue` exact match 失败 → **创建重复 issue**（#278 而不是 PATCH #274）。

## 修复

title 只保留 ISO 周这个**稳定**部分，去掉滚动的日期范围：

```python
# 旧
return h1.replace("Weekly Review · ", "周复盘 ").strip()
# 输出: "周复盘 2026-W19 (May 02 – May 09)"

# 新
title = h1.replace("Weekly Review · ", "周复盘 ").strip()
return re.sub(r"\s*\(.+\)\s*$", "", title)
# 输出: "周复盘 2026-W19"
```

ISO 周（`isocalendar().week`）是**绝对、和"now"无关**的属性——同一 ISO 周内任何天计算都是同一个数。

日期范围信息没丢，仍在 issue body 里（H1 + window 段保留）。

## 通用启示

设计幂等键时，问自己一个问题：

> 如果脚本明天跑、后天跑、一周后跑同一逻辑事件，**这个键会不会变**？

如果会变，就不是幂等键，是**单调键**——只能用来排序 / 标识"这次执行"，不能拿来 dedupe。

常见的"看着唯一其实滚动"的字段：
- 任何 `now() - delta` 的派生量（窗口起止、相对时间标签）
- 文件名带 `today's date` 又用到第二天还跑
- "Last 7 days report" 名字本身

幂等键应该来自**事件本身的内禀属性**：
- ISO 周 / 月 / 季度
- 触发对象 ID（PR 号、commit SHA、issue 号）
- 事件命中的固定时间锚（"周一 00:00 UTC of this iso week"）

## 加固

`find_existing_issue` 还加了 `labels=weekly-retro` 过滤——即使 title 巧合冲突，label scope 也保护：

```python
params = urllib.parse.urlencode({
    "state": "all", "type": "issues", "labels": LABEL_NAME,
    "limit": 50, "sort": "newest",
})
```

## 修复后的清理

- 手动 PATCH 现有 #278 title 去掉日期范围（一次性迁移）
- 关闭 #274（chentao.jia 5/9 手动建的 stub，被 AIBot 创的 #278 取代）
- Local 重跑 `weekly-retro-issue.py` 验证：找到 #278，PATCH body，未创建新 issue ✓
