refactor: 合并 biz_support_unit/biz_execution_unit/biz_service_org 为 biz_org, 删除 2 张孤儿表, 改 biz_person.work_unit → org_id FK

主要改动:
- SQL: biz_support_unit → biz_org, 加 org_type, 加 business_nature; 删 biz_execution_unit, biz_service_org
- SQL: biz_project.support_unit_id/name + service_org_id/name → org_id/name/type
- SQL: biz_meeting.support_unit_name → org_name
- SQL: biz_person.work_unit → org_id (FK)
- 后端: 新 BizOrg entity/mapper/service/controller
- 后端: BizProject/BizMeeting 字段重命名
- 后端: 删 12 个 BizSupportUnit/BizExecutionUnit/BizServiceOrg Java 文件
- 后端: BizPersonImportVO.workUnit → orgName (导入时查 biz_org 取 org_id)
- 后端: BizAuthController.registerExecutor 完整实现 (原 registerSupplier stub)
- 前端: 新 admin/Orgs.vue + manager/Orgs.vue (原 SupportUnits)
- 前端: RegisterExecutor.vue (原 RegisterSupplier, 单页 2 步)
- 前端: sponsor/executor/manager 多个文件 workUnit → orgId/orgName 重命名
- 前端: 统一 supplier → executor, 业务命名 sponsor(赞助方) / executor(执行方=供应商)
- 前端: 全工程 execution → executor (company type / role / person unit_type)
This commit is contained in:
郭庆泰
2026-08-15 12:41:10 +08:00
commit cf5790229a
694 changed files with 102090 additions and 0 deletions
+348
View File
@@ -0,0 +1,348 @@
#!/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',
'/leader/home', '/leader/projects', '/leader/meetings', '/leader/account',
'/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 替代
# 期望 router 中存在 path: 'leader/home' 或 name: 'leader-home'
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, 必须出现的字段)]
'leader': ('Home.vue', 'leader-home.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('登录按角色路由', 'leader' in login_js and '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 ['/leader/home','/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 = {
'leader': ['Home.vue','Projects.vue','Meetings.vue','Account.vue'],
'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}')