402 lines
14 KiB
Python
402 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
为指定企业补「订单运营 + 分销推广」种子数据,量级与 wechat_users 池大致匹配(取总用户约 1/12 订单条数作中间值)。
|
||
|
||
- orders:enterpriseId 对齐;orderNo 前缀 SEED5_ 可幂等跳过
|
||
- 为已支付/已完成订单写入一条 test_results(带 orderId),供分销「产品佣金分布」按测评类型归类
|
||
- distribution_agents + distribution_bindings(enterprise 维度)+ commission_records
|
||
|
||
读 api/.env 的 DATABASE_*。
|
||
|
||
用法:
|
||
python3 scripts/seed_ent5_orders_and_distribution_mysql.py --enterprise-id 5
|
||
python3 scripts/seed_ent5_orders_and_distribution_mysql.py --enterprise-id 5 --dry-run
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import hashlib
|
||
import random
|
||
import re
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
|
||
try:
|
||
import pymysql
|
||
except ImportError:
|
||
sys.exit("需要: pip install pymysql")
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
ENV_PATH = ROOT / "api" / ".env"
|
||
ORDER_PREFIX = "SEED5_"
|
||
|
||
|
||
def load_env(path: Path) -> dict[str, str]:
|
||
cfg: dict[str, str] = {}
|
||
if not path.is_file():
|
||
return cfg
|
||
for line in path.read_text(encoding="utf-8").splitlines():
|
||
line = line.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
m = re.match(r"^([A-Z0-9_]+)\s*=\s*(.*)$", line)
|
||
if m:
|
||
cfg[m.group(1)] = m.group(2).strip()
|
||
return cfg
|
||
|
||
|
||
def pick_type(i: int) -> str:
|
||
r = (i * 7919) % 100
|
||
if r < 55:
|
||
return "face"
|
||
if r < 72:
|
||
return "mbti"
|
||
if r < 86:
|
||
return "disc"
|
||
return "pdp"
|
||
|
||
|
||
def product_title(pt: str) -> str:
|
||
return {
|
||
"face": "AI人脸性格分析完整报告",
|
||
"mbti": "MBTI 职业性格测评",
|
||
"disc": "DISC 行为风格测评",
|
||
"pdp": "PDP 天赋特质测评",
|
||
}.get(pt, "测评服务")
|
||
|
||
|
||
def mini_result_json(pt: str) -> str:
|
||
import json
|
||
|
||
if pt == "mbti":
|
||
return json.dumps({"mbtiType": "INTJ", "type": "INTJ"}, ensure_ascii=False)
|
||
if pt == "disc":
|
||
return json.dumps({"description": {"type": "D型"}}, ensure_ascii=False)
|
||
if pt == "pdp":
|
||
return json.dumps({"description": {"type": "老虎型"}}, ensure_ascii=False)
|
||
return json.dumps(
|
||
{"overview": "订单关联占位", "mbti": {"type": "INTJ"}},
|
||
ensure_ascii=False,
|
||
)
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--enterprise-id", type=int, default=5)
|
||
ap.add_argument("--dry-run", action="store_true")
|
||
ap.add_argument("--inviters", type=int, default=80)
|
||
ap.add_argument("--bindings", type=int, default=520)
|
||
args = ap.parse_args()
|
||
eid = args.enterprise_id
|
||
|
||
env = load_env(ENV_PATH)
|
||
host = env.get("DATABASE_HOSTNAME", "127.0.0.1")
|
||
port = int(env.get("DATABASE_HOSTPORT", "3306"))
|
||
user = env.get("DATABASE_USERNAME", "root")
|
||
password = env.get("DATABASE_PASSWORD", "")
|
||
database = env.get("DATABASE_DATABASE", "mbti")
|
||
prefix = env.get("DATABASE_PREFIX", "mbti_")
|
||
orders_t = f"{prefix}orders"
|
||
tr_t = f"{prefix}test_results"
|
||
wu_t = f"{prefix}wechat_users"
|
||
ag_t = f"{prefix}distribution_agents"
|
||
bd_t = f"{prefix}distribution_bindings"
|
||
cr_t = f"{prefix}commission_records"
|
||
|
||
conn = pymysql.connect(
|
||
host=host,
|
||
port=port,
|
||
user=user,
|
||
password=password,
|
||
database=database,
|
||
charset="utf8mb4",
|
||
autocommit=False,
|
||
)
|
||
rnd = random.Random(20260330 + eid)
|
||
now = int(time.time())
|
||
|
||
try:
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
f"SELECT COUNT(*) FROM `{wu_t}` WHERE `enterpriseId`=%s",
|
||
(eid,),
|
||
)
|
||
n_users = int(cur.fetchone()[0])
|
||
cur.execute(
|
||
f"SELECT `id`,`nickname` FROM `{wu_t}` WHERE `enterpriseId`=%s ORDER BY `id` ASC",
|
||
(eid,),
|
||
)
|
||
rows = cur.fetchall()
|
||
user_ids = [int(r[0]) for r in rows]
|
||
nick_map = {int(r[0]): (r[1] or "") for r in rows}
|
||
|
||
if not user_ids:
|
||
print({"ok": False, "error": "no users"})
|
||
return
|
||
|
||
target_orders = max(400, min(1400, n_users // 12))
|
||
existing_seed = 0
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
f"SELECT COUNT(*) FROM `{orders_t}` WHERE `orderNo` LIKE %s",
|
||
(ORDER_PREFIX + "%",),
|
||
)
|
||
existing_seed = int(cur.fetchone()[0])
|
||
|
||
if existing_seed >= target_orders * 0.9 and not args.dry_run:
|
||
print(
|
||
{
|
||
"ok": True,
|
||
"skipped": True,
|
||
"reason": "SEED5 orders already near target",
|
||
"existing_seed_orders": existing_seed,
|
||
"target_orders": target_orders,
|
||
}
|
||
)
|
||
return
|
||
|
||
need_orders = max(0, target_orders - existing_seed)
|
||
if args.dry_run:
|
||
print(
|
||
{
|
||
"ok": True,
|
||
"dry_run": True,
|
||
"n_users": n_users,
|
||
"target_orders": target_orders,
|
||
"existing_seed": existing_seed,
|
||
"would_insert_orders": need_orders,
|
||
"inviters": min(args.inviters, len(user_ids) // 4),
|
||
"bindings_cap": min(args.bindings, len(user_ids) // 2),
|
||
}
|
||
)
|
||
return
|
||
|
||
inviter_n = min(args.inviters, max(20, len(user_ids) // 6))
|
||
inviters = user_ids[:inviter_n]
|
||
pool_bind = user_ids[inviter_n + 50 : inviter_n + 50 + args.bindings]
|
||
if len(pool_bind) < 100:
|
||
pool_bind = user_ids[inviter_n + 10 :]
|
||
|
||
# 1) distribution_agents
|
||
inviter_to_agent: dict[int, int] = {}
|
||
with conn.cursor() as cur:
|
||
for uid in inviters:
|
||
cur.execute(f"SELECT id FROM `{ag_t}` WHERE userId=%s LIMIT 1", (uid,))
|
||
ex = cur.fetchone()
|
||
if ex:
|
||
inviter_to_agent[uid] = int(ex[0])
|
||
continue
|
||
name = (nick_map.get(uid) or f"用户{uid}")[:100]
|
||
cur.execute(
|
||
f"""
|
||
INSERT INTO `{ag_t}`
|
||
(`userId`,`agentName`,`contactPhone`,`contactEmail`,`totalOrders`,`totalCommission`,
|
||
`availableCommission`,`status`,`createdAt`,`updatedAt`)
|
||
VALUES (%s,%s,NULL,NULL,0,0,0,1,%s,%s)
|
||
""",
|
||
(uid, name, now, now),
|
||
)
|
||
inviter_to_agent[uid] = int(cur.lastrowid)
|
||
conn.commit()
|
||
|
||
# 2) orders + linked test_results (paid/completed only)
|
||
order_rows: list[tuple] = []
|
||
expire_at = now + 86400 * 400
|
||
ins_order = f"""
|
||
INSERT INTO `{orders_t}`
|
||
(`orderNo`,`userId`,`enterpriseId`,`productType`,`productTitle`,`amount`,`status`,
|
||
`payMethod`,`payTime`,`createdAt`,`updatedAt`)
|
||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||
"""
|
||
ins_tr = f"""
|
||
INSERT INTO `{tr_t}`
|
||
(`userId`,`testType`,`resultData`,`requiresPayment`,`isPaid`,`paidAmount`,`paidAt`,
|
||
`createdAt`,`updatedAt`,`enterpriseId`,`testScope`,`orderId`)
|
||
VALUES (%s,%s,%s,0,1,%s,%s,%s,%s,%s,'enterprise',%s)
|
||
"""
|
||
|
||
for k in range(need_orders):
|
||
uid = user_ids[(k * 9973) % len(user_ids)]
|
||
pt = pick_type(k)
|
||
amt = rnd.choice([100, 150, 200, 299, 399, 101, 201])
|
||
st = rnd.choices(
|
||
["paid", "completed", "pending"],
|
||
weights=[42, 28, 30],
|
||
k=1,
|
||
)[0]
|
||
ts = now - rnd.randint(0, 86400 * 20)
|
||
pay_t = ts if st in ("paid", "completed") else None
|
||
h = hashlib.md5(f"{uid}{k}{ts}".encode()).hexdigest()[:10]
|
||
ono = f"{ORDER_PREFIX}{ts}{uid}{k}{h}"[:48]
|
||
order_rows.append(
|
||
(ono, uid, eid, pt, product_title(pt), amt, st, "wechat", pay_t, ts, ts)
|
||
)
|
||
|
||
oid_map: list[int] = []
|
||
with conn.cursor() as cur:
|
||
for row in order_rows:
|
||
cur.execute(ins_order, row)
|
||
oid_map.append(int(cur.lastrowid))
|
||
oid = oid_map[-1]
|
||
st = row[6]
|
||
if st not in ("paid", "completed"):
|
||
continue
|
||
uid = row[1]
|
||
pt = row[3]
|
||
amt = row[5]
|
||
ts = row[9]
|
||
pay_t = row[8] or ts
|
||
cur.execute(
|
||
ins_tr,
|
||
(
|
||
uid,
|
||
pt,
|
||
mini_result_json(pt),
|
||
amt,
|
||
pay_t,
|
||
ts,
|
||
ts,
|
||
eid,
|
||
oid,
|
||
),
|
||
)
|
||
conn.commit()
|
||
|
||
# 3) bindings
|
||
bindings_done = 0
|
||
binding_ids: list[tuple[int, int, int]] = [] # id, inviter, invitee
|
||
with conn.cursor() as cur:
|
||
for i, invitee in enumerate(pool_bind[: args.bindings]):
|
||
inv = inviters[i % len(inviters)]
|
||
if invitee == inv:
|
||
continue
|
||
cur.execute(
|
||
f"""
|
||
SELECT id FROM `{bd_t}`
|
||
WHERE inviteeId=%s AND scope='enterprise' AND enterpriseId=%s
|
||
""",
|
||
(invitee, eid),
|
||
)
|
||
if cur.fetchone():
|
||
continue
|
||
tsb = now - rnd.randint(86400, 86400 * 60)
|
||
cur.execute(
|
||
f"""
|
||
INSERT INTO `{bd_t}`
|
||
(`inviterId`,`inviteeId`,`scope`,`enterpriseId`,`expireAt`,`status`,
|
||
`prevInviterId`,`overriddenAt`,`createdAt`,`updatedAt`)
|
||
VALUES (%s,%s,'enterprise',%s,%s,'active',NULL,NULL,%s,%s)
|
||
""",
|
||
(inv, invitee, eid, expire_at, tsb, tsb),
|
||
)
|
||
bid = int(cur.lastrowid)
|
||
binding_ids.append((bid, inv, invitee))
|
||
bindings_done += 1
|
||
conn.commit()
|
||
|
||
# 4) commission_records(与订单/绑定挂钩)
|
||
ins_cr = f"""
|
||
INSERT INTO `{cr_t}`
|
||
(`agentId`,`orderId`,`commissionRate`,`commissionAmount`,`status`,`paidAt`,
|
||
`createdAt`,`updatedAt`,`scope`,`enterpriseId`,`inviterId`,`inviteeId`,
|
||
`bindingId`,`orderAmount`,`commissionFen`,`frozenAt`,`unfrozenAt`,
|
||
`testResultId`,`commissionSource`)
|
||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,'enterprise',%s,%s,%s,%s,%s,%s,NULL,NULL,NULL,%s)
|
||
"""
|
||
cr_n = 0
|
||
with conn.cursor() as cur:
|
||
for j, oid in enumerate(oid_map[: min(len(oid_map), 450)]):
|
||
row = order_rows[j]
|
||
if row[6] not in ("paid", "completed"):
|
||
continue
|
||
uid_buyer = row[1]
|
||
inv = inviters[j % len(inviters)]
|
||
if uid_buyer == inv:
|
||
inv = inviters[(j + 1) % len(inviters)]
|
||
agent_id = inviter_to_agent.get(inv)
|
||
if not agent_id:
|
||
continue
|
||
bid_row = next((b for b in binding_ids if b[2] == uid_buyer), None)
|
||
bid = bid_row[0] if bid_row else None
|
||
oamt = row[5]
|
||
cfen = max(10, min(500, int(oamt * rnd.uniform(0.08, 0.22))))
|
||
st_f = rnd.choices(["paid", "frozen"], weights=[82, 18], k=1)[0]
|
||
paid_at = now - rnd.randint(0, 86400 * 14) if st_f == "paid" else None
|
||
tsr = paid_at or (now - rnd.randint(0, 86400 * 10))
|
||
camt = round(cfen / 100, 2)
|
||
cur.execute(
|
||
ins_cr,
|
||
(
|
||
agent_id,
|
||
oid,
|
||
10.0,
|
||
camt,
|
||
st_f,
|
||
paid_at,
|
||
tsr,
|
||
tsr,
|
||
eid,
|
||
inv,
|
||
uid_buyer,
|
||
bid,
|
||
oamt,
|
||
cfen,
|
||
"payment",
|
||
),
|
||
)
|
||
cr_n += 1
|
||
|
||
extra = min(120, len(binding_ids))
|
||
for j in range(extra):
|
||
bid, inv, invitee = binding_ids[j]
|
||
agent_id = inviter_to_agent.get(inv)
|
||
if not agent_id:
|
||
continue
|
||
cfen = rnd.randint(20, 180)
|
||
st_f = "paid"
|
||
tsr = now - rnd.randint(0, 86400 * 7)
|
||
cur.execute(
|
||
ins_cr,
|
||
(
|
||
agent_id,
|
||
None,
|
||
0.0,
|
||
round(cfen / 100, 2),
|
||
st_f,
|
||
tsr,
|
||
tsr,
|
||
tsr,
|
||
eid,
|
||
inv,
|
||
invitee,
|
||
bid,
|
||
0,
|
||
cfen,
|
||
"seed_invite",
|
||
),
|
||
)
|
||
cr_n += 1
|
||
|
||
conn.commit()
|
||
|
||
print(
|
||
{
|
||
"ok": True,
|
||
"enterpriseId": eid,
|
||
"n_users": n_users,
|
||
"inserted_orders": len(oid_map),
|
||
"bindings_inserted": bindings_done,
|
||
"commission_rows": cr_n,
|
||
"note": "已支付订单各带 1 条 test_results(orderId),概览「已完成测试」会增加;分销饼图按订单关联测评类型归类。",
|
||
}
|
||
)
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|