diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index ea4707df7..2c1b9d860 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -14,10 +14,15 @@ from app.api.pl.vehicle_fuel_card import router as vehicle_fuel_card_router from app.api.pl.dispatch_calendar import router as dispatch_calendar_router from app.api.pl.site_dispatch_calendar import router as site_dispatch_calendar_router from app.api.wechat_notices import router as wechat_notices_router -from app.api.pl.quota_cert import router as quota_cert_router, legal_router as legal_use_document_router +from app.api.pl.quota_cert import ( + router as quota_cert_router, + legal_router as legal_use_document_router, + cert_ready_router, +) __all__ = [ "quota_cert_router", + "cert_ready_router", "legal_use_document_router", "wechat_notices_router", "public_router", diff --git a/backend/app/api/pl/quota_cert.py b/backend/app/api/pl/quota_cert.py index 07c0969f7..5da4bd085 100644 --- a/backend/app/api/pl/quota_cert.py +++ b/backend/app/api/pl/quota_cert.py @@ -11,7 +11,7 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user -from app.core.menu_guard import require_menu_code +from app.core.menu_guard import get_user_menu_codes, require_admin_or_super_admin, require_menu_code from app.core.oper_log_decorator import oper_log from app.core.tenant_context import get_tenant_no from app.schemas.pl.quota_cert import InputBody, CertConfirmBody, CertVerifyBody, CertTotalAdjustBody @@ -36,6 +36,26 @@ router = APIRouter(prefix='/transport/quota-certs', tags=['购买证与运输证 dependencies=[Depends(require_menu_code('pl:quota-cert:list'))]) legal_router = APIRouter(prefix='/transport/legal-use-documents', tags=['合法使用证明'], dependencies=[Depends(require_menu_code('pl:legal-use-document:list'))]) + + +def _require_cert_ready_menu( + current_user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + """证书齐备页面权限。小程序运输证件仍走原入口,不要求单独的 Web 菜单。""" + if current_user.get("role") == "super_admin": + return current_user + codes = get_user_menu_codes(db, current_user.get("user_id")) + if "pl:cert-ready:list" in codes or "mini:pl:plan-cert:list" in codes: + return current_user + raise HTTPException(status_code=403, detail="无菜单访问权限") + + +cert_ready_router = APIRouter( + prefix='/transport/quota-certs', + tags=['证书齐备'], + dependencies=[Depends(_require_cert_ready_menu)], +) CertType = Literal['purchase', 'transport'] @@ -86,7 +106,7 @@ def plan_overview(keyword: str = '', status: Optional[str] = None, conversion_only=conversion_only)) -@router.get('/plans/cert-ready') +@cert_ready_router.get('/plans/cert-ready') def list_cert_ready(keyword: str = '', waived: Optional[str] = Query(None, max_length=1), plan_no: Optional[str] = Query(None, alias='planNo', max_length=200), license_plate: Optional[str] = Query(None, alias='licensePlate', max_length=100), @@ -111,7 +131,7 @@ def list_cert_ready(keyword: str = '', waived: Optional[str] = Query(None, max_l waived_time_to=waived_time_to)) -@router.post('/plans/{plan_id}/cert-ready') +@cert_ready_router.post('/plans/{plan_id}/cert-ready') def mark_cert_ready(plan_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): from app.services.pl_quota_waiver import mark_ready return _write(db, lambda: mark_ready(db, user, plan_id)) diff --git a/backend/app/main.py b/backend/app/main.py index ca297dabc..84512d23b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -36,6 +36,7 @@ from app.api import ( site_dispatch_calendar_router, wechat_notices_router, quota_cert_router, + cert_ready_router, legal_use_document_router, ) from app.api.pl import pl_router @@ -185,6 +186,7 @@ app.include_router(bx_router, prefix=settings.API_V1_PREFIX) app.include_router(db_router, prefix=settings.API_V1_PREFIX) app.include_router(pl_router, prefix=settings.API_V1_PREFIX) app.include_router(quota_cert_router, prefix=settings.API_V1_PREFIX) +app.include_router(cert_ready_router, prefix=settings.API_V1_PREFIX) app.include_router(legal_use_document_router, prefix=settings.API_V1_PREFIX) app.include_router(vehicle_fuel_card_router, prefix=settings.API_V1_PREFIX) app.include_router(dispatch_calendar_router, prefix=settings.API_V1_PREFIX) diff --git a/docs/sql/migrations/2026-11-06-cert-ready-menu-perm.sql b/docs/sql/migrations/2026-11-06-cert-ready-menu-perm.sql new file mode 100644 index 000000000..2bc486280 --- /dev/null +++ b/docs/sql/migrations/2026-11-06-cert-ready-menu-perm.sql @@ -0,0 +1,15 @@ +-- 「证书齐备」与「计划段次用证」拆开权限码。 +-- 仍勾选该菜单的角色自动获得 pl:cert-ready:list;已解除授权的角色不会恢复。 +-- MySQL 5.7。可重复执行。仅 shanxia2.0。 +SET NAMES utf8mb4; + +UPDATE sys_menu +SET perms='pl:cert-ready:list', + update_by='system', + update_time=NOW() +WHERE id='PL_CERT_READY' + AND tenant_no='shanxia2.0' + AND del_flag=0 + AND menu_type='C' + AND path='cert-ready' + AND perms='pl:plan-cert:list'; diff --git a/docs/sql/migrations/2026-11-06-restore-transport-cert-menu-folder.sql b/docs/sql/migrations/2026-11-06-restore-transport-cert-menu-folder.sql new file mode 100644 index 000000000..60cb9911a --- /dev/null +++ b/docs/sql/migrations/2026-11-06-restore-transport-cert-menu-folder.sql @@ -0,0 +1,79 @@ +-- 恢复 Web「运输证件管理」目录,并把五个证件页面挂回其下。 +-- 不改子页面、按钮的权限码,不删除已有角色授权;仅为目录补齐已拥有任一子页面的角色。 +-- MySQL 5.7。可重复执行。仅 shanxia2.0。 +SET NAMES utf8mb4; +START TRANSACTION; + +UPDATE sys_menu +SET menu_name='运输证件管理', + parent_id='PL_MENU_ROOT', + menu_type='M', + path='cert-upload', + visible='1', + status='1', + order_num=3, + del_flag=0, + remark='运输证件管理目录', + update_by='system', + update_time=NOW() +WHERE id='PL_GROUP_CERT' + AND tenant_no='shanxia2.0' + AND parent_id='PL_MENU_ROOT'; + +UPDATE sys_menu +SET parent_id='PL_GROUP_CERT', + order_num=CASE id + WHEN 'PL_QUOTA_CERT' THEN 1 + WHEN 'PL_CONTRACT_QUOTA' THEN 2 + WHEN 'PL_CERTIFICATE_USAGE' THEN 3 + WHEN 'PL_PLAN_CERT' THEN 4 + WHEN 'PL_CERT_READY' THEN 5 + END, + update_by='system', + update_time=NOW() +WHERE tenant_no='shanxia2.0' + AND del_flag=0 + AND menu_type='C' + AND id IN ( + 'PL_QUOTA_CERT', + 'PL_CONTRACT_QUOTA', + 'PL_CERTIFICATE_USAGE', + 'PL_PLAN_CERT', + 'PL_CERT_READY' + ) + AND parent_id IN ('PL_MENU_ROOT', 'PL_GROUP_CERT'); + +INSERT INTO sys_role_menu ( + id, role_id, menu_id, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_time, del_flag +) +SELECT MD5(CONCAT(access_row.role_id, '|PL_GROUP_CERT|cert-folder|v1')), + access_row.role_id, 'PL_GROUP_CERT', access_row.tenant_no, 0, + '恢复运输证件管理目录:沿用已有子菜单角色', + 'system', '', NOW(), NOW(), 0 +FROM ( + SELECT DISTINCT role_id, tenant_no + FROM sys_role_menu + WHERE del_flag=0 + AND tenant_no='shanxia2.0' + AND menu_id IN ( + 'PL_QUOTA_CERT', + 'PL_CONTRACT_QUOTA', + 'PL_CERTIFICATE_USAGE', + 'PL_PLAN_CERT', + 'PL_CERT_READY' + ) +) access_row +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_menu existing + WHERE existing.role_id=access_row.role_id + AND existing.menu_id='PL_GROUP_CERT' + AND existing.tenant_no=access_row.tenant_no + AND existing.del_flag=0 +) +ON DUPLICATE KEY UPDATE + del_flag=0, + update_time=NOW(), + remark='恢复运输证件管理目录:沿用已有子菜单角色'; + +COMMIT; diff --git a/web-admin/scripts/quota-statistics.test.ts b/web-admin/scripts/quota-statistics.test.ts index cba328445..9cd2f2329 100644 --- a/web-admin/scripts/quota-statistics.test.ts +++ b/web-admin/scripts/quota-statistics.test.ts @@ -165,7 +165,7 @@ test('cert tabs keep their identities and legal proof is a sibling menu', () => 'pl:quota-cert:list', 'pl:quota-cert:list', 'pl:plan-cert:list', - 'pl:plan-cert:list' + 'pl:cert-ready:list' ] ) assert.equal(legal.children![0].id, 'PL_LEGAL_USE_DOCUMENT') diff --git a/web-admin/src/router/modules/pl.ts b/web-admin/src/router/modules/pl.ts index 02d9bd6fe..d5314a9c5 100644 --- a/web-admin/src/router/modules/pl.ts +++ b/web-admin/src/router/modules/pl.ts @@ -94,7 +94,7 @@ export const plRoutes: AppRouteRecord[] = [ name: 'PlCertReady', path: 'cert-ready', component: '/pl/cert-ready/index', - meta: { title: '证书齐备', keepAlive: false, menuCode: 'pl:plan-cert:list' } + meta: { title: '证书齐备', keepAlive: false, menuCode: 'pl:cert-ready:list' } }, { id: 'PL_REDIRECT_LEGAL_USE_OLD', @@ -286,7 +286,7 @@ export const plRoutes: AppRouteRecord[] = [ name: 'PlCertReadyRedirect', path: 'cert-ready', redirect: '/pl/cert-upload/cert-ready', - meta: { title: '证书齐备', isHide: true } + meta: { title: '证书齐备', isHide: true, menuCode: 'pl:cert-ready:list' } }, { id: 'PL_REDIRECT_FREIGHT', diff --git a/web-admin/src/utils/router/menu-icon.ts b/web-admin/src/utils/router/menu-icon.ts index 1ffac1778..a7c3279a9 100644 --- a/web-admin/src/utils/router/menu-icon.ts +++ b/web-admin/src/utils/router/menu-icon.ts @@ -150,6 +150,7 @@ export const SIDEBAR_MENU_ICON_BY_CODE: Record = { 'pl:plan:list': 'ri:route-line', 'pl:plan-archive:list': 'ri:folder-check-line', 'pl:plan-cert:list': 'ri:shield-check-line', + 'pl:cert-ready:list': 'ri:shield-check-line', 'pl:freight:list': 'ri:money-cny-box-line', 'pl:freight-approval:list': 'ri:checkbox-circle-line', 'pl:freight-disburse:list': 'ri:bank-card-line',