# 🧱 B19｜AI 生成模块化脚本与错误处理 — 配套资产

> **系列**：AI 办公实战：从小白到达人
> **文章号**：B19（Track 3 自动化 / Level 3 进阶）
> **配套中文标题**：AI 生成模块化脚本与错误处理
> **配套英文标题**：Modular Scripts & Error Handling

---

## 一、这是给谁的？解决什么问题？

写给**写过一两段脚本，但脚本一长就崩、报错红字看不懂、第二次跑又得改路径**的朋友。问题集中在三件事没做：

- **没拆模块**：一两百行"一坨"，改个路径得翻代码 80 行；
- **没兜错误**：第三个文件出错，整个脚本"啪"地崩了，前面白跑；
- **没记日志**：出错了只甩一行红字，不知道哪个文件、哪一步炸的。

本目录给你一套**真实可运行**的"模块化 + try/catch + 日志 + 参数化"骨架：

| 文件 | 干什么 |
|------|--------|
| `Logging_zh.psm1` | 模块：Write-Log 函数（同时输出到屏幕 + 文件） |
| `Merge-OneCsv_zh.ps1` | 函数：合并"一个"CSV 到总表，带 try/catch 兜底 |
| `合并报表_zh.ps1` | 主流程：参数化入口，把文件夹里所有 CSV 合并 |
| `调用示例_zh.ps1` | 演示：3 种调用方式 + 故意造坏文件看错误处理 |

跑完本目录，你就能拿到一份"拆得开、兜得住、查得到"的脚本模板——以后换自己的活儿（重命名、转格式、发邮件都行），模块和日志原样留着，主体改一改就上线。

---

## 二、使用方法（5 步跑通示例）

### 第一步：环境准备

- Windows 10 / 11 自带 PowerShell；
- 第一次跑 `.ps1` 被拦？先执行：
  ```powershell
  Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
  ```

### 第二步：把 .ps1 / .psm1 存成"UTF-8 带 BOM"

这是 PowerShell 5.1 的硬性要求（脚本里有中文注释时尤其重要）：

- **VS Code**：右下角点"UTF-8" → 选"**Save with Encoding**" → 选 **UTF-8 with BOM**（对 4 个 .ps1 + 2 个 .psm1 都做）；
- **记事本**：另存为 → 编码选 **UTF-8**（默认就是带 BOM 的 ANSI/UTF-8，但注意不是"UTF-8 无 BOM"）；
- **懒得管？** 升级到 PowerShell 7（`winget install Microsoft.PowerShell`），它能认 UTF-8 无 BOM，从此再也不用管编码。

### 第三步：准备测试 CSV

```powershell
mkdir 报表
@'
name,amount
A,100
B,200
'@ | Out-File -Encoding UTF8 报表\a.csv
@'
name,amount
C,300
'@ | Out-File -Encoding UTF8 报表\b.csv
```

> 想演示"坏文件"被 try/catch 兜住？再造一份损坏的 CSV 即可（直接用本目录 `调用示例_zh.ps1`，它自带造文件逻辑）。

### 第四步：跑主流程脚本

```powershell
# 进入 B19 目录
cd "C:\Users\cynix\WorkBuddy\VBA.net\配套资产\B19_模块化脚本"

# 用参数化方式跑（路径不写死，全在命令行里）
powershell -ExecutionPolicy Bypass -File .\合并报表_zh.ps1 `
    -SourceFolder ".\报表" `
    -OutputFile   ".\总表.csv" `
    -LogFile      ".\运行日志.log"
```

跑完会生成：
- `总表.csv`：合并结果，多了一列 `来源文件`（方便溯源）；
- `运行日志.log`：每一步的 `[时间][级别] 消息`，包括坏文件的失败原因。

### 第五步：跑调用示例看 3 种调用方式

```powershell
powershell -ExecutionPolicy Bypass -File .\调用示例_zh.ps1
```

它会：① 演示如何点 sourcing 调 `Merge-OneCsv`；② 直接调 `合并报表_zh.ps1`；③ 故意造坏 CSV 验证 try/catch。

---

## 三、文件清单

| 文件名 | 类型 | 作用 | 行数 |
|--------|------|------|------|
| `B19_模块化脚本_中文README.md` | Markdown | 本文件 | — |
| `B19_ModularScript_EnglishREADME.md` | Markdown | 英文版 README | — |
| `Logging_zh.psm1` | 模块 | `Write-Log` 函数（中文） | ~60 |
| `Logging_en.psm1` | 模块 | `Write-Log` 函数（英文） | ~60 |
| `Merge-OneCsv_zh.ps1` | 函数 | 合并单个 CSV + try/catch（中文） | ~50 |
| `Merge-OneCsv_en.ps1` | 函数 | 同上（英文） | ~50 |
| `合并报表_zh.ps1` | 流程 | 主流程脚本，参数化入口（中文） | ~70 |
| `合并报表_en.ps1` | 流程 | 同上（英文） | ~70 |
| `调用示例_zh.ps1` | 示例 | 3 种调用方式 + 坏文件演示 | ~90 |
| `调用示例_en.ps1` | 示例 | 同上（英文） | ~90 |

---

## 四、实战代码（关键片段逐行解读）

### ① 模块 Logging_zh.psm1 的 Write-Log 函数

```powershell
function Write-Log {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,
        [ValidateSet('INFO', 'WARN', 'ERROR')]
        [string]$Level = 'INFO'
    )
    $time = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $line = "[$time][$Level] $Message"

    # 同时输出到屏幕 + 日志文件；-Encoding UTF8 避免中文乱码
    Add-Content -Path $script:LogFile -Value $line -Encoding UTF8
    Write-Host $line
}
```

**核心三行**：`Get-Date` 取时间戳；`Add-Content -Encoding UTF8` 写文件（必须显式 UTF8，否则中文乱码）；`Write-Host` 回显屏幕。
**ValidateSet**：限定 `$Level` 只能是 `INFO / WARN / ERROR`，写错会立刻报错（避免拼成 `INFOO` 这种小错）。

### ② Merge-OneCsv_zh.ps1 的 try/catch 兜底

```powershell
function Merge-OneCsv {
    param([string]$SourceFile, [string]$OutputFile)
    try {
        $rows = Import-Csv -Path $SourceFile -Encoding UTF8 -ErrorAction Stop   # ← 注意 Stop
        $rows | Export-Csv -Path $OutputFile -Encoding UTF8 -NoTypeInformation `
                           -Append -ErrorAction Stop
        Write-Log -Message "已合并：$SourceFile" -Level 'INFO'
    }
    catch {
        Write-Log -Message "合并失败：$SourceFile —— $($_.Exception.Message)" -Level 'ERROR'
        # 注意：这里不 throw —— 让脚本继续处理下一个文件
    }
}
```

**两处关键**：
- `Import-Csv -ErrorAction Stop`：PowerShell 默认出错只警告、不进 `catch`；只有加 `Stop`，错误才真正被抓住。
- `catch` 里**不 `throw`**：只记日志不抛出，所以下一个文件照常处理——这正是"某个文件坏了不影响其他"的实现方式。

### ③ 主流程 `合并报表_zh.ps1` 的参数化入口

```powershell
# ========== 参数化入口（必须放在脚本最前面）==========
param(
    [string]$SourceFolder = '.\报表',
    [string]$OutputFile   = '.\总表.csv',
    [string]$LogFile      = '.\运行日志.log'
)
```

**三个好处**：① 改路径只动命令行、不动代码；② 默认值让脚本"开箱即用"；③ 别人不用打开代码就知道有哪些可调参数。

---

## 五、常见问题 / 避坑指南

1. **中文乱码 / 解析报"缺少终结符"？**
   99% 是 `.ps1` 没存成 UTF-8 BOM。VS Code → 右下角编码 → "Save with Encoding" → 选 UTF-8 with BOM；或者干脆装 PowerShell 7。

2. **错误不进 catch？**
   `Import-Csv` 默认出错只警告。加 `-ErrorAction Stop` 才会真进 `catch`。本文每个 PowerShell 数据命令后面都带上了。

3. **日志里中文变 `?????`？**
   `Add-Content -Encoding UTF8` 必须显式指定。PowerShell 5.1 默认写文件不是 UTF-8，换台机器就乱码。

4. **`Export-Csv -Append` 报错？**
   `-Append` 是 PowerShell 3.0+ 才有的参数。Win7 / 早期 Windows 默认是 2.0，得把结果先收集到变量、最后一次性 `Export-Csv`。

5. **Excel COM 对象卡后台关不掉？**
   `try { ... } finally { $excel.Quit(); ReleaseComObject($excel) }` —— `finally` 块是 COM 对象的"必跑收尾"。漏了就等着看任务管理器里一堆 `EXCEL.EXE`。

---

## 六、下一步

跑通本文档，你就有了一套可复用的"模块化 + 错误处理 + 日志 + 参数化"骨架。下一步推荐两个方向：

- **B20《自动化流水线》**：把今天的 `合并报表_zh.ps1` 接上 Windows 任务计划程序，每天凌晨自动合并昨日报表，你睡醒直接看总表；或者干脆不写代码——Excel 里的 **Power Query**（数据 → 获取数据 → 从文件夹）点几下鼠标就能做"合并 CSV"，是脚本的"无代码平替"。
- **B17 / B18 基础补课**：如果你还没配好环境、看到黑框就手心出汗，先回看 B17 跑通第一条命令；觉得脚本还不顺手，再回 B18 把"批量 Word/Excel"的实战跑一遍。

---

> 📌 **快速核查清单**（跑之前再确认一次）：
> - [ ] `.ps1` / `.psm1` 都是 UTF-8 **带 BOM**（PowerShell 5.1 必需）；
> - [ ] 跑过 `Set-ExecutionPolicy RemoteSigned -Scope CurrentUser`；
> - [ ] 改了 `-SourceFolder / -OutputFile / -LogFile` 的路径；
> - [ ] 测试 CSV 的字段名（表头）一致；
> - [ ] `Import-Csv` / `Export-Csv` 都带了 `-Encoding UTF8 -ErrorAction Stop`。