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.
156 lines
7.0 KiB
156 lines
7.0 KiB
|
18 hours ago
|
# -*- coding: utf-8 -*-
|
||
|
|
"""customer-defectfix 手工复核:I-07 同款含 INSERT 行的 APPEND_ONLY 导入(D-05 终验)
|
||
|
|
|
||
|
|
流程(任务书 §三.5):
|
||
|
|
1. GET /api/customer/import/template 下载模板(模板端点可用性顺带验证)
|
||
|
|
2. openpyxl 往模板「客户」sheet 追加 2 行新客户(INSERT 行,e2c-defix-* 前缀)
|
||
|
|
—— 若模板结构意外,退回 I-07 同款自造 9 列文件(表头已验证兼容)
|
||
|
|
3. APPEND_ONLY 上传 → preview insertCount=2
|
||
|
|
4. confirm → 轮询终态:期望 DONE(status=2);修复前该路径必 FAILED(D-05)
|
||
|
|
5. DB 断言:customer.is_biz_negotiated=0 且 is_child=0(D-05 修复落库口径)
|
||
|
|
落盘:.scratch/customer-defectfix/manual-import-result.json
|
||
|
|
清理:客户行留在库内由残留清理阶段(e2c-* + archive 正门)统一回收。
|
||
|
|
"""
|
||
|
|
import sys, io, json, time
|
||
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||
|
|
import requests
|
||
|
|
import pymysql
|
||
|
|
from openpyxl import Workbook, load_workbook
|
||
|
|
|
||
|
|
BASE = 'http://localhost:8080'
|
||
|
|
ADMIN = '739564171091247104'
|
||
|
|
OUT = '.scratch/customer-defectfix/manual-import-result.json'
|
||
|
|
TPL = '.scratch/customer-defectfix/_template-download.xlsx'
|
||
|
|
XLSX = '.scratch/customer-defectfix/_manual-insert.xlsx'
|
||
|
|
PFX = 'e2c-defix'
|
||
|
|
TS = time.strftime('%H%M%S')
|
||
|
|
CTYPE = 'customer_type_01'
|
||
|
|
|
||
|
|
|
||
|
|
def db():
|
||
|
|
return pymysql.connect(host='8.129.84.155', port=3306, user='root', password='Itc@123456',
|
||
|
|
database='crm', charset='utf8mb4', autocommit=True,
|
||
|
|
connect_timeout=10, cursorclass=pymysql.cursors.DictCursor)
|
||
|
|
|
||
|
|
|
||
|
|
def dbq(sql, args=None):
|
||
|
|
with db() as conn, conn.cursor() as cur:
|
||
|
|
cur.execute(sql, args)
|
||
|
|
return cur.fetchall()
|
||
|
|
|
||
|
|
|
||
|
|
def get_token(uid):
|
||
|
|
r = requests.get(f'{BASE}/api/auth/debug/token', params={'userId': uid}, timeout=30)
|
||
|
|
assert r.status_code == 200, f'debug/token 失败 HTTP {r.status_code}'
|
||
|
|
d = r.json().get('data')
|
||
|
|
tok = d if isinstance(d, str) else (d or {}).get('token')
|
||
|
|
assert tok, f'token 空: {r.text[:120]}'
|
||
|
|
return tok
|
||
|
|
|
||
|
|
|
||
|
|
S = requests.Session()
|
||
|
|
S.headers['Authorization'] = f'Bearer {get_token(ADMIN)}'
|
||
|
|
report = {'steps': []}
|
||
|
|
|
||
|
|
|
||
|
|
def step(name, ok, detail=''):
|
||
|
|
report['steps'].append({'step': name, 'ok': ok, 'detail': detail})
|
||
|
|
print((' pass' if ok else ' FAIL'), name, '—', detail)
|
||
|
|
return ok
|
||
|
|
|
||
|
|
|
||
|
|
# ---- 1. 下载模板 ----
|
||
|
|
r = S.get(f'{BASE}/api/customer/import/template', timeout=60)
|
||
|
|
tpl_ok = r.status_code == 200 and r.content[:2] == b'PK'
|
||
|
|
step('template 下载', tpl_ok, f'HTTP {r.status_code} bytes={len(r.content)} magic={r.content[:2]!r}')
|
||
|
|
if not tpl_ok:
|
||
|
|
json.dump(report, open(OUT, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
|
||
|
|
sys.exit(1)
|
||
|
|
open(TPL, 'wb').write(r.content)
|
||
|
|
|
||
|
|
# ---- 2. 追加 INSERT 行(优先模板路径,退回自造) ----
|
||
|
|
# 行须带全 NOT NULL 无默认列的值(编号/省/市/行业——同 D-05 同族列群;heavy F-14 r1 同形态),
|
||
|
|
# 缺任一列执行期即行级失败(首轮实证:city_code doesn't have a default value,D-04 截断明细保留)
|
||
|
|
ind = dbq("SELECT i.code c 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 NOT NULL "
|
||
|
|
"ORDER BY i.sort_no LIMIT 1")
|
||
|
|
ind_code = ind[0]['c'] if ind else ''
|
||
|
|
name_a, name_b = f'{PFX}-插入A-{TS}', f'{PFX}-插入B-{TS}'
|
||
|
|
rows = [
|
||
|
|
[f'E2C-DEFIX-{TS}A', name_a, CTYPE, None, '440000', '440100', ind_code, '4', 'defix D-05 复核行A'],
|
||
|
|
[f'E2C-DEFIX-{TS}B', name_b, CTYPE, None, '440000', '440100', ind_code, '5', 'defix D-05 复核行B'],
|
||
|
|
]
|
||
|
|
used_tpl = False
|
||
|
|
try:
|
||
|
|
wb = load_workbook(TPL)
|
||
|
|
ws = wb[wb.sheetnames[0]]
|
||
|
|
head = [c.value for c in ws[1]]
|
||
|
|
if head and '客户名称' in head:
|
||
|
|
for row in rows:
|
||
|
|
ws.append([(row[i] if i < len(row) else None) for i in range(len(head))])
|
||
|
|
wb.save(XLSX)
|
||
|
|
used_tpl = True
|
||
|
|
except Exception as e:
|
||
|
|
print(' 模板追加失败,退回自造:', e)
|
||
|
|
if not used_tpl:
|
||
|
|
wb = Workbook()
|
||
|
|
ws = wb.active
|
||
|
|
ws.title = '客户'
|
||
|
|
ws.append(['客户编号', '客户名称', '客户类型', '统一社会信用代码',
|
||
|
|
'省份编码', '城市编码', '行业编码', '客户星级(1-5)', '备注'])
|
||
|
|
for row in rows:
|
||
|
|
ws.append(row)
|
||
|
|
wb.save(XLSX)
|
||
|
|
step('构造 INSERT 行文件', True, f'path={XLSX} used_template={used_tpl} names={name_a},{name_b}')
|
||
|
|
|
||
|
|
# ---- 3. APPEND_ONLY 上传 ----
|
||
|
|
with open(XLSX, 'rb') as f:
|
||
|
|
r = S.post(f'{BASE}/api/customer/import/upload',
|
||
|
|
files={'file': ('manual-insert.xlsx', f,
|
||
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')},
|
||
|
|
data={'importMode': 'APPEND_ONLY'}, timeout=60)
|
||
|
|
body = r.json() if r.status_code == 200 else {'_http': r.status_code, '_raw': r.text[:300]}
|
||
|
|
data = body.get('data') or {}
|
||
|
|
task_id = data.get('taskId') or data.get('id')
|
||
|
|
step('upload APPEND_ONLY', r.status_code == 200 and body.get('code') == 0 and task_id is not None,
|
||
|
|
f'code={body.get("code")} task={task_id} preview={json.dumps(data, ensure_ascii=False)[:260]}')
|
||
|
|
report['taskId'] = task_id
|
||
|
|
report['preview'] = data
|
||
|
|
if not task_id:
|
||
|
|
json.dump(report, open(OUT, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
# ---- 4. confirm + 轮询终态 ----
|
||
|
|
r = S.post(f'{BASE}/api/customer/import/confirm', params={'taskId': task_id}, timeout=30)
|
||
|
|
cb = r.json() if r.status_code == 200 else {'_http': r.status_code}
|
||
|
|
step('confirm', cb.get('code') == 0, f'code={cb.get("code")} msg={cb.get("message")}')
|
||
|
|
status, result = None, {}
|
||
|
|
for _ in range(30):
|
||
|
|
time.sleep(1)
|
||
|
|
r = S.get(f'{BASE}/api/customer/import/result', params={'taskId': task_id}, timeout=30)
|
||
|
|
result = (r.json().get('data') or {}) if r.status_code == 200 else {}
|
||
|
|
status = str(result.get('status'))
|
||
|
|
if status in ('2', '3'):
|
||
|
|
break
|
||
|
|
report['result'] = result
|
||
|
|
done = status == '2'
|
||
|
|
step('任务终态 DONE(修复前该路径必 FAILED=D-05)', done,
|
||
|
|
f'status={status} insertCount={result.get("insertCount")} failCount={result.get("failCount")} '
|
||
|
|
f'failReason={str(result.get("failReason"))[:120]}')
|
||
|
|
|
||
|
|
# ---- 5. DB 断言 ----
|
||
|
|
rows_db = dbq("SELECT id, customer_no, customer_name, is_biz_negotiated b, is_child c, "
|
||
|
|
"customer_stage st, owner_user_id o FROM customer WHERE customer_name IN (%s,%s)",
|
||
|
|
(name_a, name_b))
|
||
|
|
ok_db = len(rows_db) == 2 and all(str(x['b']) == '0' and str(x['c']) == '0' for x in rows_db)
|
||
|
|
step('DB is_biz_negotiated=0 且 is_child=0(INSERT 行缺省口径)', ok_db,
|
||
|
|
json.dumps([{k: str(v) for k, v in x.items()} for x in rows_db], ensure_ascii=False))
|
||
|
|
report['db'] = [{k: str(v) for k, v in x.items()} for x in rows_db]
|
||
|
|
|
||
|
|
fails = dbq("SELECT COUNT(*) n FROM customer_import_fail WHERE task_id=%s", (int(task_id),))
|
||
|
|
step('成功任务无失败明细行', int(fails[0]['n']) == 0, f"fail_rows={fails[0]['n']}")
|
||
|
|
|
||
|
|
report['ALL_OK'] = all(s['ok'] for s in report['steps'])
|
||
|
|
json.dump(report, open(OUT, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
|
||
|
|
print('\n== 手工复核', 'ALL_OK ✅' if report['ALL_OK'] else '存在 FAIL ❌', ',证据落盘', OUT, '==')
|