From 726575aaf2873e32a29d51d3b8d76c65eab37247 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Sun, 1 Nov 2026 22:11:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(pl):=20=E7=8B=AC=E7=AB=8B=E7=BB=84=E5=90=88?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E7=BC=96=E5=8F=B7=E7=AD=9B=E9=80=89=E4=B8=8E?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E8=AF=8D=E6=90=9C=E7=B4=A2=E4=BB=A5=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E6=9D=A1=E4=BB=B6=E8=A2=AB=E5=BF=BD=E7=95=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/pl/quota_cert.py | 6 ++++-- backend/app/services/pl_quota_statistics.py | 12 +++++++---- backend/tests/test_pl_quota_api.py | 21 ++++++++++++++++--- backend/tests/test_pl_quota_statistics.py | 11 ++++++++++ web-admin/scripts/quota-statistics.test.ts | 17 ++++++++++++++- web-admin/src/api/pl/quota-cert.ts | 2 ++ .../pl/quota-statistics/StatisticsPanel.vue | 5 +++-- 7 files changed, 62 insertions(+), 12 deletions(-) diff --git a/backend/app/api/pl/quota_cert.py b/backend/app/api/pl/quota_cert.py index 7ada5f120..c950c0565 100644 --- a/backend/app/api/pl/quota_cert.py +++ b/backend/app/api/pl/quota_cert.py @@ -148,20 +148,22 @@ def save_bindings(contract_id: str, body: ContractCertBindingsBody, @router.get('/statistics/contracts') def contract_statistics_list(keyword: str = Query('', max_length=200), + contract_no: str = Query('', alias='contractNo', max_length=200), page: int = Query(1, ge=1), page_size: int = Query(20, alias='pageSize', ge=1, le=100), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): - return _wire(statistics.list_contracts(db, user, keyword=keyword, page=page, page_size=page_size)) + return _wire(statistics.list_contracts(db, user, keyword=keyword, page=page, page_size=page_size, contract_no=contract_no)) @router.get('/statistics/certificates') def certificate_statistics_list(keyword: str = Query('', max_length=200), + cert_no: str = Query('', alias='certNo', max_length=200), cert_type: Optional[CertType] = Query(None, alias='certType'), page: int = Query(1, ge=1), page_size: int = Query(20, alias='pageSize', ge=1, le=100), db: Session = Depends(get_db), user: DbUserContext = Depends(current_user)): return _wire(statistics.list_certificates(db, user, keyword=keyword, cert_type=cert_type, - page=page, page_size=page_size)) + page=page, page_size=page_size, cert_no=cert_no)) @router.get('/contracts/{contract_id}/statistics') diff --git a/backend/app/services/pl_quota_statistics.py b/backend/app/services/pl_quota_statistics.py index 09c9326c2..fead46085 100644 --- a/backend/app/services/pl_quota_statistics.py +++ b/backend/app/services/pl_quota_statistics.py @@ -22,7 +22,7 @@ def _contracts(db, user): user.to_user_info(), db, user.tenant_no) -def _certificates(db, user, keyword=None, cert_type=None): +def _certificates(db, user, keyword=None, cert_type=None, cert_no=None): queries = [] for kind, model in MODELS.items(): if cert_type and kind != cert_type: @@ -31,6 +31,8 @@ def _certificates(db, user, keyword=None, cert_type=None): if keyword: query = query.filter(or_(model.cert_no.contains(keyword, autoescape=True), model.product_name_on_cert.contains(keyword, autoescape=True))) + if cert_no: + query = query.filter(model.cert_no.contains(cert_no, autoescape=True)) queries.append(query.with_entities(literal(kind).label('cert_type'), model.id.label('cert_id'), *[getattr(model, field).label(field) for field in ('cert_no', 'product_name_on_cert', 'quantity_unit', 'total_qty', 'reserved_qty', 'used_qty', 'status', @@ -131,7 +133,7 @@ def _details(db, user, keys, contract_ids=None): return groups, usage_items -def list_contracts(db, user, keyword=None, page=1, page_size=20): +def list_contracts(db, user, keyword=None, page=1, page_size=20, contract_no=None): certs = _certificates(db, user) linked = _active(db, user, Link).join(certs, and_(Link.cert_type == certs.c.cert_type, Link.cert_id == certs.c.cert_id)).with_entities(Link.contract_id) @@ -139,6 +141,8 @@ def list_contracts(db, user, keyword=None, page=1, page_size=20): if keyword: query = query.filter(or_(CrmContract.contract_no.contains(keyword, autoescape=True), CrmContract.contract_name.contains(keyword, autoescape=True))) + if contract_no: + query = query.filter(CrmContract.contract_no.contains(contract_no, autoescape=True)) total = query.count() rows = query.order_by(CrmContract.create_time.desc(), CrmContract.id).offset((page-1)*page_size).limit(page_size).all() counts = dict(db.query(Link.contract_id, func.count(Link.id)).join(certs, @@ -149,8 +153,8 @@ def list_contracts(db, user, keyword=None, page=1, page_size=20): 'total': total, 'page': page, 'pageSize': page_size} -def list_certificates(db, user, keyword=None, cert_type=None, page=1, page_size=20): - certs = _certificates(db, user, keyword, cert_type) +def list_certificates(db, user, keyword=None, cert_type=None, page=1, page_size=20, cert_no=None): + certs = _certificates(db, user, keyword, cert_type, cert_no) query = db.query(certs) total = query.count() rows = query.order_by(certs.c.create_time.desc(), certs.c.cert_type, certs.c.cert_id).offset( diff --git a/backend/tests/test_pl_quota_api.py b/backend/tests/test_pl_quota_api.py index 6b88770d2..b45be0d17 100644 --- a/backend/tests/test_pl_quota_api.py +++ b/backend/tests/test_pl_quota_api.py @@ -165,9 +165,24 @@ def test_statistics_routes_are_static_paginated_and_strict(api, monkeypatch): return {'list': [], 'total': 0, 'page': 2, 'pageSize': 3} monkeypatch.setattr(stats, 'list_certificates', capture) result = client.get('/api' + prefix + '/statistics/certificates', params={ - 'keyword': '硫酸', 'certType': 'purchase', 'page': 2, 'pageSize': 3}) + 'keyword': '硫酸', 'certNo': 'P', 'certType': 'purchase', 'page': 2, 'pageSize': 3}) assert result.status_code == 200 and result.json()['pageSize'] == 3 - assert received == [{'keyword': '硫酸', 'cert_type': 'purchase', 'page': 2, 'page_size': 3}] - for params in ({'page': 0}, {'pageSize': 101}, {'certType': 'bad'}, {'keyword': 'x' * 201}): + assert received == [{'keyword': '硫酸', 'cert_type': 'purchase', 'page': 2, 'page_size': 3, 'cert_no': 'P'}] + for params in ({'page': 0}, {'pageSize': 101}, {'certType': 'bad'}, {'keyword': 'x' * 201}, {'certNo': 'x' * 201}): assert client.get('/api' + prefix + '/statistics/certificates', params=params).status_code == 422 assert client.get('/api' + prefix + '/contracts/missing/statistics').status_code == 404 + + +def test_contract_statistics_route_forwards_number_and_keyword_independently(api, monkeypatch): + from app.services import pl_quota_statistics as stats + client, app, auth, _ = api + app.dependency_overrides[auth] = lambda: {'role': 'super_admin', 'user_id': 'u', 'tenant_no': 't'} + received = [] + def capture(*args, **kwargs): + received.append(kwargs) + return {'list': [], 'total': 0, 'page': 1, 'pageSize': 20} + monkeypatch.setattr(stats, 'list_contracts', capture) + result = client.get('/api/transport/quota-certs/statistics/contracts', params={'keyword': '目标', 'contractNo': 'HT'}) + assert result.status_code == 200 + assert received[0]['keyword'] == '目标' and received[0]['contract_no'] == 'HT' + assert client.get('/api/transport/quota-certs/statistics/contracts', params={'contractNo': 'x' * 201}).status_code == 422 diff --git a/backend/tests/test_pl_quota_statistics.py b/backend/tests/test_pl_quota_statistics.py index 615267933..4c8c3abc5 100644 --- a/backend/tests/test_pl_quota_statistics.py +++ b/backend/tests/test_pl_quota_statistics.py @@ -235,3 +235,14 @@ def test_usage_party_is_taken_only_from_exact_source_stop_snapshot(case, source_ row = service().certificate_statistics(case.db, case.user, 'purchase', 'same')['usageItems'][0] assert (row['partyName'], row['address']) == expected assert item.source_snapshot['stops'][0]['address'] == '关联段地址' + +def test_number_filters_are_independent_from_keyword_and_applied_before_pagination(case): + s = service() + case.db.query(CrmContract).filter_by(id='a').one().contract_name = '目标合同' + case.db.commit() + assert s.list_contracts(case.db, case.user, keyword='目标', contract_no='a', page_size=1)['total'] == 1 + assert s.list_contracts(case.db, case.user, keyword='目标', contract_no='b')['total'] == 0 + assert s.list_contracts(case.db, case.user, contract_no='目标')['total'] == 0 + assert s.list_certificates(case.db, case.user, keyword='硫酸', cert_no='P', page_size=1)['total'] == 1 + assert s.list_certificates(case.db, case.user, keyword='硫酸', cert_no='missing')['total'] == 0 + assert s.list_certificates(case.db, case.user, cert_no='硫酸')['total'] == 0 diff --git a/web-admin/scripts/quota-statistics.test.ts b/web-admin/scripts/quota-statistics.test.ts index e6f2320c8..54e5e4174 100644 --- a/web-admin/scripts/quota-statistics.test.ts +++ b/web-admin/scripts/quota-statistics.test.ts @@ -126,7 +126,7 @@ function harness(fixed?: string) { const state = scope.run(() => new Function( ...Object.keys(env), - `${code}; return { rows, fixedDetail, details, load, loadDetail, allowed, canEdit }` + `${code}; return { rows, fixedDetail, details, load, loadDetail, allowed, canEdit, keyword, setFilterState }` )(...Object.values(env)) ) return { @@ -360,3 +360,18 @@ test('usage trace renders business columns and resolves contract/product by thei const stopExpression = table.match(/\s*\{\{([\s\S]*?)\}\}/)![1] assert.equal(new Function('row', `return (${stopExpression})`)(rows[0]), '送货') }) + +test('quick search and number header filter are sent as independent conditions', async () => { + const h = harness() + h.permissions[0].resolve({ all: true }) + await flush() + h.calls[0].resolve({ list: [], total: 0 }) + await flush() + h.state.keyword.value = '目标合同' + h.state.setFilterState('contractNo', { text: 'HT-2026' }) + assert.equal(h.calls[1].args[0].keyword, '目标合同') + assert.equal(h.calls[1].args[0].contractNo, 'HT-2026') + h.calls[1].resolve({ list: [], total: 0 }) + await flush() + h.stop() +}) diff --git a/web-admin/src/api/pl/quota-cert.ts b/web-admin/src/api/pl/quota-cert.ts index cf7c1fa71..20f35b296 100644 --- a/web-admin/src/api/pl/quota-cert.ts +++ b/web-admin/src/api/pl/quota-cert.ts @@ -358,6 +358,8 @@ export interface CertificateStatistics extends CertificateStatisticsSummary { unattributedUsedQty: QuotaQuantity } export interface StatisticsQuery { + contractNo?: string + certNo?: string keyword?: string page?: number pageSize?: number diff --git a/web-admin/src/views/pl/quota-statistics/StatisticsPanel.vue b/web-admin/src/views/pl/quota-statistics/StatisticsPanel.vue index b8d6491f5..a269028c5 100644 --- a/web-admin/src/views/pl/quota-statistics/StatisticsPanel.vue +++ b/web-admin/src/views/pl/quota-statistics/StatisticsPanel.vue @@ -210,7 +210,7 @@ ['validFrom', '有效期起'], ['validTo', '有效期止'] ] - ).map(([prop, label]) => ({ prop, label, type: 'text', queryKey: 'keyword' })) + ).map(([prop, label]) => ({ prop, label, type: 'text', queryKey: prop })) const { visibleColumns, checkAll, isIndeterminate, resetToDefault } = usePersistedListColumns( `pl-${props.mode}-statistics-v1`, columns @@ -252,7 +252,8 @@ query: { page: page.value, pageSize: 20, - keyword: toQueryParams().keyword || keyword.value, + keyword: keyword.value, + ...toQueryParams(), ...(props.mode === 'certificate' ? { certType: certType.value } : {}) } })