feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
# =============================================================================
|
||||
# IT智能服务台 — 资产升级审批智能推送功能测试
|
||||
# =============================================================================
|
||||
# 测试覆盖:
|
||||
# AssetService:
|
||||
# 1. find_asset — 查找资产(存在/不存在/大小写/空格)
|
||||
# 2. _parse_date — 多种日期格式解析
|
||||
# 3. _format_years — 年限格式化
|
||||
# 4. check_device_age — 完整核查流程
|
||||
# 5. _format_opinion — 审批意见生成
|
||||
# approval.py:
|
||||
# 6. _extract_asset_code — 提取资产编号(Text/Selector/未找到)
|
||||
# 7. _extract_current_approver — 提取审批人(list/dict/无审批中)
|
||||
# 8. _build_urge_description — 构建卡片描述
|
||||
# =============================================================================
|
||||
|
||||
import os
|
||||
from datetime import date, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.asset_service import AssetService
|
||||
from app.api.approval import (
|
||||
_extract_asset_code,
|
||||
_extract_current_approver,
|
||||
_build_urge_description,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 测试常量
|
||||
# =============================================================================
|
||||
|
||||
EXCEL_PATH = r"D:\资料\00-工作文件\03-资产管理\固定资产清单\资产记录\2025资产\2025资产1~12.xlsx"
|
||||
KNOWN_ASSET_CODE = "01011801-02012-041698"
|
||||
|
||||
# 检查 Excel 文件是否存在(不存在则跳过依赖真实文件的测试)
|
||||
excel_exists = os.path.isfile(EXCEL_PATH)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AssetService — find_asset 测试
|
||||
# =============================================================================
|
||||
|
||||
class TestFindAsset:
|
||||
"""find_asset 方法测试"""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def asset_service(self):
|
||||
"""创建 AssetService 实例(类级共享,避免重复加载 18000 行 Excel)"""
|
||||
if not excel_exists:
|
||||
pytest.skip(f"Excel 文件不存在: {EXCEL_PATH}")
|
||||
service = AssetService(excel_path=EXCEL_PATH)
|
||||
yield service
|
||||
service.close()
|
||||
|
||||
def test_find_asset_found(self, asset_service):
|
||||
"""查找已知存在的资产编号,验证返回dict包含正确字段"""
|
||||
result = asset_service.find_asset(KNOWN_ASSET_CODE)
|
||||
assert result is not None, f"未找到资产编号: {KNOWN_ASSET_CODE}"
|
||||
assert isinstance(result, dict)
|
||||
# 验证关键字段存在
|
||||
assert result.get("固定资产编码") is not None
|
||||
assert "sheet_name" in result
|
||||
assert "固定资产名称" in result
|
||||
assert "开始使用日期" in result
|
||||
|
||||
def test_find_asset_not_found(self, asset_service):
|
||||
"""查找不存在的资产编号,验证返回 None"""
|
||||
result = asset_service.find_asset("NONEXISTENT-CODE-99999")
|
||||
assert result is None
|
||||
|
||||
def test_find_asset_case_insensitive(self, asset_service):
|
||||
"""大小写不敏感匹配测试"""
|
||||
# 资产编号全小写搜索(编号本身无字母,验证 lower() 逻辑不报错)
|
||||
result = asset_service.find_asset(KNOWN_ASSET_CODE.lower())
|
||||
assert result is not None, "大小写不敏感匹配失败"
|
||||
|
||||
def test_find_asset_trim_whitespace(self, asset_service):
|
||||
"""前后空格trim测试"""
|
||||
result = asset_service.find_asset(f" {KNOWN_ASSET_CODE} ")
|
||||
assert result is not None, "trim 空格后匹配失败"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AssetService — _parse_date 测试
|
||||
# =============================================================================
|
||||
|
||||
class TestParseDate:
|
||||
"""_parse_date 方法测试(纯逻辑,不需要 Excel 文件)"""
|
||||
|
||||
@pytest.fixture
|
||||
def service(self):
|
||||
"""创建 AssetService 实例(不触发 Excel 加载)"""
|
||||
return AssetService(excel_path=EXCEL_PATH)
|
||||
|
||||
def test_parse_date_formats(self, service):
|
||||
"""测试多种日期格式解析:datetime/date/字符串/Excel序列号"""
|
||||
# datetime 对象 → 取 date 部分
|
||||
dt = datetime(2025, 1, 23, 10, 30, 0)
|
||||
assert service._parse_date(dt) == date(2025, 1, 23)
|
||||
|
||||
# date 对象 → 直接返回
|
||||
d = date(2025, 1, 23)
|
||||
assert service._parse_date(d) == date(2025, 1, 23)
|
||||
|
||||
# 字符串 — 4 种格式
|
||||
assert service._parse_date("2025-01-23") == date(2025, 1, 23)
|
||||
assert service._parse_date("2025/01/23") == date(2025, 1, 23)
|
||||
assert service._parse_date("2025.01.23") == date(2025, 1, 23)
|
||||
assert service._parse_date("2025年01月23日") == date(2025, 1, 23)
|
||||
|
||||
# Excel 序列号(date(1899,12,30) + N days = 目标日期)
|
||||
excel_serial = (date(2025, 1, 23) - date(1899, 12, 30)).days
|
||||
assert service._parse_date(excel_serial) == date(2025, 1, 23)
|
||||
# 浮点数序列号也能解析
|
||||
assert service._parse_date(float(excel_serial)) == date(2025, 1, 23)
|
||||
|
||||
def test_parse_date_none(self, service):
|
||||
"""None 输入返回 None"""
|
||||
assert service._parse_date(None) is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AssetService — _format_years 测试
|
||||
# =============================================================================
|
||||
|
||||
class TestFormatYears:
|
||||
"""_format_years 方法测试(纯逻辑)"""
|
||||
|
||||
@pytest.fixture
|
||||
def service(self):
|
||||
return AssetService(excel_path=EXCEL_PATH)
|
||||
|
||||
def test_format_years(self, service):
|
||||
"""测试浮点年限格式化"""
|
||||
# 5.17 年 → int(5.17*12)=62 → 5年2个月
|
||||
assert service._format_years(5.17) == "5年2个月"
|
||||
# 0.5 年 → int(0.5*12)=6 → 0年6个月
|
||||
assert service._format_years(0.5) == "0年6个月"
|
||||
# 整数年限 5.0 → 60个月 → 5年0个月
|
||||
assert service._format_years(5.0) == "5年0个月"
|
||||
# 0 年
|
||||
assert service._format_years(0.0) == "0年0个月"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AssetService — check_device_age 测试
|
||||
# =============================================================================
|
||||
|
||||
class TestCheckDeviceAge:
|
||||
"""check_device_age 方法测试"""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def asset_service(self):
|
||||
if not excel_exists:
|
||||
pytest.skip(f"Excel 文件不存在: {EXCEL_PATH}")
|
||||
service = AssetService(excel_path=EXCEL_PATH)
|
||||
yield service
|
||||
service.close()
|
||||
|
||||
def test_check_device_age_found(self, asset_service):
|
||||
"""测试完整核查流程(真实Excel),验证返回dict结构正确"""
|
||||
result = asset_service.check_device_age(KNOWN_ASSET_CODE, threshold_years=5)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["found"] is True
|
||||
assert result["asset_code"] == KNOWN_ASSET_CODE
|
||||
assert "asset_name" in result
|
||||
assert "start_date" in result
|
||||
assert "years_used" in result
|
||||
assert "years_display" in result
|
||||
assert "meets_threshold" in result
|
||||
assert "opinion" in result
|
||||
assert isinstance(result["meets_threshold"], bool)
|
||||
|
||||
def test_check_device_age_not_found(self, asset_service):
|
||||
"""资产不存在时的核查结果"""
|
||||
result = asset_service.check_device_age("NONEXISTENT-CODE-99999")
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["found"] is False
|
||||
assert "opinion" in result
|
||||
assert "未在资产清单中找到" in result["opinion"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AssetService — _format_opinion 测试
|
||||
# =============================================================================
|
||||
|
||||
class TestFormatOpinion:
|
||||
"""_format_opinion 方法测试(纯逻辑)"""
|
||||
|
||||
@pytest.fixture
|
||||
def service(self):
|
||||
return AssetService(excel_path=EXCEL_PATH)
|
||||
|
||||
def test_format_opinion_meets(self, service):
|
||||
"""满足5年条件的意见文本格式"""
|
||||
opinion = service._format_opinion(
|
||||
asset_name="电脑笔记本",
|
||||
start_date=date(2020, 1, 1),
|
||||
years_used=5.5,
|
||||
threshold=5,
|
||||
meets=True,
|
||||
)
|
||||
assert "电脑笔记本" in opinion
|
||||
assert "2020-01-01" in opinion
|
||||
assert "✅" in opinion
|
||||
assert "已满5年" in opinion
|
||||
assert "符合更换条件" in opinion
|
||||
|
||||
def test_format_opinion_not_meets(self, service):
|
||||
"""不满足5年条件的意见文本格式"""
|
||||
opinion = service._format_opinion(
|
||||
asset_name="显示器",
|
||||
start_date=date(2023, 6, 15),
|
||||
years_used=2.0,
|
||||
threshold=5,
|
||||
meets=False,
|
||||
)
|
||||
assert "显示器" in opinion
|
||||
assert "2023-06-15" in opinion
|
||||
assert "❌" in opinion
|
||||
assert "未满5年" in opinion
|
||||
assert "不符合更换条件" in opinion
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# approval.py — _extract_asset_code 测试
|
||||
# =============================================================================
|
||||
|
||||
class TestExtractAssetCode:
|
||||
"""_extract_asset_code 函数测试"""
|
||||
|
||||
def test_extract_asset_code_text(self):
|
||||
"""Text 控件类型的资产编号提取"""
|
||||
detail = {
|
||||
"info": {
|
||||
"apply_data": {
|
||||
"contents": [
|
||||
{
|
||||
"control": "Text",
|
||||
"id": "Text-1",
|
||||
"title": [{"text": "资产编号", "lang": "zh_CN"}],
|
||||
"value": {"text": "01011801-02012-041698"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = _extract_asset_code(detail)
|
||||
assert result == "01011801-02012-041698"
|
||||
|
||||
def test_extract_asset_code_selector(self):
|
||||
"""Selector 控件类型的资产编号提取"""
|
||||
detail = {
|
||||
"info": {
|
||||
"apply_data": {
|
||||
"contents": [
|
||||
{
|
||||
"control": "Selector",
|
||||
"id": "Selector-1",
|
||||
"title": [{"text": "固定资产编号", "lang": "zh_CN"}],
|
||||
"value": {"value": "01011801-02012-041698"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = _extract_asset_code(detail)
|
||||
assert result == "01011801-02012-041698"
|
||||
|
||||
def test_extract_asset_code_not_found(self):
|
||||
"""表单中无资产编号字段时返回 None"""
|
||||
detail = {
|
||||
"info": {
|
||||
"apply_data": {
|
||||
"contents": [
|
||||
{
|
||||
"control": "Text",
|
||||
"id": "Text-1",
|
||||
"title": [{"text": "申请理由", "lang": "zh_CN"}],
|
||||
"value": {"text": "电脑太旧了"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = _extract_asset_code(detail)
|
||||
assert result is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# approval.py — _extract_current_approver 测试
|
||||
# =============================================================================
|
||||
|
||||
class TestExtractCurrentApprover:
|
||||
"""_extract_current_approver 函数测试"""
|
||||
|
||||
def test_extract_current_approver_list(self):
|
||||
"""approver 为列表格式时的提取(企微API标准格式)"""
|
||||
detail = {
|
||||
"info": {
|
||||
"sp_record": [
|
||||
{
|
||||
"status": 1,
|
||||
"type": 1,
|
||||
"approverattr": 1,
|
||||
"approver": [
|
||||
{"userid": "zhangsan", "partyid": "2"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
result = _extract_current_approver(detail)
|
||||
assert result == "zhangsan"
|
||||
|
||||
def test_extract_current_approver_dict(self):
|
||||
"""approver 为 dict 格式时的兼容提取"""
|
||||
detail = {
|
||||
"info": {
|
||||
"sp_record": [
|
||||
{
|
||||
"status": 1,
|
||||
"type": 1,
|
||||
"approverattr": 1,
|
||||
"approver": {"userid": "lisi", "partyid": "3"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
result = _extract_current_approver(detail)
|
||||
assert result == "lisi"
|
||||
|
||||
def test_extract_current_approver_no_pending(self):
|
||||
"""无审批中节点时返回 None"""
|
||||
detail = {
|
||||
"info": {
|
||||
"sp_record": [
|
||||
{
|
||||
"status": 2, # 已通过,非审批中
|
||||
"type": 1,
|
||||
"approver": [{"userid": "zhangsan", "partyid": "2"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
result = _extract_current_approver(detail)
|
||||
assert result is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# approval.py — _build_urge_description 测试
|
||||
# =============================================================================
|
||||
|
||||
class TestBuildUrgeDescription:
|
||||
"""_build_urge_description 函数测试"""
|
||||
|
||||
def test_build_urge_description(self):
|
||||
"""验证卡片描述文本包含所有必要字段"""
|
||||
check_result = {
|
||||
"found": True,
|
||||
"asset_code": "01011801-02012-041698",
|
||||
"asset_name": "电脑笔记本",
|
||||
"start_date": "2020-01-23",
|
||||
"years_display": "5年6个月",
|
||||
"meets_threshold": True,
|
||||
}
|
||||
applyer_userid = "sxn"
|
||||
|
||||
desc = _build_urge_description(check_result, applyer_userid)
|
||||
|
||||
# 验证所有关键字段都在描述中
|
||||
assert "01011801-02012-041698" in desc
|
||||
assert "电脑笔记本" in desc
|
||||
assert "2020-01-23" in desc
|
||||
assert "5年6个月" in desc
|
||||
assert "✅" in desc
|
||||
assert "sxn" in desc
|
||||
assert "请点击查看审批详情" in desc
|
||||
Reference in New Issue
Block a user