From 0e64b413ce97d45eeebefbb6c71802c105bed032 Mon Sep 17 00:00:00 2001
From: zlwu <704541015@qq.com>
Date: Fri, 30 Oct 2026 20:42:05 +0800
Subject: [PATCH] feat(oa): add permission-controlled shared finance remarks to
done center
Share one finance remark per approval instance across Web and mobile. Require the explicit permission and the current tenant user's own done task for every read and write; keep general instance responses free of financial remarks.
Add idempotent MySQL migration without automatic role grants, cross-end editors, isolated backend and frontend tests, and rollout requirements. Refs #7; platform validation and target database migration remain pending.
---
backend/app/api/oa_approval.py | 52 ++-
backend/app/models/models.py | 1 +
backend/app/services/oa_finance_remark.py | 100 ++++++
backend/tests/test_oa_finance_remark.py | 195 ++++++++++++
docs/README.md | 2 +
.../2026-10-30-oa-done-finance-remark.sql | 50 +++
mobile-h5/pages/oa/done/index.vue | 258 ++++++++++++---
mobile-h5/utils/oa-api.js | 16 +
.../changes/oa-done-finance-remark/design.md | 12 +
.../oa-done-finance-remark/proposal.md | 15 +
.../specs/oa-done-finance-remark/spec.md | 22 ++
.../changes/oa-done-finance-remark/tasks.md | 13 +
web-admin/scripts/test-oa-finance-remark.mjs | 299 ++++++++++++++++++
web-admin/src/api/oaApproval.ts | 17 +-
.../src/views/oa-approval/done/index.vue | 214 ++++++++++---
15 files changed, 1188 insertions(+), 78 deletions(-)
create mode 100644 backend/app/services/oa_finance_remark.py
create mode 100644 backend/tests/test_oa_finance_remark.py
create mode 100644 docs/sql/migrations/2026-10-30-oa-done-finance-remark.sql
create mode 100644 openspec/changes/oa-done-finance-remark/design.md
create mode 100644 openspec/changes/oa-done-finance-remark/proposal.md
create mode 100644 openspec/changes/oa-done-finance-remark/specs/oa-done-finance-remark/spec.md
create mode 100644 openspec/changes/oa-done-finance-remark/tasks.md
create mode 100644 web-admin/scripts/test-oa-finance-remark.mjs
diff --git a/backend/app/api/oa_approval.py b/backend/app/api/oa_approval.py
index a27e70b0..d5ab2881 100644
--- a/backend/app/api/oa_approval.py
+++ b/backend/app/api/oa_approval.py
@@ -2,7 +2,7 @@
from datetime import datetime
from typing import Any, Dict, List, Optional
-from fastapi import APIRouter, Depends, Query
+from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
@@ -15,6 +15,7 @@ from app.crud import oa_instance_draft as oid_crud
from app.crud import oa_receive_account as ora_crud
from app.models.models import SysUser
from app.services.oa_receive_account import normalize_account_fields
+from app.services import oa_finance_remark as finance_remark_svc
router = APIRouter(prefix="/web/admin/oa-approval", tags=["OA审批"])
@@ -1054,6 +1055,44 @@ async def list_oa_instance_form_attachments(
return error_response(400, str(e))
+class OaFinanceRemarkBody(BaseModel):
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
+ finance_remark: str = Field(..., alias="financeRemark", max_length=512, strict=True)
+
+
+@router.get("/instances/{instance_id}/finance-remark")
+async def get_oa_finance_remark(
+ instance_id: str,
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ try:
+ access = finance_remark_svc.resolve_access(db, user)
+ return success_response(finance_remark_svc.get_finance_remark(db, access, instance_id))
+ except finance_remark_svc.OaFinanceRemarkError as exc:
+ raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
+
+
+@router.put("/instances/{instance_id}/finance-remark")
+async def update_oa_finance_remark(
+ instance_id: str,
+ body: OaFinanceRemarkBody,
+ user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+):
+ try:
+ access = finance_remark_svc.resolve_access(db, user)
+ result = finance_remark_svc.update_finance_remark(db, access, instance_id, body.finance_remark)
+ db.commit()
+ return success_response(result, message="财务备注已保存")
+ except finance_remark_svc.OaFinanceRemarkError as exc:
+ db.rollback()
+ raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
+ except Exception:
+ db.rollback()
+ raise
+
+
@router.get("/instances/{instance_id}/bank-receipts")
async def list_oa_bank_receipts(
instance_id: str,
@@ -1365,7 +1404,8 @@ async def list_oa_tasks(
from app.services.oa_engine import OaEngineError, list_tasks
try:
- tenant_no = _get_tenant_no(db, user)
+ access = finance_remark_svc.resolve_access(db, user) if kind == "done" else None
+ tenant_no = access.tenant_no if access else _get_tenant_no(db, user)
data = list_tasks(
db,
tenant_no=tenant_no,
@@ -1381,7 +1421,13 @@ async def list_oa_tasks(
page=page,
page_size=pageSize,
)
- return success_response({"list": data.get("list") or [], "total": int(data.get("total") or 0)})
+ result = {"list": data.get("list") or [], "total": int(data.get("total") or 0)}
+ if access is not None:
+ finance_remark_svc.add_list_remarks(db, access, result["list"])
+ result["canManageFinanceRemark"] = access.can_manage
+ return success_response(result)
+ except finance_remark_svc.OaFinanceRemarkError as exc:
+ raise HTTPException(status_code=exc.status_code, detail=str(exc)) from exc
except OaEngineError as e:
return error_response(400, str(e))
diff --git a/backend/app/models/models.py b/backend/app/models/models.py
index 2e156311..60bf1632 100644
--- a/backend/app/models/models.py
+++ b/backend/app/models/models.py
@@ -920,6 +920,7 @@ class OaInstance(Base):
default="running",
comment="running/returned/completed/rejected/cancelled",
)
+ finance_remark = Column(String(512), nullable=True, comment="已办财务备注(专用权限控制)")
source_instance_id = Column(String(64), nullable=True, comment="撤销重提原实例ID")
current_round = Column(Integer, nullable=False, default=1, comment="当前提交轮次")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
diff --git a/backend/app/services/oa_finance_remark.py b/backend/app/services/oa_finance_remark.py
new file mode 100644
index 00000000..de9f4150
--- /dev/null
+++ b/backend/app/services/oa_finance_remark.py
@@ -0,0 +1,100 @@
+"""已办中心实例级财务备注;权限与本人已办范围同时满足才可访问。"""
+from dataclasses import dataclass
+from datetime import datetime
+from typing import Any, Dict, List
+
+from sqlalchemy.orm import Session
+
+from app.models.models import OaInstance, OaTask, SysUser
+from app.services.web_auth_roles import iter_user_menu_perm_codes
+
+PERMISSION = "oa:done:finance-remark"
+DONE_STATUSES = ("approved", "rejected", "returned", "transferred")
+
+
+class OaFinanceRemarkError(ValueError):
+ def __init__(self, status_code: int, message: str):
+ super().__init__(message)
+ self.status_code = status_code
+
+
+@dataclass(frozen=True)
+class FinanceRemarkAccess:
+ user_id: str
+ tenant_no: str
+ can_manage: bool
+
+
+def resolve_access(db: Session, user: dict) -> FinanceRemarkAccess:
+ # 不使用缺省租户、客户端租户或角色名称作为敏感字段的授权依据。
+ uid = str(user.get("user_id") or "").strip()
+ identity = db.query(SysUser.id, SysUser.tenant_no).filter(
+ SysUser.id == uid, SysUser.del_flag == 0, SysUser.status == "1"
+ ).first()
+ if not identity or not str(identity.tenant_no or "").strip():
+ raise OaFinanceRemarkError(403, "用户不可用")
+ tenant = str(identity.tenant_no).strip()
+ granted = iter_user_menu_perm_codes(db, uid, tenant_no=tenant)
+ return FinanceRemarkAccess(uid, tenant, PERMISSION in granted)
+
+
+def _accessible_instances(db: Session, access: FinanceRemarkAccess):
+ own_done = db.query(OaTask.id).filter(
+ OaTask.instance_id == OaInstance.id,
+ OaTask.tenant_no == access.tenant_no,
+ OaTask.assignee_user_id == access.user_id,
+ OaTask.task_type == "approval",
+ OaTask.status.in_(DONE_STATUSES),
+ OaTask.del_flag == 0,
+ ).exists()
+ return db.query(OaInstance).filter(
+ OaInstance.tenant_no == access.tenant_no,
+ OaInstance.del_flag == 0,
+ own_done,
+ )
+
+
+def _require_instance(db: Session, access: FinanceRemarkAccess, instance_id: str) -> OaInstance:
+ if not access.can_manage:
+ raise OaFinanceRemarkError(403, "无权查看或修改财务备注")
+ instance = _accessible_instances(db, access).filter(OaInstance.id == instance_id).first()
+ if instance is None:
+ raise OaFinanceRemarkError(404, "审批单不在本人已办范围内")
+ return instance
+
+
+def _remark_data(instance: OaInstance) -> Dict[str, str]:
+ return {"instanceId": instance.id, "financeRemark": instance.finance_remark or ""}
+
+
+def get_finance_remark(db: Session, access: FinanceRemarkAccess, instance_id: str) -> Dict[str, str]:
+ return _remark_data(_require_instance(db, access, instance_id))
+
+
+def update_finance_remark(
+ db: Session, access: FinanceRemarkAccess, instance_id: str, finance_remark: str
+) -> Dict[str, str]:
+ instance = _require_instance(db, access, instance_id)
+ if not isinstance(finance_remark, str) or len(finance_remark) > 512:
+ raise OaFinanceRemarkError(422, "财务备注最多 512 字")
+ instance.finance_remark = finance_remark
+ instance.update_by = access.user_id
+ instance.update_time = datetime.now()
+ db.flush()
+ return _remark_data(instance)
+
+
+def add_list_remarks(db: Session, access: FinanceRemarkAccess, rows: List[Dict[str, Any]]) -> None:
+ # 即使调用方未来调整序列化,也先移除敏感字段,再按权限补充。
+ for row in rows:
+ row.pop("financeRemark", None)
+ if not access.can_manage:
+ return
+ ids = {row.get("instanceId") for row in rows if row.get("instanceId")}
+ if not ids:
+ return
+ instances = _accessible_instances(db, access).filter(OaInstance.id.in_(ids)).all()
+ remarks = {inst.id: inst.finance_remark or "" for inst in instances}
+ for row in rows:
+ if row.get("instanceId") in remarks:
+ row["financeRemark"] = remarks[row["instanceId"]]
diff --git a/backend/tests/test_oa_finance_remark.py b/backend/tests/test_oa_finance_remark.py
new file mode 100644
index 00000000..11e56720
--- /dev/null
+++ b/backend/tests/test_oa_finance_remark.py
@@ -0,0 +1,195 @@
+"""OA 财务备注:真实隔离数据库和 HTTP 路由的读写/权限契约。"""
+import pytest
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.pool import StaticPool
+
+from app.api.oa_approval import router
+from app.core.auth import get_current_user
+from app.core.database import Base, get_db
+from app.models.models import (
+ OaApp, OaAppVersion, OaInstance, OaTask,
+ SysUser, SysRole, SysUserRole, SysMenu, SysRoleMenu,
+ SysPost, SysPostRole, SysUserPost,
+)
+
+PERM = 'oa:done:finance-remark'
+PREFIX = '/web/admin/oa-approval'
+URL = PREFIX + '/instances/i1/finance-remark'
+
+
+@pytest.fixture
+def case():
+ engine = create_engine('sqlite://', connect_args={'check_same_thread': False}, poolclass=StaticPool)
+ tables = (OaApp, OaAppVersion, OaInstance, OaTask, SysUser, SysRole,
+ SysUserRole, SysMenu, SysRoleMenu, SysPost, SysPostRole, SysUserPost)
+ Base.metadata.create_all(engine, tables=[m.__table__ for m in tables])
+ db = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)()
+ for uid, tenant in [('u1', 'a'), ('u2', 'a'), ('outsider', 'a'), ('other', 'b')]:
+ db.add(SysUser(id=uid, user_no=uid, nick_name=uid, password='unused', tenant_no=tenant, status='1'))
+ db.add_all([
+ OaApp(id='app', app_name='报销', tenant_no='a'),
+ OaAppVersion(id='v1', app_id='app', version_no=1, tenant_no='a'),
+ SysRole(id='finance', role_name='财务', tenant_no='a', status='1'),
+ SysMenu(id='menu', menu_name='财务备注', parent_id='done', menu_type='F', perms=PERM, tenant_no='a', status='1'),
+ SysRoleMenu(id='rm', role_id='finance', menu_id='menu', tenant_no='a'),
+ ])
+ for uid in ['u1', 'u2', 'outsider']:
+ db.add(SysUserRole(id='ur-'+uid, user_id=uid, role_id='finance', tenant_no='a'))
+ for iid, tenant in [('i1', 'a'), ('i2', 'a'), ('foreign', 'b')]:
+ inst = OaInstance(id=iid, app_id='app', app_version_id='v1', title=iid,
+ initiator_user_id='u1', tenant_no=tenant, status='completed', form_data_json='{}')
+ inst.finance_remark = '原备注'
+ db.add(inst)
+ for tid, uid, iid, tenant, status in [
+ ('t1', 'u1', 'i1', 'a', 'approved'), ('t2', 'u2', 'i1', 'a', 'rejected'),
+ ('pending', 'u1', 'i2', 'a', 'pending'), ('foreign-task', 'u1', 'foreign', 'b', 'approved'),
+ ]:
+ db.add(OaTask(id=tid, instance_id=iid, assignee_user_id=uid, tenant_no=tenant,
+ task_type='approval', status=status))
+ db.commit()
+ user = {'user_id': 'u1', 'tenant_no': 'a', 'role': 'admin'}
+ app = FastAPI()
+ app.include_router(router)
+ app.dependency_overrides[get_current_user] = lambda: user
+ app.dependency_overrides[get_db] = lambda: db
+ with TestClient(app) as client:
+ yield db, client, user
+ db.close()
+ engine.dispose()
+
+
+def test_shared_save_reload_and_clear(case):
+ db, client, user = case
+ result = client.get(URL)
+ assert result.status_code == 200
+ assert result.json()['data'] == {'instanceId': 'i1', 'financeRemark': '原备注'}
+ for text in ['财务核对完成\n<标签按原文显示>', '汉' * 512, '']:
+ user['user_id'] = 'u1'
+ response = client.put(URL, json={'financeRemark': text})
+ assert response.status_code == 200
+ assert response.json()['code'] == 200
+ db.expire_all()
+ assert db.get(OaInstance, 'i1').finance_remark == text
+ assert db.get(OaInstance, 'i1').update_by == 'u1'
+ user['user_id'] = 'u2'
+ assert client.get(URL).json()['data']['financeRemark'] == text
+ rows = client.get(PREFIX + '/tasks/done').json()['data']
+ assert rows['canManageFinanceRemark'] is True
+ assert rows['list'][0]['financeRemark'] == text
+
+
+@pytest.mark.parametrize('role', ['admin', 'super_admin'])
+def test_no_permission_hides_list_and_denies_both_routes(case, role):
+ db, client, user = case
+ db.get(SysRoleMenu, 'rm').del_flag = 1
+ db.commit()
+ user['role'] = role
+ data = client.get(PREFIX + '/tasks/done').json()['data']
+ assert data['canManageFinanceRemark'] is False
+ assert len(data['list']) == 1
+ assert 'financeRemark' not in data['list'][0]
+ assert client.get(URL).status_code == 403
+ assert client.put(URL, json={'financeRemark': '泄漏'}).status_code == 403
+
+
+@pytest.mark.parametrize('target', ['i2', 'foreign', 'missing'])
+def test_only_own_done_instances_are_accessible(case, target):
+ _, client, _ = case
+ url = PREFIX + '/instances/' + target + '/finance-remark'
+ assert client.get(url).status_code == 404
+ assert client.put(url, json={'financeRemark': '越权'}).status_code == 404
+
+
+def test_authorized_outsider_cannot_read_or_write(case):
+ _, client, user = case
+ user['user_id'] = 'outsider'
+ assert client.get(URL).status_code == 404
+ assert client.put(URL, json={'financeRemark': '越权'}).status_code == 404
+
+
+@pytest.mark.parametrize('model,key', [(OaInstance, 'i1'), (OaTask, 't1')])
+def test_deleted_instance_or_done_task_denies_access(case, model, key):
+ db, client, _ = case
+ db.get(model, key).del_flag = 1
+ db.commit()
+ assert client.get(URL).status_code == 404
+ assert client.put(URL, json={'financeRemark': '越权'}).status_code == 404
+ data = client.get(PREFIX + '/tasks/done').json()['data']
+ assert all('financeRemark' not in row for row in data['list'])
+
+
+@pytest.mark.parametrize('status', ['approved', 'rejected', 'returned', 'transferred'])
+def test_all_existing_done_statuses_can_manage(case, status):
+ db, client, _ = case
+ db.get(OaTask, 't1').status = status
+ db.commit()
+ assert client.get(URL).status_code == 200
+
+
+@pytest.mark.parametrize('status', ['pending', 'done', 'cancelled'])
+def test_non_done_statuses_cannot_manage(case, status):
+ db, client, _ = case
+ db.get(OaTask, 't1').status = status
+ db.commit()
+ assert client.get(URL).status_code == 404
+
+
+def test_forged_tenant_uses_real_user_tenant(case):
+ _, client, user = case
+ user['tenant_no'] = 'b'
+ assert client.get(URL).json()['data']['financeRemark'] == '原备注'
+ data = client.get(PREFIX + '/tasks/done').json()['data']
+ assert [r['instanceId'] for r in data['list']] == ['i1']
+ assert client.get(PREFIX + '/instances/foreign/finance-remark').status_code == 404
+
+
+@pytest.mark.parametrize('model,key,field,value', [
+ (SysUser, 'u1', 'status', '0'), (SysUser, 'u1', 'del_flag', 1),
+ (SysRole, 'finance', 'status', '0'), (SysRole, 'finance', 'tenant_no', 'b'),
+ (SysMenu, 'menu', 'status', '0'), (SysMenu, 'menu', 'tenant_no', 'b'),
+ (SysUserRole, 'ur-u1', 'tenant_no', 'b'),
+])
+def test_inactive_or_cross_tenant_grants_deny_access(case, model, key, field, value):
+ db, client, _ = case
+ setattr(db.get(model, key), field, value)
+ db.commit()
+ assert client.get(URL).status_code == 403
+ assert client.put(URL, json={'financeRemark': '越权'}).status_code == 403
+
+
+def test_post_role_permission_union(case):
+ db, client, _ = case
+ db.get(SysUserRole, 'ur-u1').del_flag = 1
+ db.add_all([
+ SysPost(id='post', post_name='财务岗', post_code='finance', tenant_no='a', status='1'),
+ SysUserPost(id='up', user_id='u1', post_id='post', tenant_no='a'),
+ SysPostRole(id='pr', post_id='post', role_id='finance', tenant_no='a'),
+ ])
+ db.commit()
+ assert client.get(URL).status_code == 200
+ db.get(SysPost, 'post').status = '0'
+ db.commit()
+ assert client.get(URL).status_code == 403
+
+
+@pytest.mark.parametrize('body', [
+ {}, {'financeRemark': None}, {'financeRemark': 123}, {'financeRemark': 'a' * 513},
+ {'financeRemark': 'note', 'tenantNo': 'b'}, {'financeRemark': 'note', 'status': 'running'},
+])
+def test_invalid_body_rejected_without_writes(case, body):
+ db, client, _ = case
+ assert client.put(URL, json=body).status_code == 422
+ db.expire_all()
+ assert db.get(OaInstance, 'i1').finance_remark == '原备注'
+
+
+def test_generic_instance_serialization_does_not_expose_finance_remark(case):
+ db, client, _ = case
+ from app.services.oa_engine import _instance_to_dict
+ assert 'financeRemark' not in _instance_to_dict(db.get(OaInstance, 'i1'))
+ for kind in ['mine', 'pending']:
+ data = client.get(PREFIX + '/tasks/' + kind).json()['data']
+ assert all('financeRemark' not in row for row in data['list'])
diff --git a/docs/README.md b/docs/README.md
index 73b1527d..ca3c1de2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -13,6 +13,8 @@
| 数据库 | [SQL 初始化与迁移](sql/) |
| 部署 | [部署导航](../deploy/README.md) |
+OA 已办财务备注上线前先执行 [2026-10-30 迁移](sql/migrations/2026-10-30-oa-done-finance-remark.sql),再更新应用;迁移不自动赋权。通过角色菜单配置授予已办中心下“财务备注(查看和修改)”权限(`oa:done:finance-remark`),同一权限作用于 Web 和移动端,仍限本人已办单据。
+
`docs/sql/zkxw.sql` 仍被交付打包和 Compose 引用,但当前初始化基线不完整,不能据此宣称可从空库部署。SOP HTML 是导入脚本的输入资源,继续保留。
历史细胞产品导入必须显式指定 `--file`,不再自带默认 TSV。实际业务测试保留在各应用源码目录;删除历史报告不代表现有测试已经执行或通过。
diff --git a/docs/sql/migrations/2026-10-30-oa-done-finance-remark.sql b/docs/sql/migrations/2026-10-30-oa-done-finance-remark.sql
new file mode 100644
index 00000000..f35d4d32
--- /dev/null
+++ b/docs/sql/migrations/2026-10-30-oa-done-finance-remark.sql
@@ -0,0 +1,50 @@
+-- OA 已办中心财务备注(Gitea #7),MySQL 5.7,先于应用代码部署。
+-- 在目标业务库执行;结构与菜单插入均可重复执行,不自动授予任何角色权限。
+-- 权限配置:在已办中心下勾选「财务备注(查看和修改)」;同一权限码作用于两端。
+-- 菜单按当前租户中既有 OA 已办页面定位,不挂载到旧工作流已办中心。
+
+SET @oa_finance_column_exists = (
+ SELECT COUNT(*) FROM information_schema.columns
+ WHERE table_schema = DATABASE() AND table_name = 'oa_instance'
+ AND column_name = 'finance_remark'
+);
+SET @oa_finance_ddl = IF(
+ @oa_finance_column_exists = 0,
+ 'ALTER TABLE oa_instance ADD COLUMN finance_remark VARCHAR(512) NULL COMMENT ''已办财务备注(专用权限控制)'' AFTER status',
+ 'SELECT ''oa_instance.finance_remark already exists'' AS migration_status'
+);
+PREPARE oa_finance_stmt FROM @oa_finance_ddl;
+EXECUTE oa_finance_stmt;
+DEALLOCATE PREPARE oa_finance_stmt;
+
+-- 由租户及父菜单生成稳定的 UUID 形状 ID,避免多行 SELECT 中 UUID 表达式被复用。
+-- 已有软删/停用权限节点不自动复活,由管理员决定是否恢复。
+INSERT INTO sys_menu (
+ id, menu_name, parent_id, menu_type, path, component, perms,
+ is_frame, visible, status, tenant_no, order_num, remark,
+ create_by, create_dept_id, create_time, update_by, update_time, del_flag
+)
+SELECT
+ INSERT(INSERT(MD5(CONCAT('oa-finance-remark:', parent.tenant_no, ':', parent.id)),
+ 13, 1, '3'), 17, 1, '8'),
+ '财务备注(查看和修改)', parent.id, 'F', '', NULL,
+ 'oa:done:finance-remark', 1, '1', '1', parent.tenant_no, 90,
+ '控制本人已办审批单的共享财务备注;Web 与移动端共用权限',
+ 'migration', '', NOW(), 'migration', NOW(), 0
+FROM sys_menu AS parent
+WHERE parent.del_flag = 0 AND parent.status = '1' AND parent.menu_type = 'C'
+ AND (
+ parent.perms IN ('oa:done', 'mini:oa:done')
+ OR TRIM(BOTH '/' FROM parent.component) = 'oa-approval/done/index'
+ OR TRIM(BOTH '/' FROM parent.path) IN ('pages/oa/done/index', 'oa/done/index')
+ )
+ AND NOT EXISTS (
+ SELECT 1 FROM sys_menu AS existing
+ WHERE existing.parent_id = parent.id
+ AND existing.tenant_no = parent.tenant_no
+ AND existing.perms = 'oa:done:finance-remark'
+ );
+
+-- 验证必须至少存在当前租户的权限节点;若无结果,先核对该租户 OA 已办菜单配置。
+SELECT tenant_no, parent_id, id, menu_name, perms, status
+FROM sys_menu WHERE perms = 'oa:done:finance-remark' AND del_flag = 0;
diff --git a/mobile-h5/pages/oa/done/index.vue b/mobile-h5/pages/oa/done/index.vue
index 3190dea7..e8e0597b 100644
--- a/mobile-h5/pages/oa/done/index.vue
+++ b/mobile-h5/pages/oa/done/index.vue
@@ -32,11 +32,7 @@
@@ -58,23 +54,22 @@
}}
-
+
{{ line }}
+
@@ -82,12 +77,52 @@
加载更多…
已经到底了
+
+
+
+