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:
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` |
|
||||
| `安徽金浔新能源材料有限公司` | 按库结果连续小写首字母 |
|
||||
|
||||
|
||||
Reference in new issue
Block a user