You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

361 lines
20 KiB

# -*- coding: utf-8 -*-
"""demo_showcase.py — 用户 demo 验收演示数据(拟真,非 e2c- 测试名)
设计:
- 19 户拟真客户(15 admin 名下 + 3 公海 + 1 已归档),多行业/多地区/三阶段/星级分布
- 联系人 30+、跟进 30+(follow_way 真实字典值)、拟真商机 4 个并绑定客户、关注/重点/成员样例
- 幂等 = 按内置名单先清后造;清理范围仅本脚本名单,不碰 e2c-/e2e- 命名空间
- 中文 body 一律 Python requests(坑㉑);跟进 nextFollowTime ISO T 分隔
"""
import sys, io, json, os, argparse
from datetime import datetime, timedelta
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
import requests
import pymysql
ap = argparse.ArgumentParser(description='验收演示数据造数(可指向测试环境)')
ap.add_argument('--base', default='http://localhost:8080', help='API 根地址(测试环境如 http://<测试服>:8080)')
ap.add_argument('--admin', default='739564171091247104', help='主用户 userId(默认罗伟健开发库 id;测试库不同时传测试库真实 userId)')
ap.add_argument('--buddy', default='744842318024015872', help='协同用户 userId(同上)')
ap.add_argument('--db-host', default='8.129.84.155', help='DB 主机(--skip-db 时忽略)')
ap.add_argument('--db-port', type=int, default=3306)
ap.add_argument('--db-user', default='root')
ap.add_argument('--db-pass', default='Itc@123456')
ap.add_argument('--db-name', default='crm')
ap.add_argument('--skip-db', action='store_true', help='DB 不可达时:跳过清理/stage 直拨/库检(全部潜在客户;重跑前需手工清名单)')
A = ap.parse_args()
BASE = A.base
ADMIN = A.admin
BUDDY = A.buddy
HAS_DB = not A.skip_db
IDS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'showcase-ids.json')
CONF = dict(host=A.db_host, port=A.db_port, user=A.db_user, password=A.db_pass,
database=A.db_name, charset='utf8mb4', autocommit=True, connect_timeout=10,
cursorclass=pymysql.cursors.DictCursor) if HAS_DB else None
def dbq(sql, args=None, fetch=True):
assert CONF, 'dbq 调用于 --skip-db 模式(不应发生)'
with pymysql.connect(**CONF) as conn, conn.cursor() as cur:
cur.execute(sql, args)
return cur.fetchall() if fetch else cur.rowcount
def get_token(uid):
d = requests.get(f'{BASE}/api/auth/debug/token', params={'userId': uid}, timeout=30).json().get('data')
return d if isinstance(d, str) else (d or {}).get('token')
S = requests.Session()
S.headers['Authorization'] = f'Bearer {get_token(ADMIN)}'
def api(method, path, form=None, params=None, step=''):
try:
r = S.post(BASE + path, data=form, params=params, timeout=30) if method == 'POST' else \
S.put(BASE + path, data=form, params=params, timeout=30) if method == 'PUT' else \
S.request(method, BASE + path, params=params, timeout=30)
except Exception as e:
print(f' x {step}: 网络 {e}'); return None
if r.status_code != 200:
print(f' x {step}: HTTP {r.status_code} {r.text[:150]}'); return None
b = r.json()
if b.get('code') not in (0, '0', 200, '200'):
print(f' x {step}: code={b.get("code")} {str(b.get("message"))[:150]}'); return None
return b.get('data')
fails = []
def need(cond, label):
if not cond:
fails.append(label); print(f' x 校验: {label}')
# ---------------- 0. 字典/地区 ----------------
print('== 0. 字典与地区 ==')
if HAS_DB:
CTYPES = [r['code'] for r in dbq(
"SELECT i.code FROM dict_item i JOIN dict_group g ON i.group_id=g.id "
"WHERE g.code='customer_type' AND i.deleted=0 ORDER BY i.sort_no")]
FOLLOW_WAY = [r['code'] for r in dbq(
"SELECT i.code FROM dict_item i JOIN dict_group g ON i.group_id=g.id "
"WHERE g.code='follow_way' AND i.deleted=0 ORDER BY i.sort_no")]
INDUSTRIES = [r['code'] for r in dbq(
"SELECT i.code FROM dict_item i JOIN dict_group g ON i.group_id=g.id "
"WHERE g.code='industry' AND i.deleted=0 AND i.parent_id IS NULL ORDER BY i.sort_no")]
else: # DB 不可达 → 字典走 API
def codes_api(group):
d = api('GET', '/api/dict/item/enabled-list', params={'groupCode': group}, step=f'dict {group}')
return [x.get('code') for x in (d or []) if x.get('code')]
CTYPES = codes_api('customer_type')
FOLLOW_WAY = codes_api('follow_way')
INDUSTRIES = codes_api('industry')
need(CTYPES and FOLLOW_WAY and INDUSTRIES, '字典组非空')
REGIONS = [ # (省, 市, 区) 国标码;先对 sys_region 验证,缺的回退广州荔湾
('440000', '440100', '440103'), ('440000', '440300', '440305'),
('330000', '330100', '330106'), ('510000', '510100', '510107'),
('420000', '420100', '420111'), ('610000', '610100', '610113'),
('320000', '320100', '320102'), ('441900', '441900', '441900005'),
('440600', '440600', '440604'), ('350200', '350200', '350203'),
('370200', '370200', '370212'), ('430100', '430100', '430104')]
valid = {r['code'] for r in dbq("SELECT code FROM sys_region WHERE deleted=0")} if HAS_DB else None
REGIONS = [r if (valid is None or all(c in valid for c in r)) else ('440000', '440100', '440103') for r in REGIONS]
print(f' customer_type={len(CTYPES)} follow_way={len(FOLLOW_WAY)} industry={len(INDUSTRIES)} 地区 {len(REGIONS)}{"有效" if HAS_DB else "(未校验,--skip-db)"}')
# ---------------- 1. 演示数据定义 ----------------
# (名称, 行业idx, 地区idx, 类型idx, 客户星, 关系星, 商务谈判)
FIRMS = [
('广州晨曦医疗器械有限公司', 0, 0, 1, 5, 4, 1),
('深圳蓝湾冷链供应链有限公司', 1, 1, 2, 4, 3, 0),
('杭州云栖软件技术有限公司', 2, 2, 0, 5, 5, 1),
('成都锦官建材集团有限公司', 3, 3, 3, 3, 2, 0),
('武汉江城物流股份有限公司', 4, 4, 4, 4, 3, 1),
('西安大唐文旅发展有限公司', 5, 5, 5, 2, 2, 0),
('广州珠江光电科技有限公司', 0, 0, 6, 4, 4, 1),
('深圳前海惠金信息服务有限公司', 7, 1, 7, 3, 2, 0),
('杭州西湖茶业有限公司', 8, 2, 8, 1, 1, 0),
('成都熊猫慢递文化传播有限公司', 9, 3, 9, 3, 3, 0),
('武汉光谷生物医药有限公司', 10, 4, 10, 4, 2, 0),
('广州白云机场快运有限公司', 4, 0, 11, 5, 3, 1),
('南京金陵楼宇物业管理有限公司', 11, 6, 12, 2, 2, 0),
('东莞松山湖精密制造有限公司', 12, 7, 13, 4, 3, 0),
('佛山季华铝业有限公司', 13, 8, 14, 3, 2, 0),
]
POOLS = [ # (名称, 行业idx, 地区idx)——无主进公海
('厦门鹭岛酒店用品有限公司', 14, 9),
('青岛海诺环保工程有限公司', 15, 10),
('长沙湘江智能科技有限公司', 16, 11),
]
ARCHIVED = ('天津渤海贸易有限公司', 17, 0)
STAGE2 = {1, 4, 6, 9, 13} # 重潜(DB 直拨)
STAGE3 = {2, 11} # 已成交:云栖软件 / 白云机场快运
# 统一为 7 元组(名称, 行业, 地区, 类型, 星, 关系星, 商务谈判)
EXTRA7 = [(nm, ind, reg, 0, 1, 1, 0) for nm, ind, reg in POOLS] + [(ARCHIVED[0], ARCHIVED[1], ARCHIVED[2], 0, 1, 1, 0)]
CONTACTS = [ # (客户idx, 姓名, 职务, 手机, 关键, 内线, 礼品备注)
(0, '陈志强', '总经理', '13802000001', 1, 1, '偏好茶叶'),
(0, '林晓婷', '采购总监', '13802000002', 1, 0, ''),
(0, '黄伟杰', '设备科主任', '13802000003', 0, 0, ''),
(1, '吴敏', '供应链总监', '13803000001', 1, 0, '子女升学季,勿中午来电'),
(1, '刘建华', '仓储经理', '13803000002', 0, 0, ''),
(2, '郑雅文', '董事长助理', '13805000001', 1, 1, '重点维护,决策链关键人'),
(2, '杨帆', 'IT 经理', '13805000002', 0, 0, ''),
(3, '周丽娟', '财务总监', '13806000001', 1, 0, ''),
(4, '王海涛', '运营副总', '13807000001', 1, 0, ''),
(4, '徐静', '调度中心主任', '13807000002', 0, 0, ''),
(5, '孙明', '市场部经理', '13808000001', 0, 0, ''),
(6, '马晓燕', '采购部长', '13809000001', 1, 0, '对交期敏感'),
(6, '朱国强', '生产厂长', '13809000002', 0, 0, ''),
(7, '胡雪梅', '合规负责人', '13810000001', 0, 0, ''),
(8, '高翔', '总经理', '13811000001', 1, 0, '本人即决策人'),
(9, '林芳', '创意总监', '13812000001', 0, 0, ''),
(10, '何伟', '研发副总', '13813000001', 1, 0, ''),
(11, '罗嘉欣', '信息技术部长', '13814000001', 1, 1, ''),
(11, '邓超群', '车队队长', '13814000002', 0, 0, ''),
(12, '曹颖', '物业总经理', '13815000001', 0, 0, ''),
(13, '彭志远', '厂长', '13816000001', 1, 0, ''),
(14, '梁诗敏', '外贸部经理', '13817000001', 0, 0, ''),
]
FOLLOWS = [ # (客户idx, follow_way下标, 内容, next 天数)
(0, 1, '上门演示手术室整体方案,陈总对配置清单基本认可,报价待财务复核', 2),
(0, 0, '电话确认资质文件已收悉,对方进入内部立项流程', 5),
(1, 0, '沟通冷链季度框架意向,需先补充承运资质与温控记录样例', 3),
(2, 4, '郑助理转达二期预算已批复,下周安排实施经理进场调研', 1),
(2, 0, '回访 ERP 一期使用情况,财务模块反馈良好', 7),
(3, 2, '建材行情下行,客户压缩采购计划,转推维修维护场景', 6),
(4, 0, '江城物流区域扩张计划确认,武汉/长沙两地仓配需求明确', 4),
(5, 3, '文旅旺季前预算未落地,保持月度联络', 14),
(6, 1, '车间改造项目勘察完成,等待技术方案评审', 2),
(7, 0, '惠金信息新设合规团队,有系统对接需求,发产品白皮书', 5),
(8, 0, '春茶采购季结束,转入日常维护联络', 20),
(9, 1, '熊猫慢递新址开业,送乔迁贺礼并演示会员系统', 8),
(10, 0, '光谷生物实验数据管理需求初步确认,约技术交流会', 3),
(11, 4, '白云快运调度系统验收会通过,商务条款法务审核中', 2),
(12, 0, '楼宇物业续约谈判节点临近,整理服务履约材料', 6),
(13, 1, '松山湖精密二期产线规划中,邀请参观标杆工厂', 9),
(14, 0, '季华铝业出口订单回暖,关注汇率对采购预算影响', 4),
]
OPPS = [ # (名称, 客户idx, 甲方)
('云栖软件-ERP 二期实施项目', 2, '杭州云栖软件技术有限公司'),
('珠江光电-车间智能化改造', 6, '广州珠江光电科技有限公司'),
('白云快运-车辆调度系统采购', 11, '广州白云机场快运有限公司'),
('晨曦医疗-手术室设备集采', 0, '广州晨曦医疗器械有限公司'),
]
SHOW_NAMES = [f[0] for f in FIRMS] + [p[0] for p in POOLS] + [ARCHIVED[0]]
# ---------------- 2. 清理(仅本名单) ----------------
print('== 2. 清理上一轮演示数据 ==')
if HAS_DB:
n_all = dbq('SELECT COUNT(*) c FROM customer')[0]['c']
assert n_all >= 3, f'customer 仅 {n_all} 行,疑似连错库,中止'
ph = ','.join(['%s'] * len(SHOW_NAMES))
rows = dbq(f'SELECT id FROM customer WHERE customer_name IN ({ph})', SHOW_NAMES)
cids = [str(r['id']) for r in rows]
with pymysql.connect(**CONF) as conn, conn.cursor() as cur:
if cids:
p2 = ','.join(['%s'] * len(cids))
cur.execute(f'SELECT DISTINCT transfer_id FROM customer_transfer_detail WHERE customer_id IN ({p2})', cids)
tids = [r['transfer_id'] for r in cur.fetchall()]
if tids:
p3 = ','.join(['%s'] * len(tids))
cur.execute(f'DELETE FROM customer_transfer_detail WHERE transfer_id IN ({p3})', tids)
cur.execute(f'DELETE FROM customer_transfer WHERE id IN ({p3})', tids)
cur.execute(f'DELETE FROM opportunity_customer WHERE customer_id IN ({p2})', cids)
oph = ','.join(['%s'] * len(OPPS))
cur.execute(f'SELECT id FROM opportunity WHERE opp_name IN ({oph})', [o[0] for o in OPPS])
oids = [r['id'] for r in cur.fetchall()]
if oids:
p4 = ','.join(['%s'] * len(oids))
cur.execute(f'DELETE FROM opportunity_customer WHERE opportunity_id IN ({p4})', oids)
cur.execute(f'DELETE FROM opportunity WHERE id IN ({p4})', oids)
for t in ['customer_contact', 'customer_oplog', 'customer_team_member', 'customer_follow',
'customer_pending_notice', 'customer_focus', 'customer_view_log']:
cur.execute(f'DELETE FROM {t} WHERE customer_id IN ({p2})', cids)
cur.execute(f'DELETE FROM customer WHERE id IN ({p2})', cids)
print(f'{len(cids)} 户 + 关联商机 {len(oids or [])}')
else:
print(' 无残留')
else:
ph = None
print(' --skip-db:跳过清理(重跑前需手工清名单或换名单,否则 L3 撞码 67003 硬拦)')
# ---------------- 3. 造客户 ----------------
print('== 3. 造拟真客户 ==')
CIDS = []
for i, (name, ind, reg, typ, star, rstar, biz) in enumerate(FIRMS + EXTRA7):
reg_ = REGIONS[reg]
credit = f'91440101SHOW{10000 + i:05d}X' # 8+5+4+1=18 位
form = dict(customerName=name, customerType=CTYPES[typ % len(CTYPES)],
provinceCode=reg_[0], cityCode=reg_[1], districtCode=reg_[2],
industryCode=INDUSTRIES[ind % len(INDUSTRIES)],
customerStarLevel=star, relationStarLevel=rstar, isBizNegotiated=biz, isChild=0,
unifiedCreditCode=credit, legalRepresentative=CONTACTS[i % len(CONTACTS)][1],
establishedDate=f'{2010 + i % 14}-0{1 + i % 9}-1{i % 9}',
registeredCapital=f'{(i + 1) * 500}万元人民币',
businessScope='许可经营项目凭许可证经营;一般经营项目自主选择',
staffSize=['50-99人', '100-499人', '500-999人'][i % 3],
annualRevenue=f'{(i % 8 + 1) * 1000}万元',
ownerUserId=None if 15 <= i < 18 else ADMIN, # 公海 3 户无主;天津渤海挂 admin 供归档样例
confirmSimilar='true',
remark='验收演示数据(demo_showcase.py)')
d = api('POST', '/api/customer', form=form, step=f'create {name}')
need(d is not None, f'create {name}')
if d is None:
CIDS.append(None); continue
cid = str(d.get('id')) if isinstance(d, dict) else str(d)
CIDS.append(cid)
print(f' + {name} id={cid}')
# stage 直拨(潜在=1 默认;API 不可达行,仅 HAS_DB)
if HAS_DB:
for idx in sorted(STAGE2):
if CIDS[idx]:
dbq('UPDATE customer SET customer_stage=2 WHERE id=%s', (CIDS[idx],), fetch=False)
for idx in sorted(STAGE3):
if CIDS[idx]:
dbq('UPDATE customer SET customer_stage=3 WHERE id=%s', (CIDS[idx],), fetch=False)
print(f' stage 直拨:重潜 {sorted(STAGE2)} 已成交 {sorted(STAGE3)}')
else:
print(' --skip-db:stage 保持潜在客户,重潜/已成交分布不可用')
# 归档
if CIDS[-1]:
api('POST', '/api/customer/archive', params={'id': CIDS[-1]}, step='archive 天津渤海')
# ---------------- 4. 联系人 ----------------
print('== 4. 联系人 ==')
for idx, nm, job, ph_, key, internal, gift in CONTACTS:
if not CIDS[idx]:
continue
api('POST', '/api/customer/contact',
form=dict(customerId=CIDS[idx], name=nm, jobTitleName=job, phone=ph_,
isKeyContact=key, isInternal=internal, giftRemark=gift,
source='contact_source_0%d' % (idx % 4 + 1)),
step=f'contact {nm}')
# ---------------- 5. 跟进 ----------------
print('== 5. 跟进 ==')
now = datetime.now()
for idx, way, content, ndays in FOLLOWS:
if not CIDS[idx]:
continue
nxt = (now + timedelta(days=ndays)).strftime('%Y-%m-%dT09:30:00')
api('POST', f'/api/customer/{CIDS[idx]}/follow',
form=dict(followWay=FOLLOW_WAY[way % len(FOLLOW_WAY)], followContent=content,
nextFollowTime=nxt), step=f'follow #{idx}')
# ---------------- 6. 商机 + 绑定 ----------------
print('== 6. 商机关联 ==')
for name, cidx, party in OPPS:
if not CIDS[cidx]:
continue
reg_ = REGIONS[FIRMS[cidx][2]]
d = api('POST', '/api/opportunity',
form=dict(opportunityName=name, oppSource='opp_source_02',
industryCode=INDUSTRIES[FIRMS[cidx][1] % len(INDUSTRIES)],
localityType='locality_type_01', bidForm='bid_form_01',
provinceCode=reg_[0], cityCode=reg_[1],
partyAClear=1, partyA=party, remark='验收演示商机'),
step=f'opp {name}')
if d is not None:
oid = str(d)
api('POST', '/api/opportunity/customer/add',
form=dict(oppId=oid, customerId=CIDS[cidx], customerNameSnapshot=FIRMS[cidx][0],
customerRole='customer_role_01', isPrimaryIntended=1),
step=f'bind {name}')
# 商机跟进一条,丰富详情
api('POST', f'/api/opportunity/follow/add' if False else f'/api/customer/{CIDS[cidx]}/follow',
form=dict(followWay=FOLLOW_WAY[0],
followContent=f'{name}】商机推进沟通:确认技术方案与商务节奏',
nextFollowTime=(now + timedelta(days=3)).strftime('%Y-%m-%dT14:00:00')),
step=f'opp-follow {name}')
# ---------------- 7. 关注/重点/成员 ----------------
print('== 7. 关注/重点/成员 ==')
for idx in (0, 2, 11):
if CIDS[idx]:
api('POST', f'/api/customer/{CIDS[idx]}/focus', step=f'focus #{idx}')
for idx in (0, 2):
if CIDS[idx]:
api('POST', f'/api/customer/{CIDS[idx]}/star', step=f'star #{idx}')
for idx in (1, 4):
if CIDS[idx]:
api('POST', f'/api/customer/{CIDS[idx]}/members',
form={'memberUserIds': BUDDY}, step=f'member #{idx}+曾偲青')
# ---------------- 8. 校验 ----------------
print('== 8. 校验 ==')
if HAS_DB:
got_names = dbq(f'SELECT COUNT(*) c FROM customer WHERE customer_name IN ({ph})', SHOW_NAMES)[0]['c']
need(got_names == len(SHOW_NAMES), f'演示客户总数 {got_names}=={len(SHOW_NAMES)}')
stages = sorted(r['customer_stage'] for r in dbq(
f"SELECT customer_stage FROM customer WHERE customer_name IN ({ph}) AND deleted=0", SHOW_NAMES))
s2 = sum(1 for s in stages if s == 2); s3 = sum(1 for s in stages if s == 3)
need(s2 == len(STAGE2) and s3 == len(STAGE3), f'stage 分布 重潜{s2} 已成交{s3}')
pools = dbq(f"SELECT COUNT(*) c FROM customer WHERE customer_name IN ({ph}) AND owner_user_id IS NULL", SHOW_NAMES)[0]['c']
need(pools == 3, f'公海 {pools}==3')
cc = dbq(f'SELECT COUNT(*) c FROM customer_contact ct JOIN customer c ON c.id=ct.customer_id '
f"WHERE c.customer_name IN ({ph})", SHOW_NAMES)[0]['c']
need(cc >= len(CONTACTS) - 2, f'联系人 {cc}>={len(CONTACTS) - 2}')
fc = dbq(f'SELECT COUNT(*) c FROM customer_follow f JOIN customer c ON c.id=f.customer_id '
f"WHERE c.customer_name IN ({ph})", SHOW_NAMES)[0]['c']
need(fc >= len(FOLLOWS) - 2, f'跟进 {fc}>={len(FOLLOWS) - 2}')
oc = dbq(f'SELECT COUNT(*) c FROM opportunity_customer oc JOIN customer cu ON cu.id=oc.customer_id '
f"WHERE cu.customer_name IN ({ph})", SHOW_NAMES)[0]['c']
need(oc == len(OPPS), f'商机绑定 {oc}=={len(OPPS)}')
else:
print(' --skip-db:跳过 DB 校验(分布核对以 demo 页面观感为准)')
json.dump({'customers': {f[0]: cid for f, cid in zip(FIRMS + POOLS + [ARCHIVED], CIDS) if cid}},
open(IDS_FILE, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
mine = api('POST', '/api/customer/workspace/mine/page', form={'current': 1, 'size': 1}, step='mine total')
pool = api('POST', '/api/customer/workspace/pool/page', form={'current': 1, 'size': 1}, step='pool total')
print(f'\nmine total={(mine or {}).get("total")} pool total={(pool or {}).get("total")}')
print(f'{"SHOWCASE ALL GREEN" if not fails else f"{len(fails)} 项失败: {fails}"}')
sys.exit(0 if not fails else 1)