Files
pinmap-to-pinlist/docs/modification-assessment-v1.5.md

880 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PinMAP ↔ PinList 双向转换器 — v1.5.0 修改需求评估
> **版本**: v1.5.0
> **日期**: 2026-06-06
> **评估人**: 脚本架构师 (Script Architect)
> **状态**: 待审批
> **变更**: 1 个 P0 Bug 修复 + 3 个 P1 模板分离功能F009~F012
---
## 1. 修改需求总览
| 编号 | 类型 | 标题 | 优先级 | 复杂度 | 依赖 |
|------|------|------|--------|--------|------|
| F012 | Bug修复 | 修复 PinMAP 生成中上/下边 PinName 位置 | **P0** | 低 | 无 |
| F009 | 功能 | MAP→List 使用 balllist 模板 | P1 | 中 | 无 |
| F010 | 功能 | List→MAP 使用 ballmap 模板 | P1 | 中 | 无 |
| F011 | 功能 | 模板格式提取式应用 | P1 | 中 | F009, F010 |
---
## 2. 当前代码状态分析
### 2.1 代码库结构v1.4.x 基线)
```
pinmap-to-pinlist/
├── run.bat
├── Code/
│ ├── src/
│ │ ├── main.py # ✏️ 需修改 (F009/F010/F011)
│ │ ├── file_selector.py # (不变)
│ │ ├── models.py # (不变)
│ │ ├── pinlist_generator.py # (不变)
│ │ ├── pinlist_parser.py # (不变)
│ │ ├── pinlist_validator.py # (不变)
│ │ ├── pinmap_generator.py # (不变/需确认 F012 影响)
│ │ ├── pinmap_layout.py # ✏️ 可能需修改 (F012)
│ │ ├── pinmap_parser.py # (不变)
│ │ ├── template_reader.py # ✏️ 可能需修改 (F011)
│ │ ├── utils.py # (不变)
│ │ ├── validator.py # (不变)
│ │ ├── xls_reader.py # (不变)
│ │ ├── xlsx_reader.py # (不变)
│ │ ├── xlsx_writer.py # (不变/需确认 F011 影响)
│ │ └── test_pinmap.py # ✏️ 需更新 (F012)
│ └── docs/
│ ├── architecture-design.md
│ ├── modification-assessment.md
│ ├── QUICKSTART.md
│ ├── README.md
│ ├── RELEASE.md
│ └── team.md
├── docs/
│ ├── bugs.md
│ ├── features.md
│ ├── modification-assessment-v1.3.md
│ ├── requirements.md
│ └── tasks.md
├── Test/
│ ├── fixtures/
│ │ ├── sample_4x4.xlsx
│ │ ├── sample_rect.xlsx
│ │ ├── error_*.xlsx
│ │ └── warning_missing.xlsx
│ ├── run_tests.py
│ └── test_report.md
└── Releases/
```
### 2.2 现有模板机制
当前代码v1.4.x使用**单一模板文件** `PinMAP-Template.xlsx`(位于项目根目录):
```python
# main.py → _find_template_path()
# 查找根目录下的 PinMAP-Template.xlsx
def _find_template_path() -> str | None:
src_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(src_dir))
template_path = os.path.join(root_dir, "PinMAP-Template.xlsx")
...
```
**当前行为**
- `run_map_to_list()`MAP→List`run_list_to_map()`List→MAP都查找同一个 `PinMAP-Template.xlsx`
- 两个方向共用同一个模板文件
- v1.5.0 需求:将两个方向的模板分离
### 2.3 各模块当前实现要点
#### `main.py` — 入口流程编排
```python
# 模板查找:单一模板
def _find_template_path() -> str | None:
"""查找 PinMAP-Template.xlsx"""
# run_map_to_list(): MAP→List 流程
# 使用 _find_template_path() 读取模板 → 应用到 PinList 输出
# run_list_to_map(): List→MAP 流程
# 使用 _find_template_path() 读取模板 → 应用到 PinMAP 输出
```
**关键观察**:两个方向都通过 `_find_template_path()` 查找同一模板。v1.5.0 需要分离。
#### `pinmap_layout.py` — PinMAP 布局计算 + Name 坐标
```python
# 单元格坐标体系0-based
# 左边: 序号在 (r, 0), Name 在 (r, c+1) = (r, 1)
# 下边: 序号在 (rows, c), Name 在 (r-1, c) = (rows-1, c)
# 右边: 序号在 (r, cols), Name 在 (r, c-1) = (r, cols-1)
# 上边: 序号在 (1, c), Name 在 (r+1, c) = (2, c)
def get_name_cell(num_cell, edge_name):
if edge_name == "left": return (r, c+1) # Name 右侧
elif edge_name == "bottom": return (r-1, c) # Name 上方
elif edge_name == "right": return (r, c-1) # Name 左侧
elif edge_name == "top": return (r+1, c) # Name 下方
```
**关于 F012 的关键分析**(详见第 3.1 节):
| 边 | 当前 Name 位置 | 相对序号 | F012 声称"当前" | F012 声称"应改为" |
|----|---------------|---------|----------------|-----------------|
| bottom | `(r-1, c)` = max_row-1 | Name 在序号**上方** | min_row+1 | max_row-1 |
| top | `(r+1, c)` = min_row+1 | Name 在序号**下方** | max_row-1 | min_row+1 |
**结论**:当前代码实际行为已经符合 F012 的"应改为"目标bottom Name 在 max_row-1top Name 在 min_row+1。F012 描述中的"当前"状态可能指向旧版本代码或使用不同坐标基准的描述。详见 3.1 节分析。
#### `pinmap_generator.py` — PinMAP 单元格数据构建
```python
def generate_pinmap(entries, rows, cols, package_info,
template_style=None, output_path=None):
# 1. 计算布局 → layout (dict[str, EdgePins])
# 2. 先写入 PinName 单元格
# 3. 再写入序号单元格(后可覆盖同名单元格)
# 4. 写入文件(模板样式或默认样式)
```
#### `template_reader.py` — 模板样式提取
```python
# 关键能力:
# - 解析 xl/styles.xml → fonts, fills, borders, cellXfs
# - 解析 sheet1.xml → column_widths, row_heights
# - 优雅降级:模板不存在/解析失败 → 返回 None
@dataclass
class TemplateStyle:
fonts: list[FontStyle]
borders: list[BorderStyle]
fills: list[FillStyle]
cell_xfs: list[dict] # xf index → {fontId, borderId, fillId, alignment}
column_widths: dict[int, float]
row_heights: dict[int, float]
```
#### `xlsx_writer.py` — XLSX 输出(含样式)
```python
# StyledXLSXWriter — 生成含 styles.xml 的 xlsx
# _styles_xml(): 读取模板字体/填充/边框 → 构建 styles.xml
# 当前实现:使用硬编码样式(模板字体仅作参考,主体样式内置)
# _sheet_xml(): 应用列宽/行高 + 单元格样式索引
# style_idx: 0=default, 1=centered+border, 2=bold(A1), 3=fill
```
**关键观察**`StyledXLSXWriter._styles_xml()` 目前是**硬编码样式**(内置字体、边框定义),仅从模板读取字体名称/大小作为参考。列宽和行高从模板的**实际行列**读取,但会按输出数据的实际行列扩展。
---
## 3. 逐项修改方案
---
### 3.1 F012: 修复 PinMAP 生成中上/下边 PinName 位置
#### 3.1.1 需求分析
**F012 描述原文**
> 修复 PinMAP 生成中上/下边 PinName 位置
> - 当前:下边 Name 在 min_row+1上边 Name 在 max_row-1
> - 应改为:下边 Name 在 max_row-1上边 Name 在 min_row+1
**4×4 参考示例**
```
A4:1, A5:2, B4:Pin1, B5:Pin2 (上边)
C7:3, D7:4, C6:Pin3, D6:Pin4 (右边)
F5:5, F4:6, E5:Pin5, E4:Pin6 (下边)
D2:7, C2:8, D3:Pin7, C3:Pin8 (左边)
```
#### 3.1.2 代码现状追踪
**当前 `get_name_cell` 实现**
```python
def get_name_cell(num_cell, edge_name):
r, c = num_cell
if edge_name == "left": return (r, c+1) # Name 在序号右侧
elif edge_name == "bottom": return (r-1, c) # Name 在序号上方 (max_row-1)
elif edge_name == "right": return (r, c-1) # Name 在序号左侧
elif edge_name == "top": return (r+1, c) # Name 在序号下方 (min_row+1)
```
**当前 `calculate_layout` 单元格坐标**
```python
# 下边:序号在 (rows, c),即 max_row
# → get_name_cell 返回 (rows-1, c) = max_row-1 ← 上方
#
# 上边:序号在 (1, c),即 min_row
# → get_name_cell 返回 (2, c) = min_row+1 ← 下方
```
#### 3.1.3 关键发现:代码可能已经正确
将 F012 的"应改为"目标与当前代码对比:
| 边 | F012"应改为"目标 | 当前代码实际位置 | 是否一致 |
|----|-----------------|-----------------|---------|
| 下边 (bottom) | max_row-1 | `r-1` = rows-1 = max_row-1 | ✅ 一致 |
| 上边 (top) | min_row+1 | `r+1` = 1+1 = min_row+1 | ✅ 一致 |
**`test_pinmap.py` 中的 4×4 测试数据验证**
```python
# Test data (0-based):
# bottom edge: numbers at (6,2)=3, (6,3)=4; names at (5,2)=Pin3, (5,3)=Pin4
# → max_row=6, names at row 5 = max_row-1 ✓
# top edge: numbers at (1,3)=7, (1,2)=8; names at (2,3)=Pin7, (2,2)=Pin8
# → min_row=1, names at row 2 = min_row+1 ✓
```
**该测试在当前代码中已通过**,表明当前 `get_name_cell` 返回值与测试数据一致。
#### 3.1.4 可能的问题场景
如果确实存在问题,以下场景需排查:
1. **PinMAP 解析方向**`pinmap_parser.py` 解析 PinMAP 时,底边 Name 读自 `max_row-1`,顶边 Name 读自 `min_row+1`。这与生成方向一致。但如果解析时使用了错误的位置假设,则来回转换会导致 Name 错位。
2. **边名称语义混淆**F012 描述中的 `(上边)` `(右边)` `(下边)` `(左边)` 标签可能使用了不同的约定(例如,该标签可能对应的是"Name 边的位置"而非"序号所在边")。
3. **网格区域偏移**:如果 `A1` 被保留为封装信息,网格区域从第 2 行开始1-based → 0-based row=1`min_row``max_row` 的起始值需要重新校准。
#### 3.1.5 修改方案
**方案 A确认代码已正确仅添加回归测试**
如果代码已正确,则:
1. 不修改 `pinmap_layout.py`
2.`test_pinmap.py` 中增加显式的上/下边 Name 位置验证测试
3. 更新测试覆盖率
**方案 B按 F012 描述修改(如果确定存在 Bug**
如果确实需要交换,修改 `get_name_cell`
```python
# 修改前:
elif edge_name == "bottom": return (r-1, c) # Name 在序号上方
elif edge_name == "top": return (r+1, c) # Name 在序号下方
# 修改后(交换 bottom/top 的 Name 位置):
elif edge_name == "bottom": return (r+1, c) # Name 移到序号下方
elif edge_name == "top": return (r-1, c) # Name 移到序号上方
```
**但此修改会导致现有 `test_4x4_parse` 测试失败**,因为测试数据中 bottom Name 在 `max_row-1`
**方案 C仅修改 `pinmap_parser.py` 的 Name 读取位置**
如果问题是解析方向MAP→List使用的位置不正确
修改 `pinmap_parser.py` 中 bottom/top 的 Name 查找行号。
#### 3.1.6 建议
**强烈建议在实施前与需求方确认具体的 Bug 表现**。基于代码分析:
| 事实 | 结论 |
|------|------|
| `get_name_cell` 中 bottom Name 在 max_row-1 | 符合 F012 的"应改为" |
| `get_name_cell` 中 top Name 在 min_row+1 | 符合 F012 的"应改为" |
| 4×4 测试已通过 | 代码内部一致 |
| `pinmap_parser.py` 使用相同约定 | 读写一致 |
| F012 描述中的"当前"与代码不符 | 可能存在描述误差 |
**实施步骤**(按优先级):
1. **先确认问题**:使用实际的 4×4 PinMAP 输入文件,运行完整的 PinList→PinMAP 转换,检查输出
2. **定位差异**:对比输出与预期,定位是 `get_name_cell` 还是 `pinmap_parser` 的差异
3. **修改代码**:根据确认结果修改对应的函数
4. **更新测试**:同步更新 `test_pinmap.py` 中的测试数据和断言
**影响范围**
| 文件 | 修改内容 | 可能性 |
|------|---------|--------|
| `pinmap_layout.py` | 修改 `get_name_cell` 的 bottom/top 逻辑 | 低(可能需要确认) |
| `test_pinmap.py` | 更新测试数据/断言 | 中 |
| `pinmap_parser.py` | 修改 Name 读取行号 | 低 |
**风险评估**
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|---------|
| 修改后破坏往返转换一致性 | **高** | 中 | 运行完整的 MAP→List→MAP 往返测试 |
| 修改后破坏现有用户输出 | 中 | 低 | 确认用户实际使用场景 |
| 与需求方沟通不足导致反复修改 | 中 | 中 | **先确认再改** |
**工作量**0.51 小时(含需求确认)
---
### 3.2 F009: MAP→List 使用 balllist 模板
#### 3.2.1 需求
> PinMAP→PinList 转换方向查找并使用 `BallList-Template.xlsx`,不再共用 PinMAP 模板
**变更前**`run_map_to_list()` 使用 `_find_template_path()``PinMAP-Template.xlsx`
**变更后**`run_map_to_list()` 查找并使用 `BallList-Template.xlsx`
#### 3.2.2 影响范围
仅在 `main.py` 中修改模板查找逻辑,核心转换代码不受影响。
**涉及文件**
| 文件 | 修改级别 | 说明 |
|------|---------|------|
| `main.py` | ✏️ 修改 | 新增 `_find_balllist_template_path()` 函数;修改 `run_map_to_list()` 中的模板查找调用 |
**不涉及**`pinmap_parser.py`, `pinlist_generator.py`, `xlsx_writer.py`, `template_reader.py`(这些模块只接收 `TemplateStyle` 对象,不关心模板文件名)
#### 3.2.3 具体修改方案
**3.2.3.1 新增 `_find_balllist_template_path()` 函数**
```python
def _find_balllist_template_path() -> str | None:
"""查找根目录下的 BallList-Template.xlsx。
搜索顺序:
1. 与 run.bat 同级的根目录
2. 当前工作目录
"""
src_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
template_path = os.path.join(root_dir, "BallList-Template.xlsx")
if os.path.exists(template_path):
return template_path
cwd_template = os.path.join(os.getcwd(), "BallList-Template.xlsx")
if os.path.exists(cwd_template):
return cwd_template
return None
```
**3.2.3.2 修改 `run_map_to_list()` 中的模板查找**
```python
# 修改前:
template_path = _find_template_path()
template_style = None
if template_path:
template_style = read_template_styles(template_path)
# 修改后:
template_path = _find_balllist_template_path()
template_style = None
if template_path:
template_style = read_template_styles(template_path)
if template_style:
print(f"[INFO] 已加载 BallList 模板样式: {template_path}")
else:
print("[WARN] BallList 模板文件存在但解析失败,使用默认样式")
else:
print("[INFO] 未检测到 BallList-Template.xlsx使用默认样式")
```
**3.2.3.3 输出路径保持不变**
PinList 输出路径仍为 `{input_base}_PinList.xlsx`,不受模板变更影响。
#### 3.2.4 风险评估
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|---------|
| 用户未放置 BallList-Template.xlsx | 低 | 高 | 模板不存在 → 优雅降级使用默认样式(已实现) |
| 新旧模板共存产生混淆 | 低 | 低 | 日志明确输出使用的模板文件名 |
| BallList-Template.xlsx 解析失败 | 低 | 低 | template_reader 已有 try-except 降级 |
**工作量**15 分钟
---
### 3.3 F010: List→MAP 使用 ballmap 模板
#### 3.3.1 需求
> PinList→PinMAP 转换方向查找并使用 `BallMAP-Template.xlsx`,不再共用 PinMAP 模板
**变更前**`run_list_to_map()` 使用 `_find_template_path()``PinMAP-Template.xlsx`
**变更后**`run_list_to_map()` 查找并使用 `BallMAP-Template.xlsx`
#### 3.3.2 影响范围
仅在 `main.py` 中修改模板查找逻辑。
**涉及文件**
| 文件 | 修改级别 | 说明 |
|------|---------|------|
| `main.py` | ✏️ 修改 | 新增 `_find_ballmap_template_path()` 函数;修改 `run_list_to_map()` 中的模板查找调用 |
#### 3.3.3 具体修改方案
**3.3.3.1 新增 `_find_ballmap_template_path()` 函数**
```python
def _find_ballmap_template_path() -> str | None:
"""查找根目录下的 BallMAP-Template.xlsx。
搜索顺序:
1. 与 run.bat 同级的根目录
2. 当前工作目录
"""
src_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(src_dir)) # pinmap-to-pinlist/
template_path = os.path.join(root_dir, "BallMAP-Template.xlsx")
if os.path.exists(template_path):
return template_path
cwd_template = os.path.join(os.getcwd(), "BallMAP-Template.xlsx")
if os.path.exists(cwd_template):
return cwd_template
return None
```
**3.3.3.2 修改 `run_list_to_map()` 中的模板查找**
```python
# 修改前:
template_path = _find_template_path()
template_style = None
if template_path:
template_style = read_template_styles(template_path)
...
# 修改后:
template_path = _find_ballmap_template_path()
template_style = None
if template_path:
template_style = read_template_styles(template_path)
if template_style:
print(f"[INFO] 已加载 BallMAP 模板样式: {template_path}")
else:
print("[WARN] BallMAP 模板文件存在但解析失败,使用默认样式")
else:
print("[INFO] 未检测到 BallMAP-Template.xlsx使用默认样式")
```
#### 3.3.4 风险评估
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|---------|
| 用户未放置 BallMAP-Template.xlsx | 低 | 高 | 优雅降级使用默认样式 |
| 新旧模板共存产生混淆 | 低 | 低 | 日志明确输出使用的模板文件名 |
**工作量**15 分钟
---
### 3.4 F011: 模板格式提取式应用
#### 3.4.1 需求
> 从模板仅提取格式信息(字体、边框、对齐、列宽、行高),输出文件行列数由实际 Pin 数量决定,不复制模板行列结构。
**依赖**F009BallList-Template.xlsx+ F010BallMAP-Template.xlsx
#### 3.4.2 当前实现分析
当前模板应用于 `StyledXLSXWriter``xlsx_writer.py`),其行为:
**`_sheet_xml()` 中的列宽/行高处理**
```python
# 当前逻辑:
# 1. 遍历模板 style.column_widths按最大 col 扩展
# 2. 对于模板有定义的列使用模板宽度,否则默认 8.0
# 3. 遍历 style.row_heights有定义的行使用模板行高
col_widths_xml = ''
if self._style and self._style.column_widths:
max_width_col = max(self._style.column_widths.keys())
max_width_col = max(max_width_col, max_col)
for c in range(max_width_col + 1):
width = self._style.column_widths.get(c, 8.0)
...
```
**`_styles_xml()` 中的样式处理**
```python
# 当前逻辑:硬编码 4 种 xf 样式
# - xf 0: default
# - xf 1: centered + thin borderpin cells
# - xf 2: bold + centeredA1
# - xf 3: centered + border + light fillheader
#
# 仅从模板提取:
# - font[0] 的 name/size/color用于 xf 0, 1, 3
# - font[0] 被复制为 font[1]bold 版,用于 xf 2 = A1
```
#### 3.4.3 关键分析:当前实现是否已满足 F011 要求
| F011 要求 | 当前实现 | 是否满足 |
|-----------|---------|---------|
| 仅提取格式信息 | ✅ 模板仅用于样式 | ✅ 已满足 |
| 字体 | ✅ 从模板 font[0] 提取 | ✅ 已满足 |
| 边框 | ❌ 硬编码 thin border | ⚠️ 部分满足 |
| 对齐 | ✅ 硬编码 center/center | ⚠️ 部分满足 |
| 列宽 | ✅ 从模板提取 | ✅ 已满足 |
| 行高 | ✅ 从模板提取(有则不覆盖) | ✅ 已满足 |
| 输出行列由实际 Pin 决定 | ✅ 不复制模板行列结构 | ✅ 已满足 |
| 不复制模板行列结构 | ✅ dim 由数据决定 | ✅ 已满足 |
**核心差距**:当前 `_styles_xml()` 中边框和对齐是硬编码的,未从模板的 `cellXfs` 中提取。F011 要求**完全**从模板提取格式。
#### 3.4.4 修改方案
**目标**:将 `_styles_xml()` 改造为从模板的 `cellXfs``fonts``borders``fills` 中提取并原样输出样式,而非硬编码。
**涉及文件**
| 文件 | 修改级别 | 说明 |
|------|---------|------|
| `template_reader.py` | ✏️ 修改 | 增强样式提取能力:提取 cellXfs 的完整信息(含 numFmtId, xfId 等) |
| `xlsx_writer.py` | ✏️ 修改 | 重写 `_styles_xml()` 使用模板原始样式定义 |
**3.4.4.1 增强 `template_reader.py`**
当前 `TemplateReader._parse_styles_xml()` 已能提取:
- ✅ fontsFontStyle: name, size, bold, italic, color
- ✅ fillsFillStyle: pattern_type, fg_color
- ✅ bordersBorderStyle: top, bottom, left, right, color
- ✅ cell_xfslist[dict]: numFmtId, fontId, fillId, borderId, alignment
**需要增加**
- `cell_xfs` 中的 `xfId` 属性(用于样式继承)
- `cellXfs``count` 属性
当前已提取的信息足以满足 F011。**不需要大幅修改** `template_reader.py`
**3.4.4.2 重写 `xlsx_writer.py` 中的 `_styles_xml()`**
```python
def _styles_xml(self) -> str:
"""Build xl/styles.xml from template styles or defaults."""
s = self._style
# ── Fonts ────────────────────────────────────────────────
if s and s.fonts:
fonts_xml = self._build_fonts_xml(s.fonts)
else:
fonts_xml = self._default_fonts_xml()
# ── Fills ────────────────────────────────────────────────
if s and s.fills:
fills_xml = self._build_fills_xml(s.fills)
else:
fills_xml = self._default_fills_xml()
# ── Borders ──────────────────────────────────────────────
if s and s.borders:
borders_xml = self._build_borders_xml(s.borders)
else:
borders_xml = self._default_borders_xml()
# ── Cell XFs ─────────────────────────────────────────────
if s and s.cell_xfs:
cell_xfs_xml = self._build_cell_xfs_xml(s.cell_xfs)
else:
cell_xfs_xml = self._default_cell_xfs_xml()
return (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">\n'
+ fonts_xml + '\n'
+ fills_xml + '\n'
+ borders_xml + '\n'
+ cell_xfs_xml + '\n'
+ '</styleSheet>'
)
```
**关键设计决策**
| 决策点 | 方案 | 理由 |
|--------|------|------|
| 是否完全复制模板 cellXfs | ✅ 是 | 模板定义什么样式就用什么样式 |
| 是否需要额外样式(如 A1 bold | ⚠️ 在模板中应有对应的 xf | 鼓励用户在模板中定义 A1 的样式 |
| 无模板时的默认样式 | 保持现有硬编码 | 向后兼容 |
| 列宽/行高处理 | 保持现有逻辑 | 已满足 F011 |
**3.4.4.3 cellXfs 映射策略**
模板的 cellXfs 索引需要映射到输出单元格:
- 模板中可能有多种 xf如 xf 0=default, xf 1=边框, xf 2=粗体...
- 输出时需决定每个单元格使用哪个 xf
**推荐策略**
1. 分析模板 cellXfs找到最合适的 "pin cell" 样式(通常是带边框+居中的 xf
2. A1 使用模板中带 bold 的 xf如果存在否则用 pin cell 样式
3. 普通 pin cell 使用模板中的 "pin cell" xf
4. 如果模板只有 1 个 xfdefault使用默认样式作为补充
**简化方案**(推荐首版):
- 保留现有的 4 个样式槽位xf 0~3
- 但从模板读取实际的字体、边框、填充定义填充到对应槽位
- 而非硬编码 "thin" 边框和 "center" 对齐
**实现优先级**
1. **字体信息**:已从模板读取 ✅
2. **边框样式**:从模板 borders 读取 → 替换硬编码的 "thin"
3. **填充样式**:从模板 fills 读取 → 替换硬编码的 "FFF0F0F0"
4. **对齐方式**:从模板 alignment 读取 → 替换硬编码的 "center"
5. **列宽/行高**:已从模板读取 ✅
#### 3.4.5 风险评估
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|---------|
| 模板 cellXfs 结构与输出不匹配 | 中 | 中 | 提供合理的 fallback至少 xf 0 和 xf 1 |
| 模板无边框定义导致输出无网格线 | 中 | 低 | 模板无边框时保留默认 thin border |
| 样式重写引入 OOXML 兼容性问题 | 低 | 低 | 使用 Python 标准库 XML 构建,避免硬编码 namespace 错误 |
| 已有用户无模板场景不受影响 | 低 | - | 无模板时完全回退到现有硬编码样式 |
**工作量**23 小时
---
## 4. 模板文件命名规范建议
### 4.1 v1.5.0 新模板命名
| 用途 | 文件名 | 位置 | 格式 |
|------|--------|------|------|
| MAP→List 输出样式 | `BallList-Template.xlsx` | 项目根目录(与 run.bat 同级) | `.xlsx` |
| List→MAP 输出样式 | `BallMAP-Template.xlsx` | 项目根目录(与 run.bat 同级) | `.xlsx` |
### 4.2 向后兼容
| 旧文件名 | 状态 | 说明 |
|---------|------|------|
| `PinMAP-Template.xlsx` | ⚠️ 废弃(不再自动查找) | v1.5.0 后不再被代码引用 |
### 4.3 命名规范说明
```
模板文件名格式:
{输出格式简称}-Template.xlsx
其中:
- BallList → PinMAP→PinList 转换的输出格式PinList
- BallMAP → PinList→PinMAP 转换的输出格式PinMAP
命名原则:
1. 以输出格式命名,而非输入格式
2. 使用 "Ball" 前缀避免与 "Pin" 名混淆
3. 保持 PascalCase 风格
4. 使用 "-Template" 后缀明确表示模板文件
```
### 4.4 搜索路径优先级
```
1. {project_root}/BallList-Template.xlsx (与 run.bat 同级)
2. {cwd}/BallList-Template.xlsx (当前工作目录)
3. 无 → 使用默认样式(硬编码 Calibri 11pt + thin border + center align
```
---
## 5. 修改影响矩阵
| 文件 | F012 | F009 | F010 | F011 | 总改动量 | 备注 |
|------|------|------|------|------|---------|------|
| `main.py` | — | ✏️ 模板查找 | ✏️ 模板查找 | — | ~30行 | 新增2个函数+修改2处调用 |
| `pinmap_layout.py` | ✏️ 可能 | — | — | — | ~5行 | 仅在确认问题后修改 |
| `template_reader.py` | — | — | — | ✏️ 增强 | ~20行 | 增加 xfId 提取 |
| `xlsx_writer.py` | — | — | — | ✏️ 重写 | ~100行 | 重写 _styles_xml() |
| `test_pinmap.py` | ✏️ 更新 | — | — | — | ~10行 | 增加 F012 回归测试 |
| **合计** | **2 文件** | **1 文件** | **1 文件** | **2 文件** | **4-5 文件** | |
---
## 6. 优先级排序
| 优先级 | 编号 | 原因 | 推荐执行顺序 |
|--------|------|------|------------|
| **P0** | F012 | 核心布局 Bug需先确认影响所有 List→MAP 转换 | 第1先确认再决定是否修改 |
| **P1-1** | F009 | MAP→List 模板分离,独立于其他改动 | 第2可与 F010 并行) |
| **P1-2** | F010 | List→MAP 模板分离,独立于其他改动 | 第2可与 F009 并行) |
| **P1-3** | F011 | 依赖 F009+F010 的模板文件存在后才能测试 | 第3 |
---
## 7. 测试要点
### 7.1 F012 测试P0
#### 核心验证
| 测试项 | 输入 | 预期 | 方法 |
|--------|------|------|------|
| 4×4 PinMAP 往返一致性 | PinList(rows=4, cols=4, 8 Pins) → PinMAP → PinList | 往返后 PinList 数据不变 | 自动化 |
| 下边 Name 位置 | rows=4, cols=4 的 PinMAP 输出 | 下边 PinName 在序号行上方一行max_row-1 | 检查输出 xlsx |
| 上边 Name 位置 | rows=4, cols=4 的 PinMAP 输出 | 上边 PinName 在序号行下方一行min_row+1 | 检查输出 xlsx |
| 与 parser 一致性 | 生成的 PinMAP 再用 parser 解析 | 解析出的 Pin 数据和原始一致 | 自动化 |
| 非方形网格 | rows=3, cols=5 的 PinMAP | 上/下边 Name 位置正确 | 检查输出 |
#### 边界条件
| 测试项 | 输入 | 预期 |
|--------|------|------|
| 最小网格 2×2 | PinList(rows=2, cols=2, 8 Pins) | 四条边各 2 Pin角点正确 |
| 大网格 15×15 | PinList(rows=15, cols=15, 60 Pins) | 60 Pin 全部正确分配到四条边 |
| max_row-1 与 min_row+1 重合 | rows=2min_row=1, max_row=2 → max_row-1=1=min_row | Name 不与序号重叠 |
#### 现有测试回归
- `test_4x4_parse()` — 确认不受影响或同步更新
- `test_4x4_validate()` — 确认不受影响
- `test_12pin_square()` — 确认不受影响
### 7.2 F009 测试P1
| 测试项 | 方法 | 预期 |
|--------|------|------|
| BallList-Template.xlsx 存在时加载 | 放置模板 → MAP→List 转换 | 日志显示"已加载 BallList 模板样式" |
| BallList-Template.xlsx 不存在时降级 | 删除模板 → MAP→List 转换 | 日志显示"未检测到 BallList-Template.xlsx",输出使用默认样式 |
| 不加载 PinMAP-Template.xlsx | 仅放置 PinMAP-Template.xlsx → MAP→List 转换 | 不加载旧模板,使用默认样式 |
| 模板样式应用到 PinList 输出 | 放置带自定义字体/边框的模板 → 转换 | 输出 PinList 使用模板字体和边框 |
### 7.3 F010 测试P1
| 测试项 | 方法 | 预期 |
|--------|------|------|
| BallMAP-Template.xlsx 存在时加载 | 放置模板 → List→MAP 转换 | 日志显示"已加载 BallMAP 模板样式" |
| BallMAP-Template.xlsx 不存在时降级 | 删除模板 → List→MAP 转换 | 日志显示"未检测到 BallMAP-Template.xlsx",输出使用默认样式 |
| 不加载 PinMAP-Template.xlsx | 仅放置 PinMAP-Template.xlsx → List→MAP 转换 | 不加载旧模板,使用默认样式 |
### 7.4 F011 测试P1
| 测试项 | 方法 | 预期 |
|--------|------|------|
| 模板字体正确应用 | 模板字体=微软雅黑 12pt → 输出 | 输出文件字体为微软雅黑 12pt |
| 模板边框正确应用 | 模板边框=medium → 输出 | 输出文件边框为 medium 而非 thin |
| 模板填充正确应用 | 模板背景色=黄色 → 输出 | 输出对应单元格有黄色背景 |
| 模板对齐正确应用 | 模板左对齐 → 输出 | 输出文件单元格左对齐 |
| 列宽与模板一致 | 模板 A 列宽=20 → 输出 | 输出对应列宽=20 |
| 行高与模板一致 | 模板行高=25 → 输出 | 输出对应行高=25 |
| 输出行列由实际 Pin 决定 | rows=5, cols=5 → 输出 | 输出为 5×5 网格,不含模板的额外行列 |
| 无模板时使用默认样式 | 无模板文件 → 输出 | 使用 Calibri 11pt + thin border + center align |
### 7.5 集成测试
| 测试项 | 输入 | 预期 |
|--------|------|------|
| 往返转换 (MAP→List→MAP) | sample_4x4.xlsx → PinList → PinMAP | 与原始 PinMAP 一致 |
| 往返转换 (List→MAP→List) | PinList 文件 → PinMAP → PinList | 与原始 PinList 一致 |
| 两个方向使用不同模板 | BallList-Template 和 BallMAP-Template 同时存在 | MAP→List 用 BallListList→MAP 用 BallMAP |
| 一个大模板一个小模板 | BallList 字体=12pt, BallMAP 字体=10pt | 各方向使用各自的字体 |
---
## 8. 开发顺序建议
```
阶段 1: F012 需求确认30 分钟)
├─ 用实际 PinMAP 输入文件测试当前代码的 PinName 位置
├─ 确认 Bug 具体表现
└─ 决定修改方案A/B/C
阶段 2: F009 + F010 模板分离30 分钟,可并行)
├─ main.py: 新增 _find_balllist_template_path()
├─ main.py: 新增 _find_ballmap_template_path()
├─ main.py: 修改 run_map_to_list() 模板查找
├─ main.py: 修改 run_list_to_map() 模板查找
└─ 废弃 _find_template_path()(可选,保留无调用不影响)
阶段 3: F012 修复如需要3060 分钟)
├─ pinmap_layout.py: 修改 get_name_cell()
├─ test_pinmap.py: 更新测试数据和断言
└─ 运行完整测试套件
阶段 4: F011 格式提取23 小时)
├─ template_reader.py: 增加提取项
├─ xlsx_writer.py: 重写 _styles_xml()
├─ xlsx_writer.py: 新增 _build_*_xml() 辅助函数
└─ 模板+无模板场景测试
阶段 5: 收尾30 分钟)
├─ 更新 CHANGELOG.md
├─ 更新 features.md 状态
├─ 更新 VERSION 文件
└─ 运行完整回归测试
```
---
## 9. 风险评估汇总
| 风险 | 影响 | 概率 | 缓解措施 | 相关功能 |
|------|------|------|---------|---------|
| F012 需求描述与代码行为不一致,修改方向错误 | **高** | 中 | **先确认再改**,使用实际文件测试 | F012 |
| F012 修改破坏往返转换一致性 | **高** | 中 | 运行完整 MAP→List→MAP 往返测试 | F012 |
| 模板 cellXfs 结构与输出不兼容 | 中 | 中 | 提供 fallback模板解析失败 = 默认样式 | F011 |
| 用户未放置新模板文件 | 低 | 高 | 优雅降级,日志明确提示缺失的模板名 | F009/F010 |
| 旧 PinMAP-Template.xlsx 被意外加载 | 低 | 低 | 显式删除旧模板查找调用 | F009/F010 |
| F011 样式重写引入 OOXML 兼容性问题 | 低 | 低 | 严格 XML 构建,测试 Excel/WPS 打开 | F011 |
| 两个模板同时存在造成混淆 | 低 | 低 | 日志明确标注每个方向使用的模板文件名 | F009/F010 |
---
## 10. 工作量估算
| 阶段 | 任务 | 预估时间 | 依赖 |
|------|------|---------|------|
| 确认 | F012 需求确认(测试+分析) | 30 分钟 | 无 |
| 开发 | F009 MAP→List 模板分离 | 15 分钟 | 无 |
| 开发 | F010 List→MAP 模板分离 | 15 分钟 | 无 |
| 开发 | F012 修复(如需要) | 3060 分钟 | 需求确认 |
| 开发 | F011 模板格式提取式应用 | 23 小时 | F009+F010 |
| 测试 | 单元测试更新 | 30 分钟 | 所有开发 |
| 测试 | 集成/回归测试 | 30 分钟 | 所有开发 |
| 文档 | CHANGELOG + features.md 更新 | 15 分钟 | 所有开发 |
| **总计** | | **4.56 小时** | |
---
## 11. 总结
| 项目 | 内容 |
|------|------|
| 修改文件数 | 45 个main.py, pinmap_layout.py, template_reader.py, xlsx_writer.py, test_pinmap.py |
| 新增模板文件 | 2 个BallList-Template.xlsx, BallMAP-Template.xlsx |
| 影响核心模块 | 是xlsx_writer.py 样式生成逻辑重写) |
| 技术难度 | 中F011 样式重写需理解 OOXML styles.xml 结构) |
| 预估工作量 | 4.56 小时 |
| 推荐 Agent | Python 编码 Agent |
| 风险等级 | 中F012 需先确认F011 样式重写有复杂度) |
**关键结论**
1. **F012P0**代码当前行为可能已经正确bottom Name 在 max_row-1top Name 在 min_row+1 已与 F012 "应改为"目标一致)。**强烈建议在实施前用实际文件验证**,避免过度修改。如果确认无误,仅需增加回归测试。
2. **F009+F010P1**:改动量小(约 30 行),完全独立,可快速完成。核心是新增两个模板查找函数并替换调用点。
3. **F011P1**:改动量最大(约 120 行),需重写 `xlsx_writer.py``_styles_xml()` 方法。当前代码已部分满足 F011字体、列宽、行高从模板提取主要差距在边框和对齐的硬编码。建议采用"从模板读取实际值填充现有样式槽位"的渐进方案。
4. **向后兼容**:无模板时所有功能完全回退到现有默认样式,不影响已有用户。
5. **推荐开发顺序**:确认 F012 → 实现 F009+F010 → 修复 F012如需要 → 实现 F011 → 测试收尾。
---
*文档结束 — 请审批后进入编码阶段*
##