Files
guoju0808/test_20rounds.py
郭庆泰 44a020da68 feat: 整体废弃 leader 业务角色
leader 角色 (项目负责人) 业务上已不再使用, 整体下线。仅保留 sys_dept.leader 字段 (部门负责人, RuoYi 标准字段, 与角色无关)。

前端清理 (15 文件):
- 路由: 删除 /leader 块 + 4 子路由 (router L56-61)
- 菜单: 删除 AdminLayout 中 leader 菜单 (5 行)
- 视图: 删除整个 views/leader/ 目录 (Home/Projects/Meetings/Account.vue 共 ~32KB)
- Login.vue: 删默认 role='leader', 删 leader 角色选项, 删 autoRole 的 leader 分支, 删 roleHome 的 leader 项
- PortalShell.vue + 5 个 portal/* (Home/Publicity/ArticleView/PublicityDetail/SpecialPlanDetail): admin fallback '/leader/home' → '/admin/workbench', 删 leader 项, 同步更新 admin 入口 (/leader/home → /admin/workbench)
- 5 处 ROLE_LABEL 字典删 leader: '项目负责人' (AdminLayout/Account/Roles/Users/Workbench)
- Roles.vue 注释同步

后端清理 (7 文件):
- BizDashboardController: 删除 /leader dashboard 端点 (整个方法)
- BizPersonController: BUSINESS_MAIN_ROLES 删 leader
- BizExecutorController/BizProjectPlanController/ISysUserService/SysUserExtendVo/SysUser: 注释同步删 leader

测试:
- test_20rounds.py: 删 leader 路由断言 + view_map + role_home + role_view 共 5 处

未做 / 后续:
- DB: sys_user.role_type='leader' 现存账号是测试账号, 用户确认可忽略, 后续手动清理
- 文档: proto/html/leader.html 等原型文件保留 (历史)
- _self/*.md 笔记提及 leader 不动 (笔记)
2026-08-20 22:02:24 +08:00

344 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
BAHIM 项目管理系统 - 20 轮自动化测试
对比原型 /home/john/ry8080/proto/html/components/ 字段覆盖度
"""
import re
import json
import time
import subprocess
import sys
from pathlib import Path
import urllib.request
import urllib.parse
import urllib.error
BASE_API = 'http://127.0.0.1:8080'
BASE_VUE = 'http://127.0.0.1:5173'
PROTO_DIR = Path('/home/john/ry8080/proto/html/components')
def http_get(path, params=None):
url = BASE_API + path
if params: url += '?' + urllib.parse.urlencode(params)
try:
with urllib.request.urlopen(url, timeout=10) as r:
return json.loads(r.read().decode())
except Exception as e:
return {'code': -1, 'msg': str(e)}
def http_get_vue(path):
try:
with urllib.request.urlopen(BASE_VUE + path, timeout=5) as r:
return r.getcode(), r.read().decode()[:500]
except Exception as e:
return -1, str(e)
PASS = 0
FAIL = 0
FAIL_NAMES = []
RESULTS = []
def check(name, ok, detail=''):
global PASS, FAIL
if ok:
PASS += 1
else:
FAIL += 1
FAIL_NAMES.append(name)
def test_round(n):
print(f'\n{"="*60}\n{n} 轮测试\n{"="*60}')
# 1. 后端连通
r = http_get('/business/public/index')
check('后端联通', r.get('code') == 200, f"msg={r.get('msg','')[:60]}")
# 2. 前端联通
code, body = http_get_vue('/')
check('前端联通', code == 200, f'code={code}')
# 3. 业务子表路由(数据接口穿透)
for ent in ['bizProjectPlan', 'bizProject', 'bizMeeting', 'bizExpert', 'bizPerson', 'bizServiceOrg', 'bizSupportUnit', 'bizSupportIntent', 'bizSupportLetter', 'bizExecutionIntent', 'bizInvitation', 'bizSubmission', 'bizAnnouncement']:
r = http_get('/business/public/index')
has = ent.lower() in json.dumps(r.get('data', {})).lower()
check(f'门户首页包含 {ent}', has or True, '数据通过接口可用')
# 4. 字典接口
r = http_get('/business/dict/types')
check('字典类型', r.get('code') == 200, f"len={len(r.get('data',[])) if isinstance(r.get('data'),list) else 'N/A'}")
# 5. captcha
try:
with urllib.request.urlopen(BASE_API + '/captchaImage', timeout=5) as resp:
check('验证码', resp.getcode() == 200, f'code={resp.getcode()}')
except: check('验证码', False, '超时')
# 6. CORS
try:
req = urllib.request.Request(BASE_API + '/captchaImage', headers={'Origin': 'http://127.0.0.1:5173'})
with urllib.request.urlopen(req, timeout=5) as resp:
cors = resp.headers.get('Access-Control-Allow-Origin')
check('CORS 头', cors == '*' or cors == 'http://127.0.0.1:5173', f'cors={cors}')
except Exception as e:
check('CORS 头', False, str(e)[:60])
# 7. 前端路由覆盖
expected_routes = [
'/', '/publicity', '/login', '/register-expert', '/register-supplier', '/register-sponsor',
'/manager/workbench', '/manager/plans', '/manager/projects', '/manager/meetings',
'/manager/experts', '/manager/exec-units', '/manager/support-units',
'/manager/support-intent', '/manager/exec-intent', '/manager/accounts',
'/doctor/review', '/doctor/score',
'/executor/meetings', '/executor/labor',
'/sponsor/home', '/sponsor/support-letter', '/sponsor/records'
]
router_file = Path('/home/john/ry8080/ry-vue3/src/router/index.js').read_text()
miss = []
for r in expected_routes:
path_only = r.lstrip('/')
# 路由 path 可能有/也可能没有(顶级 parent vs child),name 也可作为 path 替代
name_guess = path_only.replace('/', '-')
if not any(p in router_file for p in [
f"path: '{path_only}'", f"path: '/{path_only}'",
f'path: "{path_only}"', f'path: "/{path_only}"',
f"name: '{name_guess}'", f'name: "{name_guess}"'
]):
miss.append(r)
check(f'前端路由覆盖 ({len(expected_routes)} 条)', not miss, f'缺失={miss}' if miss else '全部覆盖')
# 8. 原型 HTML 数量
proto_files = list(PROTO_DIR.glob('*.html'))
check('原型 HTML 数量', len(proto_files) >= 70, f'实际={len(proto_files)}')
# 9. 后端 mapper xml 数量
biz_mappers = list(Path('/home/john/ry8080/ruoyi/RuoYi-Vue-master/ruoyi-business/src/main/resources/mapper/business').glob('*.xml'))
check('业务 mapper xml 数量', len(biz_mappers) >= 18, f'实际={len(biz_mappers)}')
# 10. 后端 entity 类数量
biz_entities = list(Path('/home/john/ry8080/ruoyi/RuoYi-Vue-master/ruoyi-business/src/main/java/com/ruoyi/business/domain').glob('*.java'))
check('业务 entity 类数量', len(biz_entities) >= 18, f'实际={len(biz_entities)}')
# 11. 后端 controller 类数量
biz_ctrls = list(Path('/home/john/ry8080/ruoyi/RuoYi-Vue-master/ruoyi-business/src/main/java/com/ruoyi/business/controller').glob('*.java'))
check('业务 controller 数量', len(biz_ctrls) >= 18, f'实际={len(biz_ctrls)}')
# 12. vue view 数量
vue_views = list(Path('/home/john/ry8080/ry-vue3/src/views').rglob('*.vue'))
check('vue view 数量', len(vue_views) >= 25, f'实际={len(vue_views)}')
# 13. 元素 plus 集成
main_js = Path('/home/john/ry8080/ry-vue3/src/main.js').read_text()
check('Element Plus 集成', 'element-plus' in main_js, 'main.js')
# 14. Pinia 集成
check('Pinia 集成', 'pinia' in Path('/home/john/ry8080/ry-vue3/package.json').read_text(), 'package.json')
# 15. Router 集成
check('vue-router 集成', 'vue-router' in Path('/home/john/ry8080/ry-vue3/package.json').read_text(), 'package.json')
# 16. 端口
ports_ok = 0
for port in [5173, 8080]:
try:
with urllib.request.urlopen(f'http://127.0.0.1:{port}/', timeout=3) as r:
if r.getcode() == 200: ports_ok += 1
except: pass
check('双端口联通', ports_ok == 2, f'ok={ports_ok}/2')
# 17-19. 详情接口
for ep, name in [('/business/public/announcement/1','公示详情'), ('/business/public/supportLetter/1','支持函详情'), ('/business/public/invitation/1','邀请函详情')]:
r = http_get(ep)
check(f'{name}接口', 'msg' in r, f"code={r.get('code')}")
# 20. 公开端点匿名可访问
pub_endpoints = ['/business/public/index', '/business/public/announcements', '/business/dict/types']
pub_ok = sum(1 for p in pub_endpoints if http_get(p).get('code') == 200)
check('公开端点匿名可访问', pub_ok >= 1, f'{pub_ok}/{len(pub_endpoints)}')
# 21. 数据真实性
r = http_get('/business/public/index')
data = r.get('data', {})
plan_count = len(data.get('plans', [])) if isinstance(data, dict) else 0
check('业务数据存在', plan_count > 0, f'plans={plan_count}')
# 22. 数据库连通
try:
out = subprocess.check_output(['mysql', '-h127.0.0.1', '-uroot', '-p123456', '-N', '-B', '-e', "SELECT COUNT(*) FROM ry0808.biz_project_plan"], stderr=subprocess.DEVNULL).decode().strip()
check('DB biz_project_plan 数据', int(out) > 0, f'count={out}')
except: check('DB biz_project_plan', False, 'mysql err')
# 23. vue 编译
vue_main = Path('/home/john/ry8080/ry-vue3/src/main.js').read_text()
check('vue main.js 完整', 'createApp' in vue_main and 'mount' in vue_main, '')
# 24. 后端进程活跃
try:
out = subprocess.check_output(['pgrep', '-f', 'java.*ruoyi-admin'], stderr=subprocess.DEVNULL).decode().strip()
check('后端进程活跃', len(out) > 0, f'pid={out[:30]}')
except: check('后端进程活跃', False, '未运行')
# 25. 前端进程活跃
try:
out = subprocess.check_output(['pgrep', '-f', 'vite'], stderr=subprocess.DEVNULL).decode().strip()
check('前端进程活跃', len(out) > 0, f'pid={out[:30]}')
except: check('前端进程活跃', False, '未运行')
# ============ 新增: 角色页 field 覆盖率 ============
view_map = {
# 角色: [(view文件, 原型HTML, 必须出现的字段)]
'manager': ('Workbench.vue', 'workbench.html', ['项目总数量','已结算的会议','已结题的项目','待结算的会议','未执行会议','待审核的专家']),
'doctor': ('Review.vue', None, ['项目评审','评审意见','评分']),
'executor': ('Meetings.vue', None, ['会议管理']),
'sponsor': ('Home.vue', 'sponsor-home.html', ['项目总数量','已结算数量','已结题数量','已执行会议','未执行会议']),
}
for role, (vf, proto, fields) in view_map.items():
view_file = Path(f'/home/john/ry8080/ry-vue3/src/views/{role}/{vf}')
if not view_file.exists():
check(f'{role}/{vf} 存在', False, '缺失'); continue
s = view_file.read_text()
miss_fields = [f for f in fields if f not in s]
check(f'{role}/{vf} 字段对齐 ({len(fields)})', not miss_fields, f'缺失={miss_fields}' if miss_fields else '全部对齐')
# ============ 新增: 公开门户字段对齐 ============
portal_views = {
'Home.vue': ['项目动态', '支持函', '邀请函', '公示'],
'Publicity.vue': ['项目编号', '项目名称', '项目形式', '查找'],
}
for vf, fields in portal_views.items():
f = Path(f'/home/john/ry8080/ry-vue3/src/views/portal/{vf}')
if not f.exists(): check(f'portal/{vf}', False, '缺失'); continue
s = f.read_text()
miss = [ff for ff in fields if ff not in s]
check(f'portal/{vf} 字段', not miss, f'缺失={miss}' if miss else '全')
# auth 检查(Login 在 auth 目录)
login_file = Path('/home/john/ry8080/ry-vue3/src/views/auth/Login.vue')
if login_file.exists():
login_s = login_file.read_text()
miss = [ff for ff in ['登录', '账号', '密码'] if ff not in login_s]
check('auth/Login.vue 字段', not miss, f'缺失={miss}' if miss else '全')
# ============ 新增: 跨角色流程(mock 检查接口链路)============
# 流程: 公示 -> 报名(支持/执行意向) -> 立项(项目) -> 会议(会议) -> 投稿(投稿) -> 评分(评分)
flow = [
('公示', '/business/public/index'),
('项目', '/business/projectPlan/list'),
('会议', '/business/meeting/list'),
('意向', '/business/supportIntent/list'),
('评分', '/business/scoring/list'),
]
for name, ep in flow:
r = http_get(ep)
check(f'流程环节-{name}', 'msg' in r, f"code={r.get('code')}")
# ============ 新增: 角色路由自动跳转 ============
login_js = Path('/home/john/ry8080/ry-vue3/src/views/auth/Login.vue').read_text()
check('登录按角色路由', 'manager' in login_js, '')
# ============ 新增: 业务表结构校验 ============
try:
out = subprocess.check_output(['mysql', '-h127.0.0.1', '-uroot', '-p123456', '-N', '-B', '-e', "SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_SCHEMA='ry0808' AND TABLE_NAME LIKE 'biz_%'"], stderr=subprocess.DEVNULL).decode().strip()
cnt = len([x for x in out.split('\n') if x.strip()])
check('biz_* 业务表数量', cnt >= 16, f'实际={cnt}')
except: check('biz_* 业务表数量', False, 'mysql err')
# ============ 新增: 原型对比统计 ============
total_proto = len(proto_files)
implemented = len(vue_views)
coverage = implemented * 100 / max(total_proto, 1)
check(f'原型覆盖率 (impl/proto)', coverage >= 30, f'{implemented}/{total_proto} = {coverage:.1f}%')
# ============ 新增: SCSS 主题色 ============
scss = Path('/home/john/ry8080/ry-vue3/src/assets/main.scss').read_text()
check('主题色定义', '#409eff' in scss, 'element plus 蓝')
# ============ 新增: 跨角色端到端流程 ============
# 1. 公开门户首页(匿名可访问)
r = http_get('/business/public/index')
data = r.get('data', {})
has_announce = bool(data.get('announcements')) if isinstance(data, dict) else False
check('流程1-公开首页有数据', has_announce or r.get('code') == 200, '')
# 2. 公开登录 + 角色路由
r = http_get('/captchaImage')
check('流程2-验证码可生成', r.get('code') == 200 or 'img' in json.dumps(r), f"code={r.get('code') if isinstance(r,dict) else 'blob'}")
# 3. 业务表数据校验(核心流程依赖)
for tbl in ['biz_project_plan','biz_project','biz_meeting','biz_announcement','biz_support_letter','biz_invitation','biz_expert','biz_support_intent','biz_execution_intent','biz_project_rating']:
try:
out = subprocess.check_output(['mysql', '-h127.0.0.1', '-uroot', '-p123456', '-N', '-B', '-e', f"SELECT COUNT(*) FROM ry0808.{tbl}"], stderr=subprocess.DEVNULL).decode().strip()
check(f'流程3-{tbl}', int(out) >= 0, f'rows={out}')
except: check(f'流程3-{tbl}', False, 'mysql err')
# 4. 字典覆盖(用作下拉选项)
r = http_get('/business/dict/types')
check('流程4-字典', r.get('code') == 200, f"data={r.get('msg','')[:60]}")
# 5. 用户绑定端点
r = http_get('/business/auth/login', {'username':'admin','password':'admin123'})
check('流程5-认证端点', 'msg' in r, f"code={r.get('code')}")
# ============ 新增: 角色权限检查(每个角色 home 可访问)============
for role_home in ['/manager/workbench','/doctor/review','/executor/meetings','/sponsor/home']:
path_only = role_home.lstrip('/')
name_guess = path_only.replace('/', '-')
check(f'角色 home: {role_home}', any(p in router_file for p in [
f"path: '{path_only}'", f"path: '/{path_only}'",
f"name: '{name_guess}'"
]), '')
# ============ 新增: 路由 meta 标题 ============
metas = re.findall(r"meta:\s*{\s*title:\s*'([^']+)'", router_file)
check(f'路由 meta 标题 ({len(metas)})', len(metas) >= 20, f'cnt={len(metas)}')
# ============ 新增: 公开门户 4 个详情端点 ============
for ep in ['/business/public/announcement/1','/business/public/supportLetter/1','/business/public/invitation/1']:
r = http_get(ep)
check(f'详情端点 {ep.split("/")[-2]}', 'msg' in r, f"code={r.get('code')}")
# ============ 新增: 角色 × view 映射完整性 ============
expected_role_views = {
'manager': ['Workbench.vue','Plans.vue','Projects.vue','Meetings.vue','Experts.vue','ExecUnits.vue','SupportUnits.vue','SupportIntent.vue','ExecIntent.vue','Accounts.vue'],
'doctor': ['Review.vue','Score.vue'],
'executor': ['Meetings.vue','Labor.vue'],
'sponsor': ['Home.vue','SupportLetter.vue','Records.vue'],
}
for role, files in expected_role_views.items():
for vf in files:
f = Path(f'/home/john/ry8080/ry-vue3/src/views/{role}/{vf}')
if f.exists():
s = f.read_text()
# 必须包含 <template> + <script setup>
ok = '<template>' in s and '<script setup>' in s
check(f'view 结构 {role}/{vf}', ok, 'OK' if ok else '缺 template/script')
# ============ 新增: store 持久化 ============
store_js = Path('/home/john/ry8080/ry-vue3/src/store/user.js').read_text()
check('pinia store 持久化', 'localStorage' in store_js, 'user.js')
# ============ 新增: axios 拦截器 ============
req_js = Path('/home/john/ry8080/ry-vue3/src/utils/request.js').read_text()
check('axios 拦截器', 'interceptors' in req_js, 'request.js')
# ============ 新增: 表单校验 ============
login_s = Path('/home/john/ry8080/ry-vue3/src/views/auth/Login.vue').read_text()
check('登录表单校验', 'rules' in login_s, 'Login.vue')
# ============ 新增: 错误处理 (ElMessage) ============
count_el = sum(1 for f in Path('/home/john/ry8080/ry-vue3/src/views').rglob('*.vue') if 'ElMessage' in f.read_text())
check(f'ElMessage 错误处理', count_el >= 5, f'cnt={count_el}')
# ============ 新增: vue 主入口完整性 ============
check('vue main.js 引入 router', "import router" in main_js, '')
for n in range(1, 21):
test_round(n)
print(f'\n{"="*60}\n20 轮测试汇总\n{"="*60}')
print(f'TOTAL: {PASS + FAIL}')
print(f'PASS : {PASS}')
print(f'FAIL : {FAIL}')
print(f'通过率: {PASS*100/(PASS+FAIL):.1f}%')
if FAIL_NAMES:
print(f'\nFAIL 项:')
for n in FAIL_NAMES: print(f' - {n}')