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

17 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 轮询终态期望 DONEstatus=2修复前该路径必 FAILEDD-05
5. DB 断言customer.is_biz_negotiated=0 is_child=0D-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, '==')