From 2ccc3b612afffe8666085763cffe3ebdb2899a84 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Fri, 7 Aug 2026 17:34:55 +0800 Subject: [PATCH] docs: mark CRM name pinyin abbr design implemented and note go-live checklist. Co-authored-by: Cursor --- .../plans/2026-08-07-crm-name-pinyin-abbr.md | 696 ++++++++++++++++++ .../2026-08-07-crm-name-pinyin-abbr-design.md | 9 +- 2 files changed, 701 insertions(+), 4 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-07-crm-name-pinyin-abbr.md diff --git a/docs/superpowers/plans/2026-08-07-crm-name-pinyin-abbr.md b/docs/superpowers/plans/2026-08-07-crm-name-pinyin-abbr.md new file mode 100644 index 000000000..ebd7aa858 --- /dev/null +++ b/docs/superpowers/plans/2026-08-07-crm-name-pinyin-abbr.md @@ -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 + +
+ + + + 重新生成 + +
+
+``` + +- `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 写明。若产品坚持按钮瞬间填回缩写,需另开任务引入前端拼音库(本期不做)。 diff --git a/docs/superpowers/specs/2026-08-07-crm-name-pinyin-abbr-design.md b/docs/superpowers/specs/2026-08-07-crm-name-pinyin-abbr-design.md index 9c6822c0a..c8e791156 100644 --- a/docs/superpowers/specs/2026-08-07-crm-name-pinyin-abbr-design.md +++ b/docs/superpowers/specs/2026-08-07-crm-name-pinyin-abbr-design.md @@ -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` | | `安徽金浔新能源材料有限公司` | 按库结果连续小写首字母 |