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.
266 lines
13 KiB
266 lines
13 KiB
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
票据 05:公海池与线索种子数据铺设脚本(幂等可重跑)
|
|
|
|
覆盖目标(frontend-integration/issues/05-testdata-seeding.md):
|
|
1. 公海池 ×2(不同部门、不同省份、不同领取规则),各配负责人
|
|
2. 线索种子:待领取 / 已领取 / 跟进中 / 已转商机(尽力,商机模块未实现时记录缺口)
|
|
3. 「我的关注」有数据(follow 一条线索)
|
|
4. 验收:四视图分页均有数据,打印统计
|
|
|
|
用法:
|
|
python seed-testdata.py # 默认 http://localhost:8081
|
|
SEED_BASE=http://x.x.x.x python seed-testdata.py
|
|
python seed-testdata.py --user 739564171091247104
|
|
|
|
前置:verify profile 运行中(/api/auth/debug/token 可用);目标用户具备 ROLE_ADMIN。
|
|
幂等策略:部门/公海池按名称匹配,线索按 leadName 精确匹配,流转按当前状态守卫。
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
BASE = os.environ.get("SEED_BASE", "http://localhost:8081")
|
|
USER_ID = os.environ.get("SEED_USER_ID", "739564171091247104")
|
|
|
|
STATUS_NAMES = {1: "未分发", 2: "待领取", 3: "已领取", 4: "跟进中",
|
|
5: "已转商机", 6: "过期失效", 7: "线索作废"}
|
|
|
|
DEPT_NAME = "华南销售部(seed)"
|
|
POOL_A = {
|
|
"poolName": "华南公海池(seed)", "claimRule": 1,
|
|
"recycleDays": 7, "expireDays": 180, "dailyClaimLimit": 10, "holdLimit": 200,
|
|
"provinceCode": "440000", "cityCode": "440100",
|
|
}
|
|
POOL_B = {
|
|
"poolName": "华北公海池(seed)", "claimRule": 2,
|
|
"recycleDays": 10, "expireDays": 90, "dailyClaimLimit": 5, "holdLimit": 100,
|
|
"provinceCode": "110000", "cityCode": None,
|
|
}
|
|
|
|
# 线索种子:target 流转目标(claim=已领取 follow=跟进中 convert=已转商机),follow_me=当前用户关注
|
|
LEADS = [
|
|
{"leadName": "网络奇迹有限公司", "phone": "15611229615", "pool": "A", "target": None,
|
|
"provinceCode": "440000", "cityCode": "440100", "channelCode": "exhibition",
|
|
"brandCode": "itc", "productCode": "led_display", "sceneCode": "meeting_room",
|
|
"consultContent": "展会现场咨询LED显示屏,意向会议室改造项目", "follow_me": True},
|
|
{"leadName": "广州智联科技有限公司", "phone": "13800138001", "pool": "A", "target": "claim",
|
|
"provinceCode": "440000", "cityCode": "440100", "channelCode": "jd",
|
|
"brandCode": "shichang", "productCode": "conference_system", "sceneCode": "auditorium",
|
|
"consultContent": "京东渠道咨询会议系统", "follow_me": False},
|
|
{"leadName": "深圳华彩光电有限公司", "phone": "13800138002", "pool": "A", "target": "follow",
|
|
"provinceCode": "440000", "cityCode": "440300", "channelCode": "zhihu",
|
|
"brandCode": "itc", "productCode": "video_conference", "sceneCode": "command_center",
|
|
"consultContent": "知乎渠道咨询视频会议+指挥中心场景", "follow_me": False},
|
|
{"leadName": "东莞声视电子有限公司", "phone": "13800138003", "pool": "A", "target": "convert",
|
|
"provinceCode": "440000", "cityCode": "441900", "channelCode": "digital_av",
|
|
"brandCode": "three_a", "productCode": "sound_system", "sceneCode": "sports_venue",
|
|
"consultContent": "数字音视渠道咨询扩声系统(文体场馆)", "follow_me": False},
|
|
{"leadName": "北京京彩传媒有限公司", "phone": "13800138004", "pool": "B", "target": None,
|
|
"provinceCode": "110000", "cityCode": None, "channelCode": "netease_news",
|
|
"brandCode": "itc", "productCode": "record_broadcast", "sceneCode": "theater",
|
|
"consultContent": "网易新闻渠道咨询录播系统(剧院)", "follow_me": True},
|
|
]
|
|
|
|
FINDINGS = [] # 铺设过程发现的阻塞/异常(票据第 4 项)
|
|
|
|
|
|
def http(method, path, params=None, form=None, token=None):
|
|
url = BASE + path
|
|
if params:
|
|
url += "?" + urllib.parse.urlencode(params, doseq=True)
|
|
data = None
|
|
if form is not None:
|
|
form = {k: v for k, v in form.items() if v is not None}
|
|
data = urllib.parse.urlencode(form, doseq=True).encode("utf-8")
|
|
req = urllib.request.Request(url, data=data, method=method)
|
|
if token:
|
|
req.add_header("Authorization", "Bearer " + token)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
body = json.loads(resp.read().decode("utf-8"))
|
|
except urllib.error.HTTPError as e:
|
|
body = json.loads(e.read().decode("utf-8") or "{}")
|
|
if body.get("success") is not True:
|
|
raise RuntimeError(f"{method} {path} -> code={body.get('code')} msg={body.get('message')}")
|
|
return body.get("data")
|
|
|
|
|
|
def main():
|
|
user_id = USER_ID
|
|
for i, a in enumerate(sys.argv[1:]):
|
|
if a == "--user":
|
|
user_id = sys.argv[i + 2]
|
|
if a == "--base":
|
|
global BASE
|
|
BASE = sys.argv[i + 2]
|
|
|
|
print(f"== 票据05 测试数据铺设 @ {BASE} (操作人 {user_id}) ==")
|
|
|
|
# ---- 0. 调试 token(verify profile 后门,ADR-0014)----
|
|
token = http("GET", "/api/auth/debug/token", {"userId": user_id})
|
|
print("[ok] 获取调试 token")
|
|
|
|
# ---- 1. 行政区划 ID 定位(sys_region 数据库主键,供公海池区域关联)----
|
|
provinces = http("GET", "/api/rule/region/level", {"level": 1}, token=token)
|
|
prov_by_code = {p["code"]: p for p in provinces}
|
|
region_ids = {} # poolKey -> {"province": id, "city": id|None}
|
|
first_city_by_prov = {} # 省 code -> 首个下级市 code(直辖市等无明确市的线索回填用)
|
|
for key, spec in (("A", POOL_A), ("B", POOL_B)):
|
|
prov = prov_by_code.get(spec["provinceCode"])
|
|
if not prov:
|
|
raise RuntimeError(f"省份 {spec['provinceCode']} 未入库(票据01 区划种子缺失)")
|
|
city_id = None
|
|
city_code = spec["cityCode"]
|
|
children = http("GET", "/api/rule/region/list", {"parentId": prov["id"]}, token=token)
|
|
if children:
|
|
first_city_by_prov[spec["provinceCode"]] = children[0]["code"]
|
|
if city_code is None:
|
|
# 直辖市:取第一个下级作为市
|
|
if children:
|
|
city_code = children[0]["code"]
|
|
city_id = children[0]["id"]
|
|
spec["cityCode"] = city_code
|
|
else:
|
|
hit = next((c for c in children if c["code"] == city_code), None)
|
|
if not hit:
|
|
raise RuntimeError(f"市 {city_code} 不在省 {spec['provinceCode']} 下")
|
|
city_id = hit["id"]
|
|
region_ids[key] = {"province": prov["id"], "city": city_id}
|
|
print(f"[ok] 池{key} 区域定位:省 {prov['name']}({prov['id']})"
|
|
+ (f" 市 {city_code}({city_id})" if city_id else " 仅省级"))
|
|
|
|
# ---- 2. 部门:保证 ≥2 个(一部门一池约束)----
|
|
def flatten(nodes, out):
|
|
for n in nodes or []:
|
|
out.append(n)
|
|
flatten(n.get("children"), out)
|
|
return out
|
|
|
|
depts = flatten(http("GET", "/api/system/depts/tree", token=token), [])
|
|
seed_dept = next((d for d in depts if d.get("deptName") == DEPT_NAME), None)
|
|
if seed_dept is None:
|
|
http("POST", "/api/system/depts/save", form={"deptName": DEPT_NAME, "sort": 90}, token=token)
|
|
depts = flatten(http("GET", "/api/system/depts/tree", token=token), [])
|
|
seed_dept = next(d for d in depts if d.get("deptName") == DEPT_NAME)
|
|
print(f"[ok] 创建部门 {DEPT_NAME}({seed_dept['id']})")
|
|
else:
|
|
print(f"[skip] 部门 {DEPT_NAME} 已存在({seed_dept['id']})")
|
|
if len(depts) < 2:
|
|
raise RuntimeError("部门数不足 2,无法建两个池")
|
|
dept_a = seed_dept["id"]
|
|
dept_b = next(d["id"] for d in depts if d["id"] != dept_a)
|
|
print(f"[ok] 池A部门={dept_a}({seed_dept.get('deptName')}) 池B部门={dept_b}")
|
|
|
|
# ---- 3. 公海池 ×2(按名称幂等)----
|
|
pool_page = http("POST", "/api/rule/pool/page", form={"current": 1, "size": 100}, token=token)
|
|
pools_by_name = {p["poolName"]: p for p in pool_page.get("content", [])}
|
|
pool_ids = {}
|
|
for key, spec, dept_id in (("A", POOL_A, dept_a), ("B", POOL_B, dept_b)):
|
|
exist = pools_by_name.get(spec["poolName"])
|
|
if exist:
|
|
pool_ids[key] = exist["id"]
|
|
print(f"[skip] 公海池 {spec['poolName']} 已存在({exist['id']})")
|
|
continue
|
|
form = {
|
|
"poolName": spec["poolName"], "deptId": dept_id,
|
|
"claimRule": spec["claimRule"], "recycleDays": spec["recycleDays"],
|
|
"expireDays": spec["expireDays"], "dailyClaimLimit": spec["dailyClaimLimit"],
|
|
"holdLimit": spec["holdLimit"], "ownerUserId": user_id,
|
|
"provinceRegionIds": [region_ids[key]["province"]],
|
|
}
|
|
if region_ids[key]["city"]:
|
|
form["cityRegionIds"] = [region_ids[key]["city"]]
|
|
http("POST", "/api/rule/pool/saveOrUpdate", form=form, token=token)
|
|
pool_page = http("POST", "/api/rule/pool/page", form={"current": 1, "size": 100}, token=token)
|
|
pools_by_name = {p["poolName"]: p for p in pool_page.get("content", [])}
|
|
pool_ids[key] = pools_by_name[spec["poolName"]]["id"]
|
|
print(f"[ok] 创建公海池 {spec['poolName']}({pool_ids[key]}) "
|
|
f"规则={spec['claimRule']} 部门={dept_id}")
|
|
|
|
# ---- 4. 线索种子(按 leadName 幂等 + 状态守卫流转)----
|
|
lead_ids = {}
|
|
for spec in LEADS:
|
|
page = http("POST", "/api/lead/page",
|
|
form={"current": 1, "size": 20, "viewType": "MANAGE",
|
|
"keyword": spec["leadName"]}, token=token)
|
|
hit = next((l for l in page.get("content", []) if l.get("leadName") == spec["leadName"]), None)
|
|
if hit:
|
|
lead_ids[spec["leadName"]] = hit["id"]
|
|
print(f"[skip] 线索 {spec['leadName']} 已存在({hit['id']} 状态={STATUS_NAMES.get(hit.get('status'))})")
|
|
continue
|
|
form = {
|
|
"leadName": spec["leadName"], "phone": spec["phone"],
|
|
"provinceCode": spec["provinceCode"],
|
|
"cityCode": spec["cityCode"] or first_city_by_prov.get(spec["provinceCode"]),
|
|
"channelCode": spec["channelCode"], "brandCode": spec["brandCode"],
|
|
"productCode": spec["productCode"], "sceneCode": spec["sceneCode"],
|
|
"consultContent": spec["consultContent"],
|
|
"poolId": pool_ids[spec["pool"]],
|
|
}
|
|
claim_on_create = spec["target"] in ("claim", "follow", "convert")
|
|
lid = http("POST", "/api/lead/create",
|
|
params={"claimOnCreate": str(claim_on_create).lower()}, form=form, token=token)
|
|
lead_ids[spec["leadName"]] = lid
|
|
print(f"[ok] 创建线索 {spec['leadName']}({lid}) claimOnCreate={claim_on_create}")
|
|
|
|
# ---- 5. 流转(状态守卫:已到位则跳过)----
|
|
def status_of(lid):
|
|
return http("GET", "/api/lead/detail", {"id": lid}, token=token).get("status")
|
|
|
|
for spec in LEADS:
|
|
lid = lead_ids[spec["leadName"]]
|
|
target = spec["target"]
|
|
if target in ("follow", "convert"):
|
|
if status_of(lid) == 3: # 已领取 → 提交反馈 → 跟进中
|
|
http("POST", "/api/lead/feedback-submit", form={
|
|
"leadId": lid, "feedbackStatus": 1,
|
|
"content": "种子铺设自动反馈:客户意向明确", "productCode": spec["productCode"],
|
|
}, token=token)
|
|
print(f"[ok] {spec['leadName']} 提交反馈 → 跟进中")
|
|
if target == "convert":
|
|
if status_of(lid) in (3, 4):
|
|
try:
|
|
http("POST", "/api/lead/convert", params={"id": lid}, form={
|
|
"opportunityName": spec["leadName"] + "商机",
|
|
"industryCode": "it", "intendedCustomer": spec["leadName"],
|
|
"regionCode": spec["cityCode"] or spec["provinceCode"],
|
|
}, token=token)
|
|
print(f"[ok] {spec['leadName']} 转商机 → 已转商机")
|
|
except RuntimeError as e:
|
|
FINDINGS.append(f"转商机被阻塞:{spec['leadName']} -> {e}")
|
|
print(f"[!!] {spec['leadName']} 转商机失败(记录为发现项):{e}")
|
|
|
|
# ---- 6. 关注(我的关注视图有数据)----
|
|
for spec in LEADS:
|
|
if spec.get("follow_me"):
|
|
lid = lead_ids[spec["leadName"]]
|
|
http("POST", "/api/lead/follow", params={"id": lid}, form={}, token=token)
|
|
print(f"[ok] 关注线索 {spec['leadName']}(幂等)")
|
|
|
|
# ---- 7. 验收:四视图 + 统计 ----
|
|
print("\n== 验收 ==")
|
|
for view in ("PUBLIC_POOL", "MY_LEAD", "MY_FOLLOW", "MANAGE"):
|
|
page = http("POST", "/api/lead/page",
|
|
form={"current": 1, "size": 10, "viewType": view}, token=token)
|
|
total = page.get("total")
|
|
names = [l.get("leadName") for l in page.get("content", [])[:5]]
|
|
print(f"[view] {view}: total={total} 样例={names}")
|
|
stats = http("POST", "/api/lead/stats", form={"viewType": "MANAGE"}, token=token)
|
|
print(f"[stats] MANAGE 状态分布: {stats}")
|
|
pool_page = http("POST", "/api/rule/pool/page", form={"current": 1, "size": 10}, token=token)
|
|
print(f"[pools] {[(p['poolName'], p.get('deptName'), p.get('ownerUserName')) for p in pool_page.get('content', [])]}")
|
|
|
|
if FINDINGS:
|
|
print("\n== 发现项(阻塞/异常,需跟进) ==")
|
|
for f in FINDINGS:
|
|
print(" - " + f)
|
|
print("\n== 铺设完成 ==")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|