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.
351 lines
18 KiB
351 lines
18 KiB
|
1 week ago
|
"""票 05 运行时验证 — D-07 暂缓态写禁 + D-09 禁领接线。
|
||
|
|
前置:服务运行中(含票 05 jar,jarcheck NEW-JAR-WITH-T05);远程库可达。
|
||
|
|
模式复用 t04-verify.py / e2e-rules.py:Sess(debug token) + 中文 form body;Result data=null 非失败。
|
||
|
|
|
||
|
|
Part P:D-07 暂缓写禁 —— 建商机造全量子表数据 → claim → pause → 14 个写端点逐个断言 66003
|
||
|
|
→ 只读放行抽查 → resume → edit 恢复成功。
|
||
|
|
Part R:D-09 禁领 —— owner_dept 非空断言(票03修复前提)→ 部门专用 allowFreeClaim=0 发布
|
||
|
|
→ claim/claim-batch 全量预检 66014 且无部分领取 → 部门专用 allowFreeClaim=1 顶替发布
|
||
|
|
→ claim/claim-batch 放行。
|
||
|
|
CLEANUP:软删票05商机及子表行 + 停用/删除 e2e-t05 前缀规则与模板。
|
||
|
|
|
||
|
|
用法:python t05-verify.py [P|R|CLEANUP ...] # 无参全量
|
||
|
|
"""
|
||
|
|
import io, sys, time
|
||
|
|
from datetime import datetime
|
||
|
|
import requests
|
||
|
|
|
||
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||
|
|
sys.stderr = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||
|
|
|
||
|
|
BASE = 'http://localhost:8080'
|
||
|
|
UID = {'ADMIN': '739564171091247104', # 罗伟健 管理员 = 商机负责人
|
||
|
|
'A': '744842565802524672', # 赖永利
|
||
|
|
'B': '744842566742048768', # 肖琴
|
||
|
|
'C': '744842318024015872'} # 曾偲青
|
||
|
|
TAG = 'e2e-t05-' + datetime.now().strftime('%H%M%S')
|
||
|
|
SC_PATH = '/api/rule/opp-scheme-template'
|
||
|
|
PR_PATH = '/api/rule/opp-pool-rule'
|
||
|
|
|
||
|
|
results, fails = [], []
|
||
|
|
|
||
|
|
def rec(case, ok, ev=''):
|
||
|
|
v = '✅' if ok else '❌'
|
||
|
|
results.append((case, ok, ev))
|
||
|
|
if not ok:
|
||
|
|
fails.append((case, ev))
|
||
|
|
print(f" [{v}] {case}" + (f" —— {ev}" if ev else ''))
|
||
|
|
|
||
|
|
|
||
|
|
class Sess:
|
||
|
|
def __init__(self, uid):
|
||
|
|
r = requests.get(f'{BASE}/api/auth/debug/token', params={'userId': uid}, timeout=15)
|
||
|
|
b = r.json()
|
||
|
|
assert b.get('code') == 0, f'debug token 失败 {b}'
|
||
|
|
self.h = {'Authorization': f"Bearer {b['data']}"}
|
||
|
|
|
||
|
|
def raw(self, method, path, form=None, params=None):
|
||
|
|
headers = dict(self.h)
|
||
|
|
data = None
|
||
|
|
if form is not None:
|
||
|
|
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||
|
|
data = form
|
||
|
|
r = requests.request(method, BASE + path, headers=headers, data=data, params=params, timeout=30)
|
||
|
|
r.encoding = 'utf-8'
|
||
|
|
return r
|
||
|
|
|
||
|
|
def api(self, method, path, form=None, params=None, step=''):
|
||
|
|
try:
|
||
|
|
b = self.raw(method, path, form, params).json()
|
||
|
|
except Exception as e:
|
||
|
|
print(f" [EXC] {step or path}: {e}")
|
||
|
|
return None
|
||
|
|
if b.get('code') != 0:
|
||
|
|
print(f" [ERR] {step or path}: code={b.get('code')} msg={b.get('message')}")
|
||
|
|
return None
|
||
|
|
return b.get('data')
|
||
|
|
|
||
|
|
|
||
|
|
def db():
|
||
|
|
import pymysql
|
||
|
|
return pymysql.connect(host='8.129.84.155', port=3306, user='root', password='Itc@123456',
|
||
|
|
database='crm', charset='utf8mb4', autocommit=True)
|
||
|
|
|
||
|
|
def db_rows(sql, args=None):
|
||
|
|
conn = db()
|
||
|
|
try:
|
||
|
|
cur = conn.cursor()
|
||
|
|
cur.execute(sql, args or ())
|
||
|
|
return cur.fetchall()
|
||
|
|
finally:
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
|
||
|
|
S = None # admin Sess
|
||
|
|
|
||
|
|
def ensure_opp(name, remark):
|
||
|
|
oid = S.api('POST', '/api/opportunity',
|
||
|
|
form={'opportunityName': name, 'oppSource': 'opp_source_02', 'industryCode': 'gov',
|
||
|
|
'localityType': 'locality_type_01', 'bidForm': 'bid_form_01',
|
||
|
|
'partyAClear': 1, 'partyA': '票05甲方', 'remark': remark},
|
||
|
|
step=f'创建 {name}')
|
||
|
|
assert oid, f'创建商机失败: {name}'
|
||
|
|
return str(oid)
|
||
|
|
|
||
|
|
def expect_code(path, form=None, params=None, code=66003, step=''):
|
||
|
|
r = S.raw('POST', path, form=form, params=params)
|
||
|
|
b = r.json()
|
||
|
|
return b.get('code') == code, f"code={b.get('code')} msg={(b.get('message') or '')[:50]}"
|
||
|
|
|
||
|
|
def form_fields(items):
|
||
|
|
f = {}
|
||
|
|
for i, n in enumerate(items):
|
||
|
|
for k, v in n.items():
|
||
|
|
if v is not None:
|
||
|
|
f[f'fields[{i}].{k}'] = v
|
||
|
|
return f
|
||
|
|
|
||
|
|
|
||
|
|
def make_card_template():
|
||
|
|
"""现场造发布中方案卡模板(通用 applyScope=1,一显示字段,非必填)。"""
|
||
|
|
defs = S.api('GET', f'{SC_PATH}/field-defs', step='field-defs')
|
||
|
|
assert defs, 'field-defs 失败'
|
||
|
|
fkey = defs[0]['fieldKey']
|
||
|
|
name = f'{TAG}-卡模板'
|
||
|
|
S.api('POST', f'{SC_PATH}/save-draft',
|
||
|
|
form={'templateName': name, 'applyScope': 1, 'isDefault': 0, 'templateDesc': '票05验证'},
|
||
|
|
step='卡模板草稿')
|
||
|
|
rows = (S.api('POST', f'{SC_PATH}/page', form={'current': 1, 'size': 50, 'keyword': name}) or {}).get('content') or []
|
||
|
|
row = next((x for x in rows if x.get('templateName') == name and x.get('status') == 1), None)
|
||
|
|
assert row, '卡模板草稿未找到'
|
||
|
|
S.api('POST', f'{SC_PATH}/publish',
|
||
|
|
form={'id': row['id'], 'templateName': name, 'applyScope': 1, 'isDefault': 0,
|
||
|
|
**form_fields([{'fieldKey': fkey, 'isVisible': 1, 'isRequired': 0}])},
|
||
|
|
step='卡模板发布')
|
||
|
|
rows = (S.api('POST', f'{SC_PATH}/page', form={'current': 1, 'size': 50, 'keyword': name}) or {}).get('content') or []
|
||
|
|
pub = next((x for x in rows if x.get('templateName') == name and x.get('status') == 2), None)
|
||
|
|
assert pub, '卡模板未发布'
|
||
|
|
return pub['id']
|
||
|
|
|
||
|
|
|
||
|
|
# ==================== Part P:D-07 暂缓态写禁 ====================
|
||
|
|
|
||
|
|
def part_p():
|
||
|
|
print(f"\n===== Part P 暂缓态写禁(D-07)@{TAG} =====")
|
||
|
|
z = ensure_opp(f'票05暂缓Z-{TAG}', '票05 D-07 验证主商机')
|
||
|
|
|
||
|
|
# ---- pause 前造数据 ----
|
||
|
|
r = S.raw('POST', '/api/opportunity/customer/add',
|
||
|
|
form={'oppId': z, 'customerId': '5001', 'customerNameSnapshot': '客户甲',
|
||
|
|
'customerRole': 'customer_role_01', 'isPrimaryIntended': 0})
|
||
|
|
assert r.json().get('code') == 0, '前置 customer/add 失败'
|
||
|
|
|
||
|
|
r = S.raw('POST', '/api/opportunity/team/add',
|
||
|
|
form={'oppId': z, 'userIds': [UID['A'], UID['B']],
|
||
|
|
'projectRole': 'project_role_02', 'duty': '票05验证'})
|
||
|
|
b = r.json()
|
||
|
|
assert b.get('code') == 0 and len(b.get('data') or []) == 2, '前置 team/add 失败'
|
||
|
|
team_list = S.api('GET', '/api/opportunity/team/list', params={'oppId': z}, step='team/list') or []
|
||
|
|
row_a = next(x for x in team_list if str(x['userId']) == UID['A'])
|
||
|
|
row_b = next(x for x in team_list if str(x['userId']) == UID['B'])
|
||
|
|
|
||
|
|
r = S.raw('POST', '/api/opportunity/follow/add',
|
||
|
|
form={'oppId': z, 'followContent': '暂停前进跟进', 'followTime': '2026-08-30 09:00:00',
|
||
|
|
'followWay': 'follow_way_01', 'customerId': '5001'})
|
||
|
|
assert r.json().get('code') == 0, '前置 follow/add 失败'
|
||
|
|
fid = r.json().get('data')
|
||
|
|
|
||
|
|
r = S.raw('POST', '/api/opportunity/site-survey/add',
|
||
|
|
form={'oppId': z, 'surveyDate': '2026-08-30', 'engineerUserId': UID['A'],
|
||
|
|
'applyWay': 'apply_way_01', 'surveySeq': 'T05-S1'})
|
||
|
|
assert r.json().get('code') == 0, '前置 site-survey/add 失败'
|
||
|
|
sid = r.json().get('data')
|
||
|
|
|
||
|
|
r = S.raw('POST', '/api/opportunity/attachment/add',
|
||
|
|
form={'oppId': z, 'fileId': '8001', 'bizType': 'biz_type_01', 'remark': '票05'})
|
||
|
|
assert r.json().get('code') == 0, '前置 attachment/add 失败'
|
||
|
|
aid = r.json().get('data')
|
||
|
|
|
||
|
|
tpl_id = make_card_template()
|
||
|
|
r = S.raw('POST', '/api/opportunity/scheme-card/save',
|
||
|
|
form={'oppId': z, 'customerId': '5001', 'templateId': tpl_id, 'biddingForm': 'bid_form_01'})
|
||
|
|
b = r.json()
|
||
|
|
assert b.get('code') == 0, f'前置 scheme-card/save 失败: {b}'
|
||
|
|
card_id = b.get('data')
|
||
|
|
rec('P 前置数据就绪(客户/团队2/跟进/勘察/附件/卡)', True, f"follow={fid} survey={sid} att={aid} card={card_id}")
|
||
|
|
|
||
|
|
# ---- pause(创建即推进中,2→3)----
|
||
|
|
b = S.raw('POST', '/api/opportunity/pause', params={'id': z}).json()
|
||
|
|
assert b.get('code') == 0, f'pause 失败: {b}'
|
||
|
|
st = db_rows('SELECT opp_status FROM opportunity WHERE id=%s', (int(z),))[0][0]
|
||
|
|
rec('P0 claim→pause 后状态=3 暂缓中', st == 3, f"opp_status={st}")
|
||
|
|
if st != 3:
|
||
|
|
return
|
||
|
|
|
||
|
|
# ---- 14 写端点逐个断言 66003 ----
|
||
|
|
cases = [
|
||
|
|
('P1 edit 主表', '/api/opportunity/edit',
|
||
|
|
{'id': z, 'opportunityName': '票05暂缓Z改名', 'oppSource': 'opp_source_02', 'industryCode': 'gov',
|
||
|
|
'localityType': 'locality_type_01', 'bidForm': 'bid_form_01', 'partyAClear': 1, 'partyA': '票05甲方'}),
|
||
|
|
('P2 follow/add', '/api/opportunity/follow/add',
|
||
|
|
{'oppId': z, 'followContent': '暂缓期跟进应被拒', 'followTime': '2026-08-30 10:00:00',
|
||
|
|
'followWay': 'follow_way_01', 'customerId': '5001'}),
|
||
|
|
('P3 follow/delete', '/api/opportunity/follow/delete', None, {'id': fid}),
|
||
|
|
('P4 site-survey/add', '/api/opportunity/site-survey/add',
|
||
|
|
{'oppId': z, 'surveyDate': '2026-08-31', 'engineerUserId': UID['B'],
|
||
|
|
'applyWay': 'apply_way_01', 'surveySeq': 'T05-S2'}),
|
||
|
|
('P5 site-survey/delete', '/api/opportunity/site-survey/delete', None, {'id': sid}),
|
||
|
|
('P6 attachment/add', '/api/opportunity/attachment/add',
|
||
|
|
{'oppId': z, 'fileId': '8002', 'bizType': 'biz_type_01'}),
|
||
|
|
('P7 attachment/delete', '/api/opportunity/attachment/delete', None, {'id': aid}),
|
||
|
|
('P8 customer/add', '/api/opportunity/customer/add',
|
||
|
|
{'oppId': z, 'customerId': '5002', 'customerNameSnapshot': '客户乙',
|
||
|
|
'customerRole': 'customer_role_02', 'isPrimaryIntended': 0}),
|
||
|
|
('P9 customer/set-primary', '/api/opportunity/customer/set-primary',
|
||
|
|
{'oppId': z, 'customerId': '5001'}),
|
||
|
|
('P10 team/add', '/api/opportunity/team/add',
|
||
|
|
{'oppId': z, 'userIds': [UID['C']], 'projectRole': 'project_role_02'}),
|
||
|
|
('P11 team/update', '/api/opportunity/team/update', {'id': row_b['id'], 'duty': '暂缓期改职责'}),
|
||
|
|
('P12 team/delete', '/api/opportunity/team/delete', None, {'id': row_a['id']}),
|
||
|
|
('P13 scheme-card/save', '/api/opportunity/scheme-card/save',
|
||
|
|
{'id': card_id, 'oppId': z, 'customerId': '5001', 'templateId': tpl_id, 'schemeBudget': '888.88'}),
|
||
|
|
('P14 scheme-card/submit', '/api/opportunity/scheme-card/submit', None, {'id': card_id}),
|
||
|
|
]
|
||
|
|
for c in cases:
|
||
|
|
name, path = c[0], c[1]
|
||
|
|
form = c[2] if len(c) > 2 else None
|
||
|
|
params = c[3] if len(c) > 3 else None
|
||
|
|
ok, ev = expect_code(path, form, params, code=66003, step=name)
|
||
|
|
rec(name + ' → 66003', ok, ev)
|
||
|
|
|
||
|
|
# ---- 拦截后无副作用抽查(团队行数不变、跟进未增)----
|
||
|
|
n_team = db_rows('SELECT COUNT(*) FROM opportunity_team WHERE opportunity_id=%s AND deleted=0 AND delete_key=0',
|
||
|
|
(int(z),))[0][0]
|
||
|
|
n_follow = db_rows('SELECT COUNT(*) FROM opportunity_follow WHERE opp_id=%s AND deleted=0 AND delete_key=0',
|
||
|
|
(int(z),))[0][0]
|
||
|
|
rec('P15 拦截无副作用(团队仍 3 行、跟进仍 1 条)', n_team == 3 and n_follow == 1,
|
||
|
|
f"team={n_team} follow={n_follow}")
|
||
|
|
|
||
|
|
# ---- 只读放行 + resume 恢复 ----
|
||
|
|
d = S.api('GET', '/api/opportunity/customer/list', params={'oppId': z}, step='只读 customer/list')
|
||
|
|
rec('P16 只读放行:customer/list 正常', d is not None and len(d) == 1, f"rows={len(d or [])}")
|
||
|
|
|
||
|
|
b = S.raw('POST', '/api/opportunity/resume', params={'id': z}).json()
|
||
|
|
rec('P17 resume 成功(3→2)', b.get('code') == 0, f"code={b.get('code')}")
|
||
|
|
b = S.raw('POST', '/api/opportunity/edit',
|
||
|
|
form={'id': z, 'opportunityName': '票05暂缓Z恢复名', 'oppSource': 'opp_source_02',
|
||
|
|
'industryCode': 'gov', 'localityType': 'locality_type_01', 'bidForm': 'bid_form_01',
|
||
|
|
'partyAClear': 1, 'partyA': '票05甲方'}).json()
|
||
|
|
rec('P18 resume 后 edit 恢复成功', b.get('code') == 0, f"code={b.get('code')} msg={b.get('message')}")
|
||
|
|
|
||
|
|
|
||
|
|
# ==================== Part R:D-09 禁领接线 ====================
|
||
|
|
|
||
|
|
def to_pool(name):
|
||
|
|
"""建商机(创建即推进中)→ release-pool → 返回待领取商机 id。"""
|
||
|
|
oid = ensure_opp(name, '票05 D-09 验证')
|
||
|
|
b = S.raw('POST', '/api/opportunity/release-pool',
|
||
|
|
form={'id': oid, 'poolReason': 'pool_reason_02'}).json()
|
||
|
|
assert b.get('code') == 0, f'前置 release-pool 失败: {oid} {b}'
|
||
|
|
return oid
|
||
|
|
|
||
|
|
def pub_dept_rule(name, dept_id, allow):
|
||
|
|
"""发布部门专用规则(同部门重复发布会顶替停用旧版)。返回发布中行 id。"""
|
||
|
|
S.api('POST', f'{PR_PATH}/save-draft',
|
||
|
|
form={'ruleName': name, 'applyScope': 2, 'isDefault': 0, 'deptIds': dept_id,
|
||
|
|
'allowManualPool': 1, 'allowFreeClaim': allow,
|
||
|
|
'autoRecycleEnabled': 0, 'recycleRemindEnabled': 0},
|
||
|
|
step=f'{name} 草稿')
|
||
|
|
rows = (S.api('POST', f'{PR_PATH}/page', form={'current': 1, 'size': 50, 'keyword': name}) or {}).get('content') or []
|
||
|
|
row = next((x for x in rows if x.get('ruleName') == name and x.get('status') == 1), None)
|
||
|
|
assert row, f'规则草稿未找到: {name}'
|
||
|
|
S.api('POST', f'{PR_PATH}/publish',
|
||
|
|
form={'id': row['id'], 'ruleName': name, 'applyScope': 2, 'isDefault': 0, 'deptIds': dept_id,
|
||
|
|
'allowManualPool': 1, 'allowFreeClaim': allow,
|
||
|
|
'autoRecycleEnabled': 0, 'recycleRemindEnabled': 0},
|
||
|
|
step=f'{name} 发布')
|
||
|
|
rows = (S.api('POST', f'{PR_PATH}/page', form={'current': 1, 'size': 50, 'keyword': name}) or {}).get('content') or []
|
||
|
|
pub = next((x for x in rows if x.get('ruleName') == name and x.get('status') == 2), None)
|
||
|
|
assert pub, f'规则未发布: {name}'
|
||
|
|
return pub['id']
|
||
|
|
|
||
|
|
def part_r():
|
||
|
|
print(f"\n===== Part R 禁领接线(D-09)@{TAG} =====")
|
||
|
|
r1 = to_pool(f'票05禁领R1-{TAG}')
|
||
|
|
|
||
|
|
dep = db_rows('SELECT owner_dept_id, opp_status FROM opportunity WHERE id=%s', (int(r1),))[0]
|
||
|
|
owner_dept = str(dep[0]) if dep[0] else None
|
||
|
|
rec('R0 owner_dept_id 非空(票03修复前提)', dep[1] == 1 and owner_dept is not None,
|
||
|
|
f"owner_dept={owner_dept} status={dep[1]}")
|
||
|
|
if not owner_dept:
|
||
|
|
return
|
||
|
|
|
||
|
|
# ---- 部门专用禁领(allowFreeClaim=0)----
|
||
|
|
pub0 = pub_dept_rule(f'{TAG}-禁领版', owner_dept, 0)
|
||
|
|
ok, ev = expect_code('/api/opportunity/claim', None, {'id': r1}, code=66014)
|
||
|
|
rec('R1 部门专用禁领 → claim 66014', ok, ev)
|
||
|
|
|
||
|
|
r2 = to_pool(f'票05禁领R2-{TAG}')
|
||
|
|
b = S.raw('POST', '/api/opportunity/claim-batch', params={'ids': [r1, r2]}).json()
|
||
|
|
st = dict((str(x[0]), x[1]) for x in db_rows(
|
||
|
|
'SELECT id, opp_status FROM opportunity WHERE id IN (%s,%s)', (int(r1), int(r2))))
|
||
|
|
rec('R2 claim-batch 全量预检 → 66014 且两单均未被领取',
|
||
|
|
b.get('code') == 66014 and st.get(str(r1)) == 1 and st.get(str(r2)) == 1,
|
||
|
|
f"code={b.get('code')} status={st}")
|
||
|
|
|
||
|
|
# ---- 部门专用放行(先停用禁领版——部门覆盖唯一约束 64012 硬拒同部门双发布)----
|
||
|
|
S.api('POST', f'{PR_PATH}/disable', params={'id': pub0}, step='停用禁领版')
|
||
|
|
pub1 = pub_dept_rule(f'{TAG}-放行版', owner_dept, 1)
|
||
|
|
b = S.raw('POST', '/api/opportunity/claim', params={'id': r1}).json()
|
||
|
|
st1 = db_rows('SELECT opp_status FROM opportunity WHERE id=%s', (int(r1),))[0][0]
|
||
|
|
rec('R3 部门专用放行 → claim 成功(status 1→2)', b.get('code') == 0 and st1 == 2,
|
||
|
|
f"code={b.get('code')} status={st1}")
|
||
|
|
b = S.raw('POST', '/api/opportunity/claim-batch', params={'ids': [r2]}).json()
|
||
|
|
st2 = db_rows('SELECT opp_status FROM opportunity WHERE id=%s', (int(r2),))[0][0]
|
||
|
|
rec('R4 claim-batch 放行 → 成功(status 1→2)', b.get('code') == 0 and st2 == 2,
|
||
|
|
f"code={b.get('code')} status={st2}")
|
||
|
|
|
||
|
|
|
||
|
|
# ==================== Part CLEANUP ====================
|
||
|
|
|
||
|
|
def part_cleanup():
|
||
|
|
print(f"\n===== CLEANUP 票05 验证数据 @ {TAG} =====")
|
||
|
|
# 规则与模板:e2e-t05 前缀,发布中停用、草稿删除
|
||
|
|
for path, nmkey in ((PR_PATH, 'ruleName'), (SC_PATH, 'templateName')):
|
||
|
|
rows = (S.api('POST', f'{path}/page', form={'current': 1, 'size': 50, 'keyword': 'e2e-t05-'}) or {}).get('content') or []
|
||
|
|
for x in rows:
|
||
|
|
nm, rid, st = x.get(nmkey) or '', x.get('id'), x.get('status')
|
||
|
|
if not nm.startswith('e2e-t05-'):
|
||
|
|
continue
|
||
|
|
if st == 2:
|
||
|
|
S.api('POST', f'{path}/disable', params={'id': rid}, step=f'disable {nm}')
|
||
|
|
elif st == 1:
|
||
|
|
S.api('POST', f'{path}/delete', params={'id': rid}, step=f'delete {nm}')
|
||
|
|
# 商机软删(票05 前缀 + 本次 TAG)
|
||
|
|
rows = db_rows("SELECT id FROM opportunity WHERE opp_name LIKE %s OR remark LIKE %s",
|
||
|
|
('票05%', '票05%'))
|
||
|
|
n = 0
|
||
|
|
for (oid,) in rows:
|
||
|
|
db_rows('UPDATE opportunity SET deleted=1 WHERE id=%s', (oid,))
|
||
|
|
for tbl in ('opportunity_customer', 'opportunity_team'): # 两表 FK 列名 = opportunity_id
|
||
|
|
db_rows(f'UPDATE {tbl} SET deleted=1 WHERE opportunity_id=%s', (oid,))
|
||
|
|
for tbl in ('opportunity_follow', 'opportunity_site_survey',
|
||
|
|
'opportunity_attachment', 'opportunity_scheme_card'): # 四表 FK 列名 = opp_id
|
||
|
|
db_rows(f'UPDATE {tbl} SET deleted=1 WHERE opp_id=%s', (oid,))
|
||
|
|
db_rows('UPDATE opportunity_oplog SET deleted=1 WHERE opp_id=%s', (oid,))
|
||
|
|
n += 1
|
||
|
|
rec(f'CLEANUP 软删 {n} 个票05商机及子表行 + 停用自建规则/模板', True, '')
|
||
|
|
|
||
|
|
|
||
|
|
PARTS = {'P': part_p, 'R': part_r, 'CLEANUP': part_cleanup}
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
args = [a.upper() for a in sys.argv[1:]] or ['P', 'R', 'CLEANUP']
|
||
|
|
S = Sess(UID['ADMIN'])
|
||
|
|
for p in args:
|
||
|
|
PARTS[p]()
|
||
|
|
print(f"\n===== 汇总 {len(results)} 项,失败 {len(fails)} =====")
|
||
|
|
for case, ev in fails:
|
||
|
|
print(f" ❌ {case}: {ev}")
|
||
|
|
sys.exit(1 if fails else 0)
|