From f8b25a7ccc6b52a2a16253e8e7bc6233f6f3baac Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Wed, 20 May 2026 14:32:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=91=98=E5=B7=A5?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E3=80=81=E6=8A=A5=E9=94=80=E5=8D=95=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E5=8F=8A=E5=89=8D=E7=AB=AF=E4=BA=A4=E4=BA=92=E7=BB=86?= =?UTF-8?q?=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复员工创建时组织架构关联与邮箱校验逻辑 - 修复报销单API端点参数及预审流程调用 - 优化审批中心、差旅详情等前端页面交互 - 更新侧边栏导航与请求视图模型 - 补充员工服务与报销单相关测试用例 --- .../app/api/v1/endpoints/reimbursements.py | 2 +- server/src/app/services/employee.py | 34 +++++++++++++------ server/src/app/services/expense_claims.py | 8 ++--- server/storage/knowledge/.index.json | 4 +-- server/tests/test_employee_service.py | 10 ++++++ server/tests/test_expense_claim_service.py | 2 +- web/src/components/layout/SidebarRail.vue | 2 +- web/src/composables/useRequests.js | 10 +++++- web/src/utils/requestViewModel.js | 2 +- web/src/views/ApprovalCenterView.vue | 2 +- web/src/views/TravelRequestDetailView.vue | 14 ++++---- web/src/views/scripts/ApprovalCenterView.js | 4 +-- .../views/scripts/EmployeeManagementView.js | 22 ++++++------ .../views/scripts/TravelRequestDetailView.js | 20 ++++++----- 14 files changed, 84 insertions(+), 52 deletions(-) diff --git a/server/src/app/api/v1/endpoints/reimbursements.py b/server/src/app/api/v1/endpoints/reimbursements.py index 437517e..82cf3a8 100644 --- a/server/src/app/api/v1/endpoints/reimbursements.py +++ b/server/src/app/api/v1/endpoints/reimbursements.py @@ -420,7 +420,7 @@ def submit_expense_claim(claim_id: str, db: DbSession, current_user: CurrentUser "/claims/{claim_id}/return", response_model=ExpenseClaimRead, summary="退回报销单", - description="财务人员或高级管理人员可将可见报销单退回到待补充状态。", + description="财务人员或高级管理人员可将可见报销单退回到待提交状态。", responses={ status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, diff --git a/server/src/app/services/employee.py b/server/src/app/services/employee.py index ebddc4f..7c356f8 100644 --- a/server/src/app/services/employee.py +++ b/server/src/app/services/employee.py @@ -1,8 +1,9 @@ from __future__ import annotations from collections import Counter -from datetime import date, datetime +from datetime import UTC, date, datetime from typing import Any +from zoneinfo import ZoneInfo from sqlalchemy import inspect, select, text from sqlalchemy.orm import Session @@ -49,6 +50,7 @@ from app.services.employee_seed import ( logger = get_logger("app.services.employee") DEFAULT_EMPLOYEE_PASSWORD = "123456" MAX_EMPLOYEE_CHANGE_LOGS = 5 +DISPLAY_TIMEZONE = ZoneInfo("Asia/Shanghai") STATUS_TONE_MAP = { "在职": "success", @@ -183,7 +185,7 @@ class EmployeeService: sync_state=payload.sync_state, spotlight=payload.spotlight, password_hash=hash_password(DEFAULT_EMPLOYEE_PASSWORD), - last_sync_at=datetime.now(), + last_sync_at=datetime.now(UTC), ) if payload.organization_unit_code: @@ -360,7 +362,7 @@ class EmployeeService: if not changed_fields and not password_changed and not role_changed: return self._serialize_employee(employee) - now = datetime.now() + now = datetime.now(UTC) employee.last_sync_at = now employee.sync_state = "已同步" @@ -401,7 +403,7 @@ class EmployeeService: if employee.employment_status == "停用": return self._serialize_employee(employee) - now = datetime.now() + now = datetime.now(UTC) employee.employment_status = "停用" employee.sync_state = "已同步" employee.last_sync_at = now @@ -422,7 +424,7 @@ class EmployeeService: if employee.employment_status != "停用": return self._serialize_employee(employee) - now = datetime.now() + now = datetime.now(UTC) employee.employment_status = "在职" employee.sync_state = "已同步" employee.last_sync_at = now @@ -484,7 +486,7 @@ class EmployeeService: logger.exception("Employee import failed during database write") raise - imported_at = self._format_datetime(datetime.now()) or "" + imported_at = self._format_datetime(datetime.now(UTC)) or "" message = f"导入成功:新增 {summary['created']} 人,更新 {summary['updated']} 人。" logger.info( "Imported employees created=%d updated=%d total=%d", @@ -624,7 +626,7 @@ class EmployeeService: } created = 0 updated = 0 - now = datetime.now() + now = datetime.now(UTC) try: for row in rows: @@ -979,7 +981,7 @@ class EmployeeService: employee=employee, action=action, owner=owner, - occurred_at=occurred_at or datetime.now(), + occurred_at=occurred_at or datetime.now(UTC), ) ) @@ -1106,19 +1108,29 @@ class EmployeeService: return None return value.strftime("%Y-%m-%d") + @staticmethod + def _to_display_datetime(value: datetime) -> datetime: + if value.tzinfo is None: + normalized = value.replace(tzinfo=UTC) + else: + normalized = value.astimezone(UTC) + return normalized.astimezone(DISPLAY_TIMEZONE) + @staticmethod def _format_datetime(value: datetime | None) -> str | None: if value is None: return None - return value.strftime("%Y-%m-%d %H:%M") + local = EmployeeService._to_display_datetime(value) + return local.strftime("%Y-%m-%d %H:%M") @staticmethod def _format_history_datetime(value: datetime | None) -> str: if value is None: return "" + local = EmployeeService._to_display_datetime(value) return ( - f"{value.year}年{value.month}月{value.day}日" - f"{value.hour}时{value.minute}分{value.second}秒" + f"{local.year}年{local.month}月{local.day}日" + f"{local.hour}时{local.minute}分" ) @staticmethod diff --git a/server/src/app/services/expense_claims.py b/server/src/app/services/expense_claims.py index ccacf5d..8bf8f17 100644 --- a/server/src/app/services/expense_claims.py +++ b/server/src/app/services/expense_claims.py @@ -867,13 +867,13 @@ class ExpenseClaimService: "source": "manual_return", "severity": "medium", "label": "人工退回", - "message": return_reason or f"{operator} 已退回该报销单,请申请人补充后重新提交。", + "message": return_reason or f"{operator} 已退回该报销单,请申请人调整后重新提交。", "operator": operator, "created_at": datetime.now(UTC).isoformat(), } claim.status = "returned" - claim.approval_stage = "待补充" + claim.approval_stage = "待提交" claim.risk_flags_json = [*list(claim.risk_flags_json or []), return_flag] self.db.commit() @@ -2634,8 +2634,8 @@ class ExpenseClaimService: def _ensure_draft_claim(self, claim: ExpenseClaim) -> None: normalized_status = str(claim.status or "").strip().lower() - if normalized_status not in {"draft", "supplement"}: - raise ValueError("只有草稿或待补充状态的报销单才允许执行该操作。") + if normalized_status not in {"draft", "supplement", "returned"}: + raise ValueError("只有草稿、待补充或退回待提交状态的报销单才允许执行该操作。") def _run_ai_submission_review(self, claim: ExpenseClaim) -> dict[str, Any]: base_flags = list(claim.risk_flags_json or []) diff --git a/server/storage/knowledge/.index.json b/server/storage/knowledge/.index.json index 9e38834..480ee35 100644 --- a/server/storage/knowledge/.index.json +++ b/server/storage/knowledge/.index.json @@ -14,8 +14,8 @@ "updated_at": "2026-05-17T09:28:28.999515+00:00", "uploaded_by": "admin", "version_number": 1, - "ingest_status": 3, - "ingest_status_updated_at": "2026-05-17T10:01:33.272539+00:00", + "ingest_status": 1, + "ingest_status_updated_at": "2026-05-20T06:29:01.123795+00:00", "ingest_completed_at": "2026-05-17T10:01:33.272539+00:00", "ingest_document_name": "远光《公司支出管理办法(2024)》.pdf", "ingest_document_updated_at": "2026-05-17T09:28:28.999515+00:00", diff --git a/server/tests/test_employee_service.py b/server/tests/test_employee_service.py index 075dc81..636d1ad 100644 --- a/server/tests/test_employee_service.py +++ b/server/tests/test_employee_service.py @@ -1,5 +1,7 @@ from __future__ import annotations +from datetime import UTC, datetime + import pytest from sqlalchemy import create_engine, func, select from sqlalchemy.orm import Session, sessionmaker @@ -241,6 +243,14 @@ def test_update_employee_changes_manager() -> None: assert updated.manager == manager.name +def test_format_history_datetime_uses_local_timezone_without_seconds() -> None: + value = datetime(2026, 5, 20, 6, 30, 45, tzinfo=UTC) + formatted = EmployeeService._format_history_datetime(value) + + assert formatted == "2026年5月20日14时30分" + assert "秒" not in formatted + + def test_update_employee_rejects_invalid_date_format() -> None: with build_session() as db: service = EmployeeService(db) diff --git a/server/tests/test_expense_claim_service.py b/server/tests/test_expense_claim_service.py index 37a1fa5..9758784 100644 --- a/server/tests/test_expense_claim_service.py +++ b/server/tests/test_expense_claim_service.py @@ -1312,7 +1312,7 @@ def test_privileged_user_can_return_and_delete_submitted_claim() -> None: assert returned is not None assert returned.status == "returned" - assert returned.approval_stage == "待补充" + assert returned.approval_stage == "待提交" assert any( isinstance(flag, dict) and flag.get("source") == "manual_return" diff --git a/web/src/components/layout/SidebarRail.vue b/web/src/components/layout/SidebarRail.vue index 4aede72..77accbd 100644 --- a/web/src/components/layout/SidebarRail.vue +++ b/web/src/components/layout/SidebarRail.vue @@ -71,7 +71,7 @@ const sidebarMeta = { overview: { label: '总览' }, workbench: { label: '个人工作台' }, requests: { label: '个人报销' }, - approval: { label: '审批中心', badge: '12' }, + approval: { label: '审批中心' }, policies: { label: '知识管理' }, audit: { label: '任务规则中心' }, logs: { label: '日志管理' }, diff --git a/web/src/composables/useRequests.js b/web/src/composables/useRequests.js index d2073ed..d560347 100644 --- a/web/src/composables/useRequests.js +++ b/web/src/composables/useRequests.js @@ -112,7 +112,11 @@ function resolveApprovalMeta(status) { return { key: 'draft', label: '草稿', tone: 'draft' } } - if (['supplement', 'returned'].includes(normalized)) { + if (normalized === 'returned') { + return { key: 'supplement', label: '待提交', tone: 'warning' } + } + + if (normalized === 'supplement') { return { key: 'supplement', label: '待补充', tone: 'warning' } } @@ -128,6 +132,10 @@ function resolveApprovalMeta(status) { } function resolveWorkflowNode(claim, approvalMeta) { + if (String(claim?.status || '').trim().toLowerCase() === 'returned') { + return '待提交' + } + const rawNode = String(claim?.approval_stage || '').trim() if (rawNode) { diff --git a/web/src/utils/requestViewModel.js b/web/src/utils/requestViewModel.js index 4b3f35c..c650c6e 100644 --- a/web/src/utils/requestViewModel.js +++ b/web/src/utils/requestViewModel.js @@ -74,7 +74,7 @@ const BACKEND_STATUS_META = { paid: { key: 'completed', label: '已完成', tone: 'success' }, completed: { key: 'completed', label: '已完成', tone: 'success' }, supplement: { key: 'supplement', label: '待补充', tone: 'warning' }, - returned: { key: 'supplement', label: '待补充', tone: 'warning' }, + returned: { key: 'supplement', label: '待提交', tone: 'warning' }, rejected: { key: 'rejected', label: '已退回', tone: 'danger' }, cancelled: { key: 'rejected', label: '已退回', tone: 'danger' } } diff --git a/web/src/views/ApprovalCenterView.vue b/web/src/views/ApprovalCenterView.vue index 5dfa4e5..9aed38e 100644 --- a/web/src/views/ApprovalCenterView.vue +++ b/web/src/views/ApprovalCenterView.vue @@ -535,7 +535,7 @@ badge="退回单据" badge-tone="warning" :title="`确认退回 ${selectedRow?.id || ''} 吗?`" - description="退回后该单据会进入待补充状态,申请人需要补充后重新提交。" + description="退回后该单据会回到待提交状态,申请人需要调整后重新提交并再次经过 AI 预审。" cancel-text="取消" confirm-text="确认退回" busy-text="退回中..." diff --git a/web/src/views/TravelRequestDetailView.vue b/web/src/views/TravelRequestDetailView.vue index 4d11440..2f81c2b 100644 --- a/web/src/views/TravelRequestDetailView.vue +++ b/web/src/views/TravelRequestDetailView.vue @@ -82,7 +82,7 @@ 智能录入 -
+