35 KiB
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(位于项目根目录):
# 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 — 入口流程编排
# 模板查找:单一模板
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 坐标
# 单元格坐标体系(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-1,top Name 在 min_row+1)。F012 描述中的"当前"状态可能指向旧版本代码或使用不同坐标基准的描述。详见 3.1 节分析。
pinmap_generator.py — PinMAP 单元格数据构建
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 — 模板样式提取
# 关键能力:
# - 解析 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 输出(含样式)
# 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 实现:
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 单元格坐标:
# 下边:序号在 (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 测试数据验证:
# 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 可能的问题场景
如果确实存在问题,以下场景需排查:
-
PinMAP 解析方向:
pinmap_parser.py解析 PinMAP 时,底边 Name 读自max_row-1,顶边 Name 读自min_row+1。这与生成方向一致。但如果解析时使用了错误的位置假设,则来回转换会导致 Name 错位。 -
边名称语义混淆:F012 描述中的
(上边)(右边)(下边)(左边)标签可能使用了不同的约定(例如,该标签可能对应的是"Name 边的位置"而非"序号所在边")。 -
网格区域偏移:如果
A1被保留为封装信息,网格区域从第 2 行开始(1-based → 0-based row=1),则min_row和max_row的起始值需要重新校准。
3.1.5 修改方案
方案 A:确认代码已正确,仅添加回归测试
如果代码已正确,则:
- 不修改
pinmap_layout.py - 在
test_pinmap.py中增加显式的上/下边 Name 位置验证测试 - 更新测试覆盖率
方案 B:按 F012 描述修改(如果确定存在 Bug)
如果确实需要交换,修改 get_name_cell:
# 修改前:
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 描述中的"当前"与代码不符 | 可能存在描述误差 |
实施步骤(按优先级):
- 先确认问题:使用实际的 4×4 PinMAP 输入文件,运行完整的 PinList→PinMAP 转换,检查输出
- 定位差异:对比输出与预期,定位是
get_name_cell还是pinmap_parser的差异 - 修改代码:根据确认结果修改对应的函数
- 更新测试:同步更新
test_pinmap.py中的测试数据和断言
影响范围:
| 文件 | 修改内容 | 可能性 |
|---|---|---|
pinmap_layout.py |
修改 get_name_cell 的 bottom/top 逻辑 |
低(可能需要确认) |
test_pinmap.py |
更新测试数据/断言 | 中 |
pinmap_parser.py |
修改 Name 读取行号 | 低 |
风险评估:
| 风险 | 影响 | 概率 | 缓解措施 |
|---|---|---|---|
| 修改后破坏往返转换一致性 | 高 | 中 | 运行完整的 MAP→List→MAP 往返测试 |
| 修改后破坏现有用户输出 | 中 | 低 | 确认用户实际使用场景 |
| 与需求方沟通不足导致反复修改 | 中 | 中 | 先确认再改 |
工作量:0.5–1 小时(含需求确认)
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() 函数:
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() 中的模板查找:
# 修改前:
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() 函数:
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() 中的模板查找:
# 修改前:
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 数量决定,不复制模板行列结构。
依赖:F009(BallList-Template.xlsx)+ F010(BallMAP-Template.xlsx)
3.4.2 当前实现分析
当前模板应用于 StyledXLSXWriter(xlsx_writer.py),其行为:
_sheet_xml() 中的列宽/行高处理:
# 当前逻辑:
# 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() 中的样式处理:
# 当前逻辑:硬编码 4 种 xf 样式
# - xf 0: default
# - xf 1: centered + thin border(pin cells)
# - xf 2: bold + centered(A1)
# - xf 3: centered + border + light fill(header)
#
# 仅从模板提取:
# - 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() 已能提取:
- ✅ fonts(FontStyle: name, size, bold, italic, color)
- ✅ fills(FillStyle: pattern_type, fg_color)
- ✅ borders(BorderStyle: top, bottom, left, right, color)
- ✅ cell_xfs(list[dict]: numFmtId, fontId, fillId, borderId, alignment)
需要增加:
cell_xfs中的xfId属性(用于样式继承)cellXfs的count属性
当前已提取的信息足以满足 F011。不需要大幅修改 template_reader.py。
3.4.4.2 重写 xlsx_writer.py 中的 _styles_xml():
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
推荐策略:
- 分析模板 cellXfs,找到最合适的 "pin cell" 样式(通常是带边框+居中的 xf)
- A1 使用模板中带 bold 的 xf(如果存在),否则用 pin cell 样式
- 普通 pin cell 使用模板中的 "pin cell" xf
- 如果模板只有 1 个 xf(default),使用默认样式作为补充
简化方案(推荐首版):
- 保留现有的 4 个样式槽位(xf 0~3)
- 但从模板读取实际的字体、边框、填充定义填充到对应槽位
- 而非硬编码 "thin" 边框和 "center" 对齐
实现优先级:
- 字体信息:已从模板读取 ✅
- 边框样式:从模板 borders 读取 → 替换硬编码的 "thin"
- 填充样式:从模板 fills 读取 → 替换硬编码的 "FFF0F0F0"
- 对齐方式:从模板 alignment 读取 → 替换硬编码的 "center"
- 列宽/行高:已从模板读取 ✅
3.4.5 风险评估
| 风险 | 影响 | 概率 | 缓解措施 |
|---|---|---|---|
| 模板 cellXfs 结构与输出不匹配 | 中 | 中 | 提供合理的 fallback(至少 xf 0 和 xf 1) |
| 模板无边框定义导致输出无网格线 | 中 | 低 | 模板无边框时保留默认 thin border |
| 样式重写引入 OOXML 兼容性问题 | 低 | 低 | 使用 Python 标准库 XML 构建,避免硬编码 namespace 错误 |
| 已有用户无模板场景不受影响 | 低 | - | 无模板时完全回退到现有硬编码样式 |
工作量:2–3 小时
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=2(min_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 用 BallList,List→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 修复(如需要,30–60 分钟)
├─ pinmap_layout.py: 修改 get_name_cell()
├─ test_pinmap.py: 更新测试数据和断言
└─ 运行完整测试套件
阶段 4: F011 格式提取(2–3 小时)
├─ 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 修复(如需要) | 30–60 分钟 | 需求确认 |
| 开发 | F011 模板格式提取式应用 | 2–3 小时 | F009+F010 |
| 测试 | 单元测试更新 | 30 分钟 | 所有开发 |
| 测试 | 集成/回归测试 | 30 分钟 | 所有开发 |
| 文档 | CHANGELOG + features.md 更新 | 15 分钟 | 所有开发 |
| 总计 | 4.5–6 小时 |
11. 总结
| 项目 | 内容 |
|---|---|
| 修改文件数 | 4–5 个(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.5–6 小时 |
| 推荐 Agent | Python 编码 Agent |
| 风险等级 | 中(F012 需先确认,F011 样式重写有复杂度) |
关键结论:
-
F012(P0):代码当前行为可能已经正确(bottom Name 在 max_row-1,top Name 在 min_row+1 已与 F012 "应改为"目标一致)。强烈建议在实施前用实际文件验证,避免过度修改。如果确认无误,仅需增加回归测试。
-
F009+F010(P1):改动量小(约 30 行),完全独立,可快速完成。核心是新增两个模板查找函数并替换调用点。
-
F011(P1):改动量最大(约 120 行),需重写
xlsx_writer.py的_styles_xml()方法。当前代码已部分满足 F011(字体、列宽、行高从模板提取),主要差距在边框和对齐的硬编码。建议采用"从模板读取实际值填充现有样式槽位"的渐进方案。 -
向后兼容:无模板时所有功能完全回退到现有默认样式,不影响已有用户。
-
推荐开发顺序:确认 F012 → 实现 F009+F010 → 修复 F012(如需要) → 实现 F011 → 测试收尾。
文档结束 — 请审批后进入编码阶段