docs: mark CRM name pinyin abbr design implemented and note go-live checklist.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-07 17:34:55 +08:00
1 parent 021bd73301
commit 2ccc3b612a
2 files changed
+701 -4

No files matched your search

@@ -0,0 +1,696 @@
# CRM 产品/客户拼音首字母检索 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为 `crm_product` / `crm_customer` 维护小写拼音首字母字段,并在列表与业务选择器检索中支持大小写不敏感的拼音模糊匹配。
**Architecture:** 后端 `pypinyin` 统一生成工具函数;表内冗余 `name_pinyin_abbr`;create/update 按「空才自动填、手改不覆盖」写入;list/options 的 keyword(及客户 `customerName`)OR `LOWER(name_pinyin_abbr) LIKE`;前端表单可编辑 +「重新生成」;上线一次回填空字段。
**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2 + `pypinyin`;Vue 3 + Element Plus;pytest。
**Spec:** `docs/superpowers/specs/2026-08-07-crm-name-pinyin-abbr-design.md`
---
## File map
| 文件 | 职责 |
|------|------|
| `backend/app/utils/pinyin_abbr.py` | 生成 / 归一化拼音首字母 |
| `backend/tests/test_pinyin_abbr.py` | 生成规则单测 |
| `docs/sql/migrations/2026-08-07-crm-name-pinyin-abbr.sql` | DDL |
| `docs/sql/shanxia2.0.sql` | 基线表结构同步加列 |
| `backend/app/models/crm_models.py` | `CrmProduct` / `CrmCustomer` 加列 |
| `backend/requirements.txt` | 增加 `pypinyin` |
| `backend/app/schemas/crm/product.py` | Create/Update 加 `namePinyinAbbr` |
| `backend/app/schemas/crm/customer.py` | 同上 |
| `backend/app/crud/crm/product.py` | dict / create / update / list / options |
| `backend/app/crud/crm/customer.py` | 同上 |
| `backend/tests/test_crm_product_options_keyword.py` | options 仍不匹配编号,但可匹配拼音 |
| `backend/tests/test_crm_name_pinyin_search.py` | list keyword 含拼音 OR 的断言 |
| `backend/scripts/backfill_name_pinyin_abbr.py` | 存量空字段回填 |
| `web-admin/src/api/crm/product.ts` | `ProductFormData` 类型 |
| `web-admin/src/components/business/product-form-dialog/index.vue` | 表单字段 + 重新生成 |
| `web-admin/src/views/crm/product/index.vue` | placeholder / 查看弹窗 |
| `web-admin/src/components/business/product-selector/index.vue` | placeholder |
| `web-admin/src/components/business/db/DbCrmProductSelect.vue` | placeholder(若有) |
| `web-admin/src/views/crm/customer/CustomerFormDialog.vue` | 表单字段 + 重新生成 |
| `web-admin/src/views/crm/customer/CustomerDetailDialog.vue` | 只读展示 |
| `web-admin/src/views/crm/customer/index.vue` | placeholder / hint |
| `web-admin/src/components/business/db/DbCrmCustomerSelect.vue` | placeholder |
---
### Task 1: 拼音工具函数(TDD)
**Files:**
- Create: `backend/app/utils/pinyin_abbr.py`
- Create: `backend/tests/test_pinyin_abbr.py`
- Modify: `backend/requirements.txt`
- [ ] **Step 1: 写入失败单测**
```python
# backend/tests/test_pinyin_abbr.py
from app.utils.pinyin_abbr import build_name_pinyin_abbr, normalize_name_pinyin_abbr
def test_empty_and_whitespace():
assert build_name_pinyin_abbr("") == ""
assert build_name_pinyin_abbr(" ") == ""
assert normalize_name_pinyin_abbr(None) == ""
assert normalize_name_pinyin_abbr(" AbC ") == "abc"
def test_chinese_and_alnum():
assert build_name_pinyin_abbr("55% 工业硫酸") == "55gylls"
assert build_name_pinyin_abbr("苯骈三氮唑BTA") == "bstnzbta"
assert build_name_pinyin_abbr("PAC") == "pac"
def test_truncate_to_100():
long_name = "测" * 120
out = build_name_pinyin_abbr(long_name)
assert len(out) == 100
```
- [ ] **Step 2: 安装依赖并确认测试失败**
```bash
cd backend && source venv/bin/activate
pip install 'pypinyin>=0.53.0'
# 先把 pypinyin>=0.53.0 写入 requirements.txt 末尾
pytest tests/test_pinyin_abbr.py -v
```
Expected: FAIL(模块不存在)或 import error。
- [ ] **Step 3: 实现工具函数**
```python
# backend/app/utils/pinyin_abbr.py
"""名称拼音首字母缩写:库内存小写,检索时大小写不敏感。"""
from __future__ import annotations
import re
from typing import Optional
from pypinyin import Style, lazy_pinyin
_MAX_LEN = 100
_ALNUM = re.compile(r"[a-zA-Z0-9]")
def normalize_name_pinyin_abbr(value: Optional[str]) -> str:
raw = (value or "").strip().lower()
if not raw:
return ""
return raw[:_MAX_LEN]
def build_name_pinyin_abbr(name: Optional[str]) -> str:
text = (name or "").strip()
if not text:
return ""
parts: list[str] = []
for ch in text:
if "\u4e00" <= ch <= "\u9fff":
pys = lazy_pinyin(ch, style=Style.FIRST_LETTER)
if pys and pys[0]:
parts.append(str(pys[0]).lower()[0])
elif _ALNUM.match(ch):
parts.append(ch.lower())
# else: drop punctuation / spaces / % / brackets
return "".join(parts)[:_MAX_LEN]
def resolve_name_pinyin_abbr(
*,
name: Optional[str],
submitted: Optional[str] = None,
existing: Optional[str] = None,
submitted_provided: bool = False,
) -> Optional[str]:
"""保存时解析拼音字段。
- submitted_provided=True:用提交值(空则按 name 生成)
- 否则:已有非空 existing 则保留;existing 空则按 name 生成
"""
if submitted_provided:
normalized = normalize_name_pinyin_abbr(submitted)
if normalized:
return normalized
generated = build_name_pinyin_abbr(name)
return generated or None
existing_norm = normalize_name_pinyin_abbr(existing)
if existing_norm:
return existing_norm
generated = build_name_pinyin_abbr(name)
return generated or None
```
- [ ] **Step 4: 跑通单测**
```bash
cd backend && pytest tests/test_pinyin_abbr.py -v
```
Expected: PASS。若 `苯骈` 默认读音导致与 `bstnzbta` 不完全一致,以 `pypinyin` 实际首字母为准,**修正断言为实际结果**,并在测试注释中写明。
- [ ] **Step 5: Commit**
```bash
git add backend/requirements.txt backend/app/utils/pinyin_abbr.py backend/tests/test_pinyin_abbr.py
git commit -m "feat: add pinyin initial abbr helper for CRM names."
```
---
### Task 2: DDL + ORM 模型
**Files:**
- Create: `docs/sql/migrations/2026-08-07-crm-name-pinyin-abbr.sql`
- Modify: `docs/sql/shanxia2.0.sql`(`crm_product` / `crm_customer` 的 CREATE TABLE 定义中,在 `product_name` / `customer_name` 后插入列)
- Modify: `backend/app/models/crm_models.py`
- [ ] **Step 1: 写迁移 SQL**
```sql
-- docs/sql/migrations/2026-08-07-crm-name-pinyin-abbr.sql
-- 产品/客户名称拼音首字母(小写);幂等可重复执行(MySQL 5.7 无 IF NOT EXISTS 加列时用存储过程或先检查)
ALTER TABLE `crm_product`
ADD COLUMN `name_pinyin_abbr` varchar(100) DEFAULT NULL COMMENT '产品名称拼音首字母(小写)' AFTER `product_name`;
ALTER TABLE `crm_customer`
ADD COLUMN `name_pinyin_abbr` varchar(100) DEFAULT NULL COMMENT '客户名称拼音首字母(小写)' AFTER `customer_name`;
```
若目标库可能已加列,实现时用「先查 `information_schema` 再 ALTER」的安全脚本包装,或文档注明仅执行一次。推荐在同文件加注释说明手动跳过 duplicate column。
- [ ] **Step 2: 同步基线 `docs/sql/shanxia2.0.sql`**
在 `crm_product` 的 `` `product_name` varchar(200) NOT NULL COMMENT '产品名称', `` 后增加:
```sql
`name_pinyin_abbr` varchar(100) DEFAULT NULL COMMENT '产品名称拼音首字母(小写)',
```
在 `crm_customer` 的 `` `customer_name` varchar(200) NOT NULL COMMENT '客户名称', `` 后增加同样列。
- [ ] **Step 3: ORM**
在 `CrmProduct` 中 `product_name` 后:
```python
name_pinyin_abbr = Column(String(100), nullable=True, comment="产品名称拼音首字母(小写)")
```
在 `CrmCustomer` 中 `customer_name` 后:
```python
name_pinyin_abbr = Column(String(100), nullable=True, comment="客户名称拼音首字母(小写)")
```
- [ ] **Step 4: Commit**
```bash
git add docs/sql/migrations/2026-08-07-crm-name-pinyin-abbr.sql docs/sql/shanxia2.0.sql backend/app/models/crm_models.py
git commit -m "feat: add name_pinyin_abbr columns for CRM product and customer."
```
---
### Task 3: 产品 CRUD + 检索
**Files:**
- Modify: `backend/app/schemas/crm/product.py`
- Modify: `backend/app/crud/crm/product.py`
- Modify: `backend/tests/test_crm_product_options_keyword.py`
- Create: `backend/tests/test_crm_name_pinyin_search.py`
- [ ] **Step 1: Schema**
在 `ProductCreateBody` / `ProductUpdateBody` 增加:
```python
name_pinyin_abbr: Optional[str] = Field(
default=None, alias="namePinyinAbbr", description="产品名称拼音首字母(小写)"
)
```
- [ ] **Step 2: `_product_row_to_dict` 增加返回**
在 `"productName": row.product_name,` 后:
```python
"namePinyinAbbr": getattr(row, "name_pinyin_abbr", None) or "",
```
- [ ] **Step 3: create_product / update_product**
`create_product` 构造 `CrmProduct(...)` 时:
```python
from app.utils.pinyin_abbr import resolve_name_pinyin_abbr
# kwargs 可能来自 camelCase 已转 snake;路由层若传 name_pinyin_abbr / namePinyinAbbr,
# 在 create 入口统一:
submitted = kwargs.get("name_pinyin_abbr")
if submitted is None and "namePinyinAbbr" in kwargs:
submitted = kwargs.get("namePinyinAbbr")
submitted_provided = "name_pinyin_abbr" in kwargs or "namePinyinAbbr" in kwargs
else:
submitted_provided = "name_pinyin_abbr" in kwargs
name_pinyin_abbr = resolve_name_pinyin_abbr(
name=kwargs.get("product_name"),
submitted=submitted,
submitted_provided=True, # 新增:空则生成
)
# 传入 CrmProduct(..., name_pinyin_abbr=name_pinyin_abbr)
```
`update_product` 的可更新字段列表加入 `"name_pinyin_abbr"`,并在字段循环**之前或之后**统一:
```python
name_keys = ("name_pinyin_abbr", "namePinyinAbbr")
submitted_provided = any(k in kwargs for k in name_keys)
submitted = kwargs.get("name_pinyin_abbr")
if submitted is None:
submitted = kwargs.get("namePinyinAbbr")
new_name = kwargs.get("product_name") if "product_name" in kwargs and kwargs.get("product_name") is not None else obj.product_name
obj.name_pinyin_abbr = resolve_name_pinyin_abbr(
name=new_name,
submitted=submitted,
existing=obj.name_pinyin_abbr,
submitted_provided=submitted_provided,
)
# 若走通用 setattr 循环,避免对 name_pinyin_abbr 再覆盖一次:从循环字段列表去掉,或先 pop kwargs
```
推荐:从 `_updatable`/field 循环中单独处理 `name_pinyin_abbr`,不要双重赋值。
- [ ] **Step 4: list_products / list_product_options keyword**
```python
from sqlalchemy import func, or_
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
kw_lower = kw.lower()
pinyin_like = f"%{kw_lower}%"
q = q.filter(
or_(
CrmProduct.product_name.like(like),
CrmProduct.product_no.like(like), # list 保留编号;options 不要编号
func.lower(func.coalesce(CrmProduct.name_pinyin_abbr, "")).like(pinyin_like),
)
)
```
`list_product_options`:**不要**加 `product_no`,仅:
```python
q = q.filter(
or_(
CrmProduct.product_name.like(f"%{kw}%"),
func.lower(func.coalesce(CrmProduct.name_pinyin_abbr, "")).like(f"%{kw.lower()}%"),
)
)
```
- [ ] **Step 5: 更新 / 新增测试**
改写 `test_crm_product_options_keyword.py`:断言 keyword 表达式含 `product_name` 与 `name_pinyin_abbr`,**不含** `product_no`。
新增 `test_crm_name_pinyin_search.py`:对 `list_products` 用 MagicMock 断言 filter 字符串同时含 `product_name` 与 `name_pinyin_abbr`。
```bash
cd backend && pytest tests/test_pinyin_abbr.py tests/test_crm_product_options_keyword.py tests/test_crm_name_pinyin_search.py -v
```
Expected: PASS。
- [ ] **Step 6: Commit**
```bash
git add backend/app/schemas/crm/product.py backend/app/crud/crm/product.py \
backend/tests/test_crm_product_options_keyword.py backend/tests/test_crm_name_pinyin_search.py
git commit -m "feat: store and search product name pinyin initials."
```
---
### Task 4: 客户 CRUD + 检索
**Files:**
- Modify: `backend/app/schemas/crm/customer.py`
- Modify: `backend/app/crud/crm/customer.py`
- Modify: `backend/tests/test_crm_name_pinyin_search.py`(追加客户用例)
- [ ] **Step 1: Schema**
`CustomerCreateBody` / `CustomerUpdateBody` 增加:
```python
name_pinyin_abbr: Optional[str] = Field(
default=None, alias="namePinyinAbbr", description="客户名称拼音首字母(小写)"
)
```
- [ ] **Step 2: `_row_to_dict`**
在 `"customerName": row.customer_name or "",` 后:
```python
"namePinyinAbbr": getattr(row, "name_pinyin_abbr", None) or "",
```
- [ ] **Step 3: create_customer / update_customer**
与产品相同,用 `resolve_name_pinyin_abbr(name=customer_name, ...)`;`update` 的 `_updatable` 不要简单盲加后双重处理——单独解析后赋值。
- [ ] **Step 4: list_customers / list_customer_options**
`keyword` 的 `or_` 增加:
```python
func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(f"%{kw.lower()}%"),
```
`customer_name` 独立筛选改为:
```python
if customer_name:
cn = customer_name.strip()
if cn:
q = q.filter(
or_(
CrmCustomer.customer_name.like(f"%{cn}%"),
func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
f"%{cn.lower()}%"
),
)
)
```
`list_customer_options` 的 `keyword` 与 `customer_name` 同样 OR 拼音。
- [ ] **Step 5: 测试**
在 `test_crm_name_pinyin_search.py` 增加 `list_customers` / `list_customer_options` 的 MagicMock 断言。
```bash
cd backend && pytest tests/test_crm_name_pinyin_search.py -v
```
- [ ] **Step 6: Commit**
```bash
git add backend/app/schemas/crm/customer.py backend/app/crud/crm/customer.py backend/tests/test_crm_name_pinyin_search.py
git commit -m "feat: store and search customer name pinyin initials."
```
---
### Task 5: 存量回填脚本
**Files:**
- Create: `backend/scripts/backfill_name_pinyin_abbr.py`
- [ ] **Step 1: 实现脚本**
```python
#!/usr/bin/env python3
"""回填 crm_product / crm_customer 的 name_pinyin_abbr(仅空字段)。
用法:
cd backend
venv/bin/python scripts/backfill_name_pinyin_abbr.py --dry-run
venv/bin/python scripts/backfill_name_pinyin_abbr.py
venv/bin/python scripts/backfill_name_pinyin_abbr.py --env-file .env.production
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
BACKEND_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(BACKEND_ROOT))
def load_settings_env(env_file: str | None) -> None:
if env_file:
os.environ["ENV_FILE"] = env_file
from dotenv import load_dotenv
for path in [
Path(env_file) if env_file else None,
BACKEND_ROOT / ".env.local",
BACKEND_ROOT / ".env",
]:
if path and Path(path).is_file():
load_dotenv(path, override=True)
break
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--env-file", default=None)
parser.add_argument("--batch-size", type=int, default=200)
args = parser.parse_args()
load_settings_env(args.env_file)
from sqlalchemy.orm import Session
from app.core.database import SessionLocal # 若项目导出名不同,改为实际 Session 工厂
from app.models.crm_models import CrmProduct, CrmCustomer
from app.utils.pinyin_abbr import build_name_pinyin_abbr
db: Session = SessionLocal()
try:
for model, name_attr in (
(CrmProduct, "product_name"),
(CrmCustomer, "customer_name"),
):
q = (
db.query(model)
.filter(model.del_flag == 0)
.filter(
(model.name_pinyin_abbr.is_(None)) | (model.name_pinyin_abbr == "")
)
)
total = q.count()
print(f"{model.__tablename__}: empty={total}")
offset = 0
updated = 0
while True:
rows = q.limit(args.batch_size).all()
if not rows:
break
for row in rows:
name = getattr(row, name_attr) or ""
abbr = build_name_pinyin_abbr(name)
if not abbr:
continue
if args.dry_run:
print(f" would set {row.id}: {abbr}")
else:
row.name_pinyin_abbr = abbr
updated += 1
if args.dry_run:
break
db.commit()
# 已填的行不再命中 filter;无需 offset 递增
print(f"{model.__tablename__}: updated={updated}")
finally:
db.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
实现时核对项目真实的 `SessionLocal` 导入路径(常见 `app.core.database`),与现有 `scripts/backfill_*.py` 对齐。
- [ ] **Step 2: dry-run 本地验证**(需已执行 DDL)
```bash
cd backend && venv/bin/python scripts/backfill_name_pinyin_abbr.py --dry-run
```
- [ ] **Step 3: Commit**
```bash
git add backend/scripts/backfill_name_pinyin_abbr.py
git commit -m "feat: add backfill script for CRM name pinyin initials."
```
---
### Task 6: 前端产品表单 / 列表 / 选择器
**Files:**
- Modify: `web-admin/src/api/crm/product.ts`
- Modify: `web-admin/src/components/business/product-form-dialog/index.vue`
- Modify: `web-admin/src/views/crm/product/index.vue`
- Modify: `web-admin/src/components/business/product-selector/index.vue`
- Modify: `web-admin/src/components/business/db/DbCrmProductSelect.vue`(placeholder)
- [ ] **Step 1: 类型**
`ProductFormData` / `ProductDetail` 增加可选:
```ts
namePinyinAbbr?: string
```
- [ ] **Step 2: ProductFormDialog**
在「产品名称」表单项后增加:
```vue
<el-form-item label="拼音首字母">
<div class="pinyin-abbr-row">
<el-input
v-model="formData.namePinyinAbbr"
placeholder="默认按名称自动生成,可手改"
maxlength="100"
clearable
/>
<el-button class="btn-ghost" @click="regeneratePinyinAbbr">
<el-icon><Refresh /></el-icon>
重新生成
</el-button>
</div>
</el-form-item>
```
- `formData` / `resetFormData` / 详情回填增加 `namePinyinAbbr`
- `regeneratePinyinAbbr`:前端简易实现——调用与后端一致较难(无 pypinyin),**两种做法任选其一(推荐 A)**:
- **A(推荐):** 按钮仅清空字段并提示「保存时将按名称重新生成」;或清空后由用户保存触发后端空值生成。更稳妥:**清空 `namePinyinAbbr`,保存时后端 `submitted_provided=True` 且空 → 生成**。若希望按钮立刻看到结果,可加轻量前端库(YAGNI,不推荐本期引入)。
- **B:** 前端用极简规则(仅保留字母数字、汉字用 `pinyin-pro`)——需新依赖,本期不做。
**按 Spec「重新生成 = 按当前名称覆盖表单值」**:若前端无法本地生成,实现为:
1. 清空拼音字段;
2. `ElMessage.info('已清空,保存时将按产品名称重新生成拼音首字母')`。
保存 payload 必须带上 `namePinyinAbbr: ''`(或显式空串),确保后端 `submitted_provided=True`。
样式:一行 flex,`gap: 8px`;输入框 `flex: 1`。图标 `Refresh` 从 `@element-plus/icons-vue` 导入。
- [ ] **Step 3: 产品列表 / 选择器文案**
- `product/index.vue`:筛选 placeholder、`filter-hint`、快速搜索 placeholder → 含「拼音首字母」
- 查看弹窗 `fields` 在产品名称旁增加 `{ label: '拼音首字母', value: detailData.namePinyinAbbr }`
- `product-selector`:快速搜索 placeholder → `产品名称/拼音首字母`
- `DbCrmProductSelect`:默认 placeholder 若为产品名搜索,追加拼音提示
- [ ] **Step 4: 手动回归** `/#/crm/product` 新增、编辑、手改拼音、清空保存、列表搜拼音。
- [ ] **Step 5: Commit**
```bash
git add web-admin/src/api/crm/product.ts \
web-admin/src/components/business/product-form-dialog/index.vue \
web-admin/src/views/crm/product/index.vue \
web-admin/src/components/business/product-selector/index.vue \
web-admin/src/components/business/db/DbCrmProductSelect.vue
git commit -m "feat: product form and search UI for pinyin initials."
```
---
### Task 7: 前端客户表单 / 列表 / 选择器
**Files:**
- Modify: `web-admin/src/views/crm/customer/CustomerFormDialog.vue`
- Modify: `web-admin/src/views/crm/customer/CustomerDetailDialog.vue`
- Modify: `web-admin/src/views/crm/customer/index.vue`
- Modify: `web-admin/src/components/business/db/DbCrmCustomerSelect.vue`
- [ ] **Step 1: CustomerFormDialog**
在「客户名称」后增加与产品相同的「拼音首字母」+「重新生成」(清空 + 保存时后端重生)。`form` / reset / 详情赋值带上 `namePinyinAbbr`;提交 body 带该字段。
- [ ] **Step 2: CustomerDetailDialog**
`basicFields` 在客户简称前或后增加:
```ts
{ label: '拼音首字母', value: c.namePinyinAbbr }
```
- [ ] **Step 3: 客户列表**
客户名称输入 placeholder → `客户名称/拼音首字母`;若有快速搜索,同步文案。
- [ ] **Step 4: DbCrmCustomerSelect**
placeholder 默认值追加拼音提示(若 props 有默认「请选择客户」之类,改为「客户名称/拼音」或保留并在文档注明远程 keyword 已支持)。
- [ ] **Step 5: 手动回归** 客户新增/编辑/拼音检索;合同或地磅页客户下拉里输拼音。
- [ ] **Step 6: Commit + 图谱**
```bash
git add web-admin/src/views/crm/customer/CustomerFormDialog.vue \
web-admin/src/views/crm/customer/CustomerDetailDialog.vue \
web-admin/src/views/crm/customer/index.vue \
web-admin/src/components/business/db/DbCrmCustomerSelect.vue
git commit -m "feat: customer form and search UI for pinyin initials."
./scripts/update-graph.sh all
```
---
### Task 8: 上线核对清单(执行环境)
- [x] **Step 1:** 对目标库执行 `docs/sql/migrations/2026-08-07-crm-name-pinyin-abbr.sql`(本地已执行;生产部署时再跑一遍,脚本幂等)
- [ ] **Step 2:** 部署后端(含 `pypinyin`)与前端到目标环境
- [x] **Step 3:** `venv/bin/python scripts/backfill_name_pinyin_abbr.py`(本地已回填 product 374 / customer 262;生产部署后需再跑)
- [ ] **Step 4:** 抽检:产品/客户列表与选择器用中文、小写、大小写混合拼音检索
- [ ] **Step 5:** (可选)`python scripts/check_field_consistency.py` 若覆盖 product/customer
#### 生产上线命令摘要
```bash
# 1) DDL(幂等)
mysql ... < docs/sql/migrations/2026-08-07-crm-name-pinyin-abbr.sql
# 2) 部署含 pypinyin 的后端 + 前端
# 3) 回填空字段
cd backend
venv/bin/python scripts/backfill_name_pinyin_abbr.py --dry-run --env-file .env.production
venv/bin/python scripts/backfill_name_pinyin_abbr.py --env-file .env.production
# 4) 抽检示例(以 pypinyin 实际结果为准)
# 产品「55% 工业硫酸」→ 搜 gyls / GYLS
# 产品「苯骈三氮唑BTA」→ 搜 bpsdzbta
```
---
## Spec 覆盖自检
| Spec 需求 | Task |
|-----------|------|
| `name_pinyin_abbr` 两表字段、小写 | Task 2 |
| 生成规则(汉字/字母数字/丢符号) | Task 1 |
| 空才自动填、手改不覆盖、重新生成 | Task 3–4、6–7 |
| list + options / 选择器检索 OR 拼音、大小写不敏感 | Task 3–4 |
| 客户 `customerName` 筛选 OR 拼音 | Task 4 |
| 表单可编辑 + 重新生成、查看只读、placeholder | Task 6–7 |
| 一次回填空字段 | Task 5、8 |
| 不含 short_name 拼音 / 公海 / 小程序 | 未排期(符合非目标) |
## Placeholder 扫描
无 TBD;「重新生成」前端采用清空 + 后端生成(因无前端拼音库),与 Spec「覆盖表单值」在保存后效果一致——已在 Task 6 写明。若产品坚持按钮瞬间填回缩写,需另开任务引入前端拼音库(本期不做)。
@@ -1,9 +1,10 @@
# CRM 产品/客户名称拼音首字母检索
**日期:** 2026-08-07
**状态:** 设计已确认
**状态:** 已实现
**选定方案:** 表内冗余字段 `name_pinyin_abbr` + 后端 `pypinyin` 统一生成;列表与业务选择器 keyword 增加不区分大小写的拼音模糊匹配
**相关模块:** 产品管理 `/#/crm/product`、客户管理 `/#/crm/customer`;合同/报价等产品选择器与客户选择器
**实现计划:** `docs/superpowers/plans/2026-08-07-crm-name-pinyin-abbr.md`
## 1. 目标
@@ -53,10 +54,10 @@
**示例(以实际 `pypinyin` 结果为准,下列为期望形态):**
| 名称 | `name_pinyin_abbr` |
| 名称 | `name_pinyin_abbr`(以 `pypinyin` 实际结果为准) |
|------|-------------------|
| `55% 工业硫酸` | `55gylls` |
| `苯骈三氮唑BTA` | `bstnzbta` |
| `55% 工业硫酸` | `55gyls` |
| `苯骈三氮唑BTA` | `bpsdzbta` |
| `PAC` | `pac` |
| `安徽金浔新能源材料有限公司` | 按库结果连续小写首字母 |