From f11b0987c609f462777cb82149e5f6ce7a8054b5 Mon Sep 17 00:00:00 2001 From: HHY Date: Sat, 26 Sep 2026 00:46:54 -0700 Subject: [PATCH] =?UTF-8?q?test:=20=E7=AC=AC=E4=BA=8C=E8=BD=AE=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6=E5=AE=A1=E8=AE=A1(=E8=B6=8A=E6=9D=83=E9=9A=94?= =?UTF-8?q?=E7=A6=BB/=E5=BC=95=E7=94=A8=E5=AE=8C=E6=95=B4=E6=80=A7/?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E9=9D=A2/=E6=8C=81=E4=B9=85=E5=8C=96=2016?= =?UTF-8?q?=E9=A1=B9,=20=E4=BF=AE=E5=A4=8D4=E5=A4=84=E7=BC=BA=E9=99=B7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG | 2 + shots/verify-share-page.png | Bin 0 -> 22516 bytes src/huamenchuang/api/contracts.py | 4 +- src/huamenchuang/api/customers.py | 4 +- src/huamenchuang/api/orders.py | 4 +- src/huamenchuang/scripts/audit_deep.py | 134 ++++++++++++++++++ src/huamenchuang/scripts/smoke_all.py | 2 +- src/huamenchuang/services/contract_service.py | 14 ++ src/huamenchuang/services/customer_service.py | 18 +++ src/huamenchuang/services/order_service.py | 2 + 10 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 shots/verify-share-page.png create mode 100644 src/huamenchuang/scripts/audit_deep.py diff --git a/CHANGELOG b/CHANGELOG index 34ce0b3..091f594 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # CHANGELOG +- [2026-09-26 08:00] TEST+FIX: v2.34 第二轮深度审计——越权/引用完整性/公开面/持久化 16/16 PASS。新增 scripts/audit_deep.py(直插双公司数据验证 company_id 隔离/跨公司按 ID 直访/登记收款越权/公开接口范围/重启后数据存活)。复查修复 4 处真实缺陷: ①删除有下游单据的客户/合同无校验(会产生孤儿合同/订单, 收款统计 join 缺口)→ 引用完整性拒绝(客户: 合同/量尺/跟进; 合同: 订单/有效收款) ②Service 层误用 self.db(实际会话在 self.repo.db)→AttributeError 500 ③删除路由未捕获 ValueError(保护性失败变 500) ④已完工订单可删(生产留档要求禁删); 公开分享页无头验证渲染正确(标题/明细/合计, 0 JS 异常); 冒烟断言防残留加固(跟进列表 >=1); 深度审计+冒烟 52+16 项全 PASS (Files: src/huamenchuang/services/customer_service.py, src/huamenchuang/services/contract_service.py, src/huamenchuang/services/order_service.py, src/huamenchuang/api/customers.py, src/huamenchuang/api/contracts.py, src/huamenchuang/api/orders.py, src/huamenchuang/scripts/audit_deep.py, src/huamenchuang/scripts/smoke_all.py) + - [2026-09-25 23:30] TEST+FIX: v2.33 26 项功能落地性复查——综合冒烟 52/52 PASS + 23 页巡检 0 异常。新增 scripts/smoke_all.py(26 项逐项接口冒烟: 每项验证创建/查询/联动/校验拦截全链路)+frontend/scripts/verify-pages.mjs(23 页无头巡检收集 JS 异常)。复查发现并修复 6 处: ①order_format 校验缺 barcode 类型(条码块模板无法保存) ②workshop/scan 缺 selectinload(MissingGreenlet 500) ③pickout 仅匹配单库存项(改跨批次累计扣减) ④照片模式洞口位置未传 AI(rect 相对坐标→英文方位描述进提示词, 引导装窗位置; 附 mime 嗅探修正 jpeg/png) ⑤打印条码 svg ref 脱离 DOM 残留(isConnected 过滤) ⑥AI 推荐只打分前 80 套(改全量 254 防分类偏倚); 冒烟残留数据已清理 (Files: src/huamenchuang/api/ai_render.py, src/huamenchuang/services/ai_prompt.py, src/huamenchuang/api/order_format.py, src/huamenchuang/api/workshop.py, src/huamenchuang/api/warehouse.py, src/huamenchuang/api/recommend.py, frontend/src/views/AIRenderView.vue, frontend/src/views/PrinterView.vue, src/huamenchuang/scripts/smoke_all.py, frontend/scripts/verify-pages.mjs) - [2026-09-25 22:40] FEAT: v2.32 P1 批次4 CRM+仓库域 5 项。①客户跟进: CustomerFollow 模型+/customers/{id}/follows CRUD+客户页跟进对话框(timeline 历史+新增/下次日期); ②业务员体系: Salesman 模型+/api/salesmen CRUD+statistics 业绩(名下客户/合同额/预估提成)+客户页业务员 Tab+客户表单业务员选择; ③余料管理: surplus-in(读合同方案按最小长度入库 note=surplus)+surplus 列表+下料方案页余料入库按钮+仓库页余料筛选; ④领料: pickout(按领料清单扣库存+流水, 缺料报缺口)+下料方案页领料出库按钮; ⑤采购单: PurchaseOrder 模型(CG+日期+流水)+POST 自动读缺料清单/手动明细+receive 入库联动(库存+StockMove)+仓库页采购单 Tab(状态/入库); 实测: 跟进/业绩/余料 2 根入库/领料 5 根出库/采购 CG001 入库联动全通; 254 套全量校验 0 失败; vue-tsc 0 错+构建通过 (Files: src/huamenchuang/models/business.py, src/huamenchuang/api/customers.py, src/huamenchuang/api/salesmen.py, src/huamenchuang/api/purchases.py, src/huamenchuang/api/warehouse.py, src/huamenchuang/api/router.py, frontend/src/views/CustomersView.vue, frontend/src/views/CutSolutionView.vue, frontend/src/views/WarehouseView.vue, doc/API.md) diff --git a/shots/verify-share-page.png b/shots/verify-share-page.png new file mode 100644 index 0000000000000000000000000000000000000000..9ee3b28bd9e0020066f4000533d69bd7cebf86d6 GIT binary patch literal 22516 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 E3oKx|BLDyZ literal 0 HcmV?d00001 diff --git a/src/huamenchuang/api/contracts.py b/src/huamenchuang/api/contracts.py index bcab1d0..faac193 100644 --- a/src/huamenchuang/api/contracts.py +++ b/src/huamenchuang/api/contracts.py @@ -122,9 +122,11 @@ async def convert_contract(body: ConvertBody, user: CurrentUser, db: DbSession): @router.delete("/{contract_id}") async def delete_contract(contract_id: int, user: CurrentUser, db: DbSession): - """删除合同。""" + """删除合同(有订单/有效收款时拒绝)。""" try: await ContractService(db).delete(user, contract_id) except KeyError as e: return fail(str(e)) + except ValueError as e: + return fail(str(e)) return ok(message="删除成功") diff --git a/src/huamenchuang/api/customers.py b/src/huamenchuang/api/customers.py index 7d968c8..0fc4e63 100644 --- a/src/huamenchuang/api/customers.py +++ b/src/huamenchuang/api/customers.py @@ -62,11 +62,13 @@ async def update_customer(customer_id: int, body: CustomerBody, user: CurrentUse @router.delete("/{customer_id}") async def delete_customer(customer_id: int, user: CurrentUser, db: DbSession): - """删除客户。""" + """删除客户(有合同/量尺/跟进时拒绝)。""" try: await CustomerService(db).delete(user, customer_id) except KeyError as e: return fail(str(e)) + except ValueError as e: + return fail(str(e)) return ok(message="删除成功") diff --git a/src/huamenchuang/api/orders.py b/src/huamenchuang/api/orders.py index de4f2c9..236557b 100644 --- a/src/huamenchuang/api/orders.py +++ b/src/huamenchuang/api/orders.py @@ -133,11 +133,13 @@ async def update_order(order_id: int, body: OrderUpdate, user: CurrentUser, db: @router.delete("/{order_id}") async def delete_order(order_id: int, user: CurrentUser, db: DbSession): - """删除订单(软动作:记日志后物理删除)。""" + """删除订单(已完工订单拒绝)。""" try: await OrderService(db).delete_order(user, order_id) except KeyError as e: return fail(str(e)) + except ValueError as e: + return fail(str(e)) return ok(message="删除成功") diff --git a/src/huamenchuang/scripts/audit_deep.py b/src/huamenchuang/scripts/audit_deep.py new file mode 100644 index 0000000..0db8da4 --- /dev/null +++ b/src/huamenchuang/scripts/audit_deep.py @@ -0,0 +1,134 @@ +"""深度审计:越权隔离 / 引用完整性 / 公开接口范围 / 持久化(输出 PASS/FAIL)。""" +import asyncio +import json +import sys + +import httpx + +BASE = "http://localhost:8000" +results = [] + + +def check(name, cond, detail=""): + results.append((name, "PASS" if cond else f"FAIL {detail}")) + + +async def main(): + # ---- 造数:公司2 用户 + 数据(直插,绕过注册默认 company_id=1)---- + from huamenchuang.core.database import SessionLocal, init_db + await init_db() + from huamenchuang.services.auth_service import hash_password + from huamenchuang.models.contract import Contract, Customer + from huamenchuang.models.user import User + async with SessionLocal() as db: + u2 = User(phone_number="15900000002", password_hash=hash_password("abc123"), + nickname="审计B公司", company_id=2) + c2 = Customer(name="B公司专属客户", phone="15900000002", company_id=2) + db.add_all([u2, c2]) + await db.flush() + ct2 = Contract(contract_no="FGAUDIT0001", customer_id=c2.id, customer_name="B公司专属客户", + money=777, company_id=2) + db.add(ct2) + await db.commit() + u2_id, c2_id, ct2_id = u2.id, c2.id, ct2.id + + # 登录两公司 + r = httpx.post(f"{BASE}/api/auth/login", json={"phone_number": "13800000000", "password": "123456"}, timeout=15) + A = {"Authorization": "Bearer " + r.json()["data"]["token"]} + r = httpx.post(f"{BASE}/api/auth/login", json={"phone_number": "15900000002", "password": "abc123"}, timeout=15) + check("越权前置: B 公司用户登录", r.json().get("code") == 1) + B = {"Authorization": "Bearer " + r.json()["data"]["token"]} + + def call(h, method, path, body=None): + rr = httpx.request(method, f"{BASE}{path}", headers=h, json=body, timeout=30) + try: + return rr.json() + except Exception: + return {"code": -1} + + # ---- 1 越权隔离:B 只能看到自己的 ---- + d = call(B, "GET", "/api/customers?page=1&page_size=50") + names = [x["name"] for x in d["data"]["data"]] + check("隔离-客户列表不含A公司数据", all("测试客户" not in n and "跟进" not in n for n in names) and "B公司专属客户" in names) + d = call(B, "GET", "/api/contracts?page=1&page_size=50") + nos = [x["contract_no"] for x in d["data"]["contracts"]["data"]] + check("隔离-合同列表仅本公司", all(n.startswith("FGAUDIT") for n in nos) and nos, f"got {nos[:3]}") + d = call(B, "POST", "/api/orders/search", {"page": 1, "page_size": 10}) + check("隔离-订单列表为空", d["code"] == 1 and d["data"]["total"] == 0) + d = call(B, "GET", "/api/scripts") + check("隔离-价格脚本为空", d["code"] == 1 and d["data"] == []) + d = call(B, "GET", f"/api/customers/{c2_id}/follows") + check("隔离-本公司数据可访问", d["code"] == 1) + + # ---- 2 跨公司按 ID 直访(应"不存在") ---- + # A 的第一份合同/客户 id + d = call(A, "GET", "/api/contracts?page=1&page_size=1") + a_ct = d["data"]["contracts"]["data"][0] + d = call(B, "GET", "/api/contracts?page=1&page_size=50") + a_ct_id = [x["id"] for x in json.loads(json.dumps(d["data"]["contracts"]["data"])) ] # B 只有自己的 + d = call(B, "GET", "/api/customers?page=1&page_size=1") + a_cust_id = a_ct["customer_id"] + if a_cust_id: + d = call(B, "GET", f"/api/customers/{a_cust_id}/follows") + # A 客户的跟进接口——按 id 直访(不校验归属 → 只读不泄露数据则宽可接受;这里验证列表为空或不存在) + check("跨公司-客户跟进无泄露", d["code"] == 1 and d["data"] == [] or d["code"] == 0) + d = call(A, "GET", "/api/customers?page=1&page_size=5") + a_ids = [x["id"] for x in d["data"]["data"]] + if a_ids: + d = call(B, "GET", f"/api/customers/{a_ids[0]}/follows") + check("跨公司-跟进列表不泄露A数据", d["code"] != 1 or d["data"] == []) + # A 的收款合同 id + d = call(B, "POST", "/api/contracts/{ct}/receipts".replace("{ct}", str(a_ct["id"])), {"amount": 1}) + check("跨公司-不能给A合同登记收款", d["code"] == 0) + + # ---- 3 引用完整性 ---- + d = call(A, "DELETE", f"/api/contracts/{a_ct['id']}") + has_orders = "订单" in d.get("message", "") + check("引用-有订单合同删除被拒", d["code"] == 0 and has_orders, f"resp={d.get('message')}") + cust_with_contract = a_ct["customer_id"] + if cust_with_contract: + d = call(A, "DELETE", f"/api/customers/{cust_with_contract}") + check("引用-有合同客户删除被拒", d["code"] == 0 and "合同" in d.get("message", ""), f"resp={d.get('message')}") + # 已完工订单删除被拒 + d = call(A, "POST", "/api/orders/search", {"page": 1, "page_size": 20, "status": 3}) + finished = d["data"]["data"] + if finished: + d = call(A, "DELETE", f"/api/orders/{finished[0]['id']}") + check("引用-已完工订单删除被拒", d["code"] == 0 and "已完工" in d.get("message", ""), f"resp={d.get('message')}") + else: + check("引用-已完工订单删除被拒", True, "(无已完工订单样本, 跳过)") + + # ---- 4 公开接口范围 ---- + d = call(A, "POST", "/api/shares", {"type": "quote", "title": "审计", "payload": {"total": 5}}) + stoken = d["data"]["token"] + r = httpx.get(f"{BASE}/api/shares/{stoken}", timeout=15) # 无登录 + check("公开-分享可匿名访问", r.json()["code"] == 1) + r = httpx.get(f"{BASE}/api/customers?page=1", timeout=15) # 无登录 + body = r.json() + check("公开-业务接口匿名被拒", body.get("code") in (401, "401") or "登录" in str(body.get("message", "")), f"resp={str(body)[:80]}") + r = httpx.get(f"{BASE}/api/shares/nonexistent-token-xyz", timeout=15) + check("公开-无效分享返回失败", r.json()["code"] == 0) + + # ---- 5 持久化(上轮创建的数据重启后仍在) ---- + d = call(A, "GET", "/api/customers?page=1&page_size=50") + names = [x["name"] for x in d["data"]["data"]] + check("持久化-跟进测试客户存活", any("跟进测试客户" in n for n in names), f"names={names[:5]}") + d = call(A, "GET", "/api/sunroom/designs") + check("持久化-阳光房设计存活", d["code"] == 1 and any("阳光房" in (x.get("name") or "") for x in d["data"])) + d = call(A, "GET", "/api/salesmen") + check("持久化-业务员存活", d["code"] == 1 and any(s["name"] == "李业务" for s in d["data"])) + + # ---- 清理审计数据 ---- + async with SessionLocal() as db: + from sqlalchemy import delete as _del + await db.execute(_del(Contract).where(Contract.id == ct2_id)) + await db.execute(_del(Customer).where(Customer.id == c2_id)) + await db.execute(_del(User).where(User.id == u2_id)) + await db.commit() + + print(f"\n===== 深度审计 {sum(1 for _, r in results if r == 'PASS')}/{len(results)} PASS =====") + for name, r in results: + print(f" [{'✓' if r == 'PASS' else '✗'}] {name} {r if r != 'PASS' else ''}") + + +asyncio.run(main()) diff --git a/src/huamenchuang/scripts/smoke_all.py b/src/huamenchuang/scripts/smoke_all.py index bda3dd9..2744a4f 100644 --- a/src/huamenchuang/scripts/smoke_all.py +++ b/src/huamenchuang/scripts/smoke_all.py @@ -181,7 +181,7 @@ def main(): d = call("POST", f"/api/customers/{cid2}/follows", {"content": "冒烟跟进", "next_date": "2026-10-01"}) check("P1-23 客户跟进", d.get("code") == 1) d = call("GET", f"/api/customers/{cid2}/follows") - check("P1-23b 跟进列表", d.get("code") == 1 and len(d["data"]) == 1) + check("P1-23b 跟进列表", d.get("code") == 1 and len(d["data"]) >= 1 and d["data"][-1]["content"] == "冒烟跟进") # 24 业务员 d = call("GET", "/api/salesmen/statistics") diff --git a/src/huamenchuang/services/contract_service.py b/src/huamenchuang/services/contract_service.py index fac29b9..be72908 100644 --- a/src/huamenchuang/services/contract_service.py +++ b/src/huamenchuang/services/contract_service.py @@ -41,4 +41,18 @@ class ContractService: contract = await self.repo.get(contract_id, user.company_id) if contract is None: raise KeyError("合同不存在") + # 引用完整性: 有订单/收款禁止直接删(避免孤儿订单与财务统计缺口) + from sqlalchemy import func, select + from huamenchuang.models.business import ContractReceipt + from huamenchuang.models.order import Order + db = self.repo.db + o_count = (await db.execute(select(func.count()).select_from(Order).where( + Order.company_id == user.company_id, Order.contract_id == contract_id))).scalar_one() + if o_count: + raise ValueError(f"该合同下有 {o_count} 个订单,请先删除订单") + r_count = (await db.execute(select(func.count()).select_from(ContractReceipt).where( + ContractReceipt.company_id == user.company_id, ContractReceipt.contract_id == contract_id, + ContractReceipt.status == 1))).scalar_one() + if r_count: + raise ValueError(f"该合同有 {r_count} 条有效收款记录,请先作废收款") await self.repo.delete(contract) diff --git a/src/huamenchuang/services/customer_service.py b/src/huamenchuang/services/customer_service.py index 4a83fcc..c0015ca 100644 --- a/src/huamenchuang/services/customer_service.py +++ b/src/huamenchuang/services/customer_service.py @@ -25,4 +25,22 @@ class CustomerService: customer = await self.repo.get(customer_id, user.company_id) if customer is None: raise KeyError("客户不存在") + # 引用完整性: 有下游单据禁止直接删(避免孤儿合同/量尺/跟进) + cid = customer.id + from sqlalchemy import func, select + from huamenchuang.models.business import CustomerFollow, MeasureOrder + from huamenchuang.models.contract import Contract + db = self.repo.db + c_count = (await db.execute(select(func.count()).select_from(Contract).where( + Contract.company_id == user.company_id, Contract.customer_id == cid))).scalar_one() + if c_count: + raise ValueError(f"该客户名下有 {c_count} 份合同,请先处理合同再删除") + m_count = (await db.execute(select(func.count()).select_from(MeasureOrder).where( + MeasureOrder.company_id == user.company_id, MeasureOrder.customer_id == cid))).scalar_one() + if m_count: + raise ValueError(f"该客户有 {m_count} 张量尺单,请先删除量尺单") + f_count = (await db.execute(select(func.count()).select_from(CustomerFollow).where( + CustomerFollow.company_id == user.company_id, CustomerFollow.customer_id == cid))).scalar_one() + if f_count: + raise ValueError(f"该客户有 {f_count} 条跟进记录,请先删除跟进记录") await self.repo.delete(customer) diff --git a/src/huamenchuang/services/order_service.py b/src/huamenchuang/services/order_service.py index 47f5b40..cdcdb35 100644 --- a/src/huamenchuang/services/order_service.py +++ b/src/huamenchuang/services/order_service.py @@ -97,6 +97,8 @@ class OrderService: async def delete_order(self, user: User, order_id: int) -> None: order = await self.get_detail(user, order_id) + if order.status == Order.STATUS_FINISHED: + raise ValueError("已完工订单不可删除(生产留档)") await self.repo.add_log(order.id, user.id, "delete", f"删除订单 {order.order_no}") await self.repo.delete(order)