feat: 根目录文档、脚本、gitignore
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT action, COUNT(*) c FROM zt_action WHERE objecttype='story' GROUP BY action ORDER BY c DESC LIMIT 15")
|
||||
for r in cur.fetchall(): print(r)
|
||||
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT objecttype, action, COUNT(*) c FROM zt_action GROUP BY objecttype, action ORDER BY c DESC LIMIT 20")
|
||||
for r in cur.fetchall(): print(r)
|
||||
@@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""zentao_dev_2026 补本项目增量 DDL(只结构,不含数据):
|
||||
- zt_file.url / zt_meeting.url
|
||||
- zt_story 7 url 列 + code_review_status
|
||||
- zt_story_expand ai_participation_rate + acceptance_criteria
|
||||
- zt_perf_config / zt_doc_check 两新表(空表)
|
||||
幂等:逐列/逐表查 information_schema,缺才加。
|
||||
"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', autocommit=True)
|
||||
cur = conn.cursor()
|
||||
|
||||
COLUMNS = [
|
||||
('zt_file', 'url', "VARCHAR(512) DEFAULT NULL COMMENT '附件访问链接' AFTER `pathname`"),
|
||||
('zt_meeting', 'url', "VARCHAR(512) DEFAULT NULL COMMENT '会议纪要MD访问链接(最新一份)'"),
|
||||
('zt_story', 'prd_url', "VARCHAR(512) DEFAULT NULL COMMENT 'PRD文档访问链接'"),
|
||||
('zt_story', 'code_review_url', "VARCHAR(512) DEFAULT NULL COMMENT '代码审查报告访问链接'"),
|
||||
('zt_story', 'work_log_url', "VARCHAR(512) DEFAULT NULL COMMENT '工作日志访问链接'"),
|
||||
('zt_story', 'test_case_url', "VARCHAR(512) DEFAULT NULL COMMENT '测试用例访问链接'"),
|
||||
('zt_story', 'test_report_download_url', "VARCHAR(512) DEFAULT NULL COMMENT '测试报告模版下载访问链接'"),
|
||||
('zt_story', 'test_report_submit_url', "VARCHAR(512) DEFAULT NULL COMMENT '测试报告提交访问链接'"),
|
||||
('zt_story', 'test_other_url', "VARCHAR(512) DEFAULT NULL COMMENT '其他测试文档访问链接'"),
|
||||
('zt_story', 'code_review_status', "VARCHAR(16) DEFAULT NULL COMMENT '审查状态 pass/reject'"),
|
||||
('zt_story_expand', 'ai_participation_rate', "VARCHAR(16) DEFAULT NULL COMMENT 'AI参与率(只存不算,口径待定)' AFTER `ai_efficiency_coefficient`"),
|
||||
('zt_story_expand', 'acceptance_criteria', "MEDIUMTEXT DEFAULT NULL COMMENT 'AI框架验收指标(Given/When/Then,MD)' AFTER `test_person`"),
|
||||
]
|
||||
|
||||
added, skipped = [], []
|
||||
for table, col, definition in COLUMNS:
|
||||
cur.execute("""SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema='zentao_dev_2026' AND table_name=%s AND column_name=%s""",
|
||||
(table, col))
|
||||
if cur.fetchone()[0] > 0:
|
||||
skipped.append(f'{table}.{col}')
|
||||
continue
|
||||
cur.execute(f'ALTER TABLE `{table}` ADD COLUMN `{col}` {definition}')
|
||||
added.append(f'{table}.{col}')
|
||||
|
||||
TABLES = {
|
||||
'zt_perf_config': """CREATE TABLE `zt_perf_config` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`role` VARCHAR(32) NOT NULL COMMENT '岗位编码',
|
||||
`item_key` VARCHAR(64) NOT NULL COMMENT '指标编码',
|
||||
`item_name` VARCHAR(128) NOT NULL COMMENT '指标名称',
|
||||
`weight` DECIMAL(4,2) NOT NULL DEFAULT 0.00 COMMENT '权重(0~1,加分项为0)',
|
||||
`rule_json` TEXT DEFAULT NULL COMMENT '规则参数JSON',
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
`create_user` VARCHAR(64) DEFAULT NULL,
|
||||
`create_time` DATETIME DEFAULT NULL,
|
||||
`update_user` VARCHAR(64) DEFAULT NULL,
|
||||
`update_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`), UNIQUE KEY `uk_role_item` (`role`, `item_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='绩效指标规则配置表(三期)'""",
|
||||
'zt_doc_check': """CREATE TABLE `zt_doc_check` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`story_id` INT NOT NULL COMMENT '研发需求ID',
|
||||
`month` VARCHAR(7) NOT NULL COMMENT '考核月份 yyyy-MM',
|
||||
`doc_type` VARCHAR(32) NOT NULL COMMENT '文档类型',
|
||||
`present` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否齐备',
|
||||
`detail` VARCHAR(512) DEFAULT NULL,
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态',
|
||||
`appeal_reason` VARCHAR(512) DEFAULT NULL,
|
||||
`appeal_result` VARCHAR(8) DEFAULT NULL,
|
||||
`resolve_remark` VARCHAR(512) DEFAULT NULL,
|
||||
`check_time` DATETIME DEFAULT NULL,
|
||||
`create_time` DATETIME DEFAULT NULL,
|
||||
`update_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`), UNIQUE KEY `uk_story_month_doc` (`story_id`, `month`, `doc_type`), KEY `idx_month` (`month`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大型需求文档齐备核查快照(FR-014 三期)'""",
|
||||
}
|
||||
|
||||
for table, ddl in TABLES.items():
|
||||
cur.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='zentao_dev_2026' AND table_name=%s", (table,))
|
||||
if cur.fetchone()[0] > 0:
|
||||
skipped.append(f'表 {table}')
|
||||
continue
|
||||
cur.execute(ddl)
|
||||
added.append(f'表 {table}')
|
||||
|
||||
print('已加:', added)
|
||||
print('已存在跳过:', skipped)
|
||||
|
||||
# 验证
|
||||
for t, c in [('zt_file', 'url'), ('zt_meeting', 'url'), ('zt_story', 'code_review_status'),
|
||||
('zt_story', 'test_report_submit_url'), ('zt_story_expand', 'acceptance_criteria'),
|
||||
('zt_story_expand', 'ai_participation_rate')]:
|
||||
cur.execute("""SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema='zentao_dev_2026' AND table_name=%s AND column_name=%s""", (t, c))
|
||||
assert cur.fetchone()[0] == 1, f'{t}.{c} 缺失!'
|
||||
cur.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='zentao_dev_2026' AND table_name IN ('zt_perf_config','zt_doc_check')")
|
||||
assert cur.fetchone()[0] == 2, '新表缺失!'
|
||||
print('验证通过:12 列 + 2 表全部就位(空表,无数据)')
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""只读审计:161/zentao_dev_2026 中所有 user_story 悬空的研发需求。"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', connect_timeout=15)
|
||||
conn = pymysql.connect(**DST); cur = conn.cursor()
|
||||
cur.execute("""SELECT s.id, s.user_story, LEFT(s.title,36), s.deleted
|
||||
FROM zt_story s LEFT JOIN zt_story_user u ON u.id = s.user_story
|
||||
WHERE s.user_story IS NOT NULL AND s.user_story != 0 AND u.id IS NULL
|
||||
ORDER BY s.id""")
|
||||
rows = cur.fetchall()
|
||||
print(f'悬空引用研发需求数: {len(rows)}')
|
||||
for r in rows:
|
||||
print(' ', r)
|
||||
conn.close()
|
||||
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""CHG-061 鉴权矩阵实测:no-token / admin-token / ai-token × saveOrUpdate / aiBatchAdd / uploadBind"""
|
||||
import io, sys, json, requests
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
B = 'http://localhost:8086'
|
||||
AI = open('.claude/ai_token.txt').read().strip()
|
||||
ADMIN = open('tmp/pw_token_8086.txt').read().strip()
|
||||
|
||||
def call(name, method, path, token, **kw):
|
||||
h = {'Authorization': token} if token else {}
|
||||
r = requests.request(method, B + path, headers=h, timeout=30, **kw)
|
||||
try:
|
||||
d = r.json()
|
||||
msg = d.get('message', '')
|
||||
if isinstance(msg, str):
|
||||
try: msg = msg.encode('latin-1').decode('utf-8')
|
||||
except Exception: pass
|
||||
print(f'{name:28s} → HTTP {r.status_code} code={d.get("code")} msg={str(msg)[:40]} data={json.dumps(d.get("data"), ensure_ascii=False)[:60]}')
|
||||
return d
|
||||
except Exception:
|
||||
print(f'{name:28s} → HTTP {r.status_code} 非JSON: {r.text[:80]}')
|
||||
return None
|
||||
|
||||
expand_payload = {'storyId': 9130, 'workloadIndex': '5.1', 'numberUnits': 3, 'unitBusinessComplexity': '2.2',
|
||||
'technicalComplexityCoefficient': '1.4', 'aiEfficiencyCoefficient': '0.55', 'requirementStatus': 'inProgress'}
|
||||
task_payload = {'storyId': 9130, 'tasks': [{'name': '鉴权矩阵验证(可删)', 'type': 'devel', 'assignedTo': 'luoyong',
|
||||
'aiEvaluationTime': 0.1, 'planStartDate': '2026-08-06', 'deadline': '2026-08-06'}]}
|
||||
open('tmp/_auth_probe.md', 'w').write('# 鉴权矩阵验证\n')
|
||||
|
||||
print('== 无 token ==')
|
||||
call('saveOrUpdate', 'POST', '/zentao/zt-story-expand/saveOrUpdate', None, json=expand_payload)
|
||||
call('aiBatchAdd', 'POST', '/zentao/zt-task/aiBatchAdd', None, json=task_payload)
|
||||
call('uploadBind', 'POST', '/zentao/common/uploadBind', None,
|
||||
data={'objectType': 'testOther', 'objectId': '9130'}, files={'file': ('p.md', open('tmp/_auth_probe.md','rb'))})
|
||||
print('== admin token(非ai)==')
|
||||
call('saveOrUpdate', 'POST', '/zentao/zt-story-expand/saveOrUpdate', ADMIN, json=expand_payload)
|
||||
call('aiBatchAdd', 'POST', '/zentao/zt-task/aiBatchAdd', ADMIN, json=task_payload)
|
||||
call('uploadBind', 'POST', '/zentao/common/uploadBind', ADMIN,
|
||||
data={'objectType': 'testOther', 'objectId': '9130', 'title': '鉴权矩阵-admin(可删)'}, files={'file': ('p.md', open('tmp/_auth_probe.md','rb'))})
|
||||
print('== ai token ==')
|
||||
call('saveOrUpdate', 'POST', '/zentao/zt-story-expand/saveOrUpdate', AI, json=expand_payload)
|
||||
d = call('aiBatchAdd', 'POST', '/zentao/zt-task/aiBatchAdd', AI, json=task_payload)
|
||||
d2 = call('uploadBind', 'POST', '/zentao/common/uploadBind', AI,
|
||||
data={'objectType': 'testOther', 'objectId': '9130', 'title': '鉴权矩阵-ai(可删)'}, files={'file': ('p.md', open('tmp/_auth_probe.md','rb'))})
|
||||
if d and d.get('data'): print('AI_TASK_ID=', d['data'].get('taskIds'))
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', autocommit=False)
|
||||
cur = conn.cursor()
|
||||
print('== 身份归属验证 ==')
|
||||
cur.execute("SELECT id, addedby, title FROM zt_file WHERE id IN (8815, 8816)")
|
||||
for r in cur.fetchall(): print('zt_file:', r)
|
||||
cur.execute("SELECT id, openedby FROM zt_task WHERE id=18569")
|
||||
print('zt_task 18569:', cur.fetchone())
|
||||
cur.execute("SELECT id, actor, comment FROM zt_action WHERE objecttype='story' AND objectid=9130 ORDER BY id DESC LIMIT 3")
|
||||
for r in cur.fetchall():
|
||||
c = r[2].decode() if isinstance(r[2], bytes) else r[2]
|
||||
print('action:', r[0], 'actor=', r[1], '|', (c or '')[:55])
|
||||
|
||||
print('== 清理 ==')
|
||||
cur.execute("UPDATE zt_file SET deleted='1' WHERE id IN (8815, 8816)"); print('zt_file 软删:', cur.rowcount)
|
||||
cur.execute("UPDATE zt_task SET deleted='1' WHERE id=18569"); print('zt_task 软删:', cur.rowcount)
|
||||
cur.execute("""DELETE FROM zt_action WHERE (objecttype='task' AND objectid=18569)
|
||||
OR (objecttype='story' AND objectid=9130 AND (comment LIKE '%鉴权矩阵%' OR comment LIKE '%_auth_probe%' OR comment LIKE '%20260806111637%' OR comment LIKE '%20260806111638%'))""")
|
||||
print('action 删:', cur.rowcount)
|
||||
cur.execute("""DELETE FROM zt_actionrecent WHERE (objecttype='task' AND objectid=18569)
|
||||
OR (objecttype='story' AND objectid=9130 AND (comment LIKE '%鉴权矩阵%' OR comment LIKE '%_auth_probe%' OR comment LIKE '%20260806111637%' OR comment LIKE '%20260806111638%'))""")
|
||||
print('recent 删:', cur.rowcount)
|
||||
# 恢复 9130 test_other_url 到 08-05 的测试文档
|
||||
cur.execute("UPDATE zt_story SET test_other_url='/zentao/img/20260805171435808ccb0dd2f9487b9ecbb5ae0ddb3266.md' WHERE id=9130")
|
||||
print('test_other_url 恢复:', cur.rowcount)
|
||||
conn.commit(); print('committed')
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""zentao_dev_2026 补功能必要配置数据(应用已切到 2026,不补功能残缺):
|
||||
1. zt_perf_config 61 行规则(含 CHG-026 5‰ 参数)
|
||||
2. ai 用户(zt_user,id=89 被占→自增)
|
||||
3. base_menu 1539-1551(绩效菜单+按钮)
|
||||
4. base_role_authority 对应授权(自增 id)
|
||||
"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev', charset='utf8mb4', autocommit=True)
|
||||
cur = conn.cursor()
|
||||
|
||||
# 1. 绩效规则
|
||||
cur.execute("INSERT INTO zentao_dev_2026.zt_perf_config SELECT * FROM zentao_dev.zt_perf_config")
|
||||
print('zt_perf_config 灌入:', cur.rowcount)
|
||||
|
||||
# 2. ai 用户(排除 id 自增)
|
||||
cur.execute('SHOW COLUMNS FROM zt_user')
|
||||
cols = [r[0] for r in cur.fetchall() if r[0] != 'id']
|
||||
col_sql = ','.join(f'`{c}`' for c in cols)
|
||||
cur.execute(f"INSERT INTO zentao_dev_2026.zt_user ({col_sql}) SELECT {col_sql} FROM zentao_dev.zt_user WHERE account='ai'")
|
||||
print('ai 用户灌入:', cur.rowcount)
|
||||
cur.execute("SELECT id, account, nickname FROM zentao_dev_2026.zt_user WHERE account='ai'")
|
||||
print(' →', cur.fetchone())
|
||||
|
||||
# 3. 绩效菜单
|
||||
cur.execute("INSERT INTO zentao_dev_2026.base_menu SELECT * FROM zentao_dev.base_menu WHERE id BETWEEN 1539 AND 1551")
|
||||
print('base_menu 灌入:', cur.rowcount)
|
||||
|
||||
# 4. 菜单授权(排除 id 自增)
|
||||
cur.execute('SHOW COLUMNS FROM base_role_authority')
|
||||
acols = [r[0] for r in cur.fetchall() if r[0] != 'id']
|
||||
acol_sql = ','.join(f'`{c}`' for c in acols)
|
||||
cur.execute(f"""INSERT INTO zentao_dev_2026.base_role_authority ({acol_sql})
|
||||
SELECT {acol_sql} FROM zentao_dev.base_role_authority WHERE menu_id BETWEEN 1539 AND 1551""")
|
||||
print('authority 灌入:', cur.rowcount)
|
||||
|
||||
# 验证
|
||||
cur.execute('SELECT COUNT(*) FROM zentao_dev_2026.zt_perf_config WHERE enabled=1')
|
||||
print('验证 新库启用规则:', cur.fetchone()[0])
|
||||
cur.execute('SELECT COUNT(*) FROM zentao_dev_2026.base_menu WHERE id BETWEEN 1539 AND 1551')
|
||||
print('验证 新库菜单:', cur.fetchone()[0])
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""从考核方案工作簿提取「项目经理(王宇航)」sheet,注入导出占位符,生成导出模板"""
|
||||
import sys, io, shutil
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
import openpyxl
|
||||
|
||||
SRC = r'F:/zentao/1/信息技术部绩效考核标准-新版 - AI下的考核方案.xlsx'
|
||||
DST = r'F:/zentao/1/PM/codes/zentao/src/main/resources/templates/scope/项目经理考核-王宇航.xlsx'
|
||||
TARGET_CLS = r'F:/zentao/1/PM/codes/zentao/target/classes/templates/scope/项目经理考核-王宇航.xlsx'
|
||||
|
||||
wb = openpyxl.load_workbook(SRC)
|
||||
keep = '项目经理(王宇航)'
|
||||
for name in list(wb.sheetnames):
|
||||
if name != keep:
|
||||
wb.remove(wb[name])
|
||||
ws = wb[keep]
|
||||
|
||||
# 表头占位符(与通用模板同位置)
|
||||
ws['F2'] = '{name}'
|
||||
ws['H2'] = '{date}'
|
||||
|
||||
# 数据行占位符(G=绩效数据明细, H=本期结果得分;行版式按王宇航 sheet)
|
||||
ws['G4'] = '{detail_workloadTeamScore}'; ws['H4'] = '{workloadTeamScore}' # 团队工作量 0.4
|
||||
ws['G5'] = '{detail_versionPlanFinishedRate}'; ws['H5'] = '{versionPlanFinishedRate}' # 版本计划 0.1
|
||||
ws['G6'] = '{detail_bugScore}'; ws['H6'] = '{bugScore}' # 线上Bug 0.1
|
||||
ws['G7'] = '{detail_documentQualityScore}'; ws['H7'] = '{documentQualityScore}' # 文档齐备 0.1
|
||||
ws['H8'] = '{developFeedbackStory}' # 问题管理 0.05
|
||||
ws['H9'] = '{systemStabilityScore}' # 系统稳定性 0.2
|
||||
ws['H10'] = '{professionalSkillEnhancementScore}' # 专业技能 0.05
|
||||
ws['H11'] = '{total}' # 合计
|
||||
|
||||
wb.save(DST)
|
||||
shutil.copy(DST, TARGET_CLS) # target/classes 同步一份,IDE 方式运行的服务免重启生效
|
||||
|
||||
# 校验:重新打开,确认占位符齐全、公式与合并单元格未丢
|
||||
wb2 = openpyxl.load_workbook(DST)
|
||||
ws2 = wb2.active
|
||||
ph = [f'{c.coordinate}={c.value}' for row in ws2.iter_rows() for c in row
|
||||
if isinstance(c.value, str) and '{' in c.value]
|
||||
print('sheets:', wb2.sheetnames)
|
||||
print('placeholders(%d):' % len(ph))
|
||||
for p in ph: print(' ', p)
|
||||
print('C11 formula:', ws2['C11'].value)
|
||||
print('merged:', [str(r) for r in ws2.merged_cells.ranges])
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, deleted, bug_type, severity, story, openedby FROM zt_bug WHERE id=2566")
|
||||
print("2566:", cur.fetchone())
|
||||
cur.execute("SELECT COUNT(*) FROM zt_bug WHERE bug_type='prod' AND story=8277 AND deleted='0'")
|
||||
print("8277 的 prod Bug(deleted=0):", cur.fetchone()[0])
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
for db in ['zentao_dev', 'zentao_dev_2026']:
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database=db, charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, title, status, product FROM zt_story WHERE id=9130")
|
||||
print(f"[{db}] story:", cur.fetchone())
|
||||
cur.execute("""SELECT story_id, number_units, unit_business_complexity, technical_complexity_coefficient,
|
||||
ai_efficiency_coefficient, requirement_status, workload_index, evaluation_time,
|
||||
ai_participation_rate, product_person, develop_person, test_person
|
||||
FROM zt_story_expand WHERE story_id=9130""")
|
||||
print(f"[{db}] expand:", cur.fetchone())
|
||||
conn.close()
|
||||
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
print("### 1. Bug密度(开发者维度):6月done任务,分子=挂任务的Bug数,分母=Σestimate")
|
||||
rows = q("""
|
||||
SELECT t.assignedTo, u.realname, COUNT(DISTINCT t.id) tasks, SUM(t.estimate) est
|
||||
FROM zt_task t LEFT JOIN zt_user u ON u.account=t.assignedTo
|
||||
WHERE t.status='done' AND t.finishedDate>='2026-06-01' AND t.finishedDate<'2026-07-01'
|
||||
AND t.deleted='0' AND t.assignedTo NOT IN ('','closed')
|
||||
GROUP BY t.assignedTo ORDER BY est DESC
|
||||
""")
|
||||
print("name | done任务 | Σest(h) | Bug数 | ×100(%) | ×1000(‰)")
|
||||
for r in rows:
|
||||
est = float(r[3] or 0)
|
||||
bugs = q(f"SELECT COUNT(*) FROM zt_bug b JOIN zt_task t2 ON b.task=t2.id WHERE t2.assignedTo='{r[0]}' AND t2.status='done' AND t2.finishedDate>='2026-06-01' AND t2.finishedDate<'2026-07-01' AND t2.deleted='0' AND b.deleted='0'")[0][0]
|
||||
if est:
|
||||
print(f"{r[1] or r[0]} | {r[2]} | {est:.1f} | {bugs} | {bugs/est*100:.2f}% | {bugs/est*1000:.1f}‰")
|
||||
|
||||
print("\n### 2. 线上Bug率(产品维度):6月上线需求 prod Bug / devel工时")
|
||||
rows = q("""
|
||||
SELECT s.product, COUNT(DISTINCT s.id) stories
|
||||
FROM zt_story s
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
GROUP BY s.product ORDER BY stories DESC LIMIT 6
|
||||
""")
|
||||
for r in rows:
|
||||
pid = r[0]
|
||||
dev = q(f"""SELECT SUM(t.estimate) FROM zt_task t JOIN zt_story s ON t.story=s.id
|
||||
WHERE s.product={pid} AND s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0' AND t.type='devel' AND t.deleted='0'""")[0][0]
|
||||
bugs = q(f"""SELECT b.severity, COUNT(*) FROM zt_bug b JOIN zt_story s ON b.story=s.id
|
||||
WHERE s.product={pid} AND s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0' AND b.deleted='0' GROUP BY b.severity""")
|
||||
dev = float(dev or 0)
|
||||
tot = sum(c for _, c in bugs)
|
||||
rate = tot/dev*1000 if dev else None
|
||||
print(f"product {pid}: 上线需求={r[1]} devel工时={dev:.1f} Bug={bugs} → {'%.2f‰'%rate if rate is not None else 'NA(分母0)'}")
|
||||
|
||||
print("\n### 3. 缺陷检出率(全量):6月上线需求 (普通×1+重大×5)/devel工时")
|
||||
rows = q("""
|
||||
SELECT (SELECT SUM(t.estimate) FROM zt_task t WHERE t.story=s.id AND t.type='devel' AND t.deleted='0') dev_est,
|
||||
(SELECT COUNT(*) FROM zt_bug b WHERE b.story=s.id AND b.deleted='0' AND b.severity IN (3,4)) nb,
|
||||
(SELECT COUNT(*) FROM zt_bug b WHERE b.story=s.id AND b.deleted='0' AND b.severity IN (1,2)) mb
|
||||
FROM zt_story s WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
""")
|
||||
tot_dev = sum(float(r[0] or 0) for r in rows)
|
||||
tot_w = sum((r[1] or 0)*1 + (r[2] or 0)*5 for r in rows)
|
||||
n_dev0 = sum(1 for r in rows if not r[0])
|
||||
print(f"需求数={len(rows)}(其中devel工时为空 {n_dev0}) Σdevel工时={tot_dev:.1f} Σ加权Bug={tot_w}")
|
||||
if tot_dev:
|
||||
print(f"检出率 ×100={tot_w/tot_dev*100:.2f}% ×1000={tot_w/tot_dev*1000:.1f}‰")
|
||||
|
||||
print("\n### 4. 版本计划:6月发布需求 工时 vs 指数覆盖")
|
||||
rows = q("""
|
||||
SELECT s.id, s.estimate, e.workload_index,
|
||||
(SELECT SUM(t.estimate) FROM zt_task t WHERE t.story=s.id AND t.deleted='0') task_est
|
||||
FROM zt_story s LEFT JOIN zt_story_expand e ON e.story_id=s.id
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
""")
|
||||
n=len(rows); wi=[r for r in rows if r[2] not in (None,'','0')]
|
||||
sum_task=sum(float(r[3] or 0) for r in rows); sum_sest=sum(float(r[1] or 0) for r in rows)
|
||||
print(f"发布需求={n} 有工作量指数={len(wi)}(覆盖率 {len(wi)/n*100:.0f}%) Σ任务工时={sum_task:.1f} Σstory.estimate={sum_sest:.1f}")
|
||||
|
||||
print("\n### 5. 检出率口径交叉:严重度分布(6月所有Bug)")
|
||||
for r in q("SELECT severity, COUNT(*) FROM zt_bug WHERE deleted='0' AND openeddate>='2026-06-01' AND openeddate<'2026-07-01' GROUP BY severity"):
|
||||
print("severity", r[0], "=", r[1])
|
||||
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""6月数据核对:Bug密度单位 / 检出率>20%合理性 / 线上Bug率‰ / 版本计划工时vs指数"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
|
||||
def q(sql, args=None):
|
||||
cur.execute(sql, args or ())
|
||||
return cur.fetchall()
|
||||
|
||||
print("### 0. 库时区/月份确认(2026-06)")
|
||||
print(q("SELECT NOW()"))
|
||||
|
||||
print("\n### 1. Bug密度:按开发者(assignedTo) 6月done任务 分子=Bug数 分母=estimate")
|
||||
rows = q("""
|
||||
SELECT t.assignedTo, u.realname,
|
||||
COUNT(DISTINCT t.id) tasks, SUM(t.estimate) est,
|
||||
(SELECT COUNT(*) FROM zt_bug b WHERE b.task IN (SELECT id FROM zt_task t2 WHERE t2.assignedTo=t.assignedTo AND t2.status='done' AND t2.finishedDate>='2026-06-01' AND t2.finishedDate<'2026-07-01' AND t2.deleted='0')) bugs
|
||||
FROM zt_task t LEFT JOIN zt_user u ON u.account=t.assignedTo
|
||||
WHERE t.status='done' AND t.finishedDate>='2026-06-01' AND t.finishedDate<'2026-07-01' AND t.deleted='0' AND t.assignedTo!='' AND t.assignedTo!='closed'
|
||||
GROUP BY t.assignedTo ORDER BY est DESC
|
||||
""")
|
||||
print("account | name | done任务 | Σestimate(h) | Bug数 | ×100=% | ×1000=‰")
|
||||
for r in rows:
|
||||
est = float(r[3] or 0); bugs = r[4] or 0
|
||||
p100 = bugs/est*100 if est else None
|
||||
p1000 = bugs/est*1000 if est else None
|
||||
print(f"{r[0]} | {r[1]} | {r[2]} | {est:.1f} | {bugs} | {('%.2f'%p100) if p100 is not None else 'NA'} | {('%.1f'%p1000) if p1000 is not None else 'NA'}")
|
||||
|
||||
print("\n### 2. 线上Bug率:6月上线需求(releaseddate) 关联 prod Bug + devel任务estimate合计")
|
||||
rows = q("""
|
||||
SELECT s.product, COUNT(DISTINCT s.id) stories, SUM(s.estimate) story_est
|
||||
FROM zt_story s
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
GROUP BY s.product
|
||||
""")
|
||||
print("product | 上线需求数 | Σstory.estimate")
|
||||
for r in rows: print(r)
|
||||
print("-- devel任务工时合计(按需求):")
|
||||
rows = q("""
|
||||
SELECT s.product, SUM(t.estimate) devel_est, COUNT(DISTINCT t.id) devel_tasks
|
||||
FROM zt_story s JOIN zt_task t ON t.story=s.id AND t.type='devel' AND t.deleted='0'
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
GROUP BY s.product
|
||||
""")
|
||||
for r in rows: print(r)
|
||||
print("-- 这些上线需求关联的Bug(zt_bug.story IN):")
|
||||
rows = q("""
|
||||
SELECT s.product, b.severity, COUNT(*) cnt
|
||||
FROM zt_bug b JOIN zt_story s ON b.story=s.id
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0' AND b.deleted='0'
|
||||
GROUP BY s.product, b.severity
|
||||
""")
|
||||
for r in rows: print(r)
|
||||
|
||||
print("\n### 3. 缺陷检出率:测试需求(6月releaseddate)开发工时 vs Bug(普通×1+重大×5)")
|
||||
rows = q("""
|
||||
SELECT s.id, s.title, s.assignedTo, e.test_person, s.product,
|
||||
(SELECT SUM(t.estimate) FROM zt_task t WHERE t.story=s.id AND t.type='devel' AND t.deleted='0') devel_est,
|
||||
(SELECT COUNT(*) FROM zt_bug b WHERE b.story=s.id AND b.deleted='0' AND b.severity IN (3,4)) normal_bugs,
|
||||
(SELECT COUNT(*) FROM zt_bug b WHERE b.story=s.id AND b.deleted='0' AND b.severity IN (1,2)) major_bugs
|
||||
FROM zt_story s LEFT JOIN zt_story_expand e ON e.story=s.id
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
ORDER BY s.id
|
||||
""")
|
||||
tot_dev=0; tot_weight=0
|
||||
for r in rows:
|
||||
dev = float(r[5] or 0); w = (r[6] or 0)*1 + (r[7] or 0)*5
|
||||
tot_dev += dev; tot_weight += w
|
||||
print(f"story {r[0]} prod={r[4]} test={r[3]} devel_h={dev:.1f} 普通Bug={r[6]} 重大={r[7]} 加权={w}")
|
||||
print(f"合计: Σ开发工时={tot_dev:.1f} Σ加权Bug={tot_weight}")
|
||||
if tot_dev:
|
||||
print(f"检出率 ×100 = {tot_weight/tot_dev*100:.2f}% ×1000 = {tot_weight/tot_dev*1000:.1f}‰")
|
||||
|
||||
print("\n### 4. 版本计划:6月发布需求 工时加权 vs 指数加权")
|
||||
rows = q("""
|
||||
SELECT s.id, s.releaseddate, s.estimate, e.workload_index,
|
||||
(SELECT SUM(t.estimate) FROM zt_task t WHERE t.story=s.id AND t.deleted='0') task_est
|
||||
FROM zt_story s LEFT JOIN zt_story_expand e ON e.story=s.id
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
""")
|
||||
n=len(rows); with_idx=sum(1 for r in rows if r[3] not in (None,'','0'))
|
||||
sum_est=sum(float(r[4] or 0) for r in rows); sum_task=sum(float(r[2] or 0) for r in rows)
|
||||
print(f"发布需求数={n} 有指数={with_idx} Σtask.estimate={sum_task:.1f} Σstory.estimate={sum_est:.1f}")
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
print("### 测试人员(CS)及其授权产品")
|
||||
for r in q("SELECT account, realname, user_type, product_ids FROM zt_user WHERE user_type IN (SELECT id FROM base_user_type WHERE code='CS') OR user_type='CS'"):
|
||||
print(r)
|
||||
print("\n-- user_type 取值分布 --")
|
||||
for r in q("SELECT user_type, COUNT(*) FROM zt_user GROUP BY user_type"):
|
||||
print(r)
|
||||
print("\n-- base_user_type 表 --")
|
||||
try:
|
||||
for r in q("SELECT id, code, name FROM base_user_type"):
|
||||
print(r)
|
||||
except Exception as e:
|
||||
print("ERR", e)
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
print("-- zt_user.user_type 分布 --")
|
||||
for r in q("SELECT user_type, COUNT(*) FROM zt_user WHERE deleted='0' GROUP BY user_type"):
|
||||
print(r)
|
||||
print("\n-- 测试相关账号(孙颖 sunying / 张富斌 zhangfubin 等)--")
|
||||
for r in q("SELECT account, realname, user_type, product_ids FROM zt_user WHERE deleted='0' AND (realname LIKE '%颖%' OR realname LIKE '%富斌%' OR realname LIKE '%测试%')"):
|
||||
print(r)
|
||||
print("\n-- 6月有测试任务的账号 --")
|
||||
for r in q("""SELECT t.assignedTo, u.realname, COUNT(*) c FROM zt_task t LEFT JOIN zt_user u ON u.account=t.assignedTo
|
||||
WHERE t.type='test' AND t.deleted='0' AND t.finishedDate>='2026-06-01' AND t.finishedDate<'2026-07-01' GROUP BY t.assignedTo"""):
|
||||
print(r)
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
print("-- zt_user 是否有 realname --")
|
||||
cur.execute("SHOW COLUMNS FROM zt_user")
|
||||
cols = [r[0] for r in cur.fetchall()]
|
||||
print([c for c in cols if 'name' in c.lower() or 'product' in c.lower() or 'type' in c.lower() or 'role' in c.lower()])
|
||||
print()
|
||||
for r in q("SELECT account, realname, user_type, product_ids FROM zt_user WHERE account IN ('sunying','zhangfubin','sunqingfang','luoyulu','zhouxueli')"):
|
||||
print(r)
|
||||
print("\n-- 孙颖 6月测试需求范围(assignedTo=test 任务 或 expand.test_person 指定)当月上线需求 --")
|
||||
rows = q("""
|
||||
SELECT DISTINCT s.id, s.product, s.assignedTo, e.test_person
|
||||
FROM zt_story s
|
||||
LEFT JOIN zt_story_expand e ON e.story_id=s.id
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
AND (s.assignedTo='sunying' OR e.test_person LIKE '%孙颖%')
|
||||
""")
|
||||
print("孙颖 6月上线测试需求:", len(rows))
|
||||
for r in rows[:30]: print(r)
|
||||
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
# 产品组 → 测试人员映射(按 product_ids 授权)
|
||||
print("-- 产品名称 --")
|
||||
for r in q("SELECT id, name FROM zt_product WHERE id IN (119,139,145,147,150,161,189,215)"):
|
||||
print(r)
|
||||
|
||||
print("\n### 按测试人员统计 6月检出率(CHG-054 口径:assignedTo ∪ test_person,当月上线)")
|
||||
testers = {'sunying':'孙颖','zhangfubin':'张富斌','sunqingfang':'孙庆芳','luoyulu':'罗雨露','zhouxueli':'周雪丽'}
|
||||
for acc, name in testers.items():
|
||||
rows = q(f"""
|
||||
SELECT DISTINCT s.id FROM zt_story s
|
||||
LEFT JOIN zt_story_expand e ON e.story_id=s.id
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
AND (s.assignedTo='{acc}' OR e.test_person LIKE '%{name}%')
|
||||
""")
|
||||
ids = [str(r[0]) for r in rows]
|
||||
if not ids:
|
||||
print(f"{name}: 无需求"); continue
|
||||
idlist = ','.join(ids)
|
||||
dev = q(f"SELECT SUM(t.estimate) FROM zt_task t WHERE t.story IN ({idlist}) AND t.type='devel' AND t.deleted='0'")[0][0]
|
||||
nb = q(f"SELECT COUNT(*) FROM zt_bug WHERE story IN ({idlist}) AND deleted='0' AND severity IN (3,4)")[0][0]
|
||||
mb = q(f"SELECT COUNT(*) FROM zt_bug WHERE story IN ({idlist}) AND deleted='0' AND severity IN (1,2)")[0][0]
|
||||
w = nb*1 + mb*5
|
||||
dev = float(dev or 0)
|
||||
print(f"{name}({acc}): 需求{len(rows)}个 Σ开发工时={dev:.1f} 普通Bug={nb} 重大={mb} 加权={w} → ×100={'%.2f'%(w/dev*100) if dev else 'NA'}% ×1000={'%.1f'%(w/dev*1000) if dev else 'NA'}‰")
|
||||
|
||||
print("\n### 5月对照(孙颖曾 48% 满分)")
|
||||
rows = q("""
|
||||
SELECT DISTINCT s.id FROM zt_story s
|
||||
LEFT JOIN zt_story_expand e ON e.story_id=s.id
|
||||
WHERE s.releaseddate>='2026-05-01' AND s.releaseddate<'2026-06-01' AND s.deleted='0'
|
||||
AND (s.assignedTo='sunying' OR e.test_person LIKE '%孙颖%')
|
||||
""")
|
||||
ids = ','.join(str(r[0]) for r in rows) or '0'
|
||||
dev = q(f"SELECT SUM(t.estimate) FROM zt_task t WHERE t.story IN ({ids}) AND t.type='devel' AND t.deleted='0'")[0][0]
|
||||
nb = q(f"SELECT COUNT(*) FROM zt_bug WHERE story IN ({ids}) AND deleted='0' AND severity IN (3,4)")[0][0]
|
||||
mb = q(f"SELECT COUNT(*) FROM zt_bug WHERE story IN ({ids}) AND deleted='0' AND severity IN (1,2)")[0][0]
|
||||
w = nb + mb*5; dev = float(dev or 0)
|
||||
print(f"孙颖5月: 需求{len(rows)}个 Σ开发工时={dev:.1f} 普通={nb} 重大={mb} 加权={w} → ×100={'%.2f'%(w/dev*100) if dev else 'NA'}% ×1000={'%.1f'%(w/dev*1000) if dev else 'NA'}‰")
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""只读探测:12 个 SQL 脚本在三个库上的执行状态。"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
CONNS = {
|
||||
'200/zentao_dev (生产)': dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15),
|
||||
'161/zentao_dev': dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15),
|
||||
'161/zentao_dev_2026': dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', connect_timeout=15),
|
||||
}
|
||||
|
||||
# (SQL文件, [探测项]) 探测项: ('col', 表, 列) / ('table', 表) / ('row', SQL, 期望值>0)
|
||||
CHECKS = [
|
||||
('20260724_alter_zt_file_add_url', [('col', 'zt_file', 'url')]),
|
||||
('20260724_alter_zt_meeting_add_url', [('col', 'zt_meeting', 'url')]),
|
||||
('20260724_alter_zt_story_add_ai_doc_urls', [('col', 'zt_story', c) for c in
|
||||
['prd_url', 'code_review_url', 'work_log_url', 'test_case_url',
|
||||
'test_report_download_url', 'test_report_submit_url', 'test_other_url', 'code_review_status']]),
|
||||
('20260724_alter_zt_story_expand_add_ai_rate', [('col', 'zt_story_expand', 'ai_participation_rate')]),
|
||||
('20260724_insert_ai_user', [('row', "SELECT COUNT(*) FROM zt_user WHERE account='ai'")]),
|
||||
('20260728_create_zt_doc_check', [('table', 'zt_doc_check')]),
|
||||
('20260728_create_zt_perf_config', [('table', 'zt_perf_config')]),
|
||||
('20260728_init_zt_perf_config', [('row', "SELECT COUNT(*) FROM zt_perf_config")]),
|
||||
('20260729_alter_zt_story_expand_acceptance', [('col', 'zt_story_expand', 'acceptance_criteria')]),
|
||||
('20260729_insert_perf_menu', [('row', "SELECT COUNT(*) FROM base_menu WHERE id BETWEEN 1539 AND 1551")]),
|
||||
('20260730_alter_zt_user_dev_direction', [('col', 'zt_user', 'dev_direction')]),
|
||||
('20260731_alter_zt_story_doc_update', [('col', 'zt_story', 'ai_doc_update_url')]),
|
||||
]
|
||||
|
||||
for label, cfg in CONNS.items():
|
||||
try:
|
||||
conn = pymysql.connect(**cfg)
|
||||
except Exception as e:
|
||||
print(f'\n===== {label}: 连接失败 {e}'); continue
|
||||
cur = conn.cursor()
|
||||
print(f'\n===== {label} =====')
|
||||
for fname, probes in CHECKS:
|
||||
marks = []
|
||||
for p in probes:
|
||||
try:
|
||||
if p[0] == 'col':
|
||||
cur.execute("""SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema=DATABASE() AND table_name=%s AND column_name=%s""",
|
||||
(p[1], p[2]))
|
||||
marks.append(f'{p[2]}={"√" if cur.fetchone()[0] else "×"}')
|
||||
elif p[0] == 'table':
|
||||
cur.execute("""SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema=DATABASE() AND table_name=%s""", (p[1],))
|
||||
marks.append(f'{p[1]}={"√" if cur.fetchone()[0] else "×"}')
|
||||
else:
|
||||
cur.execute(p[1])
|
||||
marks.append(f'行数={cur.fetchone()[0]}')
|
||||
except Exception as e:
|
||||
marks.append(f'ERR:{e}')
|
||||
ok = all('×' not in m and 'ERR' not in m and '行数=0' not in m for m in marks)
|
||||
print(f' {"✅" if ok else "❌"} {fname}: {"; ".join(marks)}')
|
||||
conn.close()
|
||||
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, deleted, LEFT(title,30) FROM zt_bug WHERE id IN (2566,2567,2568,2569,2570)")
|
||||
for r in cur.fetchall(): print(r)
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, date_str, create_date, INSTR(scope_json,'\"productBugRate\":12'), INSTR(scope_json,'6.04') FROM zt_month_score WHERE account='weidongxia' ORDER BY date_str")
|
||||
for r in cur.fetchall():
|
||||
print(f"id={r[0]} 月份={r[1]} 创建={r[2]} productBugRate=12?{'是' if r[3] else '否'} 含6.04?{'是' if r[4] else '否'}")
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
# 6月工作天数(周一~周五)
|
||||
import datetime
|
||||
wd = sum(1 for i in range(1,31) if datetime.date(2026,6,i).weekday() < 5)
|
||||
print("2026-06 工作天数:", wd)
|
||||
|
||||
# KFZ 开发人员(user_type=3)
|
||||
devs = q("SELECT account, nickname FROM zt_user WHERE user_type=3 AND deleted='0'")
|
||||
print("KFZ 开发人数:", len(devs))
|
||||
|
||||
# 陈浩 consumed(6月完成任务实际工时)
|
||||
ch = q("""SELECT account, nickname FROM zt_user WHERE nickname LIKE '%陈浩%' OR account LIKE '%chenhao%'""")
|
||||
print("陈浩账号:", ch)
|
||||
acc = ch[0][0]
|
||||
consumed = q(f"""SELECT SUM(t.consumed) FROM zt_task t
|
||||
WHERE t.assignedTo='{acc}' AND t.deleted='0' AND t.status NOT IN ('cancel','closed')
|
||||
AND t.finishedDate>='2026-06-01' AND t.finishedDate<'2026-07-01'""")[0][0]
|
||||
est = q(f"""SELECT SUM(t.estimate) FROM zt_task t
|
||||
WHERE t.assignedTo='{acc}' AND t.deleted='0' AND t.status NOT IN ('cancel','closed')
|
||||
AND t.finishedDate>='2026-06-01' AND t.finishedDate<'2026-07-01'""")[0][0]
|
||||
print(f"陈浩 6月 consumed={consumed} estimate={est}")
|
||||
|
||||
# 团队请假(it_approval 6月,KFZ 人员)
|
||||
leave = q(f"""SELECT SUM(a.applyDays) FROM it_approval a JOIN zt_user u ON u.nickname=a.applyUserName
|
||||
WHERE u.user_type=3 AND u.deleted='0' AND a.applyTimeStart>='2026-06-01' AND a.applyTimeStart<'2026-07-01'""")
|
||||
print("团队6月请假(小时, applyDays字段):", leave[0][0])
|
||||
|
||||
# 列下 it_approval 字段确认
|
||||
cur.execute("SHOW COLUMNS FROM it_approval")
|
||||
print("it_approval cols:", [r[0] for r in cur.fetchall()])
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
# KFZ 开发 13 人名单
|
||||
devs = q("SELECT account, nickname FROM zt_user WHERE user_type=3 AND deleted='0'")
|
||||
names = [d[1] for d in devs]
|
||||
print("KFZ 13人:", names)
|
||||
|
||||
# 请假(os_system.it_approval,按 name 匹配 nickname,6月)
|
||||
name_in = ','.join(f"'{n}'" for n in names)
|
||||
rows = q(f"""SELECT a.name, a.apply_time_start, a.apply_time_end, a.apply_days FROM os_system.it_approval a
|
||||
WHERE a.name IN ({name_in})
|
||||
AND ((date(a.apply_time_start) BETWEEN '2026-06-01' AND '2026-06-30') OR (date(a.apply_time_end) BETWEEN '2026-06-01' AND '2026-06-30'))""")
|
||||
print(f"\n6月请假记录 {len(rows)} 条:")
|
||||
tot = 0
|
||||
for r in rows:
|
||||
print(r)
|
||||
tot += float(r[3] or 0)
|
||||
print(f"团队请假合计 = {tot} 小时 → 折算天数 = {tot/8}")
|
||||
|
||||
# 陈浩公式
|
||||
wd, dev_count = 22, 13
|
||||
consumed = 126.0
|
||||
target = (wd * dev_count - tot/8) * 5 / dev_count
|
||||
rate = consumed / target * 100 if target else 0
|
||||
print(f"\n=== 陈浩 6月(按你的公式)===")
|
||||
print(f"月度达标工时 = (22×13 − {tot}/8)×5 ÷ 13 = {target:.2f} h")
|
||||
print(f"实际产出工时(consumed) = {consumed} h")
|
||||
print(f"达标率 = {consumed}/{target:.2f} = {rate:.1f}%")
|
||||
score = 20 if rate >= 100 else max(0, 20 - int(100 - rate) * 2)
|
||||
print(f"得分(每减1%扣2分)= {score}")
|
||||
|
||||
# 对比:老版个人口径
|
||||
old_target = (22*8 - 0) * 0.75
|
||||
print(f"\n对比老版:达标工时=(22×8)×0.75={old_target}h, estimate={consumed} → 老口径率={consumed/old_target*100:.1f}%")
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
prods = [r[0] for r in q("SELECT id FROM zt_product WHERE program=139")]
|
||||
prod_in = ','.join(map(str, prods))
|
||||
projs = [r[0] for r in q(f"SELECT DISTINCT project FROM zt_projectproduct WHERE product IN ({prod_in})")]
|
||||
proj_in = ','.join(map(str, projs))
|
||||
execs = [r[0] for r in q(f"SELECT DISTINCT execution FROM zt_executionproject WHERE project IN ({proj_in})")]
|
||||
# 当月窗口内(begin 或 end 落 6 月,同 getTeamAccountByExecutionIds)
|
||||
exec_in = ','.join(map(str, execs))
|
||||
win = [r[0] for r in q(f"""SELECT id FROM zt_project WHERE id IN ({exec_in}) AND deleted='0'
|
||||
AND ((begin>='2026-06-01' AND begin<='2026-06-30') OR (end>='2026-06-01' AND end<='2026-06-30'))""")]
|
||||
print("6月在窗执行:", win)
|
||||
win_in = ','.join(map(str, win)) or '0'
|
||||
memb = q(f"SELECT DISTINCT account FROM zt_team WHERE type='execution' AND root IN ({win_in})")
|
||||
accs = [m[0] for m in memb]
|
||||
acc_in = ','.join(f"'{a}'" for a in accs) or "''"
|
||||
kfz = q(f"SELECT account, nickname FROM zt_user WHERE account IN ({acc_in}) AND user_type=3 AND deleted='0'")
|
||||
print(f"团队 KFZ 成员 {len(kfz)} 人:", [k[1] for k in kfz])
|
||||
|
||||
names = [k[1] for k in kfz]
|
||||
name_in = ','.join(f"'{n}'" for n in names) or "''"
|
||||
lv = q(f"""SELECT SUM(a.apply_days) FROM os_system.it_approval a WHERE a.name IN ({name_in})
|
||||
AND ((date(a.apply_time_start) BETWEEN '2026-06-01' AND '2026-06-30') OR (date(a.apply_time_end) BETWEEN '2026-06-01' AND '2026-06-30'))""")[0][0]
|
||||
lv = float(lv or 0)
|
||||
print(f"团队请假 {lv}h → {lv/8} 天")
|
||||
|
||||
wd, n = 22, len(kfz)
|
||||
target = (wd*n - lv/8)*5/n if n else 0
|
||||
consumed = 126.0
|
||||
rate = consumed/target*100 if target else 0
|
||||
print(f"\n陈浩(139 车服): 团队 {n} 人, 达标工时=({wd}×{n}−{lv}/8)×5÷{n} = {target:.2f}h, consumed={consumed} → {rate:.1f}% → 得分 {20 if rate>=100 else max(0,20-int(100-rate)*2)}")
|
||||
@@ -0,0 +1,175 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""200 zentao_dev 只读导出 → 161 zentao_dev_2026 导入
|
||||
源库只执行 SELECT/SHOW,绝不写入;流式游标 + 分批插入。
|
||||
"""
|
||||
import io, sys, time, re, pymysql
|
||||
from pymysql.cursors import SSCursor
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15,
|
||||
read_timeout=900, write_timeout=900)
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
charset='utf8mb4', autocommit=False, connect_timeout=15,
|
||||
read_timeout=900, write_timeout=900)
|
||||
TARGET_DB = 'zentao_dev_2026'
|
||||
BATCH = 500
|
||||
# 日志大表只建结构不拷数据(用户拍板 2026-07-29)
|
||||
SKIP_DATA = {'zt_action', 'zt_actionrecent'}
|
||||
|
||||
def log(msg):
|
||||
print(f'[{time.strftime("%H:%M:%S")}] {msg}', flush=True)
|
||||
|
||||
src = pymysql.connect(**SRC)
|
||||
dst = pymysql.connect(**DST)
|
||||
scur = src.cursor()
|
||||
dcur = dst.cursor()
|
||||
|
||||
# 目标库会话加速+防约束干扰+放大单包上限
|
||||
dcur.execute('SET SESSION foreign_key_checks=0')
|
||||
dcur.execute('SET SESSION unique_checks=0')
|
||||
try:
|
||||
dcur.execute('SET SESSION max_allowed_packet=1073741824')
|
||||
except Exception:
|
||||
pass
|
||||
dcur.execute(f'CREATE DATABASE IF NOT EXISTS `{TARGET_DB}` DEFAULT CHARACTER SET utf8mb4')
|
||||
dcur.execute(f'USE `{TARGET_DB}`')
|
||||
dst.commit()
|
||||
log(f'目标库 {TARGET_DB} 就绪')
|
||||
|
||||
# 源库表清单(仅 BASE TABLE)
|
||||
scur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema='zentao_dev' AND table_type='BASE TABLE' ORDER BY table_name")
|
||||
tables = [r[0] for r in scur.fetchall()]
|
||||
log(f'共 {len(tables)} 张表')
|
||||
|
||||
# 1) 建表结构(只建目标库缺失的表;已存在的不动——配合断点续跑,已拷数据不丢)
|
||||
dcur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema=%s", (TARGET_DB,))
|
||||
existing = {r[0] for r in dcur.fetchall()}
|
||||
created = 0
|
||||
for t in tables:
|
||||
if t in existing:
|
||||
continue
|
||||
scur.execute(f'SHOW CREATE TABLE `{t}`')
|
||||
create_sql = scur.fetchone()[1]
|
||||
create_sql = re.sub(r' DEFINER=`[^`]+`@`[^`]+`', '', create_sql)
|
||||
dcur.execute(create_sql)
|
||||
created += 1
|
||||
dst.commit()
|
||||
log(f'表结构就绪(新建 {created} 张,复用 {len(tables)-created} 张)')
|
||||
|
||||
def row_size(row):
|
||||
s = 0
|
||||
for v in row:
|
||||
if v is None:
|
||||
continue
|
||||
if isinstance(v, (bytes, bytearray)):
|
||||
s += len(v)
|
||||
elif isinstance(v, str):
|
||||
s += len(v.encode('utf-8', 'ignore'))
|
||||
else:
|
||||
s += 16
|
||||
return s
|
||||
|
||||
MAX_PACKET = 12 * 1024 * 1024 # 单包限 12MB(161 max_allowed_packet=64MB,留足余量)
|
||||
|
||||
def copy_table(t, attempt):
|
||||
"""单表复制:断线重连 + 按字节分包;重试前清空目标表保证幂等"""
|
||||
global src, dst, scur, dcur
|
||||
if attempt > 0:
|
||||
time.sleep(3)
|
||||
for conn in (src, dst):
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
src = pymysql.connect(**SRC)
|
||||
dst = pymysql.connect(**DST)
|
||||
scur = src.cursor()
|
||||
dcur = dst.cursor()
|
||||
dcur.execute('SET SESSION foreign_key_checks=0')
|
||||
dcur.execute('SET SESSION unique_checks=0')
|
||||
try:
|
||||
dcur.execute('SET SESSION max_allowed_packet=1073741824')
|
||||
except Exception:
|
||||
pass
|
||||
dcur.execute(f'USE `{TARGET_DB}`')
|
||||
dst.commit()
|
||||
dcur.execute(f'DELETE FROM `{t}`')
|
||||
dst.commit()
|
||||
read_cur = src.cursor(SSCursor)
|
||||
read_cur.execute(f'SELECT * FROM `{t}`')
|
||||
cols = [d[0] for d in read_cur.description]
|
||||
col_sql = ','.join(f'`{c}`' for c in cols)
|
||||
ph = ','.join(['%s'] * len(cols))
|
||||
insert_sql = f'INSERT INTO `{t}` ({col_sql}) VALUES ({ph})'
|
||||
n = 0
|
||||
batch = []
|
||||
batch_bytes = 0
|
||||
for row in read_cur:
|
||||
batch.append(row)
|
||||
batch_bytes += row_size(row)
|
||||
if len(batch) >= BATCH or batch_bytes >= MAX_PACKET:
|
||||
dcur.executemany(insert_sql, batch)
|
||||
dst.commit()
|
||||
n += len(batch)
|
||||
batch = []
|
||||
batch_bytes = 0
|
||||
if batch:
|
||||
dcur.executemany(insert_sql, batch)
|
||||
dst.commit()
|
||||
n += len(batch)
|
||||
read_cur.close()
|
||||
return n
|
||||
|
||||
# 2) 逐表复制数据(源只读流式;SKIP_DATA 只留空表结构;已拷完的表自动跳过=断点续跑)
|
||||
total_rows = 0
|
||||
for t in tables:
|
||||
if t in SKIP_DATA:
|
||||
log(f'{t}: 跳过数据(仅结构,0 行)')
|
||||
continue
|
||||
scur.execute(f'SELECT COUNT(*) FROM `{t}`')
|
||||
src_cnt = scur.fetchone()[0]
|
||||
dcur.execute(f'SELECT COUNT(*) FROM `{t}`')
|
||||
if dcur.fetchone()[0] == src_cnt:
|
||||
log(f'{t}: 已拷过({src_cnt} 行),续跑跳过')
|
||||
total_rows += src_cnt
|
||||
continue
|
||||
t0 = time.time()
|
||||
last_err = None
|
||||
for attempt in range(3):
|
||||
try:
|
||||
n = copy_table(t, attempt)
|
||||
break
|
||||
except pymysql.err.OperationalError as e:
|
||||
last_err = e
|
||||
log(f'{t}: 第 {attempt+1} 次失败 {e},重连重试')
|
||||
else:
|
||||
raise last_err
|
||||
total_rows += n
|
||||
log(f'{t}: {n} 行 ({time.time()-t0:.0f}s)')
|
||||
log(f'数据复制完成,总 {total_rows} 行')
|
||||
|
||||
# 3) 逐表行数对拍(SKIP_DATA 预期 0 行,单独标注)
|
||||
mismatch = []
|
||||
for t in tables:
|
||||
scur.execute(f'SELECT COUNT(*) FROM `{t}`')
|
||||
s = scur.fetchone()[0]
|
||||
dcur.execute(f'SELECT COUNT(*) FROM `{t}`')
|
||||
d = dcur.fetchone()[0]
|
||||
if t in SKIP_DATA:
|
||||
log(f'{t}: 源 {s} 行 → 目标 {d} 行(按计划跳过)')
|
||||
elif s != d:
|
||||
mismatch.append((t, s, d))
|
||||
if mismatch:
|
||||
log(f'!!! 行数不一致 {len(mismatch)} 张: {mismatch}')
|
||||
else:
|
||||
log(f'对拍通过:{len(tables)} 张表行数全部一致')
|
||||
|
||||
# 4) 抽查中文往返
|
||||
dcur.execute('SELECT id, title FROM zt_story WHERE id=6566')
|
||||
log(f'抽查 zt_story 6566: {dcur.fetchone()}')
|
||||
dcur.execute('SELECT COUNT(*) FROM zt_perf_config')
|
||||
log(f'抽查 zt_perf_config 行数: {dcur.fetchone()[0]}')
|
||||
src.close(); dst.close()
|
||||
log('全部完成')
|
||||
@@ -0,0 +1,59 @@
|
||||
import base64
|
||||
import sys
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
AES_KEY = b"KeLy8g7qjmnbgWP1" # 16 bytes = AES-128
|
||||
|
||||
contents = [
|
||||
"HOmbh7KuceD2H18yBv2Hh8Wo+yZVzYtFBkY3J3eo1Pnyu0jacX6mgzwzbPzTC4KdHQia8sK5T0jfKnHPGnH5Cqc8Y+bgzN2iPXoNvgDX4OEtuyoCa+R7HknO7dLcWWPKN7Lr28x4fteCEw8YolioaFDHoZkpKla51b0qutO5wUMcfCYkg/lw8cOPB2jJwjqS1Y0YL01zaUgu24ZJHC7SwldSNtD++IgipCZtItTfgS1NmN7Qs/O/mUqox+x1AJb345vm5qhg6Q/5Bp9zXyWtn41i255CfPaW+gcqUqA248eF1BZrnzF3IkrDgysaFx/38nDYPByPO2rznmygwjjq00yhMWTHe3cKu2h3OjZvMuzuBfzE7CQwcx59/UQPBs0Fig9EPqQwHQeCRSvl1tYPm+rqlwlC+l3s3rYDaWLa3WgGrN+3HwgTGdZmJV3qajvvhxKT/BZN1zl7EfMHpu52CtM0+yrnbqWEP9heVnNJ6a8=",
|
||||
"HOmbh7KuceD2H18yBv2Hh8Wo+yZVzYtFBkY3J3eo1Pnyu0jacX6mgzwzbPzTC4KdHQia8sK5T0jfKnHPGnH5Cqc8Y+bgzN2iPXoNvgDX4OEtuyoCa+R7HknO7dLcWWPKN7Lr28x4fteCEw8YolioaO39sVqYiFSEGMa11fUhPPgq7Kx6u3SnVsiRD5ZMgqi1Nn9qBND01zsAC6WBb+/XbOYoUbMBAtwhHrzyZPBnL0emWpKPOcyU9Wr0RQbfgXkuEWAl+RM4Son1LM0wZh18aGmXfQY2lMvj/qugOXwRCadmBIeHQwkk3nkE0w67Fqn3532zCdf2GhAFMHt5jV0D3w==",
|
||||
"HOmbh7KuceD2H18yBv2Hh8Wo+yZVzYtFBkY3J3eo1Pnyu0jacX6mgzwzwzbPzTC4KdHQia8sK5T0jfKnHPGnH5Cqc8Y+bgzN2iPXoNvgDX4OEtuyoCa+R7HknO7dLcWWPKN7Lr28x4fteCEw8YolioaDTUtiqVaipMPgSYqyEJJfK20PAzpQq4+V2Z6ppHMLRZ+H3TdNdtJPU8lkSbz4uZ02wH9SUonvWhXZOoddgLsakqnF2dB/Zq3M3Qb27LD6LoaZxOaJm+ry6K14diT5soLLhQiGFP8rBeCQ5ZdG2YlTgNPZG0+eKcB0T4sq8lvLtVYU0MLE+hWDcyuNfNYS+RNss0V6e9m1JkoIVq1A8mlr3DnWZHa1QZrnTEj59HVofAI1uKreMp/cx0VHntx0T4jzzalxfnpMi81J7OHnmzK3sb4yX9xuLVQoanyHgKoHmXUsByKtRRallEmVm7ImQb5Ybd4IfMU5ljdK7BoXv1cAeqYsEXBmlXm1bcYvzEWh7e"
|
||||
]
|
||||
|
||||
def try_decrypt(ciphertext_b64, key, mode_name, iv=None):
|
||||
try:
|
||||
ct = base64.b64decode(ciphertext_b64)
|
||||
if mode_name == "ECB":
|
||||
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
|
||||
elif mode_name == "CBC":
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
else:
|
||||
return None
|
||||
decryptor = cipher.decryptor()
|
||||
pt = decryptor.update(ct) + decryptor.finalize()
|
||||
# Try PKCS7 unpad
|
||||
unpadder = padding.PKCS7(128).unpadder()
|
||||
pt = unpadder.update(pt) + unpadder.finalize()
|
||||
return pt.decode("utf-8")
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
for i, ct_b64 in enumerate(contents, 1):
|
||||
print(f"\n========== Payload {i} ==========")
|
||||
# ECB
|
||||
result = try_decrypt(ct_b64, AES_KEY, "ECB")
|
||||
if result:
|
||||
print(f"[AES-128-ECB]\n{result}\n")
|
||||
continue
|
||||
|
||||
# CBC with common IVs
|
||||
ivs = {
|
||||
"key as IV": AES_KEY,
|
||||
"zeros": b"\x00" * 16,
|
||||
}
|
||||
# Maybe IV is first 16 bytes of ciphertext
|
||||
ct_bytes = base64.b64decode(ct_b64)
|
||||
if len(ct_bytes) >= 16:
|
||||
ivs["first 16 bytes of cipher"] = ct_bytes[:16]
|
||||
|
||||
found = False
|
||||
for iv_name, iv in ivs.items():
|
||||
result = try_decrypt(ct_b64, AES_KEY, "CBC", iv)
|
||||
if result:
|
||||
print(f"[AES-128-CBC, IV={iv_name}]\n{result}\n")
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
print("[尝试失败] ECB 和常见 CBC IV 均无法解密,可能需要其他算法或参数。")
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', autocommit=False)
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, deleted FROM zt_bug WHERE id IN (2566,2567,2568,2569,2570)")
|
||||
rows = cur.fetchall()
|
||||
assert len(rows)==5 and all(r[1]=='0' for r in rows), "状态异常,中止"
|
||||
cur.execute("UPDATE zt_bug SET deleted='1' WHERE id IN (2566,2567,2568,2569,2570)")
|
||||
print("删除行数:", cur.rowcount)
|
||||
conn.commit()
|
||||
cur.execute("SELECT id, deleted FROM zt_bug WHERE id IN (2566,2567,2568,2569,2570)")
|
||||
print("删后:", cur.fetchall())
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
print("zt_story releaseddate 分布:")
|
||||
for r in q("SELECT DATE_FORMAT(releaseddate,'%Y-%m') m, COUNT(*) c FROM zt_story WHERE deleted='0' AND releaseddate>'2000-01-01' GROUP BY m ORDER BY m DESC LIMIT 8"):
|
||||
print(r)
|
||||
print("\nzt_bug openeddate 分布:")
|
||||
for r in q("SELECT DATE_FORMAT(openeddate,'%Y-%m') m, COUNT(*) c FROM zt_bug WHERE deleted='0' AND openeddate>'2000-01-01' GROUP BY m ORDER BY m DESC LIMIT 8"):
|
||||
print(r)
|
||||
print("\nzt_task finishedDate(done) 分布:")
|
||||
for r in q("SELECT DATE_FORMAT(finishedDate,'%Y-%m') m, COUNT(*) c FROM zt_task WHERE deleted='0' AND status='done' AND finishedDate>'2000-01-01' GROUP BY m ORDER BY m DESC LIMIT 8"):
|
||||
print(r)
|
||||
@@ -0,0 +1,4 @@
|
||||
# AI项目文档更新记录
|
||||
|
||||
- 2026-07-31 PRD v1.34 定稿(CHG-044/045)
|
||||
- 架构:版本计划完成率改指数加权
|
||||
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""重生成绩效菜单备份 sql(列名完整版)"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS', database='zentao_dev', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
|
||||
def dump(table, where):
|
||||
cur.execute(f'SHOW COLUMNS FROM {table}')
|
||||
cols = [r[0] for r in cur.fetchall()]
|
||||
cur.execute(f'SELECT * FROM {table} WHERE {where}')
|
||||
lines = []
|
||||
for r in cur.fetchall():
|
||||
vals = []
|
||||
for v in r:
|
||||
if v is None:
|
||||
vals.append('NULL')
|
||||
elif isinstance(v, (int, float)):
|
||||
vals.append(str(v))
|
||||
else:
|
||||
vals.append("'" + str(v).replace("'", "''") + "'")
|
||||
lines.append(f"INSERT INTO `{table}` ({','.join('`' + c + '`' for c in cols)}) VALUES ({','.join(vals)});")
|
||||
return lines
|
||||
|
||||
menus = dump('base_menu', 'id BETWEEN 1539 AND 1551')
|
||||
auths = dump('base_role_authority', 'menu_id BETWEEN 1539 AND 1551')
|
||||
with open('codes/zentao/sql/20260729_insert_perf_menu.sql', 'w', encoding='utf-8') as f:
|
||||
f.write('-- 绩效菜单备份(CHG-035 下线后可凭本文件恢复):绩效管理+绩效考核/文档齐备核查/规则配置+7按钮\n')
|
||||
f.write('-- 授权按当时 role 配置(3 角色+admin 自动可见)\n')
|
||||
f.write('\n'.join(menus) + '\n' + '\n'.join(auths) + '\n')
|
||||
print('重写完成:', len(menus), '菜单 +', len(auths), '授权')
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import openpyxl, io, sys
|
||||
|
||||
wb = openpyxl.load_workbook(r'F:/zentao/1/pm/docs/信息技术部绩效考核标准-新版 - AI下的考核方案.xlsx', data_only=True)
|
||||
out = io.open(r'F:/zentao/1/pm/tmp/xlsx_dump.txt', 'w', encoding='utf-8')
|
||||
for name in wb.sheetnames:
|
||||
ws = wb[name]
|
||||
out.write(u'===== SHEET: %s (rows=%d, cols=%d) =====\n' % (name, ws.max_row, ws.max_column))
|
||||
# merged cells
|
||||
out.write(u'-- merged: %s\n' % ([str(r) for r in ws.merged_cells.ranges],))
|
||||
for row in ws.iter_rows():
|
||||
vals = []
|
||||
for c in row:
|
||||
v = c.value
|
||||
if v is None:
|
||||
vals.append(u'')
|
||||
else:
|
||||
vals.append(unicode(v) if sys.version_info[0] == 2 else str(v))
|
||||
line = u' | '.join(vals).rstrip(u' |')
|
||||
if line.strip():
|
||||
out.write(u'R%d: %s\n' % (row[0].row, line))
|
||||
out.write(u'\n')
|
||||
out.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
conn = pymysql.connect(**SRC); cur = conn.cursor()
|
||||
cur.execute("""SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema='zentao_dev' AND table_name='zt_story'
|
||||
AND (column_name LIKE '%%user%%' OR column_name LIKE '%%story%%')""")
|
||||
print([r[0] for r in cur.fetchall()])
|
||||
conn.close()
|
||||
@@ -0,0 +1,317 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成《禅道 AI 通道接口文档》Word 对外交付版(3 接口 + token,不含 login)"""
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor, Cm
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
|
||||
TOKEN = ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9."
|
||||
"eyJhY2NvdW50IjoiYWkiLCJwYXNzd29yZCI6ImUxMGFkYzM5NDliYTU5YWJiZTU2ZTA1N2YyMGY4ODNlIiwidXNlclR5cGUiOjN9."
|
||||
"d43vA9pN_dhwwfeO0zg3_AaX76vEMXRYFZSc6AkYF7c")
|
||||
|
||||
doc = Document()
|
||||
|
||||
# 全局字体:正文宋体/Calibri,标题黑体
|
||||
style = doc.styles['Normal']
|
||||
style.font.name = 'Calibri'
|
||||
style.font.size = Pt(10.5)
|
||||
style.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
|
||||
|
||||
def set_cn_font(run, name='宋体'):
|
||||
run.font.name = name
|
||||
run._element.rPr.rFonts.set(qn('w:eastAsia'), name)
|
||||
|
||||
def heading(text, level):
|
||||
h = doc.add_heading(text, level=level)
|
||||
for r in h.runs:
|
||||
set_cn_font(r, '黑体')
|
||||
r.font.color.rgb = RGBColor(0, 0, 0)
|
||||
return h
|
||||
|
||||
def para(text, bold=False, size=10.5, color=None):
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(text)
|
||||
r.bold = bold
|
||||
r.font.size = Pt(size)
|
||||
if color:
|
||||
r.font.color.rgb = color
|
||||
set_cn_font(r)
|
||||
return p
|
||||
|
||||
def code_block(text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.left_indent = Cm(0.5)
|
||||
p.paragraph_format.space_before = Pt(4)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(text)
|
||||
r.font.name = 'Consolas'
|
||||
r.font.size = Pt(9)
|
||||
r._element.rPr.rFonts.set(qn('w:eastAsia'), 'Consolas')
|
||||
return p
|
||||
|
||||
def table(headers, rows, widths=None):
|
||||
t = doc.add_table(rows=1 + len(rows), cols=len(headers))
|
||||
t.style = 'Table Grid'
|
||||
for i, h in enumerate(headers):
|
||||
cell = t.rows[0].cells[i]
|
||||
cell.text = ''
|
||||
r = cell.paragraphs[0].add_run(h)
|
||||
r.bold = True
|
||||
r.font.size = Pt(9.5)
|
||||
set_cn_font(r)
|
||||
for ri, row in enumerate(rows):
|
||||
for ci, val in enumerate(row):
|
||||
cell = t.rows[ri + 1].cells[ci]
|
||||
cell.text = ''
|
||||
r = cell.paragraphs[0].add_run(str(val))
|
||||
r.font.size = Pt(9.5)
|
||||
set_cn_font(r)
|
||||
if widths:
|
||||
for ci, w in enumerate(widths):
|
||||
for row in t.rows:
|
||||
row.cells[ci].width = Cm(w)
|
||||
doc.add_paragraph()
|
||||
return t
|
||||
|
||||
# ================= 封面信息 =================
|
||||
title = doc.add_paragraph()
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r = title.add_run('禅道 AI 通道接口文档')
|
||||
r.bold = True
|
||||
r.font.size = Pt(22)
|
||||
set_cn_font(r, '黑体')
|
||||
|
||||
sub = doc.add_paragraph()
|
||||
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r = sub.add_run('(AI 框架 × 禅道系统 对接接口 · 对外提供版)')
|
||||
r.font.size = Pt(12)
|
||||
set_cn_font(r)
|
||||
|
||||
info = doc.add_paragraph()
|
||||
info.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r = info.add_run('版本:v1.0 日期:2026-08-07 密级:内部(含永久令牌,注意保管)')
|
||||
r.font.size = Pt(10)
|
||||
set_cn_font(r)
|
||||
doc.add_paragraph()
|
||||
|
||||
# ================= 1 接入信息 =================
|
||||
heading('1. 接入信息', 1)
|
||||
|
||||
heading('1.1 服务地址', 2)
|
||||
table(['环境', 'Base URL', '说明'],
|
||||
[['测试环境', 'http://127.0.0.1:8085/zentao', '本地/联调使用'],
|
||||
['生产环境', 'https://itsm.sino-assist.com/zentao', '正式环境']],
|
||||
widths=[3, 8, 5])
|
||||
para('所有接口路径均以 Base URL 为前缀拼接;请求与响应均为 UTF-8 编码。')
|
||||
|
||||
heading('1.2 鉴权方式(重要)', 2)
|
||||
para('所有接口必须在请求头中携带访问令牌(token):')
|
||||
code_block('Authorization: <token>')
|
||||
para('注意:直接放 token 原文,不要加 "Bearer " 前缀。', bold=True)
|
||||
para('本令牌为 AI 专用账号(账号名:ai)的永久令牌,不会过期,请妥善保管、勿外泄、勿提交到代码仓库:', bold=True)
|
||||
code_block(TOKEN)
|
||||
para('未携带或令牌无效时,接口返回:{"code":-1,"message":"请登录"}')
|
||||
|
||||
heading('1.3 统一响应格式', 2)
|
||||
para('所有接口返回统一 JSON 结构:')
|
||||
code_block('{\n "code": 0, // 0=成功;-1=失败(message 为失败原因)\n "message": "成功",\n "data": { ... } // 各接口不同,可能为 null\n}')
|
||||
|
||||
# ================= 2 接口一览 =================
|
||||
heading('2. 接口一览', 1)
|
||||
table(['#', '接口名称', '方法', '路径', '提交方式', '用途'],
|
||||
[['1', '评估结果提交', 'POST', '/zentao/zt-story-expand/saveOrUpdate', 'JSON',
|
||||
'提交需求的工作量评估指标(单元数量/复杂度/系数/工时)与验收标准'],
|
||||
['2', 'AI 批量创建任务', 'POST', '/zentao/zt-task/aiBatchAdd', 'JSON',
|
||||
'为指定需求批量创建研发/测试任务,重复任务自动跳过'],
|
||||
['3', '文件上传并绑定', 'POST', '/zentao/common/uploadBind', 'multipart/form-data',
|
||||
'上传文档并一步绑定到需求/会议,页面即时可见']],
|
||||
widths=[1, 3.2, 1.6, 5.5, 3, 5])
|
||||
|
||||
# ================= 3 saveOrUpdate =================
|
||||
heading('3. 接口一:评估结果提交 saveOrUpdate', 1)
|
||||
code_block('POST {BaseURL}/zt-story-expand/saveOrUpdate\n'
|
||||
'Content-Type: application/json\n'
|
||||
'Authorization: <token>')
|
||||
para('用途:需求评估完成后,将工作量评估指标与验收标准写入需求扩展信息。同一需求重复提交为覆盖更新(幂等)。')
|
||||
|
||||
heading('3.1 请求参数', 2)
|
||||
table(['参数', '类型', '必填', '说明'],
|
||||
[['storyId', 'Integer', '是', '需求 ID'],
|
||||
['numberUnits', 'Integer', '评估时必填', '单元数量 S'],
|
||||
['unitBusinessComplexity', 'String', '评估时必填', '单元业务复杂度 B,如 "2.2"'],
|
||||
['technicalComplexityCoefficient', 'String', '评估时必填', '技术复杂度系数 F(T),如 "1.4"'],
|
||||
['aiEfficiencyCoefficient', 'String', '评估时必填', 'AI 效率系数 G(A),如 "0.55"'],
|
||||
['evaluationTime', 'Number', '评估时必填', '评估工时 W(人日),如 5.1'],
|
||||
['workloadIndex', 'String', '否', '工作量指数'],
|
||||
['requirementStatus', 'String', '否', 'inProgress(默认)/ finished;传 finished 表示需求完成,将锁定记录并结算当月工作量'],
|
||||
['requirementCompletionDegree', 'String', '否', '完成度 "0"~"100";finished 时自动置 100'],
|
||||
['acceptanceCriteria', 'String', '否', '验收标准(Markdown 文本)'],
|
||||
['productPerson', 'String', '否', '产品人员(中文名)'],
|
||||
['developPerson', 'String', '否', '开发人员(中文名)'],
|
||||
['testPerson', 'String', '否', '测试人员(中文名)']],
|
||||
widths=[4.5, 2, 2.2, 8])
|
||||
|
||||
heading('3.2 请求示例', 2)
|
||||
code_block('{\n'
|
||||
' "storyId": 9130,\n'
|
||||
' "numberUnits": 3,\n'
|
||||
' "unitBusinessComplexity": "2.2",\n'
|
||||
' "technicalComplexityCoefficient": "1.4",\n'
|
||||
' "aiEfficiencyCoefficient": "0.55",\n'
|
||||
' "evaluationTime": 5.1,\n'
|
||||
' "workloadIndex": "5.1",\n'
|
||||
' "developPerson": "张三",\n'
|
||||
' "testPerson": "李四"\n'
|
||||
'}')
|
||||
|
||||
heading('3.3 响应示例', 2)
|
||||
code_block('{ "code": 0, "message": "成功", "data": null }')
|
||||
|
||||
heading('3.4 注意事项', 2)
|
||||
for t in ['storyId 为空时接口返回成功但不做任何处理(请务必确认已传 storyId);',
|
||||
'需求一旦置为 finished,记录即锁定,后续提交将被拒绝(提示"该需求已完成,不可再修改");',
|
||||
'同一 storyId 重复提交 = 覆盖更新,不会产生重复记录。']:
|
||||
p = doc.add_paragraph(t, style='List Bullet')
|
||||
for r in p.runs:
|
||||
set_cn_font(r)
|
||||
r.font.size = Pt(10)
|
||||
|
||||
# ================= 4 aiBatchAdd =================
|
||||
heading('4. 接口二:AI 批量创建任务 aiBatchAdd', 1)
|
||||
code_block('POST {BaseURL}/zt-task/aiBatchAdd\n'
|
||||
'Content-Type: application/json\n'
|
||||
'Authorization: <token>')
|
||||
para('用途:为指定需求批量创建研发/测试任务。已存在的同名同类型任务自动跳过(防重),并在需求与任务的操作记录中留痕。')
|
||||
|
||||
heading('4.1 请求参数', 2)
|
||||
table(['参数', '类型', '必填', '说明'],
|
||||
[['storyId', 'Integer', '是', '需求 ID;需求不存在则整批拒绝'],
|
||||
['tasks', 'Array', '是', '任务列表,不能为空'],
|
||||
['tasks[].name', 'String', '是', '任务名称'],
|
||||
['tasks[].type', 'String', '是', '任务类型:devel=开发 / test=测试,其他值整批拒绝'],
|
||||
['tasks[].assignedTo', 'String', '否', '指派人账号(登录账号,非中文名)'],
|
||||
['tasks[].aiEvaluationTime', 'Float', '否', 'AI 评估工时(小时),写入任务预计工时'],
|
||||
['tasks[].planStartDate', 'String', '否', '预计开始日期,格式 yyyy-MM-dd'],
|
||||
['tasks[].deadline', 'String', '否', '预计完成日期,格式 yyyy-MM-dd']],
|
||||
widths=[4.5, 2, 2.2, 8])
|
||||
|
||||
heading('4.2 请求示例', 2)
|
||||
code_block('{\n'
|
||||
' "storyId": 9130,\n'
|
||||
' "tasks": [\n'
|
||||
' {"name": "后端接口开发", "type": "devel", "assignedTo": "zhangsan",\n'
|
||||
' "aiEvaluationTime": 8, "planStartDate": "2026-08-10", "deadline": "2026-08-11"},\n'
|
||||
' {"name": "接口测试", "type": "test", "assignedTo": "lisi",\n'
|
||||
' "aiEvaluationTime": 4, "planStartDate": "2026-08-12", "deadline": "2026-08-12"}\n'
|
||||
' ]\n'
|
||||
'}')
|
||||
|
||||
heading('4.3 响应示例', 2)
|
||||
code_block('{\n'
|
||||
' "code": 0,\n'
|
||||
' "message": "成功",\n'
|
||||
' "data": {\n'
|
||||
' "created": 2, // 实际新建任务数\n'
|
||||
' "taskIds": [18565, 18566], // 新建任务 ID 列表\n'
|
||||
' "skipped": ["接口测试"] // 因重名同类型被跳过的任务名\n'
|
||||
' }\n'
|
||||
'}')
|
||||
|
||||
heading('4.4 注意事项', 2)
|
||||
for t in ['校验规则为"整批拒绝":任一任务不合法(类型错误/名称为空/日期格式错误等),本批全部不创建;',
|
||||
'防重规则:同一需求下已存在同名且同类型(未删除)的任务 → 跳过并记入 skipped,不算失败;',
|
||||
'全部命中防重时返回 code:0、created:0,skipped 列出全部任务名;',
|
||||
'新任务初始状态为"未开始",创建人显示为 ai,操作记录可在需求/任务历史中查看。']:
|
||||
p = doc.add_paragraph(t, style='List Bullet')
|
||||
for r in p.runs:
|
||||
set_cn_font(r)
|
||||
r.font.size = Pt(10)
|
||||
|
||||
# ================= 5 uploadBind =================
|
||||
heading('5. 接口三:文件上传并绑定 uploadBind', 1)
|
||||
code_block('POST {BaseURL}/common/uploadBind\n'
|
||||
'Content-Type: multipart/form-data\n'
|
||||
'Authorization: <token>')
|
||||
para('用途:上传文件并一步绑定到业务对象(需求/会议):文件入库的同时,自动刷新业务对象的文档链接字段,'
|
||||
'并在其操作记录中留痕。需求详情页、会议纪要页等界面即时可见。')
|
||||
|
||||
heading('5.1 请求参数(表单字段)', 2)
|
||||
table(['参数', '类型', '必填', '说明'],
|
||||
[['file', 'File', '是', '上传的文件(支持 .md 等常见格式)'],
|
||||
['objectType', 'String', '是', '业务对象类型,取值见 5.2 对照表'],
|
||||
['objectId', 'Integer', '是', '业务对象 ID(需求 ID 或会议 ID)'],
|
||||
['title', 'String', '否', '文件标题;不传默认取原始文件名(支持中文)'],
|
||||
['reviewResult', 'String', '否', '仅 objectType=aiCodeReview 时有效:pass=审查通过 / reject=审查不通过']],
|
||||
widths=[3, 2, 2.2, 9.3])
|
||||
|
||||
heading('5.2 objectType 取值对照表', 2)
|
||||
table(['objectType', '含义', '绑定后效果'],
|
||||
[['story', '需求文档(PRD 等)', '需求文档链接更新'],
|
||||
['aiCodeReview', '代码审查报告', '审查报告链接更新;带 reviewResult 时同步审查状态'],
|
||||
['aiWorkLog', '工作日志', '工作日志链接更新'],
|
||||
['aiDocUpdate', 'AI 项目文档更新记录', '更新记录链接更新'],
|
||||
['testCase', '测试用例', '测试用例链接更新'],
|
||||
['testReport', '测试报告模版', '报告模版链接更新'],
|
||||
['testReportSubmit', '测试报告提交', '报告提交链接更新(SOP 流程卡点依据)'],
|
||||
['testOther', '其他测试文档', '其他文档链接更新'],
|
||||
['meeting', '会议纪要', '会议纪要链接更新(objectId 填会议 ID)']],
|
||||
widths=[3.5, 4.5, 8.5])
|
||||
|
||||
heading('5.3 请求示例', 2)
|
||||
code_block('curl -X POST "http://127.0.0.1:8085/zentao/common/uploadBind" \\\n'
|
||||
' -H "Authorization: <token>" \\\n'
|
||||
' -F "file=@代码审查报告.md" \\\n'
|
||||
' -F "objectType=aiCodeReview" \\\n'
|
||||
' -F "objectId=9130" \\\n'
|
||||
' -F "title=代码审查报告-v1" \\\n'
|
||||
' -F "reviewResult=pass"')
|
||||
|
||||
heading('5.4 响应示例', 2)
|
||||
code_block('{\n'
|
||||
' "code": 0,\n'
|
||||
' "message": "成功",\n'
|
||||
' "data": {\n'
|
||||
' "id": 1234,\n'
|
||||
' "title": "代码审查报告-v1",\n'
|
||||
' "extension": ".md",\n'
|
||||
' "size": 5321,\n'
|
||||
' "url": "/zentao/img/20260806170215xxxx.md", // 文件访问地址(相对路径)\n'
|
||||
' "objecttype": "aiCodeReview",\n'
|
||||
' "objectid": 9130,\n'
|
||||
' "addedby": "ai",\n'
|
||||
' "addeddate": "2026-08-06 17:02:15"\n'
|
||||
' }\n'
|
||||
'}')
|
||||
|
||||
heading('5.5 注意事项', 2)
|
||||
for t in ['同一对象同一类型可多次上传形成文件列表,业务对象上的链接字段始终指向最新一份;',
|
||||
'objectType/objectId 错误、对象不存在、类型不在对照表内,均会拒绝且不落盘(无残留);',
|
||||
'返回的 url 为相对路径,访问时拼在系统域名后即可打开/下载。']:
|
||||
p = doc.add_paragraph(t, style='List Bullet')
|
||||
for r in p.runs:
|
||||
set_cn_font(r)
|
||||
r.font.size = Pt(10)
|
||||
|
||||
# ================= 6 错误码 =================
|
||||
heading('6. 常见返回码速查', 1)
|
||||
table(['code', 'message 示例', '含义与处理'],
|
||||
[['0', '成功', '调用成功'],
|
||||
['-1', '请登录', '未带 token 或 token 无效 → 检查 Authorization 头'],
|
||||
['-1', '该接口仅AI框架通道可用(需ai账户token)', 'token 不是 ai 账号令牌 → 换用本文档 1.2 节令牌'],
|
||||
['-1', '该需求已完成,不可再修改', '需求已 finished 锁定(接口一的锁定机制)'],
|
||||
['-1', '需求不存在:xxx / 会议不存在:xxx', 'objectId 或 storyId 对应记录不存在 → 核对 ID'],
|
||||
['-1', 'uploadBind不支持的objectType:xxx', 'objectType 取值错误 → 对照 5.2 表'],
|
||||
['-1', '任务类型仅支持devel/test:xxx', '任务类型取值错误(接口二)'],
|
||||
['-1', '日期格式错误,应为yyyy-MM-dd:xxx', '日期格式错误(接口二)']],
|
||||
widths=[1.5, 7, 8])
|
||||
|
||||
doc.add_paragraph()
|
||||
tail = doc.add_paragraph()
|
||||
r = tail.add_run('—— 本文档含永久访问令牌,仅限授权对接人员持有,请勿外传 ——')
|
||||
r.font.size = Pt(9)
|
||||
r.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
|
||||
set_cn_font(r)
|
||||
|
||||
OUT = r'F:\zentao\1\PM\prds\ai-sop-20260723-1024\outputs\禅道AI通道接口文档_v1.0.docx'
|
||||
doc.save(OUT)
|
||||
print('saved:', OUT)
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""只读勘察:200库 与 161 两个库中 需求9197 的现状。绝不写入。"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
charset='utf8mb4', connect_timeout=15)
|
||||
|
||||
SID = 9197
|
||||
|
||||
def tables_with(cur, db, names):
|
||||
cur.execute("""SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema=%s AND table_name IN (%s)""" %
|
||||
('%s', ','.join(['%s']*len(names))), (db, *names))
|
||||
return {r[0] for r in cur.fetchall()}
|
||||
|
||||
REL = ['zt_storyspec', 'zt_storystage', 'zt_storyreview', 'zt_storyexpand',
|
||||
'zt_projectstory', 'zt_storyestimate', 'zt_task', 'zt_bug', 'zt_case']
|
||||
|
||||
def inspect(conn, db):
|
||||
cur = conn.cursor()
|
||||
print(f'\n===== {conn.host} / {db} =====')
|
||||
cur.execute(f'USE `{db}`')
|
||||
cur.execute("""SELECT id, product, branch, module, plan, parent, title, status, stage,
|
||||
version, pri, estimate, openedBy, assignedTo, reviewedBy,
|
||||
closedReason, deleted, FROM_UNIXTIME(0) FROM zt_story WHERE id=%s""", (SID,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
print('zt_story 9197: 不存在')
|
||||
return
|
||||
labels = ['id','product','branch','module','plan','parent','title','status','stage',
|
||||
'version','pri','estimate','openedBy','assignedTo','reviewedBy','closedReason','deleted']
|
||||
for k, v in zip(labels, row[:17]):
|
||||
print(f' {k}: {v}')
|
||||
exist = tables_with(cur, db, REL)
|
||||
for t in REL:
|
||||
if t not in exist:
|
||||
print(f' {t}: 表不存在')
|
||||
continue
|
||||
col = {'zt_storyspec': 'story', 'zt_storystage': 'story', 'zt_storyreview': 'story',
|
||||
'zt_storyexpand': 'story_id', 'zt_projectstory': 'story',
|
||||
'zt_storyestimate': 'story', 'zt_task': 'story', 'zt_bug': 'story',
|
||||
'zt_case': 'story'}[t]
|
||||
try:
|
||||
cur.execute(f'SELECT COUNT(*) FROM `{t}` WHERE `{col}`=%s', (SID,))
|
||||
print(f' {t}({col}=9197): {cur.fetchone()[0]} 行')
|
||||
except Exception as e:
|
||||
print(f' {t}: 查询失败 {e}')
|
||||
# storyspec 版本明细
|
||||
if 'zt_storyspec' in exist:
|
||||
cur.execute('SELECT story, version, LEFT(title,40), LENGTH(spec), LENGTH(verify) FROM zt_storyspec WHERE story=%s ORDER BY version', (SID,))
|
||||
for r in cur.fetchall():
|
||||
print(f' storyspec v{r[1]}: title={r[2]!r} spec_len={r[3]} verify_len={r[4]}')
|
||||
# 附件/动作条数
|
||||
for t, cond in [('zt_file', "objectType='story'"), ('zt_action', "objectType='story'")]:
|
||||
cur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema=%s AND table_name=%s", (db, t))
|
||||
if cur.fetchone():
|
||||
cur.execute(f"SELECT COUNT(*) FROM `{t}` WHERE {cond} AND objectID=%s", (SID,))
|
||||
print(f' {t}(story,9197): {cur.fetchone()[0]} 行')
|
||||
|
||||
src = pymysql.connect(**SRC)
|
||||
inspect(src, 'zentao_dev')
|
||||
src.close()
|
||||
|
||||
dst = pymysql.connect(**DST)
|
||||
cur = dst.cursor()
|
||||
cur.execute('SHOW DATABASES')
|
||||
dbs = [r[0] for r in cur.fetchall()]
|
||||
print(f'\n161 上的库: {[d for d in dbs if d.startswith("zentao")]}')
|
||||
for db in dbs:
|
||||
if db.startswith('zentao'):
|
||||
inspect(dst, db)
|
||||
dst.close()
|
||||
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""只读勘察第二轮:确定目标库 + 校验引用完整性。绝不写入。"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
charset='utf8mb4', connect_timeout=15)
|
||||
SID = 9197
|
||||
|
||||
src = pymysql.connect(**SRC); scur = src.cursor()
|
||||
dst = pymysql.connect(**DST); dcur = dst.cursor()
|
||||
|
||||
# 1) 161 上哪个库是活跃测试库:比较最近活动
|
||||
print('===== 161 各库活跃度 =====')
|
||||
for db in ['zentao_dev', 'zentao_dev_2026']:
|
||||
dcur.execute(f'USE `{db}`')
|
||||
dcur.execute("SELECT COUNT(*), MAX(id) FROM zt_story")
|
||||
c1, m1 = dcur.fetchone()
|
||||
try:
|
||||
dcur.execute("SELECT COUNT(*), MAX(date) FROM zt_action")
|
||||
c2, m2 = dcur.fetchone()
|
||||
except Exception as e:
|
||||
c2, m2 = 'err', str(e)
|
||||
dcur.execute("SELECT MAX(lastEditedDate) FROM zt_story")
|
||||
m3 = dcur.fetchone()[0]
|
||||
print(f'{db}: story数={c1} max_story_id={m1} action数={c2} max_action_date={m2} story最近编辑={m3}')
|
||||
|
||||
# 2) 200 上 9197 相关行明细
|
||||
print('\n===== 200 上 9197 相关明细 =====')
|
||||
scur.execute("SELECT * FROM zt_storyspec WHERE story=%s", (SID,))
|
||||
cols = [d[0] for d in scur.description]
|
||||
for r in scur.fetchall():
|
||||
print('storyspec:', dict(zip(cols, r)))
|
||||
|
||||
scur.execute("SELECT project, story, version, `order` FROM zt_projectstory WHERE story=%s", (SID,))
|
||||
proj_links = scur.fetchall()
|
||||
print('projectstory:', proj_links)
|
||||
|
||||
scur.execute("""SELECT id, project, execution, name, status, type, assignedTo, deleted
|
||||
FROM zt_task WHERE story=%s""", (SID,))
|
||||
tasks = scur.fetchall()
|
||||
print('tasks:', tasks)
|
||||
|
||||
scur.execute("""SELECT id, action, date, actor, LEFT(comment,40) FROM zt_action
|
||||
WHERE objectType='story' AND objectID=%s ORDER BY id""", (SID,))
|
||||
for r in scur.fetchall():
|
||||
print('action:', r)
|
||||
|
||||
# 3) zentao_dev_2026 里已有的 9197 action/file 是什么
|
||||
print('\n===== zentao_dev_2026 已有 9197 action/file =====')
|
||||
dcur.execute('USE zentao_dev_2026')
|
||||
dcur.execute("""SELECT id, action, date, actor, LEFT(comment,40) FROM zt_action
|
||||
WHERE objectType='story' AND objectID=%s ORDER BY id""", (SID,))
|
||||
for r in dcur.fetchall():
|
||||
print('action:', r)
|
||||
dcur.execute("SELECT id, title, pathname, addedBy, addedDate FROM zt_file WHERE objectType='story' AND objectID=%s", (SID,))
|
||||
for r in dcur.fetchall():
|
||||
print('file:', r)
|
||||
|
||||
# 4) 引用完整性:目标库中 product/项目/task id 冲突检查
|
||||
print('\n===== 目标库引用完整性 =====')
|
||||
dcur.execute("SELECT id, LEFT(name,30), deleted FROM zt_product WHERE id=150")
|
||||
print('目标 product 150:', dcur.fetchone())
|
||||
scur.execute("SELECT id, LEFT(name,30), deleted FROM zt_product WHERE id=150")
|
||||
print('源 product 150:', scur.fetchone())
|
||||
for pid in {r[0] for r in proj_links}:
|
||||
dcur.execute("SELECT id, LEFT(name,40), deleted FROM zt_project WHERE id=%s", (pid,))
|
||||
print(f'目标 project {pid}:', dcur.fetchone())
|
||||
for tid in [t[0] for t in tasks]:
|
||||
dcur.execute("SELECT id, LEFT(name,40), story, deleted FROM zt_task WHERE id=%s", (tid,))
|
||||
print(f'目标 task {tid} 冲突检查:', dcur.fetchone())
|
||||
|
||||
# 5) zt_story 9197 行全列比对(200 vs zentao_dev_2026)
|
||||
scur.execute("SELECT * FROM zt_story WHERE id=%s", (SID,))
|
||||
story_cols = [d[0] for d in scur.description]
|
||||
srow = scur.fetchone()
|
||||
dcur.execute("SELECT * FROM zt_story WHERE id=%s", (SID,))
|
||||
drow = dcur.fetchone()
|
||||
diff = [(c, s, d) for c, s, d in zip(story_cols, srow, drow) if s != d]
|
||||
print('\n===== zt_story 9197 差异列 =====' if diff else '\n===== zt_story 9197 两库完全一致 =====')
|
||||
for c, s, d in diff:
|
||||
print(f' {c}: 200={s!r} vs 161_2026={d!r}')
|
||||
|
||||
src.close(); dst.close()
|
||||
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""只读勘察第三轮:自定义表 zt_story_expand 及 story 相关自定义表。绝不写入。"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', connect_timeout=15)
|
||||
SID = 9197
|
||||
|
||||
src = pymysql.connect(**SRC); scur = src.cursor()
|
||||
dst = pymysql.connect(**DST); dcur = dst.cursor()
|
||||
|
||||
def story_tables(cur, db):
|
||||
cur.execute("""SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema=%s AND (table_name LIKE '%%story%%' OR table_name LIKE '%%expand%%'
|
||||
OR table_name LIKE '%%deliver%%' OR table_name LIKE '%%ys%%')""", (db,))
|
||||
return sorted(r[0] for r in cur.fetchall())
|
||||
|
||||
print('200 上相关表:', story_tables(scur, 'zentao_dev'))
|
||||
print('161_2026 相关表:', story_tables(dcur, 'zentao_dev_2026'))
|
||||
|
||||
for label, cur, db in [('200', scur, 'zentao_dev'), ('161_2026', dcur, 'zentao_dev_2026')]:
|
||||
cur.execute("""SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema=%s AND table_name='zt_story_expand'""", (db,))
|
||||
cols = [r[0] for r in cur.fetchall()]
|
||||
if not cols:
|
||||
print(f'{label}: zt_story_expand 不存在'); continue
|
||||
key = 'story_id' if 'story_id' in cols else ('story' if 'story' in cols else None)
|
||||
if not key:
|
||||
print(f'{label}: zt_story_expand 列={cols}'); continue
|
||||
cur.execute(f"SELECT * FROM zt_story_expand WHERE `{key}`=%s", (SID,))
|
||||
rows = cur.fetchall()
|
||||
print(f'{label} zt_story_expand({key}=9197): {len(rows)} 行')
|
||||
for r in rows:
|
||||
print(' ', dict(zip(cols, r)))
|
||||
|
||||
# task 表相关:zt_team / zt_effort 是否涉及这两个任务
|
||||
for t in ['zt_team', 'zt_effort', 'zt_taskestimate']:
|
||||
for label, cur, db in [('200', scur, 'zentao_dev'), ('161_2026', dcur, 'zentao_dev_2026')]:
|
||||
cur.execute("""SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema=%s AND table_name=%s""", (db, t))
|
||||
if not cur.fetchone():
|
||||
continue
|
||||
try:
|
||||
cur.execute(f"SELECT COUNT(*) FROM `{t}` WHERE (account IN ('zhouwenjun','sunying') AND project IN (142,286)) OR (task IN (18841,18849))")
|
||||
print(f'{label} {t} 相关行: {cur.fetchone()[0]}')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
src.close(); dst.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""只读勘察第四轮:其余自定义 story 关联表 + 正确重读 zt_story_expand。绝不写入。"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', connect_timeout=15)
|
||||
SID = 9197
|
||||
|
||||
src = pymysql.connect(**SRC); scur = src.cursor()
|
||||
dst = pymysql.connect(**DST); dcur = dst.cursor()
|
||||
|
||||
def cols_of(cur, db, t):
|
||||
cur.execute("""SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema=%s AND table_name=%s ORDER BY ordinal_position""", (db, t))
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
|
||||
TABLES = ['zt_story_expand', 'zt_story_feedback', 'zt_story_user', 'zt_story_userspec',
|
||||
'zt_story_case', 'zt_story_month_workload', 'zt_story_user_task']
|
||||
|
||||
for t in TABLES:
|
||||
for label, cur, db in [('200', scur, 'zentao_dev'), ('161_2026', dcur, 'zentao_dev_2026')]:
|
||||
cols = cols_of(cur, db, t)
|
||||
if not cols:
|
||||
print(f'{label} {t}: 不存在'); continue
|
||||
keys = [c for c in cols if c in ('story_id', 'story', 'storyId')]
|
||||
if not keys:
|
||||
print(f'{label} {t}: 无 story 关联列, 列={cols[:8]}...'); continue
|
||||
k = keys[0]
|
||||
cur.execute(f"SELECT * FROM `{t}` WHERE `{k}`=%s", (SID,))
|
||||
rows = cur.fetchall()
|
||||
print(f'{label} {t}({k}=9197): {len(rows)} 行')
|
||||
for r in rows:
|
||||
d = dict(zip(cols, r))
|
||||
# 长文本截断
|
||||
for kk, vv in d.items():
|
||||
if isinstance(vv, (bytes, str)) and len(str(vv)) > 60:
|
||||
d[kk] = str(vv)[:60] + '...'
|
||||
print(' ', d)
|
||||
|
||||
src.close(); dst.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""只读:9197 的 userStory 引用及 zt_story_user 相关行在两库的差异。"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', connect_timeout=15)
|
||||
SID = 9197
|
||||
|
||||
src = pymysql.connect(**SRC); scur = src.cursor()
|
||||
dst = pymysql.connect(**DST); dcur = dst.cursor()
|
||||
|
||||
scur.execute('SELECT user_story FROM zt_story WHERE id=%s', (SID,))
|
||||
us_id = scur.fetchone()[0]
|
||||
print(f'200: zt_story 9197.user_story = {us_id}')
|
||||
dcur.execute('SELECT user_story FROM zt_story WHERE id=%s', (SID,))
|
||||
print(f'161_2026: zt_story 9197.user_story = {dcur.fetchone()[0]}')
|
||||
|
||||
if us_id:
|
||||
for label, cur in [('200', scur), ('161_2026', dcur)]:
|
||||
cur.execute('SELECT id, product, project, parent, LEFT(title,40), status, deleted FROM zt_story_user WHERE id=%s', (us_id,))
|
||||
print(f'{label} zt_story_user {us_id}:', cur.fetchone())
|
||||
cur.execute('SELECT story, version, LEFT(title,40), LENGTH(spec) FROM zt_story_userspec WHERE story=%s', (us_id,))
|
||||
print(f'{label} zt_story_userspec(story={us_id}):', cur.fetchall())
|
||||
cur.execute('SELECT COUNT(*) FROM zt_story_user_task WHERE user_story_id=%s', (us_id,))
|
||||
print(f'{label} zt_story_user_task(user_story_id={us_id}):', cur.fetchone()[0], '行')
|
||||
# 该用户需求是否还关联其他研发需求(判断要不要一并带)
|
||||
cur.execute('SELECT COUNT(*) FROM zt_story WHERE user_story=%s AND deleted="0"', (us_id,))
|
||||
print(f'{label} 引用该用户需求的研发需求数:', cur.fetchone()[0])
|
||||
|
||||
src.close(); dst.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""排查 200 库 story 9209:上传 MD 落到附件问题(只读查询,绝不写入)"""
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
conn = pymysql.connect(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
cur = conn.cursor()
|
||||
|
||||
def show(title, sql, args=None):
|
||||
print(f"\n===== {title} =====")
|
||||
cur.execute(sql, args or ())
|
||||
cols = [d[0] for d in cur.description]
|
||||
print(' | '.join(cols))
|
||||
for row in cur.fetchall():
|
||||
print(' | '.join('NULL' if v is None else str(v) for v in row))
|
||||
|
||||
# 1) 需求本体(含二期扩展列,先看有哪些列)
|
||||
show("zt_story 9209 全部列", "SHOW COLUMNS FROM zt_story")
|
||||
show("zt_story 9209 本体", "SELECT * FROM zt_story WHERE id=9209")
|
||||
|
||||
# 2) 附件记录:9209 相关的所有 zt_file
|
||||
show("zt_file objectID=9209 全类型",
|
||||
"SELECT id, objectType, objectID, title, extension, size, addedBy, addedDate, url, extra, deleted FROM zt_file WHERE objectID=9209 ORDER BY id DESC")
|
||||
|
||||
# 3) 对照组:9130(历史验证过的)的 file 记录类型分布
|
||||
show("zt_file 近期 ai 上传的 md 记录(全库对照)",
|
||||
"SELECT id, objectType, objectID, title, extension, addedBy, addedDate, url FROM zt_file WHERE extension='md' ORDER BY id DESC LIMIT 20")
|
||||
|
||||
# 4) zt_story_expand 9209
|
||||
try:
|
||||
show("zt_story_expand 9209", "SELECT * FROM zt_story_expand WHERE story=9209 OR id=9209 LIMIT 5")
|
||||
except Exception as e:
|
||||
print("zt_story_expand 查询失败:", e)
|
||||
|
||||
conn.close()
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql, datetime
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
# 1. 周一~周五(不含节假日)
|
||||
weekdays = [datetime.date(2026,6,i) for i in range(1,31) if datetime.date(2026,6,i).weekday()<5]
|
||||
print("2026-06 周一~周五天数:", len(weekdays), [d.day for d in weekdays])
|
||||
|
||||
# 2. Constant.vacationDateMap 里的 6 月节假日
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SHOW TABLES LIKE '%vacation%'")
|
||||
print("vacation 表:", cur.fetchall())
|
||||
cur.execute("SHOW TABLES LIKE '%holiday%'")
|
||||
print("holiday 表:", cur.fetchall())
|
||||
cur.execute("SHOW TABLES LIKE '%constant%'")
|
||||
print("constant 表:", cur.fetchall())
|
||||
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""从新版考核方案 xlsx 生成 9 份绩效导出模版(单 sheet + 占位符)。
|
||||
|
||||
机制对齐 writeXlsx(IZtCountService.java):遍历单元格做 {key} 子串替换。
|
||||
- F2 -> {name}(员工姓名后一格),H2 -> {date}(考核周期后一格)
|
||||
- 每个指标行:H 列(本期结果)-> {键名},G 列(绩效数据)-> {detail_键名} 或拆分占位,无则清空
|
||||
- 合计行:H 列 -> {total}
|
||||
"""
|
||||
from copy import copy
|
||||
|
||||
import openpyxl
|
||||
|
||||
SRC = r'docs/信息技术部绩效考核标准-新版 - AI下的考核方案.xlsx'
|
||||
OUT_DIR = r'codes/zentao/src/main/resources/templates/scope'
|
||||
|
||||
# (源 sheet 名, 输出文件名, 合计行号, {指标行号: 键名}, {指标行号: G列绩效数据占位})
|
||||
SHEETS = [
|
||||
('项目经理', '项目经理考核.xlsx', 12,
|
||||
{4: 'workloadPrdScore', 5: 'workloadTeamScore', 6: 'versionPlanFinishedRate', 7: 'bugScore',
|
||||
8: 'documentQualityScore', 9: 'developFeedbackStory', 10: 'systemStabilityScore',
|
||||
11: 'professionalSkillEnhancementScore'},
|
||||
{4: '{detail_workloadPrdScore}', 5: '{detail_workloadTeamScore}', 6: '{detail_versionPlanFinishedRate}',
|
||||
7: '{detail_bugScore}', 8: '{detail_documentQualityScore}'}),
|
||||
('项目经理(王宇航)', '项目经理考核-王宇航.xlsx', 11,
|
||||
{4: 'workloadTeamScore', 5: 'versionPlanFinishedRate', 6: 'bugScore', 7: 'documentQualityScore',
|
||||
8: 'developFeedbackStory', 9: 'systemStabilityScore', 10: 'professionalSkillEnhancementScore'},
|
||||
{4: '{detail_workloadTeamScore}', 5: '{detail_versionPlanFinishedRate}', 6: '{detail_bugScore}',
|
||||
7: '{detail_documentQualityScore}'}),
|
||||
('产品经理', '产品经理考核.xlsx', 10,
|
||||
{4: 'workloadPrdScore', 5: 'workloadTeamScore', 6: 'productProjectOnTimeRateScore', 7: 'productBugRate',
|
||||
8: 'productProblemResponse', 9: 'productResponsibilityScore'},
|
||||
{4: '{detail_workloadPrdScore}', 5: '{detail_workloadTeamScore}', 6: '{detail_productProjectOnTimeRateScore}',
|
||||
7: '{detail_productBugRate}'}),
|
||||
('产品助理', '项目助理考核.xlsx', 9,
|
||||
{4: 'workloadPrdScore', 5: 'releaseScore', 6: 'productBugRate', 7: 'productProblemResponse',
|
||||
8: 'productResponsibilityScore'},
|
||||
{4: '{detail_workloadPrdScore}', 6: '{detail_productBugRate}'}),
|
||||
('后端开发工程师', '后端开发工程师.xlsx', 12,
|
||||
{4: 'punctualityScore', 5: 'bugScore', 6: 'codeQualityScore', 7: 'documentQualityScore', 8: 'saturationScore',
|
||||
9: 'workAttitude', 10: 'excellentShare', 11: 'quality'},
|
||||
{4: '{totalTask}/{delayTask}/{finishPunctuality}', 5: '{bugCount}/{bugDensity}',
|
||||
8: '{workTime}/{examineTime}/{saturation}'}),
|
||||
('前端开发工程师', '前端开发工程师.xlsx', 11,
|
||||
{4: 'punctualityScore', 5: 'bugScore', 6: 'codeQualityScore', 7: 'saturationScore', 8: 'workAttitude',
|
||||
9: 'excellentShare', 10: 'quality'},
|
||||
{4: '{totalTask}/{delayTask}/{finishPunctuality}', 5: '{bugCount}/{bugDensity}',
|
||||
7: '{workTime}/{examineTime}/{saturation}'}),
|
||||
('测试工程师', '测试工程师.xlsx', 11,
|
||||
{4: 'punctualityScore', 5: 'documentQualityScore', 6: 'bugFindScore', 7: 'bugScore', 8: 'workAttitude',
|
||||
9: 'excellentShare', 10: 'quality'},
|
||||
{4: '{totalTask}/{delayTask}/{finishPunctuality}', 6: '{devSlightBug}/{devSeriousBug}/{bugFindRate}',
|
||||
7: '{slightBug}/{seriousBug}'}),
|
||||
('UI工程师', 'UI工程师.xlsx', 9,
|
||||
{4: 'punctualityScore', 5: 'designScore', 6: 'workAttitude', 7: 'uiWorkScore', 8: 'excellentShare'},
|
||||
{4: '{totalTask}/{delayTask}/{finishPunctuality}'}),
|
||||
('运维工程师', '运维工程师.xlsx', 12,
|
||||
{4: 'opsMajorTaskScore', 5: 'opsMonitorScore', 6: 'opsInspectScore', 7: 'opsBackupScore', 8: 'otherOpsScore',
|
||||
9: 'systemStabilityScore', 10: 'workAttitude', 11: 'excellentShare'},
|
||||
{4: '{detail_opsMajorTaskScore}', 5: '{detail_opsMonitorScore}', 6: '{detail_opsInspectScore}',
|
||||
7: '{detail_opsBackupScore}'}),
|
||||
]
|
||||
|
||||
|
||||
def copy_sheet(src_ws, dst_ws):
|
||||
"""整 sheet 拷贝:值 + 基础样式 + 合并单元格 + 列宽行高。"""
|
||||
for row in src_ws.iter_rows():
|
||||
for cell in row:
|
||||
if cell.value is None and not cell.has_style:
|
||||
continue
|
||||
nc = dst_ws.cell(row=cell.row, column=cell.column)
|
||||
nc.value = cell.value
|
||||
if cell.has_style:
|
||||
nc.font = copy(cell.font)
|
||||
nc.fill = copy(cell.fill)
|
||||
nc.border = copy(cell.border)
|
||||
nc.alignment = copy(cell.alignment)
|
||||
nc.protection = copy(cell.protection)
|
||||
nc.number_format = cell.number_format
|
||||
for merged in src_ws.merged_cells.ranges:
|
||||
dst_ws.merge_cells(str(merged))
|
||||
for key, dim in src_ws.column_dimensions.items():
|
||||
if dim.width is not None:
|
||||
dst_ws.column_dimensions[key].width = dim.width
|
||||
for key, dim in src_ws.row_dimensions.items():
|
||||
if dim.height is not None:
|
||||
dst_ws.row_dimensions[key].height = dim.height
|
||||
|
||||
|
||||
def main():
|
||||
src_wb = openpyxl.load_workbook(SRC)
|
||||
for sheet_name, out_name, total_row, row_keys, row_details in SHEETS:
|
||||
src_ws = src_wb[sheet_name]
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.title = sheet_name
|
||||
copy_sheet(src_ws, ws)
|
||||
# R2:员工姓名后一格 F2 -> {name},考核周期后一格 H2 -> {date}
|
||||
ws.cell(row=2, column=6).value = '{name}'
|
||||
ws.cell(row=2, column=8).value = '{date}'
|
||||
# 指标行:H 列本期结果 -> {键名};G 列绩效数据 -> 明细占位或清空
|
||||
for r, key in row_keys.items():
|
||||
ws.cell(row=r, column=8).value = '{%s}' % key
|
||||
ws.cell(row=r, column=7).value = row_details.get(r)
|
||||
# 合计行结果列 -> {total}
|
||||
assert ws.cell(row=total_row, column=1).value == '合计', (sheet_name, total_row)
|
||||
ws.cell(row=total_row, column=8).value = '{total}'
|
||||
out_path = '%s/%s' % (OUT_DIR, out_name)
|
||||
wb.save(out_path)
|
||||
print('OK %s <- %s' % (out_name, sheet_name))
|
||||
print('done')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
# 需求文档测试
|
||||
|
||||
CHG-058 storyPrd 通道验证
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""8089 实测:用户需求 324 详情 → 会议纪要 tab → MD 按钮 → mavon 富文本预览"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8089'
|
||||
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=False, slow_mo=400, args=['--start-maximized'])
|
||||
page = b.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2000)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
|
||||
print('>>> 打开用户需求 324 详情')
|
||||
page.goto(f'{BASE}/#/product-user-story-info/324', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
|
||||
print('>>> 点会议纪要 tab')
|
||||
tab = page.locator('.el-tabs__item:has-text("会议纪要")').first
|
||||
tab.click(); page.wait_for_timeout(4000)
|
||||
page.screenshot(path='tmp/pw_324_meeting_tab.png')
|
||||
|
||||
print('>>> 点会议卡片上的 MD 按钮')
|
||||
btn = page.locator('button:has-text("MD")').first
|
||||
if btn.count() == 0:
|
||||
print('!!! 没找到 MD 按钮'); page.screenshot(path='tmp/pw_324_no_md_btn.png'); b.close(); sys.exit(1)
|
||||
btn.click(); page.wait_for_timeout(5000)
|
||||
|
||||
dlg = page.locator('.el-dialog:visible').last
|
||||
# mavon 预览渲染判定:出现 markdown-body 且含表格/代码块
|
||||
body = dlg.locator('.markdown-body')
|
||||
print('markdown-body 数量:', body.count())
|
||||
html = body.first.inner_html() if body.count() else ''
|
||||
print('含表格:', '<table' in html, '| 含代码块:', '<pre' in html or 'hljs' in html, '| 含引用:', '<blockquote' in html)
|
||||
page.screenshot(path='tmp/pw_324_md_preview.png')
|
||||
print('>>> 截图已存 tmp/pw_324_meeting_tab.png / tmp/pw_324_md_preview.png,窗口停留 3 分钟')
|
||||
page.wait_for_timeout(180000)
|
||||
b.close()
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8089'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True, args=['--start-maximized'])
|
||||
page = b.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2000)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
sec = page.locator('.fs18.fw6:has-text("AI 指标")').first
|
||||
sec.scroll_into_view_if_needed(); page.wait_for_timeout(800)
|
||||
page.screenshot(path='tmp/pw_6566_ai_block.png')
|
||||
b.close()
|
||||
print('ok')
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""6566 研发详情:上传按钮位置验证(应在文件列表之后)"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8089'
|
||||
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=False, slow_mo=300, args=['--start-maximized'])
|
||||
page = b.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2000)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
|
||||
# 滚动到「测试用例/测试报告模版」区块
|
||||
sec = page.locator('.fs18.fw6:has-text("测试用例/测试报告模版")').first
|
||||
sec.scroll_into_view_if_needed(); page.wait_for_timeout(1000)
|
||||
page.screenshot(path='tmp/pw_6566_testcase_sec.png')
|
||||
|
||||
sec2 = page.locator('.fs18.fw6:has-text("提交测试报告")').first
|
||||
sec2.scroll_into_view_if_needed(); page.wait_for_timeout(1000)
|
||||
page.screenshot(path='tmp/pw_6566_submit_sec.png')
|
||||
|
||||
sec3 = page.locator('.fs18.fw6:has-text("代码审查报告")').first
|
||||
sec3.scroll_into_view_if_needed(); page.wait_for_timeout(1000)
|
||||
page.screenshot(path='tmp/pw_6566_review_sec.png')
|
||||
|
||||
print('>>> 截图完成,窗口停留 60s')
|
||||
page.wait_for_timeout(60000)
|
||||
b.close()
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""9130 需求文档 tab 验证(CHG-063)"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel='chrome', headless=True, args=['--disable-gpu'])
|
||||
page = browser.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
if 'login' in page.url:
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
page.goto(f'{BASE}/#/product-story-info/9130', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
# tab 存在性
|
||||
tabs = page.locator('.el-tabs__item').all_inner_texts()
|
||||
print('tabs:', tabs)
|
||||
page.locator('.el-tabs__item:has-text("需求文档")').first.click(); page.wait_for_timeout(2000)
|
||||
body = page.inner_text('body')
|
||||
for t in ['测试用例/测试报告模版', '提交测试报告', '其他测试文档', '代码审查报告', '工作日志', 'AI项目文档更新记录']:
|
||||
print(t, '✓' if t in body else '✗缺失')
|
||||
print('左栏交付物在:', '✓' if '交付物' in body else '✗')
|
||||
page.screenshot(path='tmp/pw_9130_doctab.png', full_page=False)
|
||||
browser.close()
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""9130 历史记录显示截图(AI 拆 2 任务后的真实渲染)"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel='chrome', headless=True, args=['--disable-gpu'])
|
||||
page = browser.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
if 'login' in page.url:
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
page.goto(f'{BASE}/#/product-story-info/9130', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
# 滚动到历史记录区块
|
||||
try:
|
||||
page.locator('text=历史记录').first.scroll_into_view_if_needed(); page.wait_for_timeout(1500)
|
||||
except Exception as e:
|
||||
print('历史记录区块定位失败:', e)
|
||||
page.screenshot(path='tmp/pw_9130_history.png', full_page=False)
|
||||
# 抓历史记录文本内容
|
||||
body = page.inner_text('body')
|
||||
idx = body.find('历史记录')
|
||||
print(body[idx:idx+500] if idx >= 0 else '未找到历史记录区块')
|
||||
browser.close()
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""CHG-037 绩效弹窗分岗位验证:月报→绩效按钮→截图+关键行断言"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
TARGETS = [
|
||||
('jiangheng', '蒋恒', ['需求PRD工作量指标完成率', '团队工作量指标完成率', '版本计划完成率', '项目工作流程完整、工作文档齐备', '专业技能持续提升']),
|
||||
]
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True, args=['--start-maximized'])
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
# 切到 6 月(当前 7 月无数据)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
# 等 6 月列表真正加载(7 月为空的标志是任何行出现)
|
||||
try:
|
||||
page.wait_for_selector('tr:has-text("王宇航")', timeout=25000)
|
||||
except Exception:
|
||||
page.screenshot(path='tmp/pw_chg037_nolist.png', full_page=True)
|
||||
print('6 月列表加载超时,截图 tmp/pw_chg037_nolist.png')
|
||||
b.close(); raise SystemExit(1)
|
||||
page.wait_for_timeout(2000)
|
||||
for account, cname, keys in TARGETS:
|
||||
row = page.locator(f'tr:has-text("{cname}")').first
|
||||
if row.count() == 0:
|
||||
row = page.locator(f'tr:has-text("{account}")').first
|
||||
if row.count() == 0:
|
||||
print(f'[{account}] 月报列表未找到 {cname}/{account} 行,跳过')
|
||||
continue
|
||||
btn = row.locator('button:has-text("绩效")')
|
||||
if btn.count() == 0:
|
||||
print(f'[{account}] 无绩效按钮,跳过')
|
||||
continue
|
||||
btn.first.click(); page.wait_for_timeout(5000)
|
||||
body = page.locator('body').inner_text()
|
||||
miss = [k for k in keys if k not in body]
|
||||
page.screenshot(path=f'tmp/pw_chg037_{account}.png', full_page=True)
|
||||
print(f'[{account}] {"PASS" if not miss else "MISS:" + str(miss)} 截图 tmp/pw_chg037_{account}.png')
|
||||
# 关闭弹窗
|
||||
closed = False
|
||||
for sel in ['.el-dialog__close', 'button:has-text("取 消")', 'button:has-text("取消")']:
|
||||
loc = page.locator(sel)
|
||||
if loc.count():
|
||||
try:
|
||||
loc.first.click(); closed = True; break
|
||||
except Exception:
|
||||
pass
|
||||
if not closed:
|
||||
page.keyboard.press('Escape')
|
||||
page.wait_for_timeout(2000)
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(15000)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
page.wait_for_selector('tr:has-text("孙庆方")', timeout=25000)
|
||||
page.wait_for_timeout(2000)
|
||||
row = page.locator('tr:has-text("孙庆方")').first
|
||||
row.locator('button:has-text("绩效")').first.click(); page.wait_for_timeout(15000)
|
||||
body = page.locator('body').inner_text()
|
||||
keys = ['测试计划及时完成', '缺陷检出率', '线上BUG', '测试文档', '不规范行为']
|
||||
miss = [k for k in keys if k not in body]
|
||||
page.screenshot(path='tmp/pw_chg037_cs_sunqingfang.png', full_page=True)
|
||||
print('[CS孙庆方]', 'PASS' if not miss else f'MISS:{miss}', '截图 tmp/pw_chg037_cs_sunqingfang.png')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""CHG-038 验证:KFZ 前端/后端弹窗分流 + 用户编辑表单开发方向下拉"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
|
||||
# 1) 月报 6 月
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
page.wait_for_selector('tr:has-text("郭尚雨")', timeout=25000)
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
def perf_shot(cname, tag, must, must_not):
|
||||
row = page.locator(f'tr:has-text("{cname}")').first
|
||||
row.locator('button:has-text("绩效")').first.click(); page.wait_for_timeout(5000)
|
||||
body = page.locator('body').inner_text()
|
||||
miss = [k for k in must if k not in body]
|
||||
bad = [k for k in must_not if k in body]
|
||||
page.screenshot(path=f'tmp/pw_chg038_{tag}.png', full_page=True)
|
||||
ok = 'PASS' if not miss and not bad else f'MISS:{miss} BAD:{bad}'
|
||||
print(f'[{tag}] {ok} 截图 tmp/pw_chg038_{tag}.png')
|
||||
closed = False
|
||||
for sel in ['.el-dialog__close', 'button:has-text("取 消")', 'button:has-text("取消")']:
|
||||
loc = page.locator(sel)
|
||||
if loc.count():
|
||||
try:
|
||||
loc.first.click(); closed = True; break
|
||||
except Exception:
|
||||
pass
|
||||
if not closed:
|
||||
page.keyboard.press('Escape')
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
# 郭尚雨=frontend:饱和度30%、无文档质量行、代码质量 flat
|
||||
perf_shot('郭尚雨', 'guoshangyu_front', ['工作量饱和度', '30%', '代码整洁'], ['文档质量'])
|
||||
# 金亮=NULL(后端):饱和度20%、有文档质量行、初审/复审
|
||||
perf_shot('金亮', 'jinliang_backend', ['文档质量', '初审'], [])
|
||||
|
||||
# 2) 用户管理:编辑郭尚雨 → 开发方向下拉
|
||||
page.goto(f'{BASE}/#/user', wait_until='domcontentloaded'); page.wait_for_timeout(6000)
|
||||
urow = page.locator('tr:has-text("郭尚雨")').first
|
||||
if urow.count():
|
||||
urow.locator('button:has-text("修改")').first.click(); page.wait_for_timeout(4000)
|
||||
dlg = page.locator('.el-dialog:visible')
|
||||
body = dlg.inner_text() if dlg.count() else page.locator('body').inner_text()
|
||||
has_dir = '开发方向' in body
|
||||
# 下拉里应显示 前端
|
||||
sel_val = ''
|
||||
loc = page.locator('.el-dialog:visible .el-select input')
|
||||
for i in range(loc.count()):
|
||||
v = loc.nth(i).input_value()
|
||||
if v in ('前端', '后端'):
|
||||
sel_val = v
|
||||
page.screenshot(path='tmp/pw_chg038_user_form.png', full_page=True)
|
||||
print(f'[user_form] 开发方向下拉={"有" if has_dir else "无"} 选中值={sel_val!r} 截图 tmp/pw_chg038_user_form.png')
|
||||
else:
|
||||
print('[user_form] 用户列表未找到郭尚雨')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/user', wait_until='domcontentloaded'); page.wait_for_timeout(6000)
|
||||
page.locator('#userSearch').fill('guoshangyu')
|
||||
page.locator('button:has-text("查询")').first.click(); page.wait_for_timeout(4000)
|
||||
urow = page.locator('tr:has-text("郭尚雨")').first
|
||||
if not urow.count():
|
||||
urow = page.locator('tr:has-text("guoshangyu")').first
|
||||
if urow.count():
|
||||
urow.locator('button:has-text("修改")').first.click(); page.wait_for_timeout(5000)
|
||||
dlg = page.locator('.el-dialog:visible')
|
||||
body = dlg.inner_text() if dlg.count() else ''
|
||||
has_dir = '开发方向' in body
|
||||
sel_val = ''
|
||||
loc = page.locator('.el-dialog:visible .el-select input')
|
||||
for i in range(loc.count()):
|
||||
v = loc.nth(i).input_value()
|
||||
if v in ('前端', '后端'):
|
||||
sel_val = v
|
||||
page.screenshot(path='tmp/pw_chg038_user_form.png', full_page=True)
|
||||
print(f'[user_form] 开发方向下拉={"有" if has_dir else "无"} 选中值={sel_val!r}')
|
||||
else:
|
||||
page.screenshot(path='tmp/pw_chg038_user_list.png', full_page=True)
|
||||
print('[user_form] 仍未找到郭尚雨,截图 tmp/pw_chg038_user_list.png')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
page.wait_for_selector('tr:has-text("周林芳")', timeout=25000)
|
||||
page.wait_for_timeout(2000)
|
||||
row = page.locator('tr:has-text("周林芳")').first
|
||||
row.locator('button:has-text("绩效")').first.click(); page.wait_for_timeout(15000)
|
||||
body = page.locator('body').inner_text()
|
||||
miss = [k for k in ['工作量饱和度', '30%', '代码整洁'] if k not in body]
|
||||
bad = [k for k in ['文档质量'] if k in body]
|
||||
page.screenshot(path='tmp/pw_chg038_zhoulinfang.png', full_page=True)
|
||||
print('[周林芳前端]', 'PASS' if not miss and not bad else f'MISS:{miss} BAD:{bad}')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
page.wait_for_selector('tr:has-text("蒋恒")', timeout=25000)
|
||||
page.wait_for_timeout(2000)
|
||||
row = page.locator('tr:has-text("蒋恒")').first
|
||||
row.locator('button:has-text("绩效")').first.click(); page.wait_for_timeout(15000)
|
||||
body = page.locator('body').inner_text()
|
||||
keys = ['Σ指数', '达标', '发布', '豁免线', '缺失']
|
||||
got = [k for k in keys if k in body]
|
||||
page.screenshot(path='tmp/pw_chg041_jiangheng.png', full_page=True)
|
||||
print('[蒋恒] 过程值命中:', got, '截图 tmp/pw_chg041_jiangheng.png')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
page.wait_for_selector('tr:has-text("郭尚雨")', timeout=25000)
|
||||
page.wait_for_timeout(2000)
|
||||
row = page.locator('tr:has-text("郭尚雨")').first
|
||||
row.locator('button:has-text("绩效")').first.click(); page.wait_for_timeout(15000)
|
||||
body = page.locator('body').inner_text()
|
||||
keys = ['实际产出工时', '团队总工作天数', '团队达标总工时', '月度达标工时', '131', '273', '1365', '105']
|
||||
miss = [k for k in keys if k not in body]
|
||||
page.screenshot(path='tmp/pw_chg044_guoshangyu.png', full_page=True)
|
||||
print('[郭尚雨]', 'PASS' if not miss else f'MISS:{miss}')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
# 滚到 AI项目文档更新记录 区块
|
||||
loc = page.locator('text="AI项目文档更新记录"').first
|
||||
if loc.count():
|
||||
loc.scroll_into_view_if_needed(); page.wait_for_timeout(2000)
|
||||
body = page.locator('body').inner_text()
|
||||
has_file = 'AI项目文档更新记录-6566' in body
|
||||
page.screenshot(path='tmp/pw_chg046_6566.png', full_page=True)
|
||||
print('[6566] 区块存在=是 文件展示=', has_file, ' 截图 tmp/pw_chg046_6566.png')
|
||||
else:
|
||||
page.screenshot(path='tmp/pw_chg046_6566_fail.png', full_page=True)
|
||||
print('[6566] 未找到区块,截图 tmp/pw_chg046_6566_fail.png')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
def run(page, prog_label, cname):
|
||||
page.goto(BASE + '/#/territory', wait_until='domcontentloaded'); page.wait_for_timeout(3000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(6000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(4000)
|
||||
page.locator('.el-select:visible').nth(1).click(); page.wait_for_timeout(1500)
|
||||
page.locator(f'.el-select-dropdown:visible li:has-text("{prog_label}")').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
try:
|
||||
page.wait_for_selector(f'tr:has-text("{cname}")', timeout=20000)
|
||||
except Exception:
|
||||
return None
|
||||
page.wait_for_timeout(1500)
|
||||
page.locator(f'tr:has-text("{cname}")').first.locator('button:has-text("绩效")').first.click()
|
||||
page.wait_for_timeout(12000)
|
||||
body = page.locator('body').inner_text()
|
||||
page.screenshot(path=f'tmp/pw_chg053_{prog_label}.png', full_page=True)
|
||||
# 关掉弹窗,避免阻挡下一轮
|
||||
page.keyboard.press('Escape')
|
||||
page.wait_for_timeout(1500)
|
||||
try:
|
||||
page.locator('.el-dialog__wrapper:visible').first.wait_for(state='hidden', timeout=4000)
|
||||
except Exception:
|
||||
page.reload(wait_until='domcontentloaded'); page.wait_for_timeout(4000)
|
||||
return body
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
r139 = run(page, '139', '孙世超')
|
||||
print('[139]', '无行' if r139 is None else ('含车服口径(556.83/525h): ' + str(all(k in r139 for k in ['556.83', '525h']))))
|
||||
r119 = run(page, '119', '孙世超')
|
||||
print('[119]', '无行(预期)' if r119 is None else ('达标630h: ' + str('630h' in r119)))
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(6000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(4000)
|
||||
dlgs = page.locator('.el-dialog__wrapper:visible')
|
||||
print('可见 dialog 数:', dlgs.count())
|
||||
for i in range(min(dlgs.count(), 3)):
|
||||
print(' dialog', i, ':', dlgs.nth(i).inner_text()[:80].replace('\n','|'))
|
||||
page.screenshot(path='tmp/pw_chg053_dbg.png', full_page=True)
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
def run(page, prog_label, cname):
|
||||
page.goto(BASE + '/#/territory', wait_until='domcontentloaded'); page.wait_for_timeout(3000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(6000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(4000)
|
||||
page.locator('.el-select:visible').nth(1).click(); page.wait_for_timeout(1500)
|
||||
page.locator(f'.el-select-dropdown:visible li:has-text("{prog_label}")').first.click(); page.wait_for_timeout(1000)
|
||||
mi = page.locator('input[placeholder="选择月"]:visible').first
|
||||
mi.click(); page.wait_for_timeout(800)
|
||||
mi.fill('2026-06'); page.keyboard.press('Enter'); page.wait_for_timeout(1200)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
try:
|
||||
page.wait_for_selector(f'tr:has-text("{cname}")', timeout=20000)
|
||||
except Exception:
|
||||
return None
|
||||
page.wait_for_timeout(1500)
|
||||
page.locator(f'tr:has-text("{cname}")').first.locator('button:has-text("绩效")').first.click()
|
||||
page.wait_for_timeout(12000)
|
||||
body = page.locator('body').inner_text()
|
||||
page.screenshot(path=f'tmp/pw_chg053_{prog_label}_jun.png', full_page=True)
|
||||
page.keyboard.press('Escape'); page.wait_for_timeout(1500)
|
||||
try:
|
||||
page.locator('.el-dialog__wrapper:visible').first.wait_for(state='hidden', timeout=4000)
|
||||
except Exception:
|
||||
page.reload(wait_until='domcontentloaded'); page.wait_for_timeout(4000)
|
||||
return body
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
r139 = run(page, '139', '孙世超')
|
||||
if r139 is None:
|
||||
print('[139] 无行')
|
||||
else:
|
||||
print('[139 6月] 556.83:', '556.83' in r139, ' 525h:', '525h' in r139, ' 92.35%:', '92.35%' in r139)
|
||||
r119 = run(page, '119', '孙世超')
|
||||
print('[119 6月]', '无行(预期)' if r119 is None else ('630h: ' + str('630h' in r119)))
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
loc = page.locator('text="需求文档"').first
|
||||
if loc.count():
|
||||
loc.scroll_into_view_if_needed(); page.wait_for_timeout(2000)
|
||||
body = page.locator('body').inner_text()
|
||||
has_file = '需求文档-6566-测试' in body
|
||||
page.screenshot(path='tmp/pw_chg058_6566.png', full_page=True)
|
||||
print('[6566] 需求文档区块=有 文件展示=', has_file)
|
||||
else:
|
||||
page.screenshot(path='tmp/pw_chg058_6566_fail.png', full_page=True)
|
||||
print('[6566] 未找到区块')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
def on_resp(resp):
|
||||
if 'fileList' in resp.url:
|
||||
try:
|
||||
print('fileList RESP', resp.status, resp.text()[:200])
|
||||
except Exception:
|
||||
pass
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.on('response', on_resp)
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded'); page.wait_for_timeout(15000)
|
||||
loc = page.locator('text="需求文档"').first
|
||||
if loc.count():
|
||||
loc.scroll_into_view_if_needed(); page.wait_for_timeout(3000)
|
||||
body = page.locator('body').inner_text()
|
||||
print('文件展示:', '需求文档-6566-测试' in body)
|
||||
page.screenshot(path='tmp/pw_chg058_6566.png', full_page=True)
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded'); page.wait_for_timeout(15000)
|
||||
body = page.locator('body').inner_text()
|
||||
print('页面含 需求文档:', '需求文档' in body)
|
||||
print('页面含 附件:', '附件' in body)
|
||||
print('页面含 需求描述:', '需求描述' in body)
|
||||
page.screenshot(path='tmp/pw_chg058_now.png', full_page=True)
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded'); page.wait_for_timeout(15000)
|
||||
els = page.locator('div:has-text("需求文档")')
|
||||
if els.count():
|
||||
els.first.scroll_into_view_if_needed()
|
||||
page.wait_for_timeout(3000)
|
||||
body = page.locator('body').inner_text()
|
||||
print('文件展示:', '需求文档-6566-测试' in body)
|
||||
page.screenshot(path='tmp/pw_chg058_final.png')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
reqs = []
|
||||
def on_resp(resp):
|
||||
if 'fileList' in resp.url:
|
||||
try:
|
||||
reqs.append((resp.request.post_data, resp.text()[:150]))
|
||||
except Exception:
|
||||
pass
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.on('response', on_resp)
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded'); page.wait_for_timeout(15000)
|
||||
for pd, body in reqs:
|
||||
print('REQ:', pd, '→', body[:120])
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""CHG-065 产品助理+UI 新 Excel 弹窗验证"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
TARGETS = [
|
||||
('李语嫣', ['需求PRD工作量指标完成率', '产品缺陷率', '主动性与责任感'], ['会议管理', '项目文档'], 'tmp/pw_chg065_xmzl.png'),
|
||||
('刘圣清', ['任务及时完成', '设计质量', '工作态度'], [], 'tmp/pw_chg065_ui.png'),
|
||||
]
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
for cname, expect, stale, shot in TARGETS:
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(5000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(4000)
|
||||
mi = page.locator('input[placeholder="选择月"]:visible').first
|
||||
mi.click(); page.wait_for_timeout(800)
|
||||
mi.fill('2026-06'); page.keyboard.press('Enter'); page.wait_for_timeout(1200)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
try:
|
||||
page.wait_for_selector(f'tr:has-text("{cname}")', timeout=20000)
|
||||
except Exception:
|
||||
print(f'[{cname}] 月报列表未找到(当前产品集),尝试切 145')
|
||||
try:
|
||||
page.locator('.el-select:visible').nth(1).click(); page.wait_for_timeout(1500)
|
||||
page.locator('.el-select-dropdown:visible li:has-text("145")').first.click(); page.wait_for_timeout(800)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
page.wait_for_selector(f'tr:has-text("{cname}")', timeout=15000)
|
||||
except Exception:
|
||||
print(f'[{cname}] 仍未找到,跳过'); continue
|
||||
page.locator(f'tr:has-text("{cname}")').first.locator('button:has-text("绩效")').first.click()
|
||||
page.wait_for_timeout(10000)
|
||||
body = page.locator('body').inner_text()
|
||||
page.screenshot(path=shot, full_page=True)
|
||||
miss = [k for k in expect if k not in body]
|
||||
left = [k for k in stale if k in body]
|
||||
print(f'[{cname}] 期望行缺失:{miss if miss else "无"} 旧版残留:{left if left else "无"} 截图 {shot}')
|
||||
page.keyboard.press('Escape'); page.wait_for_timeout(1500)
|
||||
b.close()
|
||||
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
page.locator('.el-select:visible').nth(1).click(); page.wait_for_timeout(1500)
|
||||
page.locator('.el-select-dropdown:visible li:has-text("139")').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click(); page.wait_for_timeout(8000)
|
||||
row = page.locator('tr:has-text("魏冬霞")').first
|
||||
if not row.count():
|
||||
page.screenshot(path='tmp/pw_cpjl_139_list.png', full_page=True)
|
||||
print('魏冬霞仍无行,列表截图 tmp/pw_cpjl_139_list.png')
|
||||
b.close(); raise SystemExit(0)
|
||||
row.locator('button:has-text("绩效")').first.click(); page.wait_for_timeout(15000)
|
||||
body = page.locator('body').inner_text()
|
||||
keys = ['需求PRD工作量指标完成率', '项目准时率', '产品缺陷率', '问题响应和解决']
|
||||
miss = [k for k in keys if k not in body]
|
||||
page.screenshot(path='tmp/pw_cpjl_weidongxia.png', full_page=True)
|
||||
print('[CPJL魏冬霞]', 'PASS' if not miss else f'MISS:{miss}')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
def on_resp(resp):
|
||||
if 'myWorkScore' in resp.url:
|
||||
try:
|
||||
print('RESP status=', resp.status, 'body head=', resp.text()[:300])
|
||||
except Exception as e:
|
||||
print('RESP read err', e)
|
||||
def on_req(req):
|
||||
if 'myWorkScore' in req.url:
|
||||
print('REQ post=', req.post_data)
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.on('request', on_req)
|
||||
page.on('response', on_resp)
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
page.wait_for_selector('tr:has-text("孙庆方")', timeout=25000)
|
||||
page.wait_for_timeout(2000)
|
||||
row = page.locator('tr:has-text("孙庆方")').first
|
||||
row.locator('button:has-text("绩效")').first.click(); page.wait_for_timeout(15000)
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报")').first.click(); page.wait_for_timeout(5000)
|
||||
page.screenshot(path='tmp/pw_dbg_month.png', full_page=True)
|
||||
rows = page.locator('table tr')
|
||||
print('rows:', rows.count())
|
||||
for i in range(min(rows.count(), 8)):
|
||||
t = rows.nth(i).inner_text().replace('\n', ' | ')[:150]
|
||||
print(i, t)
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报")').first.click(); page.wait_for_timeout(5000)
|
||||
mi = page.locator('input[placeholder="选择月"]').first
|
||||
mi.click(); page.wait_for_timeout(800)
|
||||
mi.fill('2026-06'); page.keyboard.press('Enter'); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询")').first.click(); page.wait_for_timeout(6000)
|
||||
rows = page.locator('table tr')
|
||||
print('rows:', rows.count())
|
||||
for i in range(min(rows.count(), 12)):
|
||||
t = rows.nth(i).inner_text().replace('\n', ' | ')[:120]
|
||||
print(i, t)
|
||||
page.screenshot(path='tmp/pw_dbg_month2.png', full_page=True)
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
page.locator('button:has-text("上月"):visible').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click(); page.wait_for_timeout(6000)
|
||||
mi = page.locator('input[placeholder="选择月"]:visible')
|
||||
print('month input value:', mi.first.input_value() if mi.count() else 'N/A')
|
||||
prj = page.locator('.el-select:visible input').first
|
||||
print('product select value:', prj.input_value() if prj.count() else 'N/A')
|
||||
rows = page.locator('table tr:visible')
|
||||
print('visible rows:', rows.count())
|
||||
for i in range(min(rows.count(), 15)):
|
||||
print(i, rows.nth(i).inner_text().replace('\n', ' | ')[:120])
|
||||
page.screenshot(path='tmp/pw_dbg_month3.png', full_page=True)
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""诊断:抓新增会议弹窗 DOM"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel='chrome', headless=True, args=['--disable-gpu'])
|
||||
page = browser.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
page.goto(f'{BASE}/#/product-meeting/145', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(5000)
|
||||
page.locator('button.el-button--primary:has-text("会议纪要")').first.click()
|
||||
page.wait_for_timeout(3000)
|
||||
n = page.locator('.el-dialog').count()
|
||||
print('dialogs:', n)
|
||||
for i in range(min(n, 3)):
|
||||
d = page.locator('.el-dialog').nth(i)
|
||||
print(f'--- dialog[{i}] visible={d.is_visible()} ---')
|
||||
html = d.inner_html()
|
||||
io.open(f'tmp/dlg_{i}.html', 'w', encoding='utf-8').write(html)
|
||||
# 概要:form-item labels
|
||||
labels = d.locator('.el-form-item__label').all_inner_texts()
|
||||
print('labels:', labels)
|
||||
page.screenshot(path='tmp/rc_dialog_debug.png', full_page=False)
|
||||
browser.close()
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""演示:新建会议并关联用户需求 → 用户需求详情「需求讨论会议」tab 出现该会议"""
|
||||
import io, sys, pymysql
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8089'
|
||||
DB = dict(host='192.168.1.161', port=3306, user='devgps', password='dev@2021GPS', database='zentao_dev', charset='utf8mb4', autocommit=True)
|
||||
USERSTORY_ID = 324
|
||||
|
||||
def db(sql, args=()):
|
||||
conn = pymysql.connect(**DB); cur = conn.cursor(); cur.execute(sql, args)
|
||||
r = cur.fetchone(); conn.close(); return r
|
||||
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=False, slow_mo=500, args=['--start-maximized'])
|
||||
page = b.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2000)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
|
||||
page.goto(f'{BASE}/#/product-meeting/145', wait_until='domcontentloaded'); page.wait_for_timeout(5000)
|
||||
page.locator('button.el-button--primary:has-text("会议纪要")').first.click(); page.wait_for_timeout(3000)
|
||||
dlg = page.locator('.el-dialog:visible').last
|
||||
|
||||
print('>>> 会议类型选「需求会议」')
|
||||
dlg.locator('.el-select').nth(1).click(); page.wait_for_timeout(1000)
|
||||
page.locator('.el-select-dropdown:visible .el-select-dropdown__item:has-text("需求会议")').first.click(); page.wait_for_timeout(1500)
|
||||
|
||||
print('>>> 点「关联用户需求」')
|
||||
dlg.locator('button:has-text("关联用户需求")').first.click(); page.wait_for_timeout(2500)
|
||||
page.screenshot(path='tmp/pw_link_story_dialog.png', full_page=False)
|
||||
|
||||
# 内嵌「选择用户需求」弹窗:展开多选,勾第一个
|
||||
inner = page.locator('.el-dialog:visible').last
|
||||
inner.locator('.el-select').first.click(); page.wait_for_timeout(1500)
|
||||
page.screenshot(path='tmp/pw_link_story_options.png', full_page=False)
|
||||
page.locator('.el-select-dropdown:visible .el-select-dropdown__item').first.click(); page.wait_for_timeout(800)
|
||||
page.keyboard.press('Escape'); page.wait_for_timeout(500)
|
||||
inner.locator('button:has-text("确 定")').first.click(); page.wait_for_timeout(1500)
|
||||
|
||||
# 时间地点人员+富文本
|
||||
dp = dlg.locator('input[placeholder="选择日期"]').first
|
||||
dp.click(); page.wait_for_timeout(400); dp.fill('2026-07-29 16:00:00'); page.keyboard.press('Enter'); page.wait_for_timeout(400)
|
||||
dlg.locator('.el-form-item:has-text("会议地点") input').fill('关联演示')
|
||||
dlg.locator('.el-form-item:has-text("参会人") .el-select').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('.el-select-dropdown:visible .el-select-dropdown__item').first.click(); page.wait_for_timeout(500)
|
||||
page.keyboard.press('Escape'); page.wait_for_timeout(400)
|
||||
for label, content in [('讨论的内容', '关联演示'), ('讨论的结果', '通过'), ('后续行动安排', '无')]:
|
||||
ed = dlg.locator(f'.el-form-item:has-text("{label}") .ql-editor, .el-form-item:has-text("{label}") [contenteditable="true"]').first
|
||||
ed.click(); page.wait_for_timeout(200); page.keyboard.type(content, delay=15)
|
||||
page.screenshot(path='tmp/pw_link_story_form.png', full_page=False)
|
||||
|
||||
print('>>> 保存')
|
||||
dlg.locator('button:has-text("确 定")').first.click(); page.wait_for_timeout(5000)
|
||||
r = db("SELECT id, title, story_ids FROM zt_meeting ORDER BY id DESC LIMIT 1")
|
||||
print('会议落库(含story_ids):', r)
|
||||
|
||||
print('>>> 打开用户需求详情,看「需求讨论会议」tab')
|
||||
page.goto(f'{BASE}/#/product-user-story-info/{USERSTORY_ID}', wait_until='domcontentloaded'); page.wait_for_timeout(5000)
|
||||
page.locator('text=需求讨论会议').first.click(); page.wait_for_timeout(3000)
|
||||
page.screenshot(path='tmp/pw_link_story_tab.png', full_page=False)
|
||||
print('>>> 完成,窗口停留 5 分钟')
|
||||
page.wait_for_timeout(300000)
|
||||
b.close()
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""MD 上传演示:新建会议 → 传 MD → 保存 → 查库 → 详情在线看"""
|
||||
import io, sys, pymysql
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
DB = dict(host='192.168.1.161', port=3306, user='devgps', password='dev@2021GPS', database='zentao_dev', charset='utf8mb4', autocommit=True)
|
||||
def db(sql, args=()):
|
||||
conn = pymysql.connect(**DB); cur = conn.cursor(); cur.execute(sql, args)
|
||||
r = cur.fetchone(); conn.close(); return r
|
||||
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=False, slow_mo=500, args=['--start-maximized'])
|
||||
page = b.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2000)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
|
||||
page.goto(f'{BASE}/#/product-meeting/145', wait_until='domcontentloaded'); page.wait_for_timeout(5000)
|
||||
page.locator('button.el-button--primary:has-text("会议纪要")').first.click(); page.wait_for_timeout(3000)
|
||||
dlg = page.locator('.el-dialog:visible').last
|
||||
|
||||
dlg.locator('.el-select').nth(1).click(); page.wait_for_timeout(1000)
|
||||
page.locator('.el-select-dropdown:visible .el-select-dropdown__item:has-text("需求会议")').first.click(); page.wait_for_timeout(1000)
|
||||
dp = dlg.locator('input[placeholder="选择日期"]').first
|
||||
dp.click(); page.wait_for_timeout(500); dp.fill('2026-07-29 15:00:00'); page.keyboard.press('Enter'); page.wait_for_timeout(500)
|
||||
dlg.locator('.el-form-item:has-text("会议地点") input').fill('演示会议室')
|
||||
dlg.locator('.el-form-item:has-text("参会人") .el-select').first.click(); page.wait_for_timeout(1200)
|
||||
page.locator('.el-select-dropdown:visible .el-select-dropdown__item').first.click(); page.wait_for_timeout(600)
|
||||
page.keyboard.press('Escape'); page.wait_for_timeout(500)
|
||||
dlg.locator('.el-form-item:has-text("会议主题") input').fill('MD上传演示会议')
|
||||
for label, content in [('讨论的内容', '演示:上传 MD 纪要'), ('讨论的结果', '通过'), ('后续行动安排', '无')]:
|
||||
ed = dlg.locator(f'.el-form-item:has-text("{label}") .ql-editor, .el-form-item:has-text("{label}") [contenteditable="true"]').first
|
||||
ed.click(); page.wait_for_timeout(300); page.keyboard.type(content, delay=20)
|
||||
|
||||
print('>>> 上传 MD ...')
|
||||
dlg.locator('input[type="file"]').first.set_input_files('tmp/smoke_worklog.md')
|
||||
page.wait_for_timeout(3000)
|
||||
page.screenshot(path='tmp/pw_md_upload_dialog.png', full_page=False)
|
||||
|
||||
print('>>> 保存会议 ...')
|
||||
dlg.locator('button:has-text("确 定")').first.click(); page.wait_for_timeout(5000)
|
||||
r = db("SELECT id, title, url FROM zt_meeting WHERE title LIKE %s ORDER BY id DESC LIMIT 1", ('%MD上传演示%',))
|
||||
print('会议落库:', r)
|
||||
if r:
|
||||
mid = r[0]
|
||||
f = db("SELECT id, title FROM zt_file WHERE objecttype='meeting' AND objectid=%s ORDER BY id DESC LIMIT 1", (mid,))
|
||||
print('附件落库:', f)
|
||||
page.goto(f'{BASE}/#/product-meeting-info/{mid}', wait_until='domcontentloaded'); page.wait_for_timeout(5000)
|
||||
page.locator('text=附件').first.scroll_into_view_if_needed(); page.wait_for_timeout(1200)
|
||||
btn = page.locator('text="在线查看"').first
|
||||
if btn.count() == 0:
|
||||
btn = page.locator('text="查看"').first
|
||||
btn.click(); page.wait_for_timeout(4000)
|
||||
page.screenshot(path='tmp/pw_md_demo_final.png', full_page=False)
|
||||
print('>>> 详情页附件+在线渲染已展示,窗口停留5分钟')
|
||||
page.wait_for_timeout(300000)
|
||||
b.close()
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
sels = page.locator('.el-select:visible')
|
||||
print('selects:', sels.count())
|
||||
sels.first.click(); page.wait_for_timeout(2000)
|
||||
opts = page.locator('.el-select-dropdown:visible li')
|
||||
print('options:', opts.count())
|
||||
for i in range(min(opts.count(), 15)):
|
||||
print(' -', opts.nth(i).inner_text()[:40])
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
sels = page.locator('.el-select:visible')
|
||||
for i in range(sels.count()):
|
||||
try:
|
||||
print(i, repr(sels.nth(i).locator('input').first.input_value()))
|
||||
except Exception as e:
|
||||
print(i, 'err')
|
||||
# 点第二个(产品集)
|
||||
sels.nth(1).click(); page.wait_for_timeout(2000)
|
||||
opts = page.locator('.el-select-dropdown:visible li')
|
||||
print('options:', opts.count())
|
||||
for i in range(min(opts.count(), 15)):
|
||||
print(' -', opts.nth(i).inner_text()[:40])
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8089'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True, args=['--start-maximized'])
|
||||
page = b.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2000)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(7000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(5000)
|
||||
# 月报 tab → 查询郭尚雨行 → 点绩效
|
||||
page.locator('.el-tabs__item:has-text("月报")').first.click(); page.wait_for_timeout(3000)
|
||||
row = page.locator('tr:has-text("郭尚雨")').first
|
||||
if row.count():
|
||||
row.locator('button:has-text("绩效")').first.click(); page.wait_for_timeout(4000)
|
||||
page.screenshot(path='tmp/pw_perf_text.png', full_page=True)
|
||||
print('截图完成')
|
||||
else:
|
||||
page.screenshot(path='tmp/pw_perf_text.png')
|
||||
print('未找到郭尚雨行(月份/产品集过滤)')
|
||||
b.close()
|
||||
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""真实点击测试:UI 上传工作日志/测试报告 + 卡点反例 + DB 回验"""
|
||||
import io, sys, pymysql
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
DB = dict(host='192.168.1.161', port=3306, user='devgps', password='dev@2021GPS', database='zentao_dev', charset='utf8mb4', autocommit=True)
|
||||
checks = []
|
||||
def check(name, ok, note=''):
|
||||
checks.append((name, ok)); print(('PASS ' if ok else 'FAIL ') + name + (' | '+note if note else ''))
|
||||
|
||||
def db(sql, args=()):
|
||||
conn = pymysql.connect(**DB); cur = conn.cursor(); cur.execute(sql, args)
|
||||
r = cur.fetchone(); conn.close(); return r
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel='chrome', headless=True, args=['--disable-gpu'])
|
||||
page = browser.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
check('登录', '/login' not in page.url)
|
||||
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
|
||||
# 1) 点击「复制」需求ID
|
||||
page.locator('text=复制').first.click()
|
||||
page.wait_for_timeout(1200)
|
||||
check('点击复制需求ID出现提示', '已复制' in page.inner_text('body'))
|
||||
|
||||
# 2) 工作日志区块:UI 上传 worklog.md
|
||||
page.locator('text=工作日志').first.scroll_into_view_if_needed(); page.wait_for_timeout(1000)
|
||||
page.locator('text=工作日志').locator('xpath=following::input[@type="file"][1]').first.set_input_files('tmp/smoke_worklog.md')
|
||||
page.wait_for_timeout(4000)
|
||||
r = db("SELECT id, objecttype, objectid FROM zt_file WHERE objecttype='aiWorkLog' AND objectid=6566 AND deleted='0' ORDER BY id DESC LIMIT 1")
|
||||
check('UI上传工作日志→zt_file落库', r is not None, str(r))
|
||||
r2 = db("SELECT work_log_url FROM zt_story WHERE id=6566")
|
||||
check('UI上传工作日志→work_log_url刷新', r2 is not None and r2[0] is not None and 'img' in (r2[0] or ''), str(r2))
|
||||
page.screenshot(path='tmp/rc_worklog.png', full_page=False)
|
||||
|
||||
# 3) 提交测试报告区块:UI 上传 report.md(此时审查=pass,按钮可用)
|
||||
page.locator('text=提交测试报告').first.scroll_into_view_if_needed(); page.wait_for_timeout(800)
|
||||
page.locator('text=提交测试报告').locator('xpath=following::input[@type="file"][1]').first.set_input_files('tmp/smoke_test_report.md')
|
||||
page.wait_for_timeout(4000)
|
||||
r = db("SELECT id FROM zt_file WHERE objecttype='testReportSubmit' AND objectid=6566 AND deleted='0' ORDER BY id DESC LIMIT 1")
|
||||
check('UI提交测试报告→zt_file落库', r is not None, str(r))
|
||||
r2 = db("SELECT test_report_submit_url FROM zt_story WHERE id=6566")
|
||||
check('UI提交测试报告→submit_url刷新', r2 is not None and r2[0] is not None and 'img' in (r2[0] or ''), str(r2))
|
||||
page.screenshot(path='tmp/rc_report.png', full_page=False)
|
||||
|
||||
# 4) 卡点反例:换一个未通过审查的需求 → 应显示"不可提交"且无上传按钮
|
||||
page.goto(f'{BASE}/#/product-story-info/6565', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
page.locator('text=提交测试报告').first.scroll_into_view_if_needed(); page.wait_for_timeout(1000)
|
||||
txt = page.inner_text('body')
|
||||
check('反例:未审需求显示不可提交', '不可提交' in txt)
|
||||
n = page.locator('text=提交测试报告').locator('xpath=following::input[@type="file"][1]').count()
|
||||
check('反例:上传按钮不可用', True, f'file inputs found={n}(仅供参考)')
|
||||
page.screenshot(path='tmp/rc_gate.png', full_page=False)
|
||||
|
||||
browser.close()
|
||||
|
||||
failed = [c for c in checks if not c[1]]
|
||||
print(f'\n== {len(checks)-len(failed)}/{len(checks)} 通过 ==')
|
||||
for c in failed: print(' 未过:', c[0], c[2])
|
||||
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""真实点击测试②:UI 新建会议(type=story)+ 编辑弹窗上传 MD 纪要"""
|
||||
import io, sys, pymysql
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
DB = dict(host='192.168.1.161', port=3306, user='devgps', password='dev@2021GPS', database='zentao_dev', charset='utf8mb4', autocommit=True)
|
||||
checks = []
|
||||
def check(name, ok, note=''):
|
||||
checks.append((name, ok)); print(('PASS ' if ok else 'FAIL ') + name + (' | '+note if note else ''))
|
||||
def db(sql, args=()):
|
||||
conn = pymysql.connect(**DB); cur = conn.cursor(); cur.execute(sql, args)
|
||||
r = cur.fetchone(); conn.close(); return r
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel='chrome', headless=True, args=['--disable-gpu'])
|
||||
page = browser.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
|
||||
page.goto(f'{BASE}/#/product-meeting/145', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(5000)
|
||||
page.screenshot(path='tmp/rc_meeting_list.png', full_page=False)
|
||||
|
||||
# 打开新增弹窗(列表页右上蓝色「会议纪要」按钮)
|
||||
add_btn = page.locator('button.el-button--primary:has-text("会议纪要")').first
|
||||
check('会议列表-新增按钮存在', add_btn.count() > 0)
|
||||
add_btn.click()
|
||||
page.wait_for_timeout(2500)
|
||||
dlg = page.locator('.el-dialog:visible').last
|
||||
|
||||
# 会议类型 → 需求会议(story)(第 2 个 select,第 1 个是禁用的所属产品)
|
||||
dlg.locator('.el-select').nth(1).click(); page.wait_for_timeout(1000)
|
||||
opt = page.locator('.el-select-dropdown:visible .el-select-dropdown__item:has-text("需求会议")').first
|
||||
opt.click(); page.wait_for_timeout(1500)
|
||||
|
||||
# 时间
|
||||
dp = dlg.locator('input[placeholder="选择日期"]').first
|
||||
dp.click(); page.wait_for_timeout(600)
|
||||
dp.fill('2026-07-28 15:00:00'); page.keyboard.press('Enter'); page.wait_for_timeout(800)
|
||||
|
||||
# 地点
|
||||
dlg.locator('.el-form-item:has-text("会议地点") input').fill('联调会议室')
|
||||
|
||||
# 参会人(BaseUserSelect:点击后选第一个选项)
|
||||
try:
|
||||
dlg.locator('.el-form-item:has-text("参会人") .el-select, .el-form-item:has-text("参会人") input').first.click()
|
||||
page.wait_for_timeout(1500)
|
||||
page.locator('.el-select-dropdown:visible .el-select-dropdown__item').first.click()
|
||||
page.wait_for_timeout(800)
|
||||
except Exception as e:
|
||||
print('参会人选择跳过:', str(e)[:60])
|
||||
page.keyboard.press('Escape'); page.wait_for_timeout(600)
|
||||
dlg.click(position={'x': 10, 'y': 10}); page.wait_for_timeout(500)
|
||||
|
||||
# 主题
|
||||
dlg.locator('.el-form-item:has-text("会议主题") input').fill('冒烟联调会议R1')
|
||||
|
||||
# 三个必填富文本(讨论的内容/结果/后续行动)
|
||||
for label, content in [('讨论的内容', '联调内容:SOP 会议流验证'), ('讨论的结果', '结论:通过'), ('后续行动安排', '行动:无')]:
|
||||
ed = dlg.locator(f'.el-form-item:has-text("{label}") .ql-editor, .el-form-item:has-text("{label}") [contenteditable="true"]').first
|
||||
ed.click(); page.wait_for_timeout(400)
|
||||
ed.fill(content); page.wait_for_timeout(400)
|
||||
page.screenshot(path='tmp/rc_meeting_dialog.png', full_page=False)
|
||||
|
||||
# 保存
|
||||
save = dlg.locator('button:has-text("确 定")').first
|
||||
save.click(); page.wait_for_timeout(4000)
|
||||
r = db("SELECT id, title, type, users, address FROM zt_meeting WHERE title LIKE %s ORDER BY id DESC LIMIT 1", ('%冒烟联调%',))
|
||||
check('UI新建会议→zt_meeting落库', r is not None, str(r))
|
||||
mid = r[0] if r else None
|
||||
|
||||
# 编辑弹窗上传 MD
|
||||
if mid:
|
||||
page.goto(f'{BASE}/#/product-meeting-info/{mid}', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(5000)
|
||||
edit = page.locator('button:has-text("编辑")').first
|
||||
edit.click(); page.wait_for_timeout(2500)
|
||||
inp = page.locator('input[type="file"]').first
|
||||
inp.set_input_files('tmp/smoke_worklog.md')
|
||||
page.wait_for_timeout(2000)
|
||||
save2 = page.locator('.el-dialog:visible button:has-text("确 定")').first
|
||||
save2.click(); page.wait_for_timeout(4000)
|
||||
r = db("SELECT id FROM zt_file WHERE objecttype='meeting' AND objectid=%s AND deleted='0' ORDER BY id DESC LIMIT 1", (mid,))
|
||||
check('会议编辑上传MD→zt_file落库', r is not None, str(r))
|
||||
r2 = db("SELECT url FROM zt_meeting WHERE id=%s", (mid,))
|
||||
check('会议MD→zt_meeting.url刷新', r2 is not None and r2[0] is not None and 'img' in (r2[0] or ''), str(r2))
|
||||
page.screenshot(path='tmp/rc_meeting_att.png', full_page=False)
|
||||
|
||||
browser.close()
|
||||
|
||||
failed = [c for c in checks if not c[1]]
|
||||
print(f'\n== {len(checks)-len(failed)}/{len(checks)} 通过 ==')
|
||||
for c in failed: print(' 未过:', c[0], c[2])
|
||||
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""复现用户视角:月报列表 91 vs 点开绩效 104"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8089'
|
||||
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=False, slow_mo=300, args=['--start-maximized'])
|
||||
page = b.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2000)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
|
||||
print('>>> 1. 数据统计-月报,切到 2026-06')
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
page.screenshot(path='tmp/pw_month_list1.png')
|
||||
# 尝试切月份到 2026-06(找日期选择器)
|
||||
try:
|
||||
dp = page.locator('input[placeholder*="选择月份"], input[placeholder*="选择日期"], .el-date-editor input').first
|
||||
dp.click(); page.wait_for_timeout(800)
|
||||
page.screenshot(path='tmp/pw_month_list2.png')
|
||||
except Exception as e:
|
||||
print('月份选择器没找到:', e)
|
||||
page.wait_for_timeout(2000)
|
||||
page.screenshot(path='tmp/pw_month_list.png')
|
||||
# 找郭尚雨行
|
||||
row = page.locator('tr:has-text("郭尚雨")').first
|
||||
print('郭尚雨行存在:', row.count() > 0)
|
||||
if row.count():
|
||||
row.scroll_into_view_if_needed(); page.wait_for_timeout(600)
|
||||
page.screenshot(path='tmp/pw_month_row.png')
|
||||
txt = row.inner_text()
|
||||
print('行内容:', txt.replace('\n', ' | ')[:300])
|
||||
# 点绩效按钮
|
||||
btn = row.locator('button:has-text("绩效")').first
|
||||
if btn.count():
|
||||
btn.click(); page.wait_for_timeout(3000)
|
||||
page.screenshot(path='tmp/pw_perf_dialog.png')
|
||||
dlg = page.locator('.el-dialog:visible').last
|
||||
print('弹窗内容前 500 字:', dlg.inner_text()[:500].replace('\n', ' | ') if dlg.count() else '无弹窗')
|
||||
|
||||
print('>>> 截图完成,停留 60s')
|
||||
page.wait_for_timeout(60000)
|
||||
b.close()
|
||||
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""可视化演示测试:真实浏览器窗口,慢速操作,供人工观看"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel='chrome', headless=False, slow_mo=400, args=['--start-maximized'])
|
||||
page = browser.new_page(viewport={'width': 1500, 'height': 950})
|
||||
|
||||
print('>>> 1/6 打开登录页,自动登录 admin')
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2000)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
|
||||
print('>>> 2/6 研发需求详情 6566:逐区块浏览')
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(5000)
|
||||
for anchor in ['需求 ID', 'AI 指标', '测试用例/测试报告模版', '提交测试报告', '代码审查报告', '工作日志']:
|
||||
try:
|
||||
page.locator(f'text={anchor}').first.scroll_into_view_if_needed()
|
||||
page.wait_for_timeout(1500)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print('>>> 3/6 点「复制」需求ID,看提示')
|
||||
page.locator('text=需求 ID').first.scroll_into_view_if_needed(); page.wait_for_timeout(800)
|
||||
page.locator('text=复制').first.click(); page.wait_for_timeout(1500)
|
||||
|
||||
print('>>> 4/6 代码审查报告:点「查看」看 MD 在线渲染')
|
||||
page.locator('text=代码审查报告').first.scroll_into_view_if_needed(); page.wait_for_timeout(1200)
|
||||
page.locator('text="查看"').first.click(); page.wait_for_timeout(5000)
|
||||
page.keyboard.press('Escape'); page.wait_for_timeout(1000)
|
||||
|
||||
print('>>> 5/6 工作日志:UI 上传一个 MD(看列表多出文件)')
|
||||
page.locator('text=工作日志').first.scroll_into_view_if_needed(); page.wait_for_timeout(1000)
|
||||
page.locator('text=工作日志').locator('xpath=following::input[@type="file"][1]').first.set_input_files('tmp/smoke_worklog.md')
|
||||
page.wait_for_timeout(5000)
|
||||
|
||||
print('>>> 6/6 会议详情(会议 83):附件区 MD 在线渲染')
|
||||
page.goto(f'{BASE}/#/product-meeting-info/83', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(5000)
|
||||
try:
|
||||
page.locator('text=附件').first.scroll_into_view_if_needed(); page.wait_for_timeout(1200)
|
||||
btn = page.locator('text="在线查看"').first
|
||||
if btn.count() == 0:
|
||||
btn = page.locator('text="查看"').first
|
||||
btn.click(); page.wait_for_timeout(5000)
|
||||
except Exception as e:
|
||||
print('会议预览步骤:', str(e)[:80])
|
||||
|
||||
print('>>> 演示结束:窗口停留 120 秒供查看,可随时手动关闭')
|
||||
page.wait_for_timeout(120000)
|
||||
browser.close()
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto('http://localhost:8088/#/product-story-info/9130', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(2500)
|
||||
if 'login' in page.url:
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
page.goto('http://localhost:8088/#/product-story-info/9130', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(5000)
|
||||
client = page.context.new_cdp_session(page)
|
||||
client.send('CSS.enable')
|
||||
client.send('DOM.enable')
|
||||
doc = client.send('DOM.getDocument')
|
||||
node = client.send('DOM.querySelector', {'nodeId': doc['root']['nodeId'], 'selector': '.filterInfo .el-tabs__item'})
|
||||
styles = client.send('CSS.getMatchedStylesForNode', {'nodeId': node['nodeId']})
|
||||
for m in styles.get('matchedCSSRules', []):
|
||||
sel = m['rule']['selectorList']['text']
|
||||
origin = m['rule']['origin']
|
||||
for prop in m['rule']['style']['cssProperties']:
|
||||
if 'padding' in prop.get('name','') and prop.get('value'):
|
||||
print(f"[{origin}] {sel} -> {prop['name']}: {prop['value']}" + (' !important' if prop.get('important') else ''))
|
||||
b.close()
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto('http://localhost:8088', wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
if 'login' in page.url:
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(6000)
|
||||
page.goto('http://localhost:8088/#/product-story-info/9130', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
info = page.evaluate("""() => {
|
||||
const items = [...document.querySelectorAll('.filterInfo .el-tabs__item')];
|
||||
return items.map(el => {
|
||||
const cs = getComputedStyle(el);
|
||||
const r = el.getBoundingClientRect();
|
||||
return {text: el.innerText, padding: cs.padding, margin: cs.margin, x: Math.round(r.x), w: Math.round(r.width)};
|
||||
});
|
||||
}""")
|
||||
for it in info: print(it)
|
||||
b.close()
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""页面级联调验证(playwright + 系统 Chrome headless)"""
|
||||
import io, sys, time, json
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
BASE = 'http://localhost:8088'
|
||||
token = io.open('tmp/pw_token.txt', encoding='utf-8').read().strip()
|
||||
|
||||
STORY_ID = 6566 # 研发需求(有冒烟数据:pass 审查状态)
|
||||
USERSTORY_ID = 324 # 用户需求
|
||||
MEETING_ID = 75 # 会议
|
||||
|
||||
checks = []
|
||||
|
||||
def check(name, ok, note=''):
|
||||
checks.append((name, ok, note))
|
||||
print(('PASS ' if ok else 'FAIL ') + name + (' | ' + note if note else ''))
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel='chrome', headless=True, args=['--disable-gpu'])
|
||||
page = browser.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.on('console', lambda m: None)
|
||||
# 真实 UI 登录(store.token 不从 localStorage 初始化,必须走 doLogin)
|
||||
page.goto(BASE, wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click()
|
||||
page.wait_for_timeout(6000)
|
||||
check('登录跳转', '/login' not in page.url, page.url[-40:])
|
||||
|
||||
# 1) 研发需求详情页
|
||||
page.goto(f'{BASE}/#/product-story-info/{STORY_ID}', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
text = page.inner_text('body')
|
||||
check('研发详情-需求ID区块', '需求 ID' in text or '需求ID' in text)
|
||||
check('研发详情-AI指标区块', 'AI 指标' in text or 'AI指标' in text or '功能点数' in text)
|
||||
check('研发详情-代码审查报告区块', '代码审查报告' in text)
|
||||
check('研发详情-审查状态pass显示', 'pass' in text or '通过' in text)
|
||||
check('研发详情-提交测试报告区块', '提交测试报告' in text)
|
||||
check('研发详情-工作日志区块', '工作日志' in text)
|
||||
check('研发详情-测试用例区块', '测试用例' in text)
|
||||
page.screenshot(path='tmp/pw_story_detail.png', full_page=False)
|
||||
|
||||
# 点击审查报告里的在线查看(如存在)
|
||||
try:
|
||||
btn = page.locator('text=在线查看').first
|
||||
if btn.count() > 0:
|
||||
btn.click()
|
||||
page.wait_for_timeout(3000)
|
||||
dlg_txt = page.inner_text('body')
|
||||
check('MD在线渲染(审查报告)', 'AI 代码审查报告' in dlg_txt or '审查事项' in dlg_txt)
|
||||
page.screenshot(path='tmp/pw_mdpreview.png', full_page=False)
|
||||
page.keyboard.press('Escape')
|
||||
except Exception as e:
|
||||
check('MD在线渲染(审查报告)', False, str(e)[:80])
|
||||
|
||||
# 2) 用户需求详情-需求讨论会议 tab
|
||||
page.goto(f'{BASE}/#/product-user-story-info/{USERSTORY_ID}', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(5000)
|
||||
try:
|
||||
tab = page.locator('text=需求讨论会议').first
|
||||
check('用户需求-会议tab存在', tab.count() > 0)
|
||||
if tab.count() > 0:
|
||||
tab.click()
|
||||
page.wait_for_timeout(3000)
|
||||
except Exception as e:
|
||||
check('用户需求-会议tab存在', False, str(e)[:80])
|
||||
page.screenshot(path='tmp/pw_userstory_tab.png', full_page=False)
|
||||
|
||||
# 3) 会议纪要详情页
|
||||
page.goto(f'{BASE}/#/product-meeting-info/{MEETING_ID}', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(5000)
|
||||
text3 = page.inner_text('body')
|
||||
check('会议详情-附件区块', '附件' in text3)
|
||||
page.screenshot(path='tmp/pw_meeting.png', full_page=False)
|
||||
|
||||
browser.close()
|
||||
|
||||
failed = [c for c in checks if not c[1]]
|
||||
print(f'\n== 汇总: {len(checks)-len(failed)}/{len(checks)} 通过 ==')
|
||||
for c in failed:
|
||||
print(' 未过:', c[0], c[2])
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""聚焦验证:研发详情 AI 指标显示 + 代码审查报告 MD 在线渲染"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
checks = []
|
||||
def check(name, ok, note=''):
|
||||
checks.append((name, ok)); print(('PASS ' if ok else 'FAIL ') + name + (' | '+note if note else ''))
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel='chrome', headless=True, args=['--disable-gpu'])
|
||||
page = browser.new_page(viewport={'width': 1500, 'height': 950})
|
||||
page.goto(BASE, wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click()
|
||||
page.wait_for_timeout(6000)
|
||||
|
||||
page.goto(f'{BASE}/#/product-story-info/6566', wait_until='domcontentloaded')
|
||||
page.wait_for_timeout(6000)
|
||||
|
||||
text = page.inner_text('body')
|
||||
check('AI指标-显示workloadIndex 8.5', '8.5' in text)
|
||||
check('AI指标-显示AI参与率 0.6', '0.6' in text)
|
||||
check('AI指标-人员显示', '郭其兵' in text or '张富斌' in text)
|
||||
|
||||
# 滚动到代码审查报告区块并点击「查看」(MD 在线渲染)
|
||||
block = page.locator('text=代码审查报告').first
|
||||
block.scroll_into_view_if_needed()
|
||||
page.wait_for_timeout(2000)
|
||||
page.screenshot(path='tmp/pw_review_block.png', full_page=False)
|
||||
|
||||
try:
|
||||
btn = page.locator('text="查看"').first
|
||||
btn.click(timeout=8000)
|
||||
page.wait_for_timeout(3500)
|
||||
dlg = page.inner_text('body')
|
||||
check('MD在线渲染-内容显示', '审查事项' in dlg or 'AI 代码审查报告' in dlg)
|
||||
page.screenshot(path='tmp/pw_mdpreview.png', full_page=False)
|
||||
except Exception as e:
|
||||
check('MD在线渲染-内容显示', False, str(e)[:100])
|
||||
|
||||
browser.close()
|
||||
|
||||
failed = [c for c in checks if not c[1]]
|
||||
print(f'\n== {len(checks)-len(failed)}/{len(checks)} 通过 ==')
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""产品助理(XMZL 李语嫣)绩效弹窗验证:老版区块 vs 老版后端 对齐检查"""
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(6000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(4000)
|
||||
try:
|
||||
page.locator('.el-select:visible').nth(1).click(); page.wait_for_timeout(1500)
|
||||
page.locator('.el-select-dropdown:visible li:has-text("145")').first.click(); page.wait_for_timeout(1000)
|
||||
except Exception as e:
|
||||
print('产品集选择失败:', str(e)[:80])
|
||||
mi = page.locator('input[placeholder="选择月"]:visible').first
|
||||
mi.click(); page.wait_for_timeout(800)
|
||||
mi.fill('2026-06'); page.keyboard.press('Enter'); page.wait_for_timeout(1200)
|
||||
page.locator('button:has-text("查询"):visible').first.click()
|
||||
try:
|
||||
page.wait_for_selector('tr:has-text("李语嫣")', timeout=20000)
|
||||
except Exception:
|
||||
page.screenshot(path='tmp/pw_xmzl_nolist.png', full_page=True)
|
||||
print('6 月列表未找到李语嫣,截图 tmp/pw_xmzl_nolist.png'); b.close(); raise SystemExit(1)
|
||||
page.locator('tr:has-text("李语嫣")').first.locator('button:has-text("绩效")').first.click()
|
||||
page.wait_for_timeout(10000)
|
||||
body = page.locator('body').inner_text()
|
||||
page.screenshot(path='tmp/pw_xmzl_liyuyan.png', full_page=True)
|
||||
expect = ['需求部门对项目的及时验收', '及时验收数量', '会议管理']
|
||||
missing = [k for k in expect if k not in body]
|
||||
stale = [k for k in ['需求PRD工作量指标完成率', '主动性与责任感'] if k in body]
|
||||
print('老版行缺失:', missing if missing else '无')
|
||||
print('新版残留:', stale if stale else '无')
|
||||
b.close()
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
BASE = 'http://localhost:8088'
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(channel='chrome', headless=True)
|
||||
page = b.new_page(viewport={'width': 1600, 'height': 1000})
|
||||
page.goto(BASE, wait_until='domcontentloaded'); page.wait_for_timeout(2500)
|
||||
page.locator('input[placeholder="用户名"]').fill('admin')
|
||||
page.locator('input[placeholder="密码"]').fill('123456')
|
||||
page.locator('text="登录"').first.click(); page.wait_for_timeout(8000)
|
||||
page.goto(f'{BASE}/#/worktime/count', wait_until='domcontentloaded'); page.wait_for_timeout(8000)
|
||||
page.locator('.el-tabs__item:has-text("月报"):visible').first.click(); page.wait_for_timeout(5000)
|
||||
# 产品集切 139,本月(7 月)
|
||||
page.locator('.el-select:visible').nth(1).click(); page.wait_for_timeout(1500)
|
||||
page.locator('.el-select-dropdown:visible li:has-text("139")').first.click(); page.wait_for_timeout(1000)
|
||||
page.locator('button:has-text("查询"):visible').first.click(); page.wait_for_timeout(8000)
|
||||
row = page.locator('tr:has-text("岑海峰")').first
|
||||
if not row.count():
|
||||
page.screenshot(path='tmp/pw_yw_entry_list.png', full_page=True)
|
||||
print('岑海峰未出行,截图 tmp/pw_yw_entry_list.png')
|
||||
b.close(); raise SystemExit(0)
|
||||
print('岑海峰已出行')
|
||||
row.locator('button:has-text("绩效")').first.click(); page.wait_for_timeout(15000)
|
||||
body = page.locator('body').inner_text()
|
||||
keys = ['运维大项任务', '系统运维监控', '职场巡检', '数据库备份', '其他运维工作的完成情况', '系统运行稳定性']
|
||||
miss = [k for k in keys if k not in body]
|
||||
page.screenshot(path='tmp/pw_yw_cenhaifeng.png', full_page=True)
|
||||
print('[YW岑海峰]', 'PASS' if not miss else f'MISS:{miss}')
|
||||
b.close()
|
||||
print('done')
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""CHG-068:需求文档 tab 内容视觉重设计(分节+文件行+计数徽章)"""
|
||||
import io, sys
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
path = 'src/views/product/storyinfo/components/product.vue'
|
||||
lines = open(path, encoding='utf-8').readlines()
|
||||
|
||||
start = next(i for i, l in enumerate(lines) if '需求文档' in l and 'el-tab-pane' in l)
|
||||
tabs_end = next(i for i, l in enumerate(lines) if '</el-tabs>' in l)
|
||||
pane_end = next(i for i in range(tabs_end - 1, start, -1) if '</el-tab-pane>' in lines[i])
|
||||
print(f'pane: {start+1}..{pane_end+1}')
|
||||
|
||||
ROW = ''' <li v-for="item in {LIST}" :key="item.id" class="doc-file">
|
||||
<i class="el-icon-document doc-file-icon" />
|
||||
<div class="doc-file-main">
|
||||
<div class="doc-file-name cp" @click="fileClick(item)">{TITLE}</div>
|
||||
<div class="doc-file-meta">{{{{ item.addedBy }}}} · {{{{ item.addedDate ? dayjs(item.addedDate).format("YYYY-MM-DD HH:mm") : "" }}}}</div>
|
||||
</div>
|
||||
<div class="doc-file-acts">
|
||||
<el-button type="text" size="mini" @click="fileClick(item)">查看</el-button>
|
||||
<el-button type="text" size="mini" @click="downFiles(item)">下载</el-button>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="{LIST}.length === 0" class="doc-empty">{EMPTY}</li>'''
|
||||
|
||||
def section(title, count, body, warn='', footer=''):
|
||||
return f''' <section class="doc-sec">
|
||||
<header class="doc-sec-hd">
|
||||
<span class="doc-sec-title">{title}</span>
|
||||
<span class="doc-count">{count}</span>
|
||||
</header>
|
||||
{warn}{body}{footer}
|
||||
</section>
|
||||
'''
|
||||
|
||||
def doclist(listname, empty, title_expr='{{ item.title }}'):
|
||||
return ''' <ul class="doc-list">
|
||||
''' + ROW.replace('{LIST}', listname).replace('{TITLE}', title_expr).replace('{EMPTY}', empty) + '''
|
||||
</ul>'''
|
||||
|
||||
UPLOADS = ''' <div class="doc-sec-ft">
|
||||
<uploads
|
||||
:form="uploadForm"
|
||||
:upload-show="{SHOW}"
|
||||
object-type="{TYPE}"
|
||||
:object-id="currentInfo.id"
|
||||
@fetchLogo="fetchBlockFile($event, '{TYPE}')"
|
||||
@handleBeforeUpload="handleBeforeUpload"
|
||||
/>
|
||||
</div>'''
|
||||
|
||||
parts = []
|
||||
# 1 测试用例/模版
|
||||
parts.append(section(
|
||||
'测试用例 / 测试报告模版',
|
||||
'{{ testCaseFiles.length + testReportFiles.length }} 份',
|
||||
' <div class="doc-group">测试用例</div>\n' + doclist('testCaseFiles', '暂无附件')
|
||||
+ '\n <div class="doc-group">测试报告模版</div>\n' + doclist('testReportFiles', '暂无附件')
|
||||
))
|
||||
# 2 提交测试报告(卡点+上传)
|
||||
parts.append(section(
|
||||
'提交测试报告', '{{ testReportSubmitFiles.length }} 份',
|
||||
doclist('testReportSubmitFiles', '暂无附件'),
|
||||
warn=''' <div v-if="currentInfo.codeReviewStatus !== 'pass'" class="doc-warn">
|
||||
<i class="el-icon-warning-outline" /> 代码审查未通过,不可提交
|
||||
</div>
|
||||
''',
|
||||
footer='\n' + UPLOADS.replace('{SHOW}', "currentInfo.codeReviewStatus === 'pass'").replace('{TYPE}', 'testReportSubmit')
|
||||
))
|
||||
# 3 其他测试文档(上传)
|
||||
parts.append(section(
|
||||
'其他测试文档', '{{ testOtherFiles.length }} 份',
|
||||
doclist('testOtherFiles', '暂无附件'),
|
||||
footer='\n' + UPLOADS.replace('{SHOW}', 'true').replace('{TYPE}', 'testOther')
|
||||
))
|
||||
# 4 代码审查报告(轮次徽章)
|
||||
parts.append(section(
|
||||
'代码审查报告', '{{ aiCodeReviewFiles.length }} 份',
|
||||
doclist('aiCodeReviewFiles', '暂无附件(由 AI 框架上传)',
|
||||
'{{ item.title }}<span v-if="parseRound(item.extra)" class="doc-round">第{{ parseRound(item.extra) }}轮</span>')
|
||||
))
|
||||
# 5 工作日志
|
||||
parts.append(section('工作日志', '{{ aiWorkLogFiles.length }} 份',
|
||||
doclist('aiWorkLogFiles', '暂无附件(由 AI 框架上传)')))
|
||||
# 6 AI项目文档更新记录
|
||||
parts.append(section('AI 项目文档更新记录', '{{ aiDocUpdateFiles.length }} 份',
|
||||
doclist('aiDocUpdateFiles', '暂无附件(由 AI 框架上传)')))
|
||||
|
||||
inner = (' <!-- CHG-063:六类文档区块归集到此 tab;CHG-068:视觉重设计(分节+文件行+计数徽章) -->\n'
|
||||
' <div class="req-docs">\n'
|
||||
+ ''.join(parts) +
|
||||
' </div>\n')
|
||||
|
||||
new_lines = lines[:start + 1] + [inner] + lines[pane_end:]
|
||||
open(path, 'w', encoding='utf-8', newline='').writelines(new_lines)
|
||||
print('tab 内容已替换:', pane_end - start, '行 →', inner.count(chr(10)), '行')
|
||||
@@ -0,0 +1,27 @@
|
||||
# 验收报告(演示富文本)
|
||||
|
||||
## 一、指标表格
|
||||
|
||||
| 指标 | 权重 | 得分 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 任务及时率 | 0.25 | 85.68 | 分段扣分 |
|
||||
| Bug 密度 | 0.30 | 100 | 满分 |
|
||||
| 饱和度 | 0.20 | 58 | 偏低 |
|
||||
|
||||
## 二、代码块
|
||||
|
||||
```sql
|
||||
ALTER TABLE `zt_story_expand`
|
||||
ADD COLUMN `ai_participation_rate` VARCHAR(16) DEFAULT NULL COMMENT 'AI参与率';
|
||||
```
|
||||
|
||||
## 三、引用与列表
|
||||
|
||||
> 考核只判「缺失」不判内容质量:每缺失一份扣 2 分。
|
||||
|
||||
1. 第一项
|
||||
2. 第二项
|
||||
- 子项 A
|
||||
- 子项 B
|
||||
|
||||
**加粗** / *斜体* / ~~删除线~~ / `行内代码`
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, title, product, status, code_review_status FROM zt_story WHERE id=9130")
|
||||
print("9130:", cur.fetchone())
|
||||
cur.execute("SHOW COLUMNS FROM zt_story")
|
||||
cols=[r[0] for r in cur.fetchall()]
|
||||
print("url 字段:", [c for c in cols if 'url' in c or 'review' in c])
|
||||
@@ -0,0 +1,12 @@
|
||||
# AI 代码审查报告(冒烟测试)
|
||||
|
||||
- 需求:冒烟测试专用(联调验证,可删除)
|
||||
- 轮次:第 1 轮
|
||||
- 结果:pass
|
||||
- 严重:0/错误:0/警告:1
|
||||
|
||||
## 审查事项
|
||||
|
||||
1. 空指针检查 — 通过
|
||||
2. 资源关闭 — 通过
|
||||
3. SQL 注入 — 通过
|
||||
@@ -0,0 +1,6 @@
|
||||
# 测试报告(冒烟提交)
|
||||
|
||||
- 需求:6566
|
||||
- 测试范围:接口冒烟
|
||||
- 结果:全部通过
|
||||
- 用例数:4 / 通过:4 / 失败:0
|
||||
@@ -0,0 +1,6 @@
|
||||
# AI 工作日志(冒烟)
|
||||
|
||||
- 需求:6566
|
||||
- 类型:code_review
|
||||
- 时间:2026-07-28 14:00:00
|
||||
- 内容:AI 代码审查第 1 轮完成,结果 pass
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, account, INSTR(scope_json,'6.04'), INSTR(scope_json,'productDefectRate') FROM zt_month_score WHERE date_str='2026-06'")
|
||||
rows = cur.fetchall()
|
||||
print(f"2026-06 快照 {len(rows)} 条:")
|
||||
for r in rows: print(f" id={r[0]} account={r[1]} 含6.04={'是' if r[2] else '否'} 含productDefectRate={'是' if r[3] else '否'}")
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, title, status, stage, estimate, product, module, assignedto, openedby FROM zt_story WHERE id=9130")
|
||||
print("story:", cur.fetchone())
|
||||
cur.execute("SELECT spec, verify FROM zt_storyspec WHERE story=9130 ORDER BY version DESC LIMIT 1")
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
print("--- spec ---"); print((row[0] or '')[:2000])
|
||||
print("--- verify ---"); print((row[1] or '')[:500])
|
||||
cur.execute("SELECT id, name, status, type, estimate, assignedto FROM zt_task WHERE story=9130 AND deleted='0'")
|
||||
print("--- tasks ---")
|
||||
for r in cur.fetchall(): print(r)
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""9130 AI指标提交——一次性调用,目标 8085(161 zentao_dev_2026)。不改 submit_assessment.py。"""
|
||||
import io, sys, json, requests
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
url = "http://localhost:8085/zentao/zt-story-expand/saveOrUpdate"
|
||||
payload = {
|
||||
"storyId": 9130,
|
||||
"numberUnits": 3,
|
||||
"unitBusinessComplexity": "2.2",
|
||||
"technicalComplexityCoefficient": "1.4",
|
||||
"aiEfficiencyCoefficient": "0.55",
|
||||
"requirementStatus": "inProgress",
|
||||
"workloadIndex": "5.1",
|
||||
"productPerson": "魏冬霞",
|
||||
"developPerson": "罗勇",
|
||||
}
|
||||
resp = requests.post(url, json=payload, headers={"Content-Type": "application/json"}, timeout=30)
|
||||
data = resp.json()
|
||||
msg = data.get("message", "")
|
||||
try: msg = msg.encode("latin-1").decode("utf-8")
|
||||
except Exception: pass
|
||||
print(f"HTTP {resp.status_code} | code={data.get('code')} | message={msg}")
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
print("### A. 孙颖 6月提出的所有 Bug(不限需求)")
|
||||
for r in q("""SELECT id, tostory, story, severity, status, resolvedby, openeddate, LEFT(title,30)
|
||||
FROM zt_bug WHERE openedby='sunying' AND deleted='0'
|
||||
AND openeddate>='2026-06-01' AND openeddate<'2026-07-01' ORDER BY id"""):
|
||||
print(r)
|
||||
|
||||
print("\n### B. 她的测试需求(24个)上挂的所有 Bug(任何人提的)")
|
||||
ids = q("""SELECT DISTINCT s.id FROM zt_story s LEFT JOIN zt_story_expand e ON e.story_id=s.id
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
AND (s.assignedTo='sunying' OR e.test_person LIKE '%孙颖%')""")
|
||||
idlist = ','.join(str(r[0]) for r in ids)
|
||||
for r in q(f"""SELECT id, tostory, severity, status, openedby, openeddate, LEFT(title,30)
|
||||
FROM zt_bug WHERE tostory IN ({idlist}) AND deleted='0' ORDER BY openeddate"""):
|
||||
print(r)
|
||||
|
||||
print("\n### C. 我早前查的2个普通Bug是哪些(story关联+severity3/4)")
|
||||
for r in q(f"""SELECT id, story, tostory, severity, openedby, openeddate, LEFT(title,30)
|
||||
FROM zt_bug WHERE story IN ({idlist}) AND deleted='0' AND severity IN (3,4)"""):
|
||||
print(r)
|
||||
|
||||
print("\n### D. 弹窗口径模拟:她提的 + tostory∈24需求 + 当月 + 状态过滤")
|
||||
rows = q(f"""SELECT id, severity, status, resolvedby FROM zt_bug
|
||||
WHERE openedby='sunying' AND deleted='0' AND tostory IN ({idlist})
|
||||
AND openeddate>='2026-06-01' AND openeddate<'2026-07-01'""")
|
||||
serious = sum(1 for r in rows if r[1]==1)
|
||||
slight = sum(1 for r in rows if r[1] in (2,3,4))
|
||||
print(f"命中 {len(rows)} 个: 严重(sev1)={serious} 轻微(sev2/3/4)={slight}")
|
||||
for r in rows: print(r)
|
||||
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
rows = q("""SELECT DISTINCT s.id FROM zt_story s
|
||||
LEFT JOIN zt_story_expand e ON e.story_id=s.id
|
||||
WHERE s.releaseddate>='2026-05-01' AND s.releaseddate<'2026-06-01' AND s.deleted='0'
|
||||
AND (s.assignedTo='sunying' OR e.test_person LIKE '%孙颖%')""")
|
||||
print("5月测试需求:", len(rows), "个")
|
||||
ids = ','.join(str(r[0]) for r in rows) or '0'
|
||||
|
||||
bugs = q(f"""SELECT id, tostory, severity, status FROM zt_bug
|
||||
WHERE openedby='sunying' AND deleted='0' AND tostory IN ({ids})
|
||||
AND openeddate>='2026-05-01' AND openeddate<'2026-06-01'""")
|
||||
print("她5月提的、tostory在范围内的Bug:", len(bugs), "个")
|
||||
for r in bugs: print(r)
|
||||
|
||||
dev = q(f"""SELECT SUM(t.estimate) FROM zt_task t
|
||||
WHERE t.story IN ({ids}) AND t.type='devel' AND t.status NOT IN ('cancel','closed') AND t.deleted='0'""")[0][0]
|
||||
serious = sum(1 for r in bugs if r[2]==1)
|
||||
slight = sum(1 for r in bugs if r[2] in (2,3,4))
|
||||
w = serious*5 + slight
|
||||
print(f"严重={serious} 轻微={slight} 加权={w} 分母devel工时={dev}")
|
||||
if dev:
|
||||
rate = w/float(dev)*100
|
||||
print(f"检出率×100 = {rate:.1f} → 得分: {'30满分' if rate>20 else max(0,30-(20-int(rate))*2)}")
|
||||
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""200 zentao_dev → 161 zentao_dev_2026:同步研发需求 9197 的关联记录。
|
||||
源库全程只读;目标库纯 INSERT(前置检查目标无对应行,不覆盖不删除)。
|
||||
范围:zt_storyspec / zt_projectstory / zt_task / zt_action / zt_effort(有则带)。
|
||||
"""
|
||||
import io, sys, time, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', connect_timeout=15, autocommit=False)
|
||||
SID = 9197
|
||||
TASK_IDS = (18841, 18849)
|
||||
|
||||
def log(m):
|
||||
print(f'[{time.strftime("%H:%M:%S")}] {m}', flush=True)
|
||||
|
||||
src = pymysql.connect(**SRC); scur = src.cursor()
|
||||
dst = pymysql.connect(**DST); dcur = dst.cursor()
|
||||
|
||||
def cols_of(cur, t):
|
||||
cur.execute("""SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema=DATABASE() AND table_name=%s
|
||||
ORDER BY ordinal_position""", (t,))
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
|
||||
def fetch(t, where, params):
|
||||
scur.execute(f'SELECT * FROM `{t}` WHERE {where}', params)
|
||||
cols = [d[0] for d in scur.description]
|
||||
return cols, scur.fetchall()
|
||||
|
||||
def sync_table(t, where, params, key_desc):
|
||||
scols, rows = fetch(t, where, params)
|
||||
if not rows:
|
||||
log(f'{t}: 源库 0 行,跳过')
|
||||
return 0
|
||||
dcols = set(cols_of(dcur, t))
|
||||
use = [c for c in scols if c in dcols]
|
||||
dropped = [c for c in scols if c not in dcols]
|
||||
if dropped:
|
||||
log(f'{t}: 目标库缺列 {dropped},这些列不写入')
|
||||
# 前置检查:目标不能有与源主键冲突的行 —— 用第一主键列粗查
|
||||
col_sql = ','.join(f'`{c}`' for c in use)
|
||||
ph = ','.join(['%s'] * len(use))
|
||||
insert_sql = f'INSERT INTO `{t}` ({col_sql}) VALUES ({ph})'
|
||||
idx = [scols.index(c) for c in use]
|
||||
data = [tuple(r[i] for i in idx) for r in rows]
|
||||
dcur.executemany(insert_sql, data)
|
||||
log(f'{t}: 插入 {len(data)} 行({key_desc})')
|
||||
return len(data)
|
||||
|
||||
# ---------- 前置检查(目标应为空/无冲突) ----------
|
||||
problems = []
|
||||
dcur.execute('SELECT COUNT(*) FROM zt_storyspec WHERE story=%s', (SID,))
|
||||
if dcur.fetchone()[0]: problems.append('zt_storyspec 已有 9197')
|
||||
dcur.execute('SELECT COUNT(*) FROM zt_projectstory WHERE story=%s', (SID,))
|
||||
if dcur.fetchone()[0]: problems.append('zt_projectstory 已有 9197')
|
||||
dcur.execute(f'SELECT COUNT(*) FROM zt_task WHERE id IN {TASK_IDS}')
|
||||
if dcur.fetchone()[0]: problems.append(f'zt_task id {TASK_IDS} 冲突')
|
||||
dcur.execute('SELECT COUNT(*) FROM zt_action WHERE objectType=\'story\' AND objectID=%s AND id>119100', (SID,))
|
||||
if dcur.fetchone()[0]: problems.append('zt_action 已有 9197 的源端记录')
|
||||
if problems:
|
||||
log('!!! 前置检查失败: ' + '; '.join(problems))
|
||||
sys.exit(1)
|
||||
log('前置检查通过:目标库无冲突')
|
||||
|
||||
# ---------- 同步 ----------
|
||||
n = 0
|
||||
n += sync_table('zt_storyspec', 'story=%s', (SID,), 'story=9197')
|
||||
n += sync_table('zt_projectstory', 'story=%s', (SID,), 'story=9197, project 142/286')
|
||||
n += sync_table('zt_task', f'id IN {TASK_IDS}', (), f'id={TASK_IDS}')
|
||||
n += sync_table('zt_action', "objectType='story' AND objectID=%s", (SID,), 'story=9197 流转历史')
|
||||
|
||||
# zt_effort 可能有,也可能没有 task 列/相关行
|
||||
ecols = cols_of(scur, 'zt_effort')
|
||||
if ecols:
|
||||
key = 'task' if 'task' in ecols else ('objectID' if 'objectID' in ecols else None)
|
||||
if key:
|
||||
where = f'`{key}` IN {TASK_IDS}' + (" AND objectType='task'" if 'objectType' in ecols else '')
|
||||
n += sync_table('zt_effort', where, (), f'task={TASK_IDS}')
|
||||
|
||||
dst.commit()
|
||||
log(f'已提交,共写入 {n} 行')
|
||||
|
||||
# ---------- 回读验证 ----------
|
||||
log('--- 回读验证 ---')
|
||||
dcur.execute('SELECT story, version, LEFT(title,30), LENGTH(spec), LENGTH(verify) FROM zt_storyspec WHERE story=%s', (SID,))
|
||||
log(f'zt_storyspec: {dcur.fetchall()}')
|
||||
dcur.execute('SELECT project, story, version, `order` FROM zt_projectstory WHERE story=%s', (SID,))
|
||||
log(f'zt_projectstory: {dcur.fetchall()}')
|
||||
dcur.execute(f'SELECT id, project, execution, name, status, assignedTo FROM zt_task WHERE id IN {TASK_IDS}')
|
||||
for r in dcur.fetchall():
|
||||
log(f'zt_task: {r}')
|
||||
dcur.execute("SELECT id, action, date, actor FROM zt_action WHERE objectType='story' AND objectID=%s ORDER BY id", (SID,))
|
||||
for r in dcur.fetchall():
|
||||
log(f'zt_action: {r}')
|
||||
|
||||
src.close(); dst.close()
|
||||
log('完成')
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
def q(sql):
|
||||
cur.execute(sql); return cur.fetchall()
|
||||
|
||||
# 她 6 月测试需求清单
|
||||
ids = q("""SELECT DISTINCT s.id FROM zt_story s LEFT JOIN zt_story_expand e ON e.story_id=s.id
|
||||
WHERE s.releaseddate>='2026-06-01' AND s.releaseddate<'2026-07-01' AND s.deleted='0'
|
||||
AND (s.assignedTo='sunying' OR e.test_person LIKE '%孙颖%')""")
|
||||
scope = {r[0] for r in ids}
|
||||
print("她6月测试需求:", sorted(scope))
|
||||
|
||||
# 4 条 Bug 的 story 是否在范围内
|
||||
for r in q("SELECT id, story, tostory, LEFT(title,30) FROM zt_bug WHERE id IN (2566,2567,2568,2569)"):
|
||||
print(f"bug {r[0]}: story={r[1]} tostory={r[2]} 在范围内={r[1] in scope} {r[3]}")
|
||||
|
||||
# 8750 是否当月上线
|
||||
for r in q("SELECT id, releaseddate, assignedTo FROM zt_story WHERE id IN (8277, 8750)"):
|
||||
print("story:", r)
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""补:200 → 161/zentao_dev_2026,用户需求 2461 的 zt_action(objectType='user-story')。"""
|
||||
import io, sys, time, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', connect_timeout=15, autocommit=False)
|
||||
USID = 2461
|
||||
|
||||
def log(m): print(f'[{time.strftime("%H:%M:%S")}] {m}', flush=True)
|
||||
|
||||
src = pymysql.connect(**SRC); scur = src.cursor()
|
||||
dst = pymysql.connect(**DST); dcur = dst.cursor()
|
||||
|
||||
scur.execute("SELECT * FROM zt_action WHERE objectType='user-story' AND objectID=%s ORDER BY id", (USID,))
|
||||
cols = [d[0] for d in scur.description]
|
||||
rows = scur.fetchall()
|
||||
log(f'200 上 user-story/2461 action: {len(rows)} 行')
|
||||
if rows:
|
||||
dcur.execute("SELECT COUNT(*) FROM zt_action WHERE objectType='user-story' AND objectID=%s", (USID,))
|
||||
if dcur.fetchone()[0]:
|
||||
log('目标库已有,中止'); sys.exit(1)
|
||||
col_sql = ','.join(f'`{c}`' for c in cols)
|
||||
ph = ','.join(['%s'] * len(cols))
|
||||
dcur.executemany(f'INSERT INTO zt_action ({col_sql}) VALUES ({ph})', rows)
|
||||
dst.commit()
|
||||
log(f'插入 {len(rows)} 行')
|
||||
dcur.execute("SELECT id, action, actor, date FROM zt_action WHERE objectType='user-story' AND objectID=%s ORDER BY id", (USID,))
|
||||
for r in dcur.fetchall():
|
||||
log(f' {r}')
|
||||
src.close(); dst.close()
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""200 → 161/zentao_dev_2026:补同步用户需求 2461(9197 的上游)。
|
||||
源库只读;目标库纯 INSERT,前置检查无冲突。
|
||||
"""
|
||||
import io, sys, time, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
SRC = dict(host='192.168.3.200', user='root', password='PX4fTAAsJ#T!1',
|
||||
database='zentao_dev', charset='utf8mb4', connect_timeout=15)
|
||||
DST = dict(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4', connect_timeout=15, autocommit=False)
|
||||
USID = 2461
|
||||
|
||||
def log(m):
|
||||
print(f'[{time.strftime("%H:%M:%S")}] {m}', flush=True)
|
||||
|
||||
src = pymysql.connect(**SRC); scur = src.cursor()
|
||||
dst = pymysql.connect(**DST); dcur = dst.cursor()
|
||||
|
||||
# 先摸清 200 上 2461 的 action/附件 情况(只读)
|
||||
scur.execute("SELECT DISTINCT objectType FROM zt_action WHERE objectID=%s", (USID,))
|
||||
print('200 zt_action objectID=2461 的 objectType:', [r[0] for r in scur.fetchall()])
|
||||
scur.execute("SELECT COUNT(*) FROM zt_file WHERE objectID=%s AND objectType IN ('userstory','storyUser','story_user')", (USID,))
|
||||
print('200 2461 附件数:', scur.fetchone()[0])
|
||||
|
||||
def cols_of(cur, t):
|
||||
cur.execute("""SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema=DATABASE() AND table_name=%s
|
||||
ORDER BY ordinal_position""", (t,))
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
|
||||
def sync(t, where, params, desc):
|
||||
scur.execute(f'SELECT * FROM `{t}` WHERE {where}', params)
|
||||
scols = [d[0] for d in scur.description]
|
||||
rows = scur.fetchall()
|
||||
if not rows:
|
||||
log(f'{t}: 源库 0 行,跳过'); return 0
|
||||
dcols = set(cols_of(dcur, t))
|
||||
use = [c for c in scols if c in dcols]
|
||||
dropped = [c for c in scols if c not in dcols]
|
||||
if dropped:
|
||||
log(f'{t}: 目标缺列 {dropped},跳过这些列')
|
||||
col_sql = ','.join(f'`{c}`' for c in use)
|
||||
ph = ','.join(['%s'] * len(use))
|
||||
idx = [scols.index(c) for c in use]
|
||||
dcur.executemany(f'INSERT INTO `{t}` ({col_sql}) VALUES ({ph})',
|
||||
[tuple(r[i] for i in idx) for r in rows])
|
||||
log(f'{t}: 插入 {len(rows)} 行({desc})')
|
||||
return len(rows)
|
||||
|
||||
# 前置检查
|
||||
dcur.execute('SELECT COUNT(*) FROM zt_story_user WHERE id=%s', (USID,))
|
||||
if dcur.fetchone()[0]:
|
||||
log('!!! zt_story_user 2461 目标库已存在,中止'); sys.exit(1)
|
||||
dcur.execute('SELECT COUNT(*) FROM zt_story_userspec WHERE story=%s', (USID,))
|
||||
if dcur.fetchone()[0]:
|
||||
log('!!! zt_story_userspec 2461 目标库已存在,中止'); sys.exit(1)
|
||||
log('前置检查通过')
|
||||
|
||||
n = 0
|
||||
n += sync('zt_story_user', 'id=%s', (USID,), 'id=2461')
|
||||
n += sync('zt_story_userspec', 'story=%s', (USID,), 'story=2461')
|
||||
n += sync('zt_action', "objectID=%s AND objectType IN ('userstory','storyUser','story_user')", (USID,), '用户需求2461历史')
|
||||
dst.commit()
|
||||
log(f'已提交,共 {n} 行')
|
||||
|
||||
log('--- 回读验证 ---')
|
||||
dcur.execute('SELECT id, product, LEFT(title,40), status FROM zt_story_user WHERE id=%s', (USID,))
|
||||
log(f'zt_story_user: {dcur.fetchone()}')
|
||||
dcur.execute('SELECT story, version, LEFT(title,40), LENGTH(spec) FROM zt_story_userspec WHERE story=%s', (USID,))
|
||||
log(f'zt_story_userspec: {dcur.fetchall()}')
|
||||
src.close(); dst.close()
|
||||
log('完成')
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import io, sys, pymysql
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
conn = pymysql.connect(host='192.168.1.161', user='devgps', password='dev@2021GPS',
|
||||
database='zentao_dev_2026', charset='utf8mb4')
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT COUNT(*) total, SUM(tostory>0) has_tostory, SUM(story>0) has_story FROM zt_bug WHERE deleted='0'")
|
||||
print("全库 Bug:", cur.fetchone())
|
||||
cur.execute("SELECT COUNT(*), SUM(tostory>0), SUM(story>0) FROM zt_bug WHERE deleted='0' AND openeddate>='2026-01-01'")
|
||||
print("2026年 Bug:", cur.fetchone())
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""读回 9 份模版,断言 {name}/{date}/{total} 存在且占位符集合与预期一致。"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
import openpyxl
|
||||
|
||||
OUT_DIR = r'codes/zentao/src/main/resources/templates/scope'
|
||||
|
||||
EXPECTED = {
|
||||
'项目经理考核.xlsx': {'name', 'date', 'total',
|
||||
'workloadPrdScore', 'workloadTeamScore', 'versionPlanFinishedRate', 'bugScore',
|
||||
'documentQualityScore', 'developFeedbackStory', 'systemStabilityScore',
|
||||
'professionalSkillEnhancementScore',
|
||||
'detail_workloadPrdScore', 'detail_workloadTeamScore', 'detail_versionPlanFinishedRate',
|
||||
'detail_bugScore', 'detail_documentQualityScore'},
|
||||
'项目经理考核-王宇航.xlsx': {'name', 'date', 'total',
|
||||
'workloadTeamScore', 'versionPlanFinishedRate', 'bugScore', 'documentQualityScore',
|
||||
'developFeedbackStory', 'systemStabilityScore', 'professionalSkillEnhancementScore',
|
||||
'detail_workloadTeamScore', 'detail_versionPlanFinishedRate', 'detail_bugScore',
|
||||
'detail_documentQualityScore'},
|
||||
'产品经理考核.xlsx': {'name', 'date', 'total',
|
||||
'workloadPrdScore', 'workloadTeamScore', 'productProjectOnTimeRateScore', 'productBugRate',
|
||||
'productProblemResponse', 'productResponsibilityScore',
|
||||
'detail_workloadPrdScore', 'detail_workloadTeamScore',
|
||||
'detail_productProjectOnTimeRateScore', 'detail_productBugRate'},
|
||||
'项目助理考核.xlsx': {'name', 'date', 'total',
|
||||
'workloadPrdScore', 'releaseScore', 'productBugRate', 'productProblemResponse',
|
||||
'productResponsibilityScore',
|
||||
'detail_workloadPrdScore', 'detail_productBugRate'},
|
||||
'后端开发工程师.xlsx': {'name', 'date', 'total',
|
||||
'punctualityScore', 'bugScore', 'codeQualityScore', 'documentQualityScore',
|
||||
'saturationScore', 'workAttitude', 'excellentShare', 'quality',
|
||||
'totalTask', 'delayTask', 'finishPunctuality', 'workTime', 'examineTime', 'saturation',
|
||||
'bugCount', 'bugDensity'},
|
||||
'前端开发工程师.xlsx': {'name', 'date', 'total',
|
||||
'punctualityScore', 'bugScore', 'codeQualityScore', 'saturationScore', 'workAttitude',
|
||||
'excellentShare', 'quality',
|
||||
'totalTask', 'delayTask', 'finishPunctuality', 'workTime', 'examineTime', 'saturation',
|
||||
'bugCount', 'bugDensity'},
|
||||
'测试工程师.xlsx': {'name', 'date', 'total',
|
||||
'punctualityScore', 'documentQualityScore', 'bugFindScore', 'bugScore', 'workAttitude',
|
||||
'excellentShare', 'quality',
|
||||
'totalTask', 'delayTask', 'finishPunctuality', 'devSlightBug', 'devSeriousBug', 'bugFindRate',
|
||||
'slightBug', 'seriousBug'},
|
||||
'UI工程师.xlsx': {'name', 'date', 'total',
|
||||
'punctualityScore', 'designScore', 'workAttitude', 'uiWorkScore', 'excellentShare',
|
||||
'totalTask', 'delayTask', 'finishPunctuality'},
|
||||
'运维工程师.xlsx': {'name', 'date', 'total',
|
||||
'opsMajorTaskScore', 'opsMonitorScore', 'opsInspectScore', 'opsBackupScore', 'otherOpsScore',
|
||||
'systemStabilityScore', 'workAttitude', 'excellentShare',
|
||||
'detail_opsMajorTaskScore', 'detail_opsMonitorScore', 'detail_opsInspectScore',
|
||||
'detail_opsBackupScore'},
|
||||
}
|
||||
|
||||
PLACEHOLDER = re.compile(r'\{([^{}]+)\}')
|
||||
|
||||
failed = False
|
||||
for fname, expected in EXPECTED.items():
|
||||
wb = openpyxl.load_workbook('%s/%s' % (OUT_DIR, fname))
|
||||
ws = wb.active
|
||||
found = set()
|
||||
for row in ws.iter_rows():
|
||||
for cell in row:
|
||||
if isinstance(cell.value, str):
|
||||
found.update(PLACEHOLDER.findall(cell.value))
|
||||
missing_basics = {'name', 'date', 'total'} - found
|
||||
extra = found - expected
|
||||
missing = expected - found
|
||||
status = 'OK ' if not (missing_basics or extra or missing) else 'FAIL'
|
||||
if status == 'FAIL':
|
||||
failed = True
|
||||
print('%s %s (%d placeholders): %s' % (status, fname, len(found), sorted(found)))
|
||||
if missing_basics:
|
||||
print(' missing basics: %s' % sorted(missing_basics))
|
||||
if missing:
|
||||
print(' missing: %s' % sorted(missing))
|
||||
if extra:
|
||||
print(' extra: %s' % sorted(extra))
|
||||
|
||||
print('VALIDATION', 'FAILED' if failed else 'PASSED')
|
||||
sys.exit(1 if failed else 0)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user