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

#!/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()