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.
 
 
 
 
 

343 lines
16 KiB

"""票 04 — 6 端点运行时验证(D-01~D-06 关联客户 add/search/set-primary + 团队 add/update/delete)。
前置:服务运行中(含票 04 jar);远程库可达。
模式复用 t03-verify.py:Sess(debug token) + 中文表单 body;Result data=null 非失败。
操作人统一 ADMIN(罗伟健)=商机负责人;团队成员 A/B/C。
客户域用编造 customerId(5001/5002/5003)——A4 未建,引用 id + 名快照语义,无外键。
用法:python t04-verify.py [C|T|CLEANUP ...] # 无参全量
"""
import io, sys
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 = 't04-' + datetime.now().strftime('%H%M%S')
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()
def link_row(opp_id, customer_id):
rows = db_rows('SELECT id, is_primary_intended FROM opportunity_customer '
'WHERE opportunity_id=%s AND customer_id=%s AND delete_key=0 AND deleted=0',
(int(opp_id), int(customer_id)))
return rows[0] if rows else None
def primary_snapshot(opp_id):
row = db_rows('SELECT primary_customer_id, primary_customer_name_snapshot FROM opportunity WHERE id=%s',
(int(opp_id),))
return row[0] if row else (None, None)
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': '票04甲方', 'remark': remark},
step=f'创建 {name}')
assert oid, f'创建商机失败: {name}'
return str(oid)
# ==================== Part C:客户域 ====================
def part_c():
print(f"\n===== Part C 客户域(D-01/02/03)@{TAG} =====")
z = ensure_opp(f'票04客户域Z-{TAG}', '票04 D-01/02/03 验证主商机')
z2 = ensure_opp(f'票04候选池Z2-{TAG}', '票04 D-02 候选池排除验证')
# D-01 负路径:缺必填
r = S.raw('POST', '/api/opportunity/customer/add',
form={'oppId': z, 'customerId': '5001', 'customerNameSnapshot': '客户甲', 'isPrimaryIntended': 0})
b = r.json()
rec('C-负1 add 缺 customerRole → 66001', b.get('code') == 66001,
f"code={b.get('code')} msg={b.get('message')}")
# D-01 主路径:普通关联 5001
r = S.raw('POST', '/api/opportunity/customer/add',
form={'oppId': z, 'customerId': '5001', 'customerNameSnapshot': '客户甲',
'customerRole': 'customer_role_01', 'isPrimaryIntended': 0})
b = r.json()
ok = b.get('code') == 0 and b.get('data')
rec('C1 add 普通关联 5001', ok, f"code={b.get('code')} id={b.get('data')}")
if not ok:
return
# D-01 负路径:重复关联
r = S.raw('POST', '/api/opportunity/customer/add',
form={'oppId': z, 'customerId': '5001', 'customerNameSnapshot': '客户甲',
'customerRole': 'customer_role_01', 'isPrimaryIntended': 0})
b = r.json()
rec('C-负2 重复关联 → 66011', b.get('code') == 66011 and '重复' in (b.get('message') or ''),
f"code={b.get('code')} msg={b.get('message')}")
# D-02 候选池:Z2 search 应命中 5001(Z 关联了它,Z2 未关联)
d = S.api('POST', '/api/opportunity/customer/search',
form={'oppId': z2, 'keyword': '客户甲', 'current': 1, 'size': 10}, step='Z2 search')
ids = [str(x['customerId']) for x in (d.get('content') or [])] if d else []
rec('C2 search Z2 命中 5001(快照池)', d is not None and '5001' in ids, f"ids={ids} total={d.get('total') if d else None}")
# D-02 排除:Z2 关联 5001 后 search 不再命中
S.raw('POST', '/api/opportunity/customer/add',
form={'oppId': z2, 'customerId': '5001', 'customerNameSnapshot': '客户甲',
'customerRole': 'customer_role_01', 'isPrimaryIntended': 0})
d = S.api('POST', '/api/opportunity/customer/search',
form={'oppId': z2, 'keyword': '客户甲', 'current': 1, 'size': 10}, step='Z2 search 排除')
ids = [str(x['customerId']) for x in (d.get('content') or [])] if d else []
rec('C3 search Z2 排除已关联 5001', d is not None and '5001' not in ids, f"ids={ids}")
# D-01 add 直设主要 + 降旧主:5002 primary=1,再 5003 primary=1
S.raw('POST', '/api/opportunity/customer/add',
form={'oppId': z, 'customerId': '5002', 'customerNameSnapshot': '客户乙',
'customerRole': 'customer_role_02', 'isPrimaryIntended': 1})
row2 = link_row(z, 5002)
old_opp = primary_snapshot(z)
S.raw('POST', '/api/opportunity/customer/add',
form={'oppId': z, 'customerId': '5003', 'customerNameSnapshot': '客户丙',
'customerRole': 'customer_role_01', 'isPrimaryIntended': 1})
row3 = link_row(z, 5003)
row2b = link_row(z, 5002)
new_opp = primary_snapshot(z)
rec('C4 add 直设主要自动降旧主(5002:1→0,5003:1)+ 主表快照回写',
bool(row2 and row2[1] == 1 and row3 and row3[1] == 1 and row2b and row2b[1] == 0
and str(new_opp[0]) == '5003' and new_opp[1] == '客户丙'),
f"5002={row2b[1] if row2b else None} 5003={row3[1] if row3 else None} "
f"主表=({new_opp[0]},{new_opp[1]}) 中途=({old_opp[0]},{old_opp[1]})")
# D-03 set-primary 三步事务
r = S.raw('POST', '/api/opportunity/customer/set-primary', form={'oppId': z, 'customerId': '5001'})
b = r.json()
r1 = link_row(z, 5001); r3 = link_row(z, 5003); snap = primary_snapshot(z)
rec('C5 set-primary 5001(旧主 5003 降 0 + 新主置 1 + 主表快照=客户甲)',
b.get('code') == 0 and r1 and r1[1] == 1 and r3 and r3[0] and r3[1] == 0
and str(snap[0]) == '5001' and snap[1] == '客户甲',
f"code={b.get('code')} 5001={r1[1] if r1 else None} 5003={r3[1] if r3 else None} 主表=({snap[0]},{snap[1]})")
# D-03 幂等
r = S.raw('POST', '/api/opportunity/customer/set-primary', form={'oppId': z, 'customerId': '5001'})
b = r.json()
rec('C6 set-primary 幂等(已是主要)', b.get('code') == 0, f"code={b.get('code')}")
# D-03 负路径:未关联客户
r = S.raw('POST', '/api/opportunity/customer/set-primary', form={'oppId': z, 'customerId': '99999'})
b = r.json()
rec('C-负3 set-primary 未关联客户 → 66001', b.get('code') == 66001 and '未关联' in (b.get('message') or ''),
f"code={b.get('code')} msg={b.get('message')}")
# customer/list:3 行存活(软删不出)
d = S.api('GET', '/api/opportunity/customer/list', params={'oppId': z}, step='Z list')
cids = sorted(str(x['customerId']) for x in (d or []))
rec('C7 customer/list 3 行(5001/5002/5003)', cids == ['5001', '5002', '5003'], f"cids={cids}")
# 操作日志抽查
d = S.api('POST', '/api/opportunity/oplog/page', form={'oppId': z, 'pageNum': 1, 'pageSize': 20}, step='Z oplog')
kinds = [x.get('opKind') for x in (d.get('content') or [])] if d else []
rec('C8 操作日志含 ROW_ADD/FIELD_CHANGE', d is not None and 'ROW_ADD' in kinds and 'FIELD_CHANGE' in kinds,
f"kinds={kinds[:8]}")
# ==================== Part T:团队域 ====================
def team_row_by_uid(opp_id, uid):
rows = db_rows('SELECT id, user_id, user_name_snapshot, project_role, duty, permission '
'FROM opportunity_team WHERE opportunity_id=%s AND user_id=%s AND delete_key=0 AND deleted=0',
(int(opp_id), int(uid)))
return rows[0] if rows else None
def part_t():
print(f"\n===== Part T 团队域(D-04/05/06)@{TAG} =====")
z = ensure_opp(f'票04团队域T-{TAG}', '票04 D-04/05/06 验证商机')
opp_row = db_rows('SELECT owner_user_id FROM opportunity WHERE id=%s', (int(z),))[0]
rec('T0 owner=ADMIN', str(opp_row[0]) == UID['ADMIN'], f"owner_user_id={opp_row[0]}")
# D-04 批量添加 A/B(统一角色职责,permission 缺省)
r = S.raw('POST', '/api/opportunity/team/add',
form={'oppId': z, 'userIds': [UID['A'], UID['B']],
'projectRole': 'project_role_02', 'duty': '验证职责'})
b = r.json()
added = b.get('data') or []
rec('T1 team/add 批量 [A,B] → added 2', b.get('code') == 0 and len(added) == 2,
f"code={b.get('code')} added={added}")
if not (b.get('code') == 0 and len(added) == 2):
return
ra = team_row_by_uid(z, UID['A'])
db_name_a = db_rows('SELECT username FROM crm_auth_user WHERE id=%s', (int(UID['A']),))
rec('T2 行字段:role_02/duty/permission 默认 1/姓名快照=crm_auth_user.username',
bool(ra and ra[3] == 'project_role_02' and ra[4] == '验证职责' and ra[5] == 1
and db_name_a and ra[2] == db_name_a[0][0]),
f"row={ra}")
# D-04 负路径:全重
r = S.raw('POST', '/api/opportunity/team/add', form={'oppId': z, 'userIds': [UID['A']]})
b = r.json()
rec('T-负1 重复添加 A → 66012「该成员已存在」',
b.get('code') == 66012 and '该成员已存在' in (b.get('message') or ''),
f"code={b.get('code')} msg={b.get('message')}")
# D-04 批量局部跳过:[A,C] → 只插 C
r = S.raw('POST', '/api/opportunity/team/add',
form={'oppId': z, 'userIds': [UID['A'], UID['C']],
'projectRole': 'project_role_02', 'duty': '验证职责'})
b = r.json()
ok = b.get('code') == 0 and len(b.get('data') or []) == 1
rc = team_row_by_uid(z, UID['C'])
rec('T3 批量局部跳过 [A,C] → 仅插 C', ok and bool(rc), f"code={b.get('code')} added={b.get('data')}")
# D-04 负路径:未知用户
r = S.raw('POST', '/api/opportunity/team/add',
form={'oppId': z, 'userIds': ['999999'], 'projectRole': 'project_role_02'})
b = r.json()
rec('T-负2 未知用户 999999 → 66001', b.get('code') == 66001 and '用户不存在' in (b.get('message') or ''),
f"code={b.get('code')} msg={b.get('message')}")
# D-06 update 普通成员改职责
r = S.raw('POST', '/api/opportunity/team/update',
form={'id': ra[0], 'duty': '新职责'})
b = r.json()
ra2 = team_row_by_uid(z, UID['A'])
rec('T4 update 改职责 A → 「新职责」', b.get('code') == 0 and ra2 and ra2[4] == '新职责',
f"code={b.get('code')} duty={ra2[4] if ra2 else None}")
# D-06 换人负路径:目标 C 已在团队 → 66012
rb = team_row_by_uid(z, UID['B'])
r = S.raw('POST', '/api/opportunity/team/update', form={'id': rb[0], 'userId': UID['C']})
b = r.json()
rec('T-负3 换人目标 C 已在团队 → 66012', b.get('code') == 66012,
f"code={b.get('code')} msg={b.get('message')}")
# D-05 移除 C(软删)→ 再换人 B 行 userId=C 成功(复用键)
r = S.raw('POST', '/api/opportunity/team/delete', form={'id': rc[0]})
b = r.json()
soft = db_rows('SELECT delete_key, deleted FROM opportunity_team WHERE id=%s', (int(rc[0]),))
d = S.api('GET', '/api/opportunity/team/list', params={'oppId': z}, step='team/list')
uids = [str(x['userId']) for x in (d or [])]
rec('T5 delete C 软删(delete_key=id, deleted=1;list 不出)',
b.get('code') == 0 and soft and int(soft[0][0]) == int(rc[0]) and int(soft[0][1]) == 1
and UID['C'] not in uids,
f"code={b.get('code')} soft={soft} list={uids}")
r = S.raw('POST', '/api/opportunity/team/update', form={'id': rb[0], 'userId': UID['C']})
b = r.json()
rb2 = team_row_by_uid(z, UID['C'])
db_name_c = db_rows('SELECT username FROM crm_auth_user WHERE id=%s', (int(UID['C']),))
rec('T6 换人成功:B 行 → C(快照服务端刷新,无需先加后删)',
b.get('code') == 0 and rb2 and db_name_c and rb2[2] == db_name_c[0][0],
f"code={b.get('code')} row={rb2}")
# D-05/06 负责人行锁定
row_owner = db_rows('SELECT id FROM opportunity_team WHERE opportunity_id=%s AND user_id=%s '
'AND delete_key=0 AND deleted=0', (int(z), int(UID['ADMIN'])))[0]
r = S.raw('POST', '/api/opportunity/team/delete', form={'id': row_owner[0]})
b = r.json()
rec('T-负4 删负责人行 → 66013「请走移交」',
b.get('code') == 66013 and '移交' in (b.get('message') or ''),
f"code={b.get('code')} msg={b.get('message')}")
r = S.raw('POST', '/api/opportunity/team/update', form={'id': row_owner[0], 'userId': UID['C']})
b = r.json()
rec('T-负5 负责人行换人 → 66013', b.get('code') == 66013, f"code={b.get('code')}")
r = S.raw('POST', '/api/opportunity/team/update',
form={'id': row_owner[0], 'projectRole': 'project_role_02'})
b = r.json()
rec('T-负6 负责人行改项目角色 → 66013', b.get('code') == 66013, f"code={b.get('code')}")
r = S.raw('POST', '/api/opportunity/team/update', form={'id': row_owner[0], 'duty': '负责人职责可改'})
b = r.json()
row_owner2 = db_rows('SELECT duty FROM opportunity_team WHERE id=%s', (int(row_owner[0]),))[0]
rec('T7 负责人行仅职责可改', b.get('code') == 0 and row_owner2[0] == '负责人职责可改',
f"code={b.get('code')} duty={row_owner2[0]}")
# oplog 抽查
d = S.api('POST', '/api/opportunity/oplog/page', form={'oppId': z, 'pageNum': 1, 'pageSize': 20}, step='T oplog')
ents = [(x.get('opKind'), x.get('entityName')) for x in (d.get('content') or [])] if d else []
rec('T8 oplog 含 opportunity_team ROW_ADD/ROW_DELETE',
d is not None and ('ROW_ADD', 'opportunity_team') in ents and ('ROW_DELETE', 'opportunity_team') in ents,
f"ents={ents[:8]}")
# ==================== Part CLEANUP ====================
def part_cleanup():
print(f"\n===== CLEANUP 票04 验证数据 @ {TAG} =====")
rows = db_rows("SELECT id FROM opportunity WHERE remark LIKE %s OR opp_name LIKE %s",
('票04%', '票04%'))
n = 0
for (oid,) in rows:
db_rows('UPDATE opportunity SET deleted=1 WHERE id=%s', (oid,))
for tbl in ('opportunity_customer', 'opportunity_team'):
db_rows(f'UPDATE {tbl} SET deleted=1 WHERE opportunity_id=%s', (oid,))
db_rows('UPDATE opportunity_oplog SET deleted=1 WHERE opp_id=%s', (oid,))
n += 1
rec(f'CLEANUP 软删 {n} 个票04商机及子表行', True, f"ids={[r[0] for r in rows]}")
PARTS = {'C': part_c, 'T': part_t, 'CLEANUP': part_cleanup}
if __name__ == '__main__':
args = [a.upper() for a in sys.argv[1:]] or ['C', 'T', '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)