From cb4f38dfd9b6c678a11e338114ee0a793b75639d Mon Sep 17 00:00:00 2001 From: liuwenxin <1009266660@qq.com> Date: Mon, 19 Oct 2026 11:25:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(m3):=20code-review=20P0=20=E5=85=AD?= =?UTF-8?q?=E9=A1=B9=E2=80=94=E2=80=94=E7=AE=97=E9=94=99=E9=92=B1=E4=B8=8E?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 经理聚合分成放大 100 倍:参数按百分数存储(12.5=12.5%),parse_fraction 后调用方又 ×100、函数内部再 ÷100,净效应「绩效×12.5」;去掉调用方 ×100,rate_text 改用实际参数原文(原硬编码 12.5%+5% 参数调整后失真) - 审批联动只认终审:财务节点通过(1→2)不再提前 mark_locked/调整单生效, 否则领导驳回后 locked_at/locked_by 残留、两级审批形同虚设;驳回在任何 节点即时联动;mark_rejected 补清锁定字段 - 调整单新建不可用:ENABLE_CODE_BY_TYPE 未登记 type=3 → is_enabled 恒 400; 补登记 approval.adjustment.enabled。另修复叠加缺陷——开关关闭分支手工 ApprovalStep(order_id=调整单id),而该外键指向 approval_orders,必然 errno 1452;统一改走 submit_order(自动通过留痕由它负责) - 重算双重计提:业务级一次性计提的「进度登记」回款无生效台账行,重算会 凭空落新行;无旧行一律 400 - 前端重算传错 id:台账行主键当 receipt_id 传给重算接口,会作废无关回款 的台账;改传 row.receipt_id(聚合行防御) - quantize 显式 ROUND_HALF_UP:9 处金额 quantize 缺省银行家舍入, x.xx5 与四舍五入口径差 1 分(docstring 却声称 HALF_UP) 测试:显式控制审批开关(V8 起种子默认开启,用例不再依赖默认值); 新增回归 4 例(经理聚合比例/调整单即生效/进度登记重算 400/财务节点不提前 锁定),test_calc+test_settlements 22 passed Co-Authored-By: Claude Code --- backend/app/services/approval_service.py | 15 +- backend/app/services/calc_service.py | 31 ++-- backend/app/services/settlement_service.py | 97 +++++++------ backend/tests/integration/test_calc.py | 47 +++++++ backend/tests/integration/test_settlements.py | 133 ++++++++++++++++-- frontend/src/views/calc/ledger/index.vue | 19 ++- 6 files changed, 274 insertions(+), 68 deletions(-) diff --git a/backend/app/services/approval_service.py b/backend/app/services/approval_service.py index ab5df13..e0de787 100644 --- a/backend/app/services/approval_service.py +++ b/backend/app/services/approval_service.py @@ -64,10 +64,12 @@ from app.services.no_generator import next_no logger = logging.getLogger(__name__) -# 开关参数编码(与 param_registry 种子同源) +# 开关参数编码(与 param_registry 种子同源;新增审批对象类型必须在此登记, +# 否则 is_enabled 走 BadRequestError 兜底、对应业务接口整体不可用) ENABLE_CODE_BY_TYPE: dict[int, str] = { 1: "approval.secondary_allocation.enabled", 2: "approval.monthly_settlement.enabled", + 3: "approval.adjustment.enabled", } # 在途状态集合(同一引用对象至多一张在途单) @@ -219,6 +221,10 @@ def act( if order.status == APPROVAL_STATUS_PENDING_FINANCE else APPROVAL_STATUS_APPROVED ) + # 生效类联动只认**终审**:财务节点通过(1→2)只是流转,不得提前 + # 锁定月结/生效调整单——否则领导驳回后锁定字段残留、审批流形同虚设。 + # 驳回在任何节点都是终态,即时联动。 + final_approved = (not rejected) and order.status == APPROVAL_STATUS_APPROVED session.add( ApprovalStep( @@ -259,12 +265,15 @@ def act( if target is not None: if rejected: settlement_service.mark_rejected(session, target, operator=operator) - else: + elif final_approved: settlement_service.mark_locked(session, target, operator=operator) else: adj = session.get(AdjustmentOrder, order.ref_id) if adj is not None: - adj.status = ADJ_REJECTED if rejected else ADJUSTMENT_STATUS_EFFECTIVE + if rejected: + adj.status = ADJ_REJECTED + elif final_approved: + adj.status = ADJUSTMENT_STATUS_EFFECTIVE # 出参(含 steps)在 **commit 之前** 固化:audit 禁止 commit 后再碰 # ORM/查询——commit 后的任何 SELECT 失败都会让「已通过的审批」被记 diff --git a/backend/app/services/calc_service.py b/backend/app/services/calc_service.py index 7c880eb..a3861dd 100644 --- a/backend/app/services/calc_service.py +++ b/backend/app/services/calc_service.py @@ -15,7 +15,7 @@ import logging from dataclasses import dataclass -from decimal import Decimal +from decimal import ROUND_HALF_UP, Decimal from fractions import Fraction from typing import Any @@ -118,11 +118,17 @@ def calc_receipt( if business.owner_personnel_id is None: raise BadRequestError("业务未指定负责营销人员,无法核算(先在业务归属中指定)") - # 重算:先作废旧版本行(is_active=0),折算体随后按当前参数重算 + # 重算:先作废旧版本行(is_active=0),折算体随后按当前参数重算。 + # 无生效行 = 该回款是「进度登记」(业务级一次性计提 #25 的第二笔及 + # 以后)——本无独立计提,继续重算会凭空落一行生效行,同业务出现 + # 两行生效行导致双重计提 → 一律 400 if is_recalc: old = performance_ledger_repository.get_active_by_receipt(session, receipt_id) - if old is not None: - performance_ledger_repository.mark_inactive(session, old) + if old is None: + raise BadRequestError( + "该回款为进度登记(业务已按一次性计提核算),无独立台账行可重算" + ) + performance_ledger_repository.mark_inactive(session, old) # 核算粒度 #25(一期口径,仅首算模式):业务级一次性计提——已有生效 # 台账行时,本笔回款仅置已核算状态(支付进度),不重复计提 @@ -195,8 +201,11 @@ def calc_receipt( "revenue": str(row_revenue), "profit": str(row_profit), # 行级折算同样固化为两位(to_decimal 对整数值无小数位, - # 快照/台账口径需与列一致) - "converted": str(row_converted.quantize(Decimal("0.01"))), + # 快照/台账口径需与列一致;显式 HALF_UP——Python 默认 + # 银行家舍入会让 x.xx5 少 1 分) + "converted": str( + row_converted.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + ), } ) @@ -244,8 +253,14 @@ def calc_receipt( snapshot_json["strategy_note"] = _NOTE_MANAGER_PENDING def q2(value: Decimal | None) -> Decimal | None: - """金额统一两位小数(HALF_UP):内存/DB/序列化三方一致。""" - return value.quantize(Decimal("0.01")) if value is not None else None + """金额统一两位小数(HALF_UP):内存/DB/序列化三方一致。 + + 显式传 rounding——quantize 缺省是上下文 ROUND_HALF_EVEN(银行家 + 舍入),x.xx5 会舍成偶数值,与业务四舍五入口径差 1 分。 + """ + if value is None: + return None + return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) row = performance_ledger_repository.add( session, diff --git a/backend/app/services/settlement_service.py b/backend/app/services/settlement_service.py index d4703af..5e8cdb3 100644 --- a/backend/app/services/settlement_service.py +++ b/backend/app/services/settlement_service.py @@ -9,7 +9,7 @@ """ import logging -from decimal import Decimal +from decimal import ROUND_HALF_UP, Decimal from sqlalchemy import select from sqlalchemy.orm import Session @@ -146,11 +146,19 @@ def generate( personnel_name=bucket["personnel_name"], role_code=bucket["role_code"], calc_type=bucket["calc_type"], - performance_sum=bucket["performance_sum"].quantize(Decimal("0.01")), - payable_sum=bucket["payable_sum"].quantize(Decimal("0.01")), + performance_sum=bucket["performance_sum"].quantize( + Decimal("0.01"), rounding=ROUND_HALF_UP + ), + payable_sum=bucket["payable_sum"].quantize( + Decimal("0.01"), rounding=ROUND_HALF_UP + ), basic_amount=basic, salary_mode=mode, - net_amount=net.quantize(Decimal("0.01")) if net is not None else None, + net_amount=( + net.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + if net is not None + else None + ), is_probation=bucket["is_probation"], probation_note=note, snapshot_json={"ledger_ids": bucket["ledger_ids"]}, @@ -160,9 +168,9 @@ def generate( settlement_repository.replace_items(session, settlement.id, items) settlement.total_performance = sum( (i.performance_sum for i in items), Decimal("0") - ).quantize(Decimal("0.01")) + ).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) settlement.total_payable = sum((i.payable_sum for i in items), Decimal("0")).quantize( - Decimal("0.01") + Decimal("0.01"), rounding=ROUND_HALF_UP ) settlement.person_count = len({i.personnel_id for i in items}) settlement.snapshot_json = {"note": _SETTLEMENT_NOTE} @@ -225,8 +233,14 @@ def mark_locked(session: Session, settlement: MonthlySettlement, *, operator: Us def mark_rejected(session: Session, settlement: MonthlySettlement, *, operator: User) -> None: - """审批驳回钩子:回草稿(可修改后重提)。""" + """审批驳回钩子:回草稿(可修改后重提)。 + + 锁定信息一并清空:草稿态残留 locked_at/locked_by 会让详情页展示 + 「已锁定」的假象(历史缺陷路径下驳回前可能已被提前锁定)。 + """ settlement.status = SETTLEMENT_STATUS_DRAFT + settlement.locked_at = None + settlement.locked_by = None def ensure_month_not_locked(session: Session, month: str) -> None: @@ -315,8 +329,12 @@ def _build_manager_aggregates(session: Session, ledger_rows: list, month: str) - aggregates: list = [] snapshot = resolve_snapshot(session, date.today()) - team_pct = parse_fraction(snapshot.get("manager.share.team")) - company_pct = parse_fraction(snapshot.get("manager.share.company")) + # 参数按百分数存储(12.5 表示 12.5%):parse_fraction 得 12.5, + # manager_team_share 内部再 ÷100 —— 调用方不得再乘 100(否则放大 100 倍) + team_text = snapshot.get("manager.share.team") + company_text = snapshot.get("manager.share.company") + team_pct = parse_fraction(team_text) + company_pct = parse_fraction(company_text) for manager_id, bucket in managers.items(): manager = session.get(Personnel, manager_id) if manager is None: @@ -340,8 +358,8 @@ def _build_manager_aggregates(session: Session, ledger_rows: list, month: str) - share = manager_team_share( bucket["performance"], company_base, - team_pct=team_pct * 100, - company_pct=company_pct * 100, + team_pct=team_pct, + company_pct=company_pct, ) member_names = "、".join(sorted(bucket["names"])) or "(无成员)" aggregates.append( @@ -356,11 +374,12 @@ def _build_manager_aggregates(session: Session, ledger_rows: list, month: str) - customer_type="personal", product_category="mixed", receipt_date=date.today(), - rate_text="聚合:12.5%+5%", + # 比例原文取实际参数(勿硬编码默认值——参数调整后台账说明会失真) + rate_text=f"聚合:{team_text}%+{company_text}%", converted_profit=None, - performance_amount=share.quantize(Decimal("0.01")), + performance_amount=share.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), barter_applied=0, - payable_amount=share.quantize(Decimal("0.01")), + payable_amount=share.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), settlement_month=month, is_probation=1 if manager.is_probation == 1 else 0, snapshot_json={ @@ -412,13 +431,8 @@ def create_adjustment( ): """新建调整单:开关分流(关=提交即生效留痕;开=待审批)。""" from app.core.constants import NO_PREFIX_ADJUSTMENT - from app.models import AdjustmentOrder, ApprovalStep - from app.models.approval import ( - APPROVAL_ACTION_AUTO_APPROVE, - APPROVAL_NODE_FINANCE, - APPROVAL_NODE_LEADER, - APPROVAL_TYPE_ADJUSTMENT, - ) + from app.models import AdjustmentOrder + from app.models.approval import APPROVAL_STATUS_APPROVED, APPROVAL_TYPE_ADJUSTMENT from app.models.settlement import ( ADJUSTMENT_STATUS_EFFECTIVE, ADJUSTMENT_STATUS_PENDING, @@ -459,30 +473,23 @@ def create_adjustment( session.add(order) session.flush() - if approval_service.is_enabled(session, APPROVAL_TYPE_ADJUSTMENT): - order.status = ADJUSTMENT_STATUS_PENDING - session.flush() - approval_service.submit_order( - session, - order_type=APPROVAL_TYPE_ADJUSTMENT, - ref_id=order.id, - refer_no=order.order_no, - submitter=operator, - ) - else: - order.status = ADJUSTMENT_STATUS_EFFECTIVE - session.add_all( - [ - ApprovalStep( - order_id=order.id, - node=node, - action=APPROVAL_ACTION_AUTO_APPROVE, - operator_name="系统", - ) - for node in (APPROVAL_NODE_FINANCE, APPROVAL_NODE_LEADER) - ] - ) - session.flush() + # 开关分流与留痕统一交给 submit_order:开关关闭时它自动通过并写两条 + # 「系统」留痕。**不得手工插 ApprovalStep** —— approval_steps.order_id + # 外键指向 approval_orders(不是 adjustment_orders),用调整单 id 落步 + # 必然外键失败(历史缺陷:关开关时新建调整单不可用) + approval_order = approval_service.submit_order( + session, + order_type=APPROVAL_TYPE_ADJUSTMENT, + ref_id=order.id, + refer_no=order.order_no, + submitter=operator, + ) + order.status = ( + ADJUSTMENT_STATUS_EFFECTIVE + if approval_order.status == APPROVAL_STATUS_APPROVED + else ADJUSTMENT_STATUS_PENDING + ) + session.flush() session.commit() logger.info("调整单创建 | order=%s | status=%s", order.order_no, order.status) return { diff --git a/backend/tests/integration/test_calc.py b/backend/tests/integration/test_calc.py index 06b3865..0463776 100644 --- a/backend/tests/integration/test_calc.py +++ b/backend/tests/integration/test_calc.py @@ -459,6 +459,53 @@ class TestRecalc: assert resp.status_code == 400 assert "尚未核算" in resp.json()["message"] + def test_进度登记回款重算400_不得凭空落行(self, client: TestClient, admin: dict) -> None: + """业务级一次性计提的第二笔回款无台账行;重算若落新行会双重计提。""" + biz, first = _create_full_chain( + client, + admin["headers"], + items=_GOLD_ITEMS, + ship_date="2026-10-01", + receipt_date="2026-10-20", + amount="20000.00", + ) + assert _calc(client, admin["headers"], first["id"]).status_code == 200 + resp = client.post( + _RECEIPTS, + json={ + "business_id": biz["id"], + "receipt_date": "2026-10-25", + "amount": "100.00", + }, + headers=admin["headers"], + ) + assert resp.status_code == 200, resp.text + second = resp.json()["data"] + assert _calc(client, admin["headers"], second["id"]).json()["data"]["repeated"] is True + + resp = client.post( + f"{_CALC}/receipts/{second['id']}/recalc", + json={"reason": "尝试重算进度登记"}, + headers=admin["headers"], + ) + assert resp.status_code == 400, resp.text + assert "进度登记" in resp.json()["message"] + + # 同业务仍只有一行生效台账(未凭空新增) + factory = get_session_factory() + with factory() as session: + rows = ( + session.execute( + select(PerformanceLedger).where( + PerformanceLedger.business_id == biz["id"], + PerformanceLedger.is_active == 1, + ) + ) + .scalars() + .all() + ) + assert len(rows) == 1 + def test_重算_普通用户与营销后勤403( self, client: TestClient, normal: dict, make_user, auth_headers ) -> None: diff --git a/backend/tests/integration/test_settlements.py b/backend/tests/integration/test_settlements.py index 84c2303..80c9815 100644 --- a/backend/tests/integration/test_settlements.py +++ b/backend/tests/integration/test_settlements.py @@ -39,30 +39,38 @@ _GOLD_ITEMS = [ ] -def _person(client: TestClient, headers: dict) -> dict: +def _role_id(role_code: str) -> int: from sqlalchemy import select - from app.core.constants import BUSINESS_ROLE_SALESPERSON from app.core.db import get_session_factory from app.models import BusinessRole factory = get_session_factory() with factory() as session: - role_id = int( + return int( session.execute( select(BusinessRole.id).where( - BusinessRole.role_code == BUSINESS_ROLE_SALESPERSON, + BusinessRole.role_code == role_code, BusinessRole.status == 1, ) ).scalar_one() ) + +def _person( + client: TestClient, + headers: dict, + *, + role_code: str = "salesperson", + manager_id: int | None = None, +) -> dict: resp = client.post( _PERSONNEL, json={ "personnel_name": f"t_月结_{uuid.uuid4().hex[:8]}", - "business_role_id": role_id, + "business_role_id": _role_id(role_code), "salary_standard": "3000.00", + **({"manager_id": manager_id} if manager_id is not None else {}), }, headers=headers, ) @@ -70,6 +78,20 @@ def _person(client: TestClient, headers: dict) -> dict: return resp.json()["data"] +def _set_switch(client: TestClient, headers: dict, code: str, value: str) -> None: + """显式设置审批开关(V8 起审批开关默认开启,用例不得依赖种子默认值)。""" + resp = client.post( + "/api/v1/system/params/changes", + json={ + "change_type": 1, + "remark": f"用例设置 {code}={value}", + "items": [{"param_code": code, "dimension_value": "", "value": value}], + }, + headers=headers, + ) + assert resp.status_code == 200, resp.text + + def _calc_one(client: TestClient, headers: dict) -> dict: """完整造一笔已核算回款(回款日 2026-10-20 → 归月 2026-10)。""" cust = customer(client, headers) @@ -161,7 +183,8 @@ class TestGenerate: ).status_code == 200 ) - # 提交(开关默认关)→ 自动通过锁定 + # 显式关开关 → 提交即自动通过锁定(用例不依赖种子默认值) + _set_switch(client, admin["headers"], "approval.monthly_settlement.enabled", "0") sid = client.get(f"{_SETTLEMENTS}", headers=admin["headers"]).json()["data"]["list"][0][ "id" ] @@ -172,8 +195,7 @@ class TestGenerate: f"{_SETTLEMENTS}/generate", json={"month": "2026-10"}, headers=admin["headers"] ) assert resp.status_code == 400 - # 已核算禁改(M2 防线)先于月结锁定拦截生效——两者均 400 拒改 - assert ("已核算" in resp.json()["message"]) or ("锁定" in resp.json()["message"]) + assert "已审定锁定" in resp.json()["message"] class TestSubmitAndLock: @@ -183,6 +205,8 @@ class TestSubmitAndLock: self, client: TestClient, admin: dict, finance: dict ) -> None: _calc_one(client, admin["headers"]) + # 显式关开关(V8 起种子默认开启,用例不得依赖默认值) + _set_switch(client, admin["headers"], "approval.monthly_settlement.enabled", "0") resp = client.post( f"{_SETTLEMENTS}/generate", json={"month": "2026-10"}, headers=admin["headers"] ) @@ -322,6 +346,13 @@ class TestSwitchOn: ) assert resp.status_code == 200, resp.text + # 财务节点通过只是流转到待领导——不得提前锁定月结(否则领导驳回后 + # 锁定字段残留、两级审批形同虚设) + mid = client.get(f"{_SETTLEMENTS}/{sid}", headers=admin["headers"]).json()["data"] + assert mid["status"] == 2, "财务通过后应仍为待审批,不得提前锁定" + assert mid["locked_at"] is None + assert mid["locked_by"] is None + # 领导通过 → 锁定 resp = client.post( f"/api/v1/system/approvals/{order['id']}/action", @@ -333,3 +364,89 @@ class TestSwitchOn: assert final["status"] == 3 assert final["locked_at"] is not None assert final["locked_by"] == leader["username"] # 审批通过人=领导账号 + + +class TestManagerAggregate: + """经理团队聚合:比例按参数实际值计算(守「百分数再乘 100」放大缺陷)。""" + + def test_聚合金额按实际比例计入(self, client: TestClient, admin: dict) -> None: + manager = _person(client, admin["headers"], role_code="sales_manager") + sales = _person(client, admin["headers"], manager_id=manager["id"]) + + cust = customer(client, admin["headers"]) + resp = client.post( + _BUSINESSES, + json=business_payload( + cust["id"], + owner_personnel_id=sales["id"], + items=[dict(_GOLD_ITEMS[0], product_name=f"t_聚合_{uuid.uuid4().hex[:6]}")], + ), + headers=admin["headers"], + ) + assert resp.status_code == 200, resp.text + biz = resp.json()["data"] + resp = client.post( + _SHIPMENTS, + json={ + "business_id": biz["id"], + "ship_date": "2026-10-01", + "items": [{"product_name": "t_发", "quantity": "1", "sale_price": "1.00"}], + }, + headers=admin["headers"], + ) + assert resp.status_code == 200, resp.text + resp = client.post( + _RECEIPTS, + json={ + "business_id": biz["id"], + "receipt_date": "2026-10-20", + "amount": "20000.00", + "shipment_id": resp.json()["data"]["id"], + }, + headers=admin["headers"], + ) + assert resp.status_code == 200, resp.text + receipt = resp.json()["data"] + assert ( + client.post( + f"{_CALC}/receipts/{receipt['id']}/calc", headers=admin["headers"] + ).status_code + == 200 + ) + + resp = client.post( + f"{_SETTLEMENTS}/generate", json={"month": "2026-10"}, headers=admin["headers"] + ) + assert resp.status_code == 200, resp.text + items = resp.json()["data"]["items"] + manager_items = [i for i in items if i["calc_type"] == 2] + assert len(manager_items) == 1 + # 业务员绩效 39.11;经理 = 39.11×12.5% + 39.11×5% = 6.84425 → 6.84 + # (若调用方把百分数再 ×100,此处会是 684.43) + assert manager_items[0]["personnel_id"] == manager["id"] + assert manager_items[0]["performance_sum"] == "6.84" + assert manager_items[0]["payable_sum"] == "6.84" + + +class TestAdjustment: + """调整单:已锁定月份补发/扣减的唯一通道。""" + + def test_新建调整单_开关关闭即生效(self, client: TestClient, admin: dict) -> None: + # 显式关开关(用例不依赖种子默认值) + _set_switch(client, admin["headers"], "approval.adjustment.enabled", "0") + person = _person(client, admin["headers"]) + resp = client.post( + f"{_SETTLEMENTS}/adjustments", + json={ + "settlement_month": "2026-10", + "personnel_id": person["id"], + "direction": 1, + "amount": "500.00", + "reason": "补发差额", + }, + headers=admin["headers"], + ) + # 守 ENABLE_CODE_BY_TYPE 漏登记 type=3:修复前此处恒 400「未知的审批对象类型」 + assert resp.status_code == 200, resp.text + out = resp.json()["data"] + assert out["status"] == 2 # 已生效 diff --git a/frontend/src/views/calc/ledger/index.vue b/frontend/src/views/calc/ledger/index.vue index 00b635a..7431967 100644 --- a/frontend/src/views/calc/ledger/index.vue +++ b/frontend/src/views/calc/ledger/index.vue @@ -40,19 +40,23 @@ const recalcTarget = ref(null) const recalcReason = ref('') function openRecalc(row: LedgerRow): void { + // 聚合行(calc_type=2)receipt_id 为 NULL,无回款可重算(按钮已限量,防御) + if (row.receipt_id == null) return recalcTarget.value = row recalcReason.value = '' recalcVisible.value = true } async function submitRecalc(): Promise { - if (!recalcTarget.value) return + const row = recalcTarget.value + if (!row || row.receipt_id == null) return if (!recalcReason.value.trim()) { ElMessage.warning('重算必须填写原因') return } try { - await calcApi.recalcReceipt(recalcTarget.value.id, recalcReason.value.trim()) + // 重算接口按回款 ID 定位(台账行主键 ≠ 回款 ID:传 row.id 会重算到错误回款) + await calcApi.recalcReceipt(row.receipt_id, recalcReason.value.trim()) ElMessage.success('重算完成(新版本已生效)') recalcVisible.value = false await load() @@ -75,9 +79,16 @@ async function submitRecalc(): Promise { /> - + + - +