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

35 KiB
Raw Blame History

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→Listrun_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-1top 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 可能的问题场景

如果确实存在问题,以下场景需排查:

  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=1min_rowmax_row 的起始值需要重新校准。

3.1.5 修改方案

方案 A确认代码已正确仅添加回归测试

如果代码已正确,则:

  1. 不修改 pinmap_layout.py
  2. test_pinmap.py 中增加显式的上/下边 Name 位置验证测试
  3. 更新测试覆盖率

方案 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 描述中的"当前"与代码不符 可能存在描述误差

实施步骤(按优先级):

  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() 函数

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 数量决定,不复制模板行列结构。

依赖F009BallList-Template.xlsx+ F010BallMAP-Template.xlsx

3.4.2 当前实现分析

当前模板应用于 StyledXLSXWriterxlsx_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 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() 改造为从模板的 cellXfsfontsbordersfills 中提取并原样输出样式,而非硬编码。

涉及文件

文件 修改级别 说明
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 属性(用于样式继承)
  • cellXfscount 属性

当前已提取的信息足以满足 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

推荐策略

  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 → 测试收尾。


文档结束 — 请审批后进入编码阶段