瀏覽代碼

测试用例

xsh_1997 1 周之前
父節點
當前提交
614d0857b3

+ 211 - 0
e2e-test/README.md

@@ -0,0 +1,211 @@
1
+# 巴清农资商城 · 端到端(E2E)测试套件
2
+
3
+> **纯 HTTP 黑盒测试,完全不改 Java 项目代码。**
4
+> 直接在你的部署机器上跑,像用户一样发真实请求,测全链路联动。
5
+
6
+---
7
+
8
+## 目录结构
9
+
10
+```
11
+e2e-test/
12
+├── README.md           ← 本文件:使用说明
13
+├── requirements.txt    ← Python 依赖
14
+├── config.py           ← 配置:API地址、账号密码(你改这里就行)
15
+├── api_client.py       ← HTTP 客户端封装(不用动)
16
+├── conftest.py         ← pytest 报告配置(不用动)
17
+├── test_flows.py       ← 全部测试用例(23 条用例)
18
+├── run_all.bat         ← 一键运行(Windows)
19
+└── run_all.sh          ← 一键运行(Linux / macOS)
20
+```
21
+
22
+---
23
+
24
+## 第一步:把整套文件拷贝到部署机器上
25
+
26
+```
27
+把 e2e-test/ 这个文件夹 整个拷贝到你的部署机器上
28
+```
29
+
30
+---
31
+
32
+## 第二步:改配置
33
+
34
+打开 `config.py`,改以下几个地方:
35
+
36
+```python
37
+# 后端的 IP 和端口
38
+API_BASE_URL = "http://127.0.0.1:8020"
39
+# 如果是局域网其他机器: http://192.168.1.100:8020
40
+# 如果是同一台机器:  http://127.0.0.1:8020
41
+
42
+# 管理员账号
43
+ADMIN_USERNAME = "admin"
44
+ADMIN_PASSWORD = "admin123"
45
+
46
+# 商家账号(需要先手动在后台创建)
47
+SELLER_USERNAME = "seller"
48
+SELLER_PASSWORD = "123456"
49
+SELLER_SHOP_ID = 1
50
+
51
+# 是否开启验证码(建议关闭)
52
+CAPTCHA_ENABLED = False
53
+```
54
+
55
+---
56
+
57
+## 第三步:运行测试
58
+
59
+### Windows 系统
60
+
61
+```
62
+双击 run_all.bat
63
+```
64
+
65
+### Linux / macOS 系统
66
+
67
+```bash
68
+chmod +x run_all.sh
69
+./run_all.sh
70
+```
71
+
72
+### 手动运行(任选一种)
73
+
74
+```bash
75
+# 全部测试
76
+pytest test_flows.py -v --html=report.html --self-contained-html
77
+
78
+# 只跑某一组
79
+pytest test_flows.py -v -k "TestPlatformAdmin" --html=report.html
80
+pytest test_flows.py -v -k "TestMemberFlow" --html=report.html
81
+pytest test_flows.py -v -k "TestSellerFlow" --html=report.html
82
+
83
+# 只跑单个用例
84
+pytest test_flows.py -v -k "test_06_home_page" --html=report.html
85
+```
86
+
87
+---
88
+
89
+## 第四步:查看报告
90
+
91
+运行完成后,打开 `report.html` 就能看到完整的测试报告,包含:
92
+
93
+- 所有测试通过/失败状态
94
+- 每个用例的执行时间
95
+- 失败时的详细错误日志
96
+- 环境信息
97
+
98
+直接用浏览器双击打开即可。
99
+
100
+---
101
+
102
+## 测试用例清单(23 条)
103
+
104
+### 平台管理员流程
105
+
106
+| 编号 | 用例 | 描述 |
107
+|---|---|---|
108
+| TC01 | 管理员登录信息 | 验证登录成功,返回用户信息和权限 |
109
+| TC02 | 入驻申请列表 | 查看商户入驻申请列表 |
110
+| TC03 | 待审核入驻数量 | 查看有多少待审核的入驻申请 |
111
+| TC04 | 待审核商品数量 | 查看有多少待审核的商品 |
112
+| TC05 | 平台商品列表 | 查看所有商品列表 |
113
+
114
+### 消费者端流程
115
+
116
+| 编号 | 用例 | 描述 |
117
+|---|---|---|
118
+| TC06 | 首页浏览 | 查看 Banner、分类、热销商品 |
119
+| TC07 | 会员注册登录 | 注册新用户,登录获取 token |
120
+| TC08 | 新增收货地址 | 添加一个默认收货地址 |
121
+| TC09 | 搜索商品 | 按关键词搜索商品 |
122
+| TC10 | 加购商品 | 将商品加入购物车 |
123
+| TC11 | 订单预览 | 预览订单信息 |
124
+| TC12 | 提交订单 | 提交订单并获取订单 ID |
125
+| TC13 | 查看订单 | 查看订单详情 |
126
+| TC14 | 模拟支付 | 调用支付接口 |
127
+| TC15 | 确认收货 | 确认收货完成订单 |
128
+
129
+### 商家端流程
130
+
131
+| 编号 | 用例 | 描述 |
132
+|---|---|---|
133
+| TC16 | 商品分类选项 | 获取商家可用的商品分类 |
134
+| TC17 | 运费模板选项 | 获取运费模板列表 |
135
+| TC18 | 商家订单列表 | 查看商家所有订单 |
136
+| TC19 | 商家商品列表 | 查看商家所有商品 |
137
+
138
+### 全链路串联测试
139
+
140
+| 编号 | 用例 | 描述 |
141
+|---|---|---|
142
+| TC20 | 管理员审核入驻 | 找到待审核的入驻申请,审核通过 |
143
+| TC21 | 消费者购买链路 | 浏览首页 → 查看商品详情 |
144
+| TC22 | 商家管理链路 | 查看商品 → 查看订单 |
145
+| TC23 | 平台管理链路 | 查看待审核商品数 → 待审核入驻数 |
146
+
147
+---
148
+
149
+## 常见问题
150
+
151
+### Q: 登录报错 "验证码错误"
152
+
153
+因为测试脚本默认不处理验证码。
154
+
155
+**解决方案一(推荐):关闭验证码**
156
+1. 登录管理后台 → 系统管理 → 系统配置
157
+2. 找到"验证码开关",关闭
158
+3. 或者直接在数据库执行:
159
+   ```sql
160
+   UPDATE sys_config SET config_value = 'false' WHERE config_key = 'sys.account.captchaEnabled';
161
+   ```
162
+
163
+**解决方案二:开启验证码模式**
164
+1. 修改 `config.py` 中 `CAPTCHA_ENABLED = True`
165
+2. 目前验证码模式需要配合手动答题,暂不完全自动化
166
+
167
+### Q: 登录报错 "账号不存在"
168
+
169
+管理后台的默认账号是 `admin / admin123`,如果改过,去 `config.py` 里改成正确的。
170
+
171
+商家账号需要先在后台创建:
172
+1. 用管理员登录后台
173
+2. 系统管理 → 用户管理 → 新增用户
174
+3. 分配商家角色
175
+
176
+### Q: 测试报错 "没有在售商品"
177
+
178
+消费者流程的加购、下单需要系统中已有审核通过的商品。
179
+
180
+解决方案:
181
+1. 先用管理员后台创建一个商品分类
182
+2. 创建一个商家账号
183
+3. 发布商品 → 提交审核 → 审核通过
184
+
185
+### Q: 测试报错 "没有收货地址"
186
+
187
+下单前需要先有收货地址。测试脚本中 TC08 会自动创建一个地址。
188
+如果报错说明之前一步失败了,先排查 TC08。
189
+
190
+### Q: 测试全部跳过(SKIP)
191
+
192
+某些测试会检查前置条件(如是否有在售商品),没有数据时会自动跳过,这是正常行为。
193
+系统中数据越完整,能跑通的用例越多。
194
+
195
+### Q: 报告在哪里?
196
+
197
+运行完成后,`report.html` 就在 `e2e-test/` 目录下,双击打开。
198
+
199
+---
200
+
201
+## 先决条件清单
202
+
203
+在部署机器上执行测试前,请确认:
204
+
205
+- [ ] 后端服务已启动(Spring Boot 运行中)
206
+- [ ] 能通过浏览器访问后端 API(如 `http://127.0.0.1:8020`)
207
+- [ ] Python 3.8+ 已安装(运行 `python --version` 确认)
208
+- [ ] 已在 config.py 中修改正确的 API 地址
209
+- [ ] 已在 config.py 中修改正确的管理员/商家账号
210
+- [ ] (推荐)验证码已关闭
211
+- [ ] 系统中有基本的测试数据(商品分类、商家、商品等)

+ 135 - 0
e2e-test/api_client.py

@@ -0,0 +1,135 @@
1
+# ============================================================
2
+# HTTP 客户端封装 — 统一处理 Token、会话、请求/响应日志
3
+# ============================================================
4
+import requests
5
+import json
6
+import time
7
+from config import API_BASE_URL, REQUEST_TIMEOUT
8
+
9
+
10
+class ApiClient:
11
+    """封装 HTTP 请求,自动管理 Token 和会话"""
12
+
13
+    def __init__(self, base_url=None):
14
+        self.base_url = base_url or API_BASE_URL
15
+        self.session = requests.Session()
16
+        self.session.headers.update({
17
+            "Content-Type": "application/json;charset=utf-8",
18
+            "User-Agent": "E2ETest/1.0",
19
+        })
20
+        self._last_response = None
21
+        self._admin_token = None   # 管理员 Token(Authorization 头)
22
+        self._member_token = None  # 会员 Token(具体头部看接口)
23
+        self._seller_token = None  # 商家 Token
24
+
25
+    # ---------- 基础请求方法 ----------
26
+
27
+    def request(self, method, path, **kwargs):
28
+        url = f"{self.base_url}{path}" if not path.startswith("http") else path
29
+        kwargs.setdefault("timeout", REQUEST_TIMEOUT)
30
+        kwargs.setdefault("headers", {})
31
+
32
+        resp = self.session.request(method, url, **kwargs)
33
+        self._last_response = resp
34
+
35
+        # 打印简略日志
36
+        _log_req(method, url, kwargs)
37
+        _log_resp(resp)
38
+
39
+        return resp
40
+
41
+    def get(self, path, **kwargs):
42
+        return self.request("GET", path, **kwargs)
43
+
44
+    def post(self, path, **kwargs):
45
+        return self.request("POST", path, **kwargs)
46
+
47
+    def put(self, path, **kwargs):
48
+        return self.request("PUT", path, **kwargs)
49
+
50
+    def delete(self, path, **kwargs):
51
+        return self.request("DELETE", path, **kwargs)
52
+
53
+    # ---------- Token 管理 ----------
54
+
55
+    def set_admin_token(self, token):
56
+        self._admin_token = token
57
+        self.session.headers["Authorization"] = f"Bearer {token}"
58
+
59
+    def set_member_token(self, token):
60
+        self._member_token = token
61
+        # 根据 MemberContext 的实现,会员 Token 也是 Authorization 头
62
+        # 但区分会员和管理员,我们用不同的头
63
+        self.session.headers["Authorization"] = f"Bearer {token}"
64
+
65
+    def set_seller_token(self, token):
66
+        self._seller_token = token
67
+        self.session.headers["Authorization"] = f"Bearer {token}"
68
+
69
+    def clear_token(self):
70
+        self._admin_token = None
71
+        self._member_token = None
72
+        self._seller_token = None
73
+        self.session.headers.pop("Authorization", None)
74
+
75
+    # ---------- 响应解析 ----------
76
+
77
+    def parse_json(self):
78
+        if self._last_response is None:
79
+            return None
80
+        try:
81
+            return self._last_response.json()
82
+        except Exception:
83
+            return None
84
+
85
+    def assert_ok(self, err_msg=None):
86
+        """断言接口返回 code == 200(AjaxResult.success)"""
87
+        data = self.parse_json()
88
+        if data is None:
89
+            raise AssertionError(f"{err_msg or '响应不是 JSON'}: {self._last_response.text[:200]}")
90
+        code = data.get("code") if isinstance(data, dict) else None
91
+        if code != 200:
92
+            msg = data.get("msg", "未知错误")
93
+            raise AssertionError(f"{err_msg or '接口错误'}: code={code}, msg={msg}")
94
+        return data
95
+
96
+    def extract_data(self):
97
+        """从 AjaxResult 中提取 data 字段"""
98
+        data = self.assert_ok()
99
+        return data.get("data")
100
+
101
+    # ---------- 工具方法 ----------
102
+
103
+    def get_captcha(self):
104
+        """
105
+        获取登录验证码(若依标准)
106
+        返回 (uuid, math_answer) 或 (None, None)
107
+        """
108
+        resp = self.get("/captchaImage")
109
+        body = self.assert_ok("获取验证码失败")
110
+        return body.get("uuid"), None  # math 验证码需要 OCR,建议测试环境关闭
111
+
112
+
113
+# ---------- 日志辅助 ----------
114
+
115
+def _log_req(method, url, kwargs):
116
+    body = kwargs.get("json") or kwargs.get("data")
117
+    snippet = ""
118
+    if body:
119
+        s = json.dumps(body, ensure_ascii=False)
120
+        snippet = f" | body={s[:120]}"
121
+    print(f"  >>> {method} {url}{snippet}")
122
+
123
+
124
+def _log_resp(resp):
125
+    status = resp.status_code
126
+    body = ""
127
+    try:
128
+        data = resp.json()
129
+        if isinstance(data, dict):
130
+            code = data.get("code", "")
131
+            msg = data.get("msg", "")
132
+            body = f"code={code} msg={msg}"
133
+    except Exception:
134
+        body = resp.text[:80]
135
+    print(f"  <<< {status} | {body}")

+ 40 - 0
e2e-test/config.py

@@ -0,0 +1,40 @@
1
+# ============================================================
2
+# 巴清农资商城 — E2E 测试配置
3
+# 拿到那台部署机器上后,先改这里的地址和账号密码
4
+# ============================================================
5
+
6
+# ------ 后端 API 地址 ------
7
+# 如果后端和前端的部署 IP 不同,用后端的 IP
8
+# 如果本机部署,就用 http://127.0.0.1:8020
9
+# 如果是局域网其他机器,用 http://192.168.x.x:8020
10
+API_BASE_URL = "http://127.0.0.1:8020"
11
+
12
+# ------ 管理后台登录 ------
13
+# 若依框架默认账号 admin / admin123
14
+ADMIN_USERNAME = "admin"
15
+ADMIN_PASSWORD = "admin123"
16
+
17
+# 是否启用验证码(默认 False,方便测试)
18
+# 如果部署环境开启了登录验证码,需要改为 True
19
+# 但建议测试环境关掉验证码,操作方式见 README.md
20
+CAPTCHA_ENABLED = False
21
+
22
+# ------ 商户账号(需要先在后台创建)------
23
+# 需要有 "商家端" 相关权限的角色
24
+SELLER_USERNAME = "seller"
25
+SELLER_PASSWORD = "123456"
26
+SELLER_SHOP_ID = 1  # 商户绑定的店铺 ID
27
+
28
+# ------ 消费者端(会员)------
29
+# 测试用的手机号,每次运行建议换一个或修改
30
+TEST_MOBILE = "13800000001"
31
+TEST_PASSWORD = "123456"
32
+TEST_NICKNAME = "测试用户"
33
+
34
+# ------ 测试数据开关 ------
35
+# True  = 测试执行时创建临时数据,测试完成不清理(方便人工检查)
36
+# False = 测试执行时创建临时数据,测试完成后尝试删除
37
+KEEP_TEST_DATA = True
38
+
39
+# ------ HTTP 请求配置 ------
40
+REQUEST_TIMEOUT = 30  # 秒

+ 124 - 0
e2e-test/conftest.py

@@ -0,0 +1,124 @@
1
+# ============================================================
2
+# pytest 配置 — 全局 Fixtures & HTML 报告
3
+# ============================================================
4
+import pytest
5
+import datetime
6
+import os
7
+import random
8
+from api_client import ApiClient
9
+from config import (
10
+    API_BASE_URL, ADMIN_USERNAME, ADMIN_PASSWORD, CAPTCHA_ENABLED,
11
+    SELLER_USERNAME, SELLER_PASSWORD, TEST_PASSWORD,
12
+)
13
+
14
+
15
+# ============================================================
16
+# HTML 报告配置
17
+# ============================================================
18
+def pytest_html_report_title(report):
19
+    report.title = "巴清农资商城 · 端到端全量测试报告"
20
+
21
+
22
+def pytest_configure(config):
23
+    now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
24
+    config._metadata = {
25
+        "项目": "巴清农资商城 (Baqing Shop)",
26
+        "测试时间": now,
27
+        "测试类型": "端到端全量测试 (E2E)",
28
+        "Python": os.popen("python --version 2>&1").read().strip(),
29
+        "API地址": API_BASE_URL,
30
+        "测试用例数": "124 条 (覆盖 63 个 Controller)",
31
+    }
32
+    for tag in ["admin_flow", "consumer_flow", "seller_flow", "full_chain"]:
33
+        config.addinivalue_line("markers", tag)
34
+
35
+
36
+@pytest.hookimpl(hookwrapper=True)
37
+def pytest_runtest_makereport(item, call):
38
+    outcome = yield
39
+    report = outcome.get_result()
40
+    if report.when in ("call", "setup"):
41
+        doc = item.function.__doc__ or ""
42
+        if doc:
43
+            report.description = doc.strip()
44
+
45
+
46
+# ============================================================
47
+# 全局 Fixtures
48
+# ============================================================
49
+
50
+@pytest.fixture(scope="session")
51
+def client():
52
+    """所有测试共享一个 HTTP 客户端(session 级别)"""
53
+    c = ApiClient()
54
+    yield c
55
+    c.session.close()
56
+
57
+
58
+@pytest.fixture(scope="session")
59
+def admin_token(client):
60
+    """管理员登录,返回 token(session 级别)"""
61
+    if CAPTCHA_ENABLED:
62
+        pytest.skip("验证码已开启,请在测试环境关闭验证码后再运行")
63
+    resp = client.post("/login", json={
64
+        "username": ADMIN_USERNAME,
65
+        "password": ADMIN_PASSWORD,
66
+        "code": "",
67
+        "uuid": "",
68
+    })
69
+    data = client.assert_ok("管理员登录失败")
70
+    token = data.get("token")
71
+    assert token, f"登录返回中没有 token"
72
+    client.set_admin_token(token)
73
+    print(f"\n  [SETUP] 管理员 {ADMIN_USERNAME} 登录成功")
74
+    return token
75
+
76
+
77
+@pytest.fixture(scope="session")
78
+def member_token(client):
79
+    """会员注册并登录(session 级别)"""
80
+    mobile = f"138{random.randint(10000000, 99999999)}"
81
+
82
+    # 注册
83
+    resp = client.post("/api/member/register", json={
84
+        "mobile": mobile,
85
+        "password": TEST_PASSWORD,
86
+        "confirmPassword": TEST_PASSWORD,
87
+        "nickName": f"E2E测试_{mobile[-4:]}",
88
+        "agreementAccepted": True,
89
+    })
90
+    data = client.assert_ok("会员注册失败")
91
+    member_id = data.get("data")
92
+    print(f"\n  [SETUP] 会员注册成功 memberId={member_id}")
93
+
94
+    # 登录
95
+    resp = client.post("/api/member/login", json={
96
+        "account": mobile,
97
+        "password": TEST_PASSWORD,
98
+        "agreementAccepted": True,
99
+    })
100
+    data = client.assert_ok("会员登录失败")
101
+    token = data.get("token")
102
+    assert token, "会员登录返回中没有 token"
103
+    client.set_member_token(token)
104
+    print(f"  [SETUP] 会员登录成功 token={token[:16]}...")
105
+    return token
106
+
107
+
108
+@pytest.fixture(scope="session")
109
+def seller_token(client):
110
+    """商家登录(session 级别)"""
111
+    if CAPTCHA_ENABLED:
112
+        pytest.skip("验证码已开启")
113
+    resp = client.post("/login", json={
114
+        "username": SELLER_USERNAME,
115
+        "password": SELLER_PASSWORD,
116
+        "code": "",
117
+        "uuid": "",
118
+    })
119
+    data = client.assert_ok("商家登录失败")
120
+    token = data.get("token")
121
+    assert token, "商家登录返回中没有 token"
122
+    client.set_seller_token(token)
123
+    print(f"\n  [SETUP] 商家 {SELLER_USERNAME} 登录成功")
124
+    return token

+ 5 - 0
e2e-test/requirements.txt

@@ -0,0 +1,5 @@
1
+requests>=2.28.0
2
+pytest>=7.4.0
3
+pytest-html>=4.0.0
4
+pytest-xdist>=3.5.0
5
+pyyaml>=6.0

+ 43 - 0
e2e-test/run_all.bat

@@ -0,0 +1,43 @@
1
+@echo off
2
+chcp 65001 >nul
3
+title 巴清农资商城 · 端到端全量测试
4
+
5
+echo ============================================
6
+echo  巴清农资商城 · E2E 全量测试套件
7
+echo  124 条用例 | 63 个 Controller | 3 个角色
8
+echo ============================================
9
+echo.
10
+echo [1/3] 检测 Python 环境...
11
+python --version >nul 2>&1
12
+if %errorlevel% neq 0 (
13
+    echo [错误] 未找到 Python,请先安装 Python 3.8+
14
+    pause
15
+    exit /b 1
16
+)
17
+python --version
18
+
19
+echo.
20
+echo [2/3] 安装依赖...
21
+pip install -r requirements.txt -q
22
+
23
+echo.
24
+echo [3/3] 运行全部测试...
25
+echo.
26
+
27
+set TEST_FILES=test_admin_flow.py test_consumer_flow.py test_seller_flow.py test_full_chain.py
28
+
29
+python -m pytest %TEST_FILES% -v --html=report.html --self-contained-html --tb=short
30
+
31
+echo.
32
+if %errorlevel% equ 0 (
33
+    echo ============================================
34
+    echo  全部测试通过!
35
+    echo  报告已生成: report.html
36
+    echo ============================================
37
+) else (
38
+    echo ============================================
39
+    echo  有失败用例,请查看 report.html 获取详情
40
+    echo ============================================
41
+)
42
+echo.
43
+pause

+ 35 - 0
e2e-test/run_all.sh

@@ -0,0 +1,35 @@
1
+#!/bin/bash
2
+# 巴清农资商城 · E2E 全量测试套件(Linux / macOS)
3
+
4
+echo "============================================"
5
+echo " 巴清农资商城 · E2E 全量测试套件"
6
+echo " 124 条用例 | 63 个 Controller"
7
+echo "============================================"
8
+echo ""
9
+
10
+if ! command -v python3 &>/dev/null; then
11
+    echo "[错误] 未找到 python3,请先安装 Python 3.8+"
12
+    exit 1
13
+fi
14
+echo "[OK] $(python3 --version)"
15
+
16
+echo "[*] 安装依赖..."
17
+pip3 install -r requirements.txt -q
18
+
19
+echo ""
20
+echo "[*] 运行全部测试..."
21
+echo ""
22
+
23
+python3 -m pytest test_admin_flow.py test_consumer_flow.py test_seller_flow.py test_full_chain.py -v --html=report.html --self-contained-html --tb=short
24
+
25
+if [ $? -eq 0 ]; then
26
+    echo ""
27
+    echo "============================================"
28
+    echo " 全部测试通过!报告: report.html"
29
+    echo "============================================"
30
+else
31
+    echo ""
32
+    echo "============================================"
33
+    echo " 有失败用例,请查看 report.html"
34
+    echo "============================================"
35
+fi

+ 509 - 0
e2e-test/test_admin_flow.py

@@ -0,0 +1,509 @@
1
+# ============================================================
2
+# 平台管理员端 — 全量端到端测试
3
+# 覆盖 20 个 Controller,约 70 个 API 端点
4
+# ============================================================
5
+import pytest
6
+import json
7
+from config import KEEP_TEST_DATA
8
+from api_client import ApiClient
9
+
10
+
11
+# ============================================================
12
+# 平台端 — 账号管理 (AccountManageController)
13
+# ============================================================
14
+class TestAdminAccountManage:
15
+
16
+    def test_01_account_list(self, client, admin_token):
17
+        """平台端账号列表查询"""
18
+        resp = client.get("/agri/accountManage/list")
19
+        client.assert_ok("查询账号列表失败")
20
+        data = client.extract_data() or {}
21
+        print(f"  [OK] 账号列表 total={data.get('total','?')}")
22
+
23
+    def test_02_account_detail(self, client, admin_token):
24
+        """平台端账号详情"""
25
+        # 从列表取第一个
26
+        resp = client.get("/agri/accountManage/list", params={"pageSize": 1, "pageNum": 1})
27
+        data = client.assert_ok()
28
+        rows = (data.get("data") or {}).get("rows") or []
29
+        if not rows:
30
+            pytest.skip("无账号数据")
31
+        uid = rows[0].get("userId")
32
+        resp = client.get(f"/agri/accountManage/{uid}")
33
+        client.assert_ok("查询账号详情失败")
34
+        print(f"  [OK] 账号详情 userId={uid}")
35
+
36
+
37
+# ============================================================
38
+# 平台端 — 管理员管理 (PlatformAdminController)
39
+# ============================================================
40
+class TestAdminPlatformAdmin:
41
+
42
+    def test_03_admin_list(self, client, admin_token):
43
+        """平台管理员列表"""
44
+        resp = client.get("/agri/platformAdmin/list")
45
+        client.assert_ok("查询管理员列表失败")
46
+        data = client.extract_data() or {}
47
+        print(f"  [OK] 管理员列表 total={data.get('total','?')}")
48
+
49
+    def test_04_admin_role_options(self, client, admin_token):
50
+        """平台管理员角色选项"""
51
+        resp = client.get("/agri/platformAdmin/roleOptions")
52
+        client.assert_ok("查询角色选项失败")
53
+        roles = client.extract_data() or []
54
+        print(f"  [OK] 角色选项 count={len(roles)}")
55
+
56
+    def test_05_admin_detail(self, client, admin_token):
57
+        """平台管理员详情"""
58
+        resp = client.get("/agri/platformAdmin/list", params={"pageSize": 1})
59
+        data = client.assert_ok()
60
+        rows = (data.get("data") or {}).get("rows") or []
61
+        if not rows:
62
+            pytest.skip("无管理员数据")
63
+        uid = rows[0].get("userId")
64
+        resp = client.get(f"/agri/platformAdmin/{uid}")
65
+        client.assert_ok("查询管理员详情失败")
66
+        print(f"  [OK] 管理员详情 userId={uid}")
67
+
68
+
69
+# ============================================================
70
+# 平台端 — 会员管理 (MemberController)
71
+# ============================================================
72
+class TestAdminMember:
73
+
74
+    def test_06_member_list(self, client, admin_token):
75
+        """平台端会员列表"""
76
+        resp = client.get("/agri/member/list")
77
+        client.assert_ok("查询会员列表失败")
78
+        data = client.extract_data() or {}
79
+        print(f"  [OK] 会员列表 total={data.get('total','?')}")
80
+
81
+    def test_07_member_levels(self, client, admin_token):
82
+        """平台端会员等级列表"""
83
+        resp = client.get("/agri/member/levels")
84
+        client.assert_ok("查询会员等级失败")
85
+        data = client.extract_data() or []
86
+        if isinstance(data, list):
87
+            print(f"  [OK] 会员等级 count={len(data)}")
88
+        else:
89
+            print(f"  [OK] 会员等级正常")
90
+
91
+    def test_08_member_detail(self, client, admin_token):
92
+        """平台端会员详情"""
93
+        resp = client.get("/agri/member/list", params={"pageSize": 1})
94
+        data = client.assert_ok()
95
+        rows = (data.get("data") or {}).get("rows") or []
96
+        if not rows:
97
+            pytest.skip("无会员数据")
98
+        mid = rows[0].get("memberId")
99
+        resp = client.get(f"/agri/member/{mid}")
100
+        client.assert_ok("查询会员详情失败")
101
+        print(f"  [OK] 会员详情 memberId={mid}")
102
+
103
+
104
+# ============================================================
105
+# 平台端 — 商家管理 (MerchantController)
106
+# ============================================================
107
+class TestAdminMerchant:
108
+
109
+    def test_09_merchant_list(self, client, admin_token):
110
+        """平台端商家列表"""
111
+        resp = client.get("/agri/merchant/list")
112
+        client.assert_ok("查询商家列表失败")
113
+        data = client.extract_data() or {}
114
+        print(f"  [OK] 商家列表 total={data.get('total','?')}")
115
+
116
+    def test_10_merchant_detail(self, client, admin_token):
117
+        """平台端商家详情"""
118
+        resp = client.get("/agri/merchant/list", params={"pageSize": 1})
119
+        data = client.assert_ok()
120
+        rows = (data.get("data") or {}).get("rows") or []
121
+        if not rows:
122
+            pytest.skip("无商家数据")
123
+        mid = rows[0].get("merchantId")
124
+        resp = client.get(f"/agri/merchant/{mid}")
125
+        client.assert_ok("查询商家详情失败")
126
+        print(f"  [OK] 商家详情 merchantId={mid}")
127
+
128
+
129
+# ============================================================
130
+# 平台端 — 入驻审核 (MerchantEntryApplyController)
131
+# ============================================================
132
+class TestAdminEntryAudit:
133
+
134
+    def test_11_entry_apply_list(self, client, admin_token):
135
+        """入驻申请列表"""
136
+        resp = client.get("/agri/merchantEntryApply/list")
137
+        client.assert_ok("查询入驻申请失败")
138
+        data = client.extract_data() or {}
139
+        total = data.get("total", 0) if isinstance(data, dict) else 0
140
+        print(f"  [OK] 入驻申请 total={total}")
141
+
142
+    def test_12_entry_pending_count(self, client, admin_token):
143
+        """待审核入驻数量"""
144
+        resp = client.get("/agri/merchantEntryApply/pendingCount")
145
+        data = client.assert_ok("查询待审核入驻数量失败")
146
+        cnt = (data.get("data") or {}).get("pendingCount", 0)
147
+        print(f"  [OK] 待审核入驻 count={cnt}")
148
+
149
+    def test_13_entry_apply_detail(self, client, admin_token):
150
+        """入驻申请详情"""
151
+        resp = client.get("/agri/merchantEntryApply/list", params={"pageSize": 1})
152
+        data = client.assert_ok()
153
+        rows = (data.get("data") or {}).get("rows") or []
154
+        if not rows:
155
+            pytest.skip("无入驻申请")
156
+        aid = rows[0].get("applyId")
157
+        resp = client.get(f"/agri/merchantEntryApply/{aid}")
158
+        client.assert_ok("查询入驻申请详情失败")
159
+        print(f"  [OK] 入驻申请详情 applyId={aid}")
160
+
161
+    def test_14_entry_approve_flow(self, client, admin_token):
162
+        """入驻审核通过流程(需有待审核数据)"""
163
+        resp = client.get("/agri/merchantEntryApply/list", params={
164
+            "pageSize": 1, "pageNum": 1
165
+        })
166
+        data = client.assert_ok()
167
+        rows = (data.get("data") or {}).get("rows") or []
168
+        if not rows:
169
+            pytest.skip("无入驻申请")
170
+
171
+        apply = rows[0]
172
+        aid = apply.get("applyId")
173
+        status = apply.get("applyStatus", "")
174
+        print(f"  [INFO] 入驻申请 applyId={aid} status={status}")
175
+
176
+        if status == "PENDING":
177
+            resp = client.put(f"/agri/merchantEntryApply/approve/{aid}", json={})
178
+            client.assert_ok("入驻审核通过失败")
179
+            print(f"  [OK] 入驻申请 {aid} 审核通过")
180
+        elif status == "APPROVED":
181
+            resp = client.put(f"/agri/merchantEntryApply/completePublicity/{aid}")
182
+            client.assert_ok("完成公示失败")
183
+            print(f"  [OK] 入驻申请 {aid} 完成公示")
184
+        else:
185
+            print(f"  [SKIP] 当前状态 {status},跳过")
186
+
187
+
188
+# ============================================================
189
+# 平台端 — 商品管理 (GoodsController + GoodsAuditController)
190
+# ============================================================
191
+class TestAdminGoods:
192
+
193
+    def test_15_goods_list(self, client, admin_token):
194
+        """平台端商品列表"""
195
+        resp = client.get("/agri/goods/list")
196
+        client.assert_ok("查询商品列表失败")
197
+        data = client.extract_data() or {}
198
+        print(f"  [OK] 商品列表 total={data.get('total','?')}")
199
+
200
+    def test_16_goods_pending_count(self, client, admin_token):
201
+        """待审核商品数量"""
202
+        resp = client.get("/agri/goods/pendingCount")
203
+        data = client.assert_ok("查询待审核商品数量失败")
204
+        cnt = (data.get("data") or {}).get("pendingCount", 0)
205
+        print(f"  [OK] 待审核商品 count={cnt}")
206
+
207
+    def test_17_goods_detail(self, client, admin_token):
208
+        """平台端商品详情"""
209
+        resp = client.get("/agri/goods/list", params={"pageSize": 1})
210
+        data = client.assert_ok()
211
+        rows = (data.get("data") or {}).get("rows") or []
212
+        if not rows:
213
+            resp = client.get("/agri/goodsAudit/list", params={"pageSize": 1})
214
+            data = client.assert_ok()
215
+            rows = (data.get("data") or {}).get("rows") or []
216
+        if not rows:
217
+            pytest.skip("无商品数据")
218
+        gid = rows[0].get("goodsId")
219
+        resp = client.get(f"/agri/goods/{gid}")
220
+        client.assert_ok("查询商品详情失败")
221
+        print(f"  [OK] 商品详情 goodsId={gid}")
222
+
223
+    def test_18_goods_audit_list(self, client, admin_token):
224
+        """商品审核列表"""
225
+        resp = client.get("/agri/goodsAudit/list")
226
+        client.assert_ok("查询商品审核列表失败")
227
+        data = client.extract_data() or {}
228
+        print(f"  [OK] 商品审核列表 total={data.get('total','?')}")
229
+
230
+    def test_19_goods_audit_pending_count(self, client, admin_token):
231
+        """商品审核待审数量"""
232
+        resp = client.get("/agri/goodsAudit/pendingCount")
233
+        data = client.assert_ok("查询审核待审数量失败")
234
+        cnt = (data.get("data") or {}).get("pendingCount", 0)
235
+        print(f"  [OK] 审核待审商品 count={cnt}")
236
+
237
+    def test_20_goods_audit_detail(self, client, admin_token):
238
+        """商品审核详情"""
239
+        resp = client.get("/agri/goodsAudit/list", params={"pageSize": 1})
240
+        data = client.assert_ok()
241
+        rows = (data.get("data") or {}).get("rows") or []
242
+        if not rows:
243
+            pytest.skip("无待审商品")
244
+        gid = rows[0].get("goodsId")
245
+        resp = client.get(f"/agri/goodsAudit/{gid}")
246
+        client.assert_ok("查询审核商品详情失败")
247
+        print(f"  [OK] 审核商品详情 goodsId={gid}")
248
+
249
+
250
+# ============================================================
251
+# 平台端 — 分类管理 (PlatformCategoryController)
252
+# ============================================================
253
+class TestAdminCategory:
254
+
255
+    def test_21_category_list(self, client, admin_token):
256
+        """平台分类列表"""
257
+        resp = client.get("/agri/category/list")
258
+        client.assert_ok("查询分类列表失败")
259
+        data = client.extract_data() or {}
260
+        print(f"  [OK] 分类列表 total={data.get('total','?')}")
261
+
262
+    def test_22_category_tree(self, client, admin_token):
263
+        """平台分类树"""
264
+        resp = client.get("/agri/category/tree")
265
+        client.assert_ok("查询分类树失败")
266
+        tree = client.extract_data() or []
267
+        print(f"  [OK] 分类树 nodes={len(tree)}")
268
+
269
+    def test_23_category_detail(self, client, admin_token):
270
+        """平台分类详情"""
271
+        resp = client.get("/agri/category/list", params={"pageSize": 1})
272
+        data = client.assert_ok()
273
+        rows = (data.get("data") or {}).get("rows") or []
274
+        if not rows:
275
+            pytest.skip("无分类数据")
276
+        cid = rows[0].get("categoryId")
277
+        resp = client.get(f"/agri/category/{cid}")
278
+        client.assert_ok("查询分类详情失败")
279
+        print(f"  [OK] 分类详情 categoryId={cid}")
280
+
281
+
282
+# ============================================================
283
+# 平台端 — Banner管理 (BannerController)
284
+# ============================================================
285
+class TestAdminBanner:
286
+
287
+    def test_24_banner_list(self, client, admin_token):
288
+        """Banner列表"""
289
+        resp = client.get("/agri/banner/list")
290
+        client.assert_ok("查询Banner列表失败")
291
+        data = client.extract_data() or {}
292
+        print(f"  [OK] Banner列表 total={data.get('total','?')}")
293
+
294
+    def test_25_banner_detail(self, client, admin_token):
295
+        """Banner详情"""
296
+        resp = client.get("/agri/banner/list", params={"pageSize": 1})
297
+        data = client.assert_ok()
298
+        rows = (data.get("data") or {}).get("rows") or []
299
+        if not rows:
300
+            pytest.skip("无Banner数据")
301
+        bid = rows[0].get("bannerId")
302
+        resp = client.get(f"/agri/banner/{bid}")
303
+        client.assert_ok("查询Banner详情失败")
304
+        print(f"  [OK] Banner详情 bannerId={bid}")
305
+
306
+
307
+# ============================================================
308
+# 平台端 — 商城设置 (MallSettingController)
309
+# ============================================================
310
+class TestAdminMallSetting:
311
+
312
+    def test_26_mall_setting_get(self, client, admin_token):
313
+        """商城设置查询"""
314
+        resp = client.get("/agri/mallSetting")
315
+        client.assert_ok("查询商城设置失败")
316
+        setting = client.extract_data() or {}
317
+        print(f"  [OK] 商城设置: {json.dumps(setting, ensure_ascii=False)[:100]}")
318
+
319
+
320
+# ============================================================
321
+# 平台端 — 服务协议 (MallServiceAgreementController)
322
+# ============================================================
323
+class TestAdminAgreement:
324
+
325
+    def test_27_service_agreement_get(self, client, admin_token):
326
+        """商城服务协议查询"""
327
+        resp = client.get("/agri/mallServiceAgreement")
328
+        client.assert_ok("查询服务协议失败")
329
+        agmt = client.extract_data() or {}
330
+        print(f"  [OK] 服务协议: {str(agmt.get('agreementType',''))[:80]}")
331
+
332
+    def test_28_entry_agreement_get(self, client, admin_token):
333
+        """入驻协议查询"""
334
+        resp = client.get("/agri/merchantEntryAgreement")
335
+        client.assert_ok("查询入驻协议失败")
336
+        agmt = client.extract_data() or {}
337
+        print(f"  [OK] 入驻协议: {str(agmt.get('agreementType',''))[:80]}")
338
+        client._test_has_agreement = True
339
+
340
+
341
+# ============================================================
342
+# 平台端 — 地区管理 (RegionController)
343
+# ============================================================
344
+class TestAdminRegion:
345
+
346
+    def test_29_region_tree(self, client, admin_token):
347
+        """地区树查询"""
348
+        resp = client.get("/agri/region/tree")
349
+        client.assert_ok("查询地区树失败")
350
+        tree = client.extract_data() or []
351
+        print(f"  [OK] 地区树 nodes={len(tree) if isinstance(tree, list) else 'ok'}")
352
+
353
+
354
+# ============================================================
355
+# 平台端 — 店铺管理 (ShopController)
356
+# ============================================================
357
+class TestAdminShop:
358
+
359
+    def test_30_shop_list(self, client, admin_token):
360
+        """平台端店铺列表"""
361
+        resp = client.get("/agri/shop/list")
362
+        client.assert_ok("查询店铺列表失败")
363
+        data = client.extract_data() or {}
364
+        total = data.get("total", 0) if isinstance(data, dict) else 0
365
+        print(f"  [OK] 店铺列表 total={total}")
366
+
367
+    def test_31_shop_detail(self, client, admin_token):
368
+        """平台端店铺详情"""
369
+        resp = client.get("/agri/shop/list", params={"pageSize": 1})
370
+        data = client.assert_ok()
371
+        rows = (data.get("data") or {}).get("rows") or []
372
+        if not rows:
373
+            pytest.skip("无店铺数据")
374
+        sid = rows[0].get("shopId")
375
+        resp = client.get(f"/agri/shop/{sid}")
376
+        client.assert_ok("查询店铺详情失败")
377
+        print(f"  [OK] 店铺详情 shopId={sid}")
378
+
379
+
380
+# ============================================================
381
+# 平台端 — 商品服务 (GoodsServiceController)
382
+# ============================================================
383
+class TestAdminGoodsService:
384
+
385
+    def test_32_goods_service_list(self, client, admin_token):
386
+        """商品服务列表"""
387
+        resp = client.get("/agri/goodsService/list")
388
+        client.assert_ok("查询商品服务列表失败")
389
+        data = client.extract_data() or {}
390
+        print(f"  [OK] 商品服务列表 total={data.get('total','?') if isinstance(data, dict) else 'ok'}")
391
+
392
+    def test_33_goods_service_detail(self, client, admin_token):
393
+        """商品服务详情"""
394
+        resp = client.get("/agri/goodsService/list", params={"pageSize": 1})
395
+        data = client.assert_ok()
396
+        # 可能返回 list 或 {rows, total}
397
+        rows = []
398
+        if isinstance(data.get("data"), dict):
399
+            rows = data["data"].get("rows", [])
400
+        elif isinstance(data.get("data"), list):
401
+            rows = data["data"]
402
+        if not rows:
403
+            pytest.skip("无商品服务数据")
404
+        sid = rows[0].get("serviceId")
405
+        resp = client.get(f"/agri/goodsService/{sid}")
406
+        client.assert_ok("查询商品服务详情失败")
407
+        print(f"  [OK] 商品服务详情 serviceId={sid}")
408
+
409
+
410
+# ============================================================
411
+# 平台端 — 财务管理 (FundOverview / WithdrawAudit / WithdrawSummary)
412
+# ============================================================
413
+class TestAdminFinance:
414
+
415
+    def test_34_fund_summary(self, client, admin_token):
416
+        """资金概览"""
417
+        resp = client.get("/agri/finance/fundOverview/summary")
418
+        client.assert_ok("查询资金概览失败")
419
+        summary = client.extract_data() or {}
420
+        print(f"  [OK] 资金概览: {json.dumps(summary, ensure_ascii=False)[:120]}")
421
+
422
+    def test_35_fund_shops(self, client, admin_token):
423
+        """资金概览-店铺列表"""
424
+        resp = client.get("/agri/finance/fundOverview/shops")
425
+        client.assert_ok("查询资金店铺列表失败")
426
+        data = client.extract_data() or {}
427
+        print(f"  [OK] 资金店铺列表: {json.dumps(data, ensure_ascii=False)[:120]}")
428
+
429
+    def test_36_fund_details(self, client, admin_token):
430
+        """资金概览-明细"""
431
+        resp = client.get("/agri/finance/fundOverview/details", params={"pageSize": 1})
432
+        client.assert_ok("查询资金明细失败")
433
+        data = client.extract_data() or {}
434
+        print(f"  [OK] 资金明细正常")
435
+
436
+    def test_37_withdraw_audit_list(self, client, admin_token):
437
+        """提现审核列表"""
438
+        resp = client.get("/agri/finance/withdrawAudit/list")
439
+        client.assert_ok("查询提现列表失败")
440
+        data = client.extract_data() or {}
441
+        total = data.get("total", 0) if isinstance(data, dict) else 0
442
+        print(f"  [OK] 提现审核列表 total={total}")
443
+
444
+    def test_38_withdraw_pending_count(self, client, admin_token):
445
+        """提现待审数量"""
446
+        resp = client.get("/agri/finance/withdrawAudit/pendingCount")
447
+        client.assert_ok("查询提现待审数量失败")
448
+        cnt = (client.extract_data() or {}).get("pendingCount", 0)
449
+        print(f"  [OK] 提现待审 count={cnt}")
450
+
451
+    def test_39_withdraw_summary_shops(self, client, admin_token):
452
+        """提现汇总-店铺"""
453
+        resp = client.get("/agri/finance/withdrawSummary/shops")
454
+        client.assert_ok("查询提现汇总店铺失败")
455
+        data = client.extract_data() or {}
456
+        print(f"  [OK] 提现汇总-店铺正常")
457
+
458
+    def test_40_withdraw_summary_details(self, client, admin_token):
459
+        """提现汇总-明细"""
460
+        resp = client.get("/agri/finance/withdrawSummary/details", params={"pageSize": 1})
461
+        client.assert_ok("查询提现汇总明细失败")
462
+        data = client.extract_data() or {}
463
+        print(f"  [OK] 提现汇总-明细正常")
464
+
465
+
466
+# ============================================================
467
+# 平台端 — 微信支付配置 (PlatformShopWechatPayController)
468
+# ============================================================
469
+class TestAdminWechatPay:
470
+
471
+    def test_41_wechat_pay_list(self, client, admin_token):
472
+        """微信支付配置列表"""
473
+        resp = client.get("/agri/shop/wechat-pay/list")
474
+        client.assert_ok("查询支付配置列表失败")
475
+        data = client.extract_data() or {}
476
+        total = data.get("total", 0) if isinstance(data, dict) else 0
477
+        print(f"  [OK] 支付配置列表 total={total}")
478
+
479
+    def test_42_wechat_pay_pending_count(self, client, admin_token):
480
+        """微信支付待审数量"""
481
+        resp = client.get("/agri/shop/wechat-pay/pendingCount")
482
+        client.assert_ok("查询支付待审数量失败")
483
+        cnt = (client.extract_data() or {}).get("pendingCount", 0)
484
+        print(f"  [OK] 支付待审 count={cnt}")
485
+
486
+    def test_43_wechat_pay_detail(self, client, admin_token):
487
+        """微信支付配置详情"""
488
+        resp = client.get("/agri/shop/wechat-pay/list", params={"pageSize": 1})
489
+        data = client.assert_ok()
490
+        rows = (data.get("data") or {}).get("rows") or []
491
+        if not rows:
492
+            pytest.skip("无支付配置数据")
493
+        sid = rows[0].get("shopId")
494
+        resp = client.get(f"/agri/shop/wechat-pay/{sid}")
495
+        client.assert_ok("查询支付配置详情失败")
496
+        print(f"  [OK] 支付配置详情 shopId={sid}")
497
+
498
+
499
+# ============================================================
500
+# 平台端 — 全局设置 (ShopGlobalSettingController)
501
+# ============================================================
502
+class TestAdminGlobalSetting:
503
+
504
+    def test_44_global_setting_get(self, client, admin_token):
505
+        """全局设置查询"""
506
+        resp = client.get("/agri/shopSetting")
507
+        client.assert_ok("查询全局设置失败")
508
+        setting = client.extract_data() or {}
509
+        print(f"  [OK] 全局设置: {json.dumps(setting, ensure_ascii=False)[:100]}")

+ 485 - 0
e2e-test/test_consumer_flow.py

@@ -0,0 +1,485 @@
1
+# ============================================================
2
+# C端消费者 — 全量端到端测试
3
+# 覆盖 19 个 Controller,约 60 个 API 端点
4
+# ============================================================
5
+import pytest
6
+import json
7
+import random
8
+from api_client import ApiClient
9
+from config import TEST_PASSWORD, KEEP_TEST_DATA
10
+
11
+
12
+# ============================================================
13
+# 首页浏览 (HomeAppController) — 无需登录
14
+# ============================================================
15
+class TestConsumerHome:
16
+
17
+    def test_45_home_banners(self, client, admin_token):
18
+        """首页Banner列表"""
19
+        client.clear_token()
20
+        resp = client.get("/api/home/banners")
21
+        client.assert_ok("查询Banner失败")
22
+        banners = client.extract_data() or []
23
+        print(f"  [OK] Banner count={len(banners)}")
24
+
25
+    def test_46_home_categories(self, client):
26
+        """首页分类导航"""
27
+        resp = client.get("/api/home/categories")
28
+        client.assert_ok("查询首页分类失败")
29
+        cats = client.extract_data() or []
30
+        print(f"  [OK] 首页分类 count={len(cats)}")
31
+
32
+    def test_47_home_hot_goods(self, client):
33
+        """热销商品"""
34
+        resp = client.get("/api/home/hot-goods")
35
+        client.assert_ok("查询热销商品失败")
36
+        goods = client.extract_data() or []
37
+        print(f"  [OK] 热销商品 count={len(goods)}")
38
+        if goods:
39
+            client._test_hot_goods_id = goods[0].get("goodsId") or goods[0].get("id")
40
+
41
+
42
+# ============================================================
43
+# 搜索 (SearchAppController)
44
+# ============================================================
45
+class TestConsumerSearch:
46
+
47
+    def test_48_search(self, client):
48
+        """全站搜索"""
49
+        resp = client.get("/api/search", params={"keyword": "测试"})
50
+        client.assert_ok("搜索失败")
51
+        result = client.extract_data() or {}
52
+        print(f"  [OK] 搜索正常: {json.dumps(result, ensure_ascii=False)[:100]}")
53
+
54
+    def test_49_search_empty(self, client):
55
+        """搜索空关键词"""
56
+        resp = client.get("/api/search", params={"keyword": ""})
57
+        client.assert_ok("空搜索失败")
58
+        print(f"  [OK] 空搜索正常")
59
+
60
+
61
+# ============================================================
62
+# 商品详情 (GoodsAppController)
63
+# ============================================================
64
+class TestConsumerGoods:
65
+
66
+    def test_50_goods_detail(self, client):
67
+        """商品详情"""
68
+        gid = getattr(client, '_test_hot_goods_id', None)
69
+        if not gid:
70
+            resp = client.get("/api/home/hot-goods")
71
+            data = client.assert_ok()
72
+            goods = data.get("data", [])
73
+            if not goods:
74
+                pytest.skip("无在售商品")
75
+            gid = goods[0].get("goodsId") or goods[0].get("id")
76
+        resp = client.get(f"/api/goods/{gid}")
77
+        client.assert_ok("查询商品详情失败")
78
+        detail = client.extract_data() or {}
79
+        print(f"  [OK] 商品详情: {detail.get('goodsName','')[:30]}")
80
+        client._test_goods_id = gid
81
+
82
+    def test_51_goods_can_purchase(self, client):
83
+        """商品可购校验"""
84
+        gid = getattr(client, '_test_goods_id', None)
85
+        if not gid:
86
+            pytest.skip("无商品ID")
87
+        resp = client.get(f"/api/goods/{gid}/can-purchase")
88
+        client.assert_ok("查询可购校验失败")
89
+        result = client.extract_data() or {}
90
+        print(f"  [OK] 可购校验: {result}")
91
+
92
+    def test_52_goods_reviews(self, client):
93
+        """商品评价列表"""
94
+        gid = getattr(client, '_test_goods_id', None)
95
+        if not gid:
96
+            pytest.skip("无商品ID")
97
+        resp = client.get(f"/api/goods/{gid}/reviews")
98
+        client.assert_ok("查询评价失败")
99
+        data = client.extract_data() or {}
100
+        total = data.get("total", 0) if isinstance(data, dict) else 0
101
+        print(f"  [OK] 商品评价 total={total}")
102
+
103
+
104
+# ============================================================
105
+# 店铺 (ShopAppController)
106
+# ============================================================
107
+class TestConsumerShop:
108
+
109
+    def test_53_shop_profile(self, client):
110
+        """店铺主页"""
111
+        # 从首页热销商品中找店铺ID
112
+        resp = client.get("/api/home/hot-goods")
113
+        data = client.assert_ok()
114
+        goods = data.get("data", [])
115
+        shop_id = None
116
+        for g in goods:
117
+            sid = g.get("shopId")
118
+            if sid:
119
+                shop_id = sid
120
+                break
121
+        if not shop_id:
122
+            pytest.skip("无店铺数据")
123
+        client._test_shop_id = shop_id
124
+        resp = client.get(f"/api/shop/{shop_id}")
125
+        client.assert_ok("查询店铺主页失败")
126
+        profile = client.extract_data() or {}
127
+        print(f"  [OK] 店铺主页: {profile.get('shopName','')[:20] if isinstance(profile, dict) else 'ok'}")
128
+
129
+    def test_54_shop_categories(self, client):
130
+        """店铺分类"""
131
+        sid = getattr(client, '_test_shop_id', None)
132
+        if not sid:
133
+            pytest.skip("无店铺ID")
134
+        resp = client.get(f"/api/shop/{sid}/categories")
135
+        client.assert_ok("查询店铺分类失败")
136
+        cats = client.extract_data() or []
137
+        print(f"  [OK] 店铺分类 count={len(cats)}")
138
+
139
+    def test_55_shop_goods(self, client):
140
+        """店铺商品列表"""
141
+        sid = getattr(client, '_test_shop_id', None)
142
+        if not sid:
143
+            pytest.skip("无店铺ID")
144
+        resp = client.get(f"/api/shop/{sid}/goods")
145
+        client.assert_ok("查询店铺商品失败")
146
+        data = client.extract_data() or {}
147
+        print(f"  [OK] 店铺商品列表正常")
148
+
149
+
150
+# ============================================================
151
+# C端分类 (CategoryAppController)
152
+# ============================================================
153
+class TestConsumerCategory:
154
+
155
+    def test_56_category_tree(self, client):
156
+        """C端分类树"""
157
+        resp = client.get("/api/category/tree")
158
+        client.assert_ok("查询分类树失败")
159
+        tree = client.extract_data() or []
160
+        print(f"  [OK] C端分类树 nodes={len(tree)}")
161
+
162
+
163
+# ============================================================
164
+# C端地区 (RegionAppController)
165
+# ============================================================
166
+class TestConsumerRegion:
167
+
168
+    def test_57_region_tree(self, client):
169
+        """C端地区树"""
170
+        resp = client.get("/api/region/tree")
171
+        client.assert_ok("查询地区树失败")
172
+        tree = client.extract_data() or []
173
+        print(f"  [OK] C端地区树 nodes={len(tree)}")
174
+
175
+
176
+# ============================================================
177
+# 商城配置 (MallConfigAppController)
178
+# ============================================================
179
+class TestConsumerMallConfig:
180
+
181
+    def test_58_mall_config(self, client):
182
+        """商城配置"""
183
+        resp = client.get("/api/mall/config")
184
+        client.assert_ok("查询商城配置失败")
185
+        config = client.extract_data() or {}
186
+        print(f"  [OK] 商城配置: {json.dumps(config, ensure_ascii=False)[:120]}")
187
+
188
+
189
+# ============================================================
190
+# 协议查询 (MallServiceAgreement + MerchantEntryAgreement)
191
+# ============================================================
192
+class TestConsumerAgreement:
193
+
194
+    def test_59_service_agreement_status(self, client):
195
+        """服务协议状态"""
196
+        resp = client.get("/api/member/serviceAgreement/status")
197
+        client.assert_ok("查询服务协议状态失败")
198
+        status = client.extract_data() or {}
199
+        print(f"  [OK] 服务协议状态: {status}")
200
+
201
+    def test_60_entry_agreement(self, client):
202
+        """入驻协议内容"""
203
+        resp = client.get("/api/merchant/entry/agreement")
204
+        client.assert_ok("查询入驻协议失败")
205
+        agmt = client.extract_data() or {}
206
+        print(f"  [OK] 入驻协议: {str(agmt)[:80]}")
207
+
208
+    def test_61_entry_agreement_status(self, client):
209
+        """入驻协议状态"""
210
+        resp = client.get("/api/merchant/entry/status")
211
+        client.assert_ok("查询入驻协议状态失败")
212
+        status = client.extract_data() or {}
213
+        print(f"  [OK] 入驻协议状态: {status}")
214
+
215
+
216
+# ============================================================
217
+# 会员注册登录 (MemberAppController)
218
+# ============================================================
219
+class TestConsumerMember:
220
+
221
+    @pytest.fixture(autouse=True)
222
+    def setup_member(self, client, member_token):
223
+        """复用 conftest 中的 member_token fixture"""
224
+        pass
225
+
226
+    def test_62_member_profile(self, client, member_token):
227
+        """会员基本信息"""
228
+        resp = client.get("/api/member/profile")
229
+        client.assert_ok("查询会员信息失败")
230
+        profile = client.extract_data() or {}
231
+        print(f"  [OK] 会员信息: {profile.get('nickName','')[:20]}, mobile={profile.get('mobile','')}")
232
+
233
+    def test_63_member_update_profile(self, client, member_token):
234
+        """会员更新信息"""
235
+        resp = client.put("/api/member/profile", json={
236
+            "nickName": f"E2E用户_{random.randint(100,999)}"
237
+        })
238
+        client.assert_ok("更新会员信息失败")
239
+        print(f"  [OK] 会员信息更新成功")
240
+
241
+    def test_64_member_address_list(self, client, member_token):
242
+        """会员地址列表"""
243
+        resp = client.get("/api/member/address/list")
244
+        client.assert_ok("查询地址列表失败")
245
+        addrs = client.extract_data() or []
246
+        print(f"  [OK] 地址列表 count={len(addrs)}")
247
+
248
+    def test_65_member_add_address(self, client, member_token):
249
+        """新增收货地址"""
250
+        resp = client.post("/api/member/address", json={
251
+            "receiverName": "张三",
252
+            "receiverMobile": "13800138000",
253
+            "province": "北京市",
254
+            "city": "北京市",
255
+            "district": "朝阳区",
256
+            "detailAddress": "测试街道100号",
257
+            "fullAddress": "北京市北京市朝阳区测试街道100号",
258
+            "isDefault": True,
259
+        })
260
+        data = client.assert_ok("新增地址失败")
261
+        addr_id = data.get("data")
262
+        client._test_address_id = addr_id
263
+        print(f"  [OK] 新增地址 addressId={addr_id}")
264
+
265
+    def test_66_member_set_default_address(self, client, member_token):
266
+        """设置默认地址"""
267
+        addr_id = getattr(client, '_test_address_id', None)
268
+        if not addr_id:
269
+            pytest.skip("无地址ID")
270
+        resp = client.put(f"/api/member/address/{addr_id}/default")
271
+        client.assert_ok("设置默认地址失败")
272
+        print(f"  [OK] 设置默认地址 {addr_id}")
273
+
274
+    def test_67_member_update_address(self, client, member_token):
275
+        """更新收货地址"""
276
+        addr_id = getattr(client, '_test_address_id', None)
277
+        if not addr_id:
278
+            pytest.skip("无地址ID")
279
+        resp = client.put("/api/member/address", json={
280
+            "addressId": addr_id,
281
+            "receiverName": "张三(改)",
282
+            "receiverMobile": "13800138000",
283
+            "detailAddress": "测试街道200号",
284
+        })
285
+        client.assert_ok("更新地址失败")
286
+        print(f"  [OK] 更新地址 {addr_id}")
287
+
288
+
289
+# ============================================================
290
+# 购物车 (CartAppController) — 需登录
291
+# ============================================================
292
+class TestConsumerCart:
293
+
294
+    def test_68_cart_list_empty(self, client, member_token):
295
+        """购物车列表(空)"""
296
+        resp = client.get("/api/cart")
297
+        client.assert_ok("查询购物车失败")
298
+        items = client.extract_data() or {}
299
+        print(f"  [OK] 购物车列表正常")
300
+
301
+    def test_69_cart_add_item(self, client, member_token):
302
+        """添加商品到购物车"""
303
+        gid = getattr(client, '_test_goods_id', None)
304
+        if not gid:
305
+            resp = client.get("/api/home/hot-goods")
306
+            data = client.assert_ok()
307
+            goods = data.get("data", [])
308
+            if not goods:
309
+                pytest.skip("无商品")
310
+            gid = goods[0].get("goodsId") or goods[0].get("id")
311
+        resp = client.post("/api/cart/items", json={
312
+            "goodsId": gid,
313
+            "quantity": 1,
314
+        })
315
+        client.assert_ok("加购失败")
316
+        print(f"  [OK] 加购成功 goodsId={gid}")
317
+
318
+    def test_70_cart_update_quantity(self, client, member_token):
319
+        """更新购物车数量"""
320
+        resp = client.get("/api/cart")
321
+        data = client.assert_ok()
322
+        cart_data = client.extract_data() or {}
323
+        items = []
324
+        if isinstance(cart_data, dict):
325
+            items = cart_data.get("items", []) or cart_data.get("cartItems", [])
326
+            if not items:
327
+                items = cart_data.get("data", [])
328
+        if not items:
329
+            # 找任何列表
330
+            for v in cart_data.values():
331
+                if isinstance(v, list) and len(v) > 0:
332
+                    items = v
333
+                    break
334
+        if not items:
335
+            pytest.skip("购物车无商品")
336
+        ci = items[0]
337
+        ciid = ci.get("cartItemId") or ci.get("id")
338
+        if not ciid:
339
+            pytest.skip("购物车项无ID")
340
+        resp = client.put(f"/api/cart/items/{ciid}/quantity", json={"quantity": 2})
341
+        client.assert_ok("更新数量失败")
342
+        print(f"  [OK] 更新数量 cartItemId={ciid} qty=2")
343
+        client._test_cart_item_id = ciid
344
+
345
+    def test_71_cart_remove_item(self, client, member_token):
346
+        """删除购物车商品"""
347
+        ciid = getattr(client, '_test_cart_item_id', None)
348
+        if not ciid:
349
+            # 先加一个再到购物车查看
350
+            gid = getattr(client, '_test_goods_id', None)
351
+            if not gid:
352
+                pytest.skip("无商品ID")
353
+            client.post("/api/cart/items", json={"goodsId": gid, "quantity": 1})
354
+            resp = client.get("/api/cart")
355
+            data = client.assert_ok()
356
+            cart_data = client.extract_data() or {}
357
+            items = []
358
+            if isinstance(cart_data, dict):
359
+                for v in cart_data.values():
360
+                    if isinstance(v, list) and len(v) > 0:
361
+                        items = v
362
+                        break
363
+            if not items:
364
+                pytest.skip("购物车无商品")
365
+            ciid = items[0].get("cartItemId") or items[0].get("id")
366
+        resp = client.delete(f"/api/cart/items/{ciid}")
367
+        client.assert_ok("删除购物车商品失败")
368
+        print(f"  [OK] 删除购物车商品 {ciid}")
369
+
370
+
371
+# ============================================================
372
+# 结算下单 (CheckoutAppController + OrderAppController)
373
+# ============================================================
374
+class TestConsumerCheckout:
375
+
376
+    def test_72_preview_and_submit_order(self, client, member_token):
377
+        """预览并提交订单"""
378
+        gid = getattr(client, '_test_goods_id', None)
379
+        if not gid:
380
+            pytest.skip("无商品")
381
+        # 先有地址
382
+        resp = client.get("/api/member/address/list")
383
+        data = client.assert_ok()
384
+        addrs = client.extract_data() or []
385
+        if not addrs:
386
+            pytest.skip("无收货地址")
387
+        addr_id = addrs[0].get("addressId")
388
+
389
+        # 预览
390
+        resp = client.post("/api/checkout/preview", json={
391
+            "source": "BUY_NOW",
392
+            "items": [{"goodsId": gid, "quantity": 1}],
393
+        })
394
+        client.assert_ok("预览订单失败")
395
+        preview = client.extract_data() or {}
396
+        print(f"  [OK] 订单预览正常")
397
+
398
+        # 提交
399
+        resp = client.post("/api/checkout/submit", json={
400
+            "source": "BUY_NOW",
401
+            "addressId": addr_id,
402
+            "payType": "WEIXIN",
403
+            "items": [{"goodsId": gid, "quantity": 1}],
404
+        })
405
+        data = client.assert_ok("提交订单失败")
406
+        order_data = data.get("data", {}) if isinstance(data.get("data"), dict) else data
407
+        order_id = order_data.get("orderId")
408
+        if not order_id:
409
+            order_ids = order_data.get("orderIds", [])
410
+            order_id = order_ids[0] if order_ids else None
411
+        if not order_id:
412
+            print(f"  [WARN] 提交订单返回结构: {json.dumps(data, ensure_ascii=False)[:150]}")
413
+            pytest.skip("未获取到orderId")
414
+        client._test_order_id = order_id
415
+        print(f"  [OK] 订单提交成功 orderId={order_id}")
416
+
417
+
418
+# ============================================================
419
+# 订单管理 (OrderAppController)
420
+# ============================================================
421
+class TestConsumerOrder:
422
+
423
+    def test_73_order_badges(self, client, member_token):
424
+        """订单角标"""
425
+        resp = client.get("/api/order/badges")
426
+        client.assert_ok("查询订单角标失败")
427
+        badges = client.extract_data() or {}
428
+        print(f"  [OK] 订单角标: {badges}")
429
+
430
+    def test_74_order_list(self, client, member_token):
431
+        """订单列表"""
432
+        resp = client.get("/api/order/list")
433
+        client.assert_ok("查询订单列表失败")
434
+        data = client.extract_data() or {}
435
+        total = data.get("total", 0) if isinstance(data, dict) else 0
436
+        print(f"  [OK] 订单列表 total={total}")
437
+
438
+    def test_75_order_detail(self, client, member_token):
439
+        """订单详情"""
440
+        oid = getattr(client, '_test_order_id', None)
441
+        if not oid:
442
+            pytest.skip("无订单ID")
443
+        resp = client.get(f"/api/order/{oid}")
444
+        client.assert_ok("查询订单详情失败")
445
+        detail = client.extract_data() or {}
446
+        status = detail.get("orderStatus", "")
447
+        print(f"  [OK] 订单详情 orderId={oid} status={status}")
448
+
449
+    def test_76_order_pay(self, client, member_token):
450
+        """订单支付(mock模式)"""
451
+        oid = getattr(client, '_test_order_id', None)
452
+        if not oid:
453
+            pytest.skip("无订单ID")
454
+        resp = client.post(f"/api/order/{oid}/pay")
455
+        data = client.assert_ok("支付失败")
456
+        print(f"  [OK] 支付调用成功")
457
+
458
+    def test_77_order_confirm_receive(self, client, member_token):
459
+        """确认收货"""
460
+        oid = getattr(client, '_test_order_id', None)
461
+        if not oid:
462
+            pytest.skip("无订单ID")
463
+        resp = client.post(f"/api/order/{oid}/confirm-receive")
464
+        data = client.assert_ok("确认收货失败")
465
+        print(f"  [OK] 确认收货 orderId={oid}")
466
+
467
+
468
+# ============================================================
469
+# 开放统计 (MallStatsOpenController)
470
+# ============================================================
471
+class TestConsumerStats:
472
+
473
+    def test_78_stats_hot_category(self, client):
474
+        """热销类目统计"""
475
+        resp = client.get("/api/open/stats/hotCategory")
476
+        client.assert_ok("查询热销类目失败")
477
+        data = client.extract_data() or {}
478
+        print(f"  [OK] 热销类目: {json.dumps(data, ensure_ascii=False)[:100]}")
479
+
480
+    def test_79_stats_overview(self, client):
481
+        """商城概览统计"""
482
+        resp = client.get("/api/open/stats/overview")
483
+        client.assert_ok("查询商城概览失败")
484
+        data = client.extract_data() or {}
485
+        print(f"  [OK] 商城概览: {json.dumps(data, ensure_ascii=False)[:100]}")

+ 215 - 0
e2e-test/test_full_chain.py

@@ -0,0 +1,215 @@
1
+# ============================================================
2
+# 全链路串联测试 — 跨角色业务流程
3
+# 模拟真实的业务协作场景
4
+# ============================================================
5
+import pytest
6
+import json
7
+from config import SELLER_SHOP_ID, KEEP_TEST_DATA
8
+
9
+
10
+class TestFullChain:
11
+
12
+    def test_chain_01_admin_entry_review(self, client, admin_token):
13
+        """管理员查看并审核入驻申请"""
14
+        resp = client.get("/agri/merchantEntryApply/list", params={
15
+            "pageSize": 5, "pageNum": 1
16
+        })
17
+        client.assert_ok()
18
+        data = client.extract_data() or {}
19
+        total = data.get("total", 0)
20
+        rows = data.get("rows", [])
21
+        print(f"  [INFO] 入驻申请共 {total} 条")
22
+        pending = [r for r in rows if r.get("applyStatus") == "PENDING"]
23
+        if pending:
24
+            apply = pending[0]
25
+            aid = apply.get("applyId")
26
+            resp = client.put(f"/agri/merchantEntryApply/approve/{aid}", json={})
27
+            client.assert_ok("审核通过失败")
28
+            print(f"  [OK]  审核通过 applyId={aid}")
29
+        else:
30
+            print(f"  [SKIP] 无待审核申请")
31
+
32
+    def test_chain_02_admin_audit_goods(self, client, admin_token):
33
+        """管理员审核待上架商品"""
34
+        resp = client.get("/agri/goodsAudit/list", params={
35
+            "pageSize": 5, "pageNum": 1
36
+        })
37
+        client.assert_ok()
38
+        data = client.extract_data() or {}
39
+        total = data.get("total", 0)
40
+        rows = data.get("rows", [])
41
+        print(f"  [INFO] 待审核商品共 {total} 条")
42
+        pending = [r for r in rows if r.get("goodsStatus") in ("SUBMITTED", "PENDING")]
43
+        if pending:
44
+            g = pending[0]
45
+            gid = g.get("goodsId")
46
+            resp = client.put("/agri/goodsAudit/audit", json={
47
+                "goodsIds": [gid],
48
+                "auditStatus": "APPROVED",
49
+                "auditRemark": "E2E测试自动审核通过"
50
+            })
51
+            data = client.assert_ok("审核失败")
52
+            print(f"  [OK]  审核通过 goodsId={gid}")
53
+        else:
54
+            print(f"  [SKIP] 无待审核商品")
55
+
56
+    def test_chain_03_seller_switch_shop(self, client, seller_token):
57
+        """商家切换店铺上下文"""
58
+        resp = client.put("/agri/seller/context/shop", json={
59
+            "shopId": SELLER_SHOP_ID
60
+        })
61
+        client.assert_ok("切换店铺失败")
62
+        print(f"  [OK]  店铺已切换 shopId={SELLER_SHOP_ID}")
63
+
64
+    def test_chain_04_seller_view_goods(self, client, seller_token):
65
+        """商家查看商品并检查待提交商品"""
66
+        resp = client.get("/agri/seller/goods/list")
67
+        client.assert_ok()
68
+        data = client.extract_data() or {}
69
+        total = data.get("total", 0)
70
+        rows = data.get("rows", [])
71
+        print(f"  [INFO] 商家商品共 {total} 条")
72
+
73
+        draft = [r for r in rows if r.get("goodsStatus") in ("DRAFT",)]
74
+        submitted = [r for r in rows if r.get("goodsStatus") in ("SUBMITTED", "PENDING")]
75
+        on_shelf = [r for r in rows if r.get("goodsStatus") == "ON_SHELF"]
76
+
77
+        if draft:
78
+            g = draft[0]
79
+            gid = g.get("goodsId")
80
+            resp = client.put(f"/agri/seller/goods/{gid}/submit")
81
+            client.assert_ok("提交上架失败")
82
+            print(f"  [OK]  提交上架 goodsId={gid}")
83
+        else:
84
+            print(f"  [SKIP] 无草稿商品需提交")
85
+
86
+        if on_shelf:
87
+            print(f"  [INFO] 在售商品 {len(on_shelf)} 个")
88
+
89
+    def test_chain_05_seller_view_orders(self, client, seller_token):
90
+        """商家查看订单并发货"""
91
+        resp = client.get("/agri/seller/order/list")
92
+        client.assert_ok()
93
+        data = client.extract_data() or {}
94
+        total = data.get("total", 0)
95
+        rows = data.get("rows", [])
96
+        print(f"  [INFO] 商家订单共 {total} 条")
97
+
98
+        unpaid = [r for r in rows if r.get("orderStatus") in ("WAIT_PAY", "UNPAID")]
99
+        shipped = [r for r in rows if r.get("orderStatus") in ("WAIT_SHIP", "UNSHIPPED")]
100
+
101
+        print(f"  [INFO] 待付款 {len(unpaid)}, 待发货 {len(shipped)}")
102
+
103
+    def test_chain_06_seller_view_stock(self, client, seller_token):
104
+        """商家查看库存"""
105
+        resp = client.get("/agri/seller/stock/query/list")
106
+        client.assert_ok()
107
+        data = client.extract_data() or {}
108
+        total = data.get("total", 0)
109
+        rows = data.get("rows", [])
110
+        low_stock = [r for r in rows if (r.get("stock") or 0) < 10]
111
+        print(f"  [INFO] 库存商品 {total} 个, 低库存 {len(low_stock)} 个")
112
+
113
+        if low_stock:
114
+            g = low_stock[0]
115
+            gid = g.get("goodsId")
116
+            print(f"  [INFO] 需补货: goodsId={gid} stock={g.get('stock','?')}")
117
+
118
+    def test_chain_07_consumer_register_browse(self, client, member_token):
119
+        """消费者注册后浏览首页"""
120
+        resp = client.get("/api/home/banners")
121
+        client.assert_ok()
122
+        banners = client.extract_data() or []
123
+        resp = client.get("/api/home/categories")
124
+        client.assert_ok()
125
+        cats = client.extract_data() or []
126
+        resp = client.get("/api/home/hot-goods")
127
+        client.assert_ok()
128
+        goods = client.extract_data() or []
129
+        print(f"  [OK]  首页浏览: Banner={len(banners)} 分类={len(cats)} 热销={len(goods)}")
130
+
131
+        if goods:
132
+            client._test_goods_id = goods[0].get("goodsId") or goods[0].get("id")
133
+            resp = client.get(f"/api/goods/{client._test_goods_id}")
134
+            client.assert_ok()
135
+            detail = client.extract_data() or {}
136
+            print(f"  [OK]  商品详情: {detail.get('goodsName','')[:20]}")
137
+
138
+    def test_chain_08_consumer_add_to_cart(self, client, member_token):
139
+        """消费者加购并下单"""
140
+        gid = getattr(client, '_test_goods_id', None)
141
+        resp = client.get("/api/home/hot-goods")
142
+        data = client.assert_ok()
143
+        goods = data.get("data", [])
144
+        if not goods:
145
+            pytest.skip("无在售商品")
146
+        gid = goods[0].get("goodsId") or goods[0].get("id")
147
+
148
+        # 加购
149
+        resp = client.post("/api/cart/items", json={"goodsId": gid, "quantity": 1})
150
+        client.assert_ok("加购失败")
151
+        print(f"  [OK]  加购 goodsId={gid}")
152
+
153
+        # 需要地址才能下单
154
+        resp = client.post("/api/member/address", json={
155
+            "receiverName": "测试用户",
156
+            "receiverMobile": "13800138000",
157
+            "province": "北京市", "city": "北京市", "district": "朝阳区",
158
+            "detailAddress": "E2E测试地址100号",
159
+            "fullAddress": "北京市北京市朝阳区E2E测试地址100号",
160
+            "isDefault": True,
161
+        })
162
+        data = client.assert_ok()
163
+        addr_id = data.get("data")
164
+
165
+        # 预览订单
166
+        resp = client.post("/api/checkout/preview", json={
167
+            "source": "BUY_NOW",
168
+            "items": [{"goodsId": gid, "quantity": 1}],
169
+        })
170
+        client.assert_ok("预览订单失败")
171
+        print(f"  [OK]  订单预览通过")
172
+
173
+        # 提交订单
174
+        resp = client.post("/api/checkout/submit", json={
175
+            "source": "BUY_NOW",
176
+            "addressId": addr_id,
177
+            "payType": "WEIXIN",
178
+            "items": [{"goodsId": gid, "quantity": 1}],
179
+        })
180
+        data = client.assert_ok("提交订单失败")
181
+        order_data = data.get("data", {}) if isinstance(data.get("data"), dict) else data
182
+        order_id = order_data.get("orderId")
183
+        if not order_id:
184
+            order_ids = order_data.get("orderIds", [])
185
+            order_id = order_ids[0] if order_ids else None
186
+        if order_id:
187
+            print(f"  [OK]  订单已提交 orderId={order_id}")
188
+
189
+    def test_chain_09_consumer_view_after_sale(self, client, member_token):
190
+        """消费者查看售后入口"""
191
+        resp = client.get("/api/order/list")
192
+        client.assert_ok()
193
+        data = client.extract_data() or {}
194
+        total = data.get("total", 0)
195
+        print(f"  [INFO] 我的订单共 {total} 条")
196
+
197
+        # 售后列表
198
+        resp = client.get("/api/order/aftersale/list")
199
+        client.assert_ok()
200
+        data = client.extract_data() or {}
201
+        total_as = data.get("total", 0) if isinstance(data, dict) else 0
202
+        print(f"  [INFO] 售后单共 {total_as} 条")
203
+
204
+    def test_chain_10_admin_stats_overview(self, client, admin_token):
205
+        """平台端统一数据概览"""
206
+        modules = [
207
+            ("商品待审", "/agri/goodsAudit/pendingCount", "pendingCount"),
208
+            ("入驻待审", "/agri/merchantEntryApply/pendingCount", "pendingCount"),
209
+            ("提现待审", "/agri/finance/withdrawAudit/pendingCount", "pendingCount"),
210
+        ]
211
+        for label, path, field in modules:
212
+            resp = client.get(path)
213
+            client.assert_ok()
214
+            val = (client.extract_data() or {}).get(field, "?")
215
+            print(f"  [INFO] {label}: {val}")

+ 479 - 0
e2e-test/test_seller_flow.py

@@ -0,0 +1,479 @@
1
+# ============================================================
2
+# 商家端 — 全量端到端测试
3
+# 覆盖 24 个 Controller,约 90 个 API 端点
4
+# ============================================================
5
+import pytest
6
+import json
7
+import random
8
+from api_client import ApiClient
9
+from config import SELLER_SHOP_ID
10
+
11
+
12
+# ============================================================
13
+# 商家 — 店铺切换 (SellerShopContextController)
14
+# ============================================================
15
+class TestSellerContext:
16
+
17
+    def test_80_switch_shop(self, client, seller_token):
18
+        """切换店铺上下文"""
19
+        resp = client.put("/agri/seller/context/shop", json={
20
+            "shopId": SELLER_SHOP_ID
21
+        })
22
+        client.assert_ok("切换店铺失败")
23
+        print(f"  [OK] 切换店铺 shopId={SELLER_SHOP_ID}")
24
+
25
+
26
+# ============================================================
27
+# 商家 — 员工管理 (MerchantEmployeeController)
28
+# ============================================================
29
+class TestSellerEmployee:
30
+
31
+    def test_81_employee_list(self, client, seller_token):
32
+        """员工列表"""
33
+        resp = client.get("/agri/seller/employee/list")
34
+        client.assert_ok("查询员工列表失败")
35
+        data = client.extract_data() or {}
36
+        print(f"  [OK] 员工列表 total={data.get('total','?')}")
37
+
38
+    def test_82_employee_quota(self, client, seller_token):
39
+        """员工名额配额"""
40
+        resp = client.get("/agri/seller/employee/quota")
41
+        client.assert_ok("查询员工配额失败")
42
+        quota = client.extract_data() or {}
43
+        print(f"  [OK] 员工配额: {quota}")
44
+
45
+    def test_83_employee_role_options(self, client, seller_token):
46
+        """员工角色选项"""
47
+        resp = client.get("/agri/seller/employee/roleOptions")
48
+        client.assert_ok("查询角色选项失败")
49
+        roles = client.extract_data() or []
50
+        print(f"  [OK] 员工角色选项 count={len(roles) if isinstance(roles, list) else 0}")
51
+
52
+
53
+# ============================================================
54
+# 商家 — 角色管理 (MerchantRoleController)
55
+# ============================================================
56
+class TestSellerRole:
57
+
58
+    def test_84_role_list(self, client, seller_token):
59
+        """角色列表"""
60
+        resp = client.get("/agri/seller/role/list")
61
+        client.assert_ok("查询角色列表失败")
62
+        data = client.extract_data() or {}
63
+        print(f"  [OK] 角色列表 total={data.get('total','?')}")
64
+
65
+    def test_85_role_menu_tree(self, client, seller_token):
66
+        """角色菜单树"""
67
+        resp = client.get("/agri/seller/role/menuTree")
68
+        client.assert_ok("查询菜单树失败")
69
+        tree = client.extract_data() or []
70
+        print(f"  [OK] 角色菜单树 nodes={len(tree)}")
71
+
72
+
73
+# ============================================================
74
+# 商家 — 商品管理 (SellerGoodsController)
75
+# ============================================================
76
+class TestSellerGoods:
77
+
78
+    def test_86_goods_list(self, client, seller_token):
79
+        """商家商品列表"""
80
+        resp = client.get("/agri/seller/goods/list")
81
+        client.assert_ok("查询商品列表失败")
82
+        data = client.extract_data() or {}
83
+        total = data.get("total", 0) if isinstance(data, dict) else 0
84
+        print(f"  [OK] 商家商品列表 total={total}")
85
+
86
+    def test_87_goods_category_options(self, client, seller_token):
87
+        """商品分类选项"""
88
+        resp = client.get("/agri/seller/goods/categoryOptions")
89
+        client.assert_ok("查询分类选项失败")
90
+        cats = client.extract_data() or []
91
+        print(f"  [OK] 分类选项 count={len(cats)}")
92
+        if cats:
93
+            client._test_seller_category_id = cats[0].get("categoryId") or cats[0].get("id")
94
+
95
+    def test_88_goods_service_options(self, client, seller_token):
96
+        """商品服务选项"""
97
+        resp = client.get("/agri/seller/goods/serviceOptions")
98
+        client.assert_ok("查询服务选项失败")
99
+        svc = client.extract_data() or {}
100
+        print(f"  [OK] 服务选项: {json.dumps(svc, ensure_ascii=False)[:100]}")
101
+
102
+    def test_89_goods_attr_template_options(self, client, seller_token):
103
+        """属性模板选项"""
104
+        resp = client.get("/agri/seller/goods/attrTemplateOptions")
105
+        client.assert_ok("查询属性模板失败")
106
+        tmpl = client.extract_data() or []
107
+        print(f"  [OK] 属性模板选项 count={len(tmpl) if isinstance(tmpl, list) else 0}")
108
+
109
+    def test_90_goods_freight_options(self, client, seller_token):
110
+        """运费模板选项"""
111
+        resp = client.get("/agri/seller/goods/freightTemplateOptions")
112
+        client.assert_ok("查询运费模板失败")
113
+        tmpl = client.extract_data() or []
114
+        print(f"  [OK] 运费模板选项 count={len(tmpl)}")
115
+        if tmpl:
116
+            client._test_seller_freight_id = tmpl[0].get("templateId") or tmpl[0].get("id")
117
+
118
+    def test_91_goods_freight_default(self, client, seller_token):
119
+        """默认运费模板"""
120
+        resp = client.get("/agri/seller/goods/freightDefault")
121
+        client.assert_ok("查询默认运费模板失败")
122
+        default = client.extract_data() or {}
123
+        print(f"  [OK] 默认运费: {json.dumps(default, ensure_ascii=False)[:80]}")
124
+
125
+    def test_92_goods_shop_category_options(self, client, seller_token):
126
+        """店铺商品分类选项"""
127
+        resp = client.get("/agri/seller/goods/shopCategoryOptions", params={"visibleOnly": True})
128
+        client.assert_ok("查询店铺分类选项失败")
129
+        cats = client.extract_data() or []
130
+        print(f"  [OK] 店铺分类选项 count={len(cats) if isinstance(cats, list) else 0}")
131
+
132
+    def test_93_goods_detail(self, client, seller_token):
133
+        """商家商品详情"""
134
+        resp = client.get("/agri/seller/goods/list", params={"pageSize": 1})
135
+        data = client.assert_ok()
136
+        rows = (data.get("data") or {}).get("rows") or []
137
+        if not rows:
138
+            pytest.skip("无商品数据")
139
+        gid = rows[0].get("goodsId")
140
+        resp = client.get(f"/agri/seller/goods/{gid}")
141
+        client.assert_ok("查询商品详情失败")
142
+        print(f"  [OK] 商家商品详情 goodsId={gid}")
143
+
144
+
145
+# ============================================================
146
+# 商家 — 订单管理 (SellerOrderController)
147
+# ============================================================
148
+class TestSellerOrder:
149
+
150
+    def test_94_seller_order_list(self, client, seller_token):
151
+        """商家订单列表"""
152
+        resp = client.get("/agri/seller/order/list")
153
+        client.assert_ok("查询订单列表失败")
154
+        data = client.extract_data() or {}
155
+        total = data.get("total", 0) if isinstance(data, dict) else 0
156
+        print(f"  [OK] 商家订单列表 total={total}")
157
+
158
+    def test_95_seller_order_detail(self, client, seller_token):
159
+        """商家订单详情"""
160
+        resp = client.get("/agri/seller/order/list", params={"pageSize": 1})
161
+        data = client.assert_ok()
162
+        rows = (data.get("data") or {}).get("rows") or []
163
+        if not rows:
164
+            pytest.skip("无订单数据")
165
+        oid = rows[0].get("orderId")
166
+        resp = client.get(f"/agri/seller/order/{oid}")
167
+        client.assert_ok("查询订单详情失败")
168
+        print(f"  [OK] 商家订单详情 orderId={oid}")
169
+
170
+
171
+# ============================================================
172
+# 商家 — 发货管理 (SellerShipController)
173
+# ============================================================
174
+class TestSellerShip:
175
+
176
+    def test_96_ship_badges(self, client, seller_token):
177
+        """发货角标"""
178
+        resp = client.get("/agri/seller/ship/badges")
179
+        client.assert_ok("查询发货角标失败")
180
+        badges = client.extract_data() or {}
181
+        print(f"  [OK] 发货角标: {badges}")
182
+
183
+    def test_97_ship_list(self, client, seller_token):
184
+        """发货列表"""
185
+        resp = client.get("/agri/seller/ship/list")
186
+        client.assert_ok("查询发货列表失败")
187
+        data = client.extract_data() or {}
188
+        total = data.get("total", 0) if isinstance(data, dict) else 0
189
+        print(f"  [OK] 发货列表 total={total}")
190
+
191
+    def test_98_ship_detail(self, client, seller_token):
192
+        """发货详情"""
193
+        resp = client.get("/agri/seller/ship/list", params={"pageSize": 1})
194
+        data = client.assert_ok()
195
+        rows = (data.get("data") or {}).get("rows") or []
196
+        if not rows:
197
+            pytest.skip("无发货数据")
198
+        oid = rows[0].get("orderId")
199
+        resp = client.get(f"/agri/seller/ship/{oid}")
200
+        client.assert_ok("查询发货详情失败")
201
+        print(f"  [OK] 发货详情 orderId={oid}")
202
+
203
+
204
+# ============================================================
205
+# 商家 — 售后管理 (SellerAftersaleController)
206
+# ============================================================
207
+class TestSellerAftersale:
208
+
209
+    def test_99_aftersale_list(self, client, seller_token):
210
+        """售后列表"""
211
+        resp = client.get("/agri/seller/aftersale/list")
212
+        client.assert_ok("查询售后列表失败")
213
+        data = client.extract_data() or {}
214
+        total = data.get("total", 0) if isinstance(data, dict) else 0
215
+        print(f"  [OK] 售后列表 total={total}")
216
+
217
+    def test_100_aftersale_detail(self, client, seller_token):
218
+        """售后详情"""
219
+        resp = client.get("/agri/seller/aftersale/list", params={"pageSize": 1})
220
+        data = client.assert_ok()
221
+        rows = (data.get("data") or {}).get("rows") or []
222
+        if not rows:
223
+            pytest.skip("无售后数据")
224
+        aid = rows[0].get("aftersaleId")
225
+        resp = client.get(f"/agri/seller/aftersale/{aid}")
226
+        client.assert_ok("查询售后详情失败")
227
+        print(f"  [OK] 售后详情 aftersaleId={aid}")
228
+
229
+
230
+# ============================================================
231
+# 商家 — 评价管理 (SellerReviewController)
232
+# ============================================================
233
+class TestSellerReview:
234
+
235
+    def test_101_review_list(self, client, seller_token):
236
+        """评价列表"""
237
+        resp = client.get("/agri/seller/review/list")
238
+        client.assert_ok("查询评价列表失败")
239
+        data = client.extract_data() or {}
240
+        total = data.get("total", 0) if isinstance(data, dict) else 0
241
+        print(f"  [OK] 评价列表 total={total}")
242
+
243
+
244
+# ============================================================
245
+# 商家 — 运费模板 (SellerFreightTemplateController)
246
+# ============================================================
247
+class TestSellerFreight:
248
+
249
+    def test_102_freight_template_list(self, client, seller_token):
250
+        """运费模板列表"""
251
+        resp = client.get("/agri/seller/freightTemplate/list")
252
+        client.assert_ok("查询运费模板失败")
253
+        data = client.extract_data() or {}
254
+        total = data.get("total", 0) if isinstance(data, dict) else 0
255
+        print(f"  [OK] 运费模板列表 total={total}")
256
+
257
+    def test_103_freight_template_detail(self, client, seller_token):
258
+        """运费模板详情"""
259
+        resp = client.get("/agri/seller/freightTemplate/list", params={"pageSize": 1})
260
+        data = client.assert_ok()
261
+        rows = (data.get("data") or {}).get("rows") or []
262
+        if not rows:
263
+            pytest.skip("无运费模板")
264
+        tid = rows[0].get("templateId")
265
+        resp = client.get(f"/agri/seller/freightTemplate/{tid}")
266
+        client.assert_ok("查询运费模板详情失败")
267
+        print(f"  [OK] 运费模板详情 templateId={tid}")
268
+
269
+
270
+# ============================================================
271
+# 商家 — 属性模板 (SellerAttrTemplateController)
272
+# ============================================================
273
+class TestSellerAttrTemplate:
274
+
275
+    def test_104_attr_template_list(self, client, seller_token):
276
+        """属性模板列表"""
277
+        resp = client.get("/agri/seller/attrTemplate/list")
278
+        client.assert_ok("查询属性模板失败")
279
+        data = client.extract_data() or {}
280
+        total = data.get("total", 0) if isinstance(data, dict) else 0
281
+        print(f"  [OK] 属性模板列表 total={total}")
282
+
283
+    def test_105_attr_template_detail(self, client, seller_token):
284
+        """属性模板详情"""
285
+        resp = client.get("/agri/seller/attrTemplate/list", params={"pageSize": 1})
286
+        data = client.assert_ok()
287
+        rows = (data.get("data") or {}).get("rows") or []
288
+        if not rows:
289
+            pytest.skip("无属性模板")
290
+        tid = rows[0].get("templateId")
291
+        resp = client.get(f"/agri/seller/attrTemplate/{tid}")
292
+        client.assert_ok("查询属性模板详情失败")
293
+        print(f"  [OK] 属性模板详情 templateId={tid}")
294
+
295
+
296
+# ============================================================
297
+# 商家 — 分类管理 (SellerPlatformCategory + ShopGoodsCategory)
298
+# ============================================================
299
+class TestSellerCategory:
300
+
301
+    def test_106_platform_category_options(self, client, seller_token):
302
+        """平台分类选项"""
303
+        resp = client.get("/agri/seller/category/platformLevel2Options")
304
+        client.assert_ok("查询平台分类选项失败")
305
+        cats = client.extract_data() or []
306
+        print(f"  [OK] 平台分类选项 count={len(cats) if isinstance(cats, list) else 0}")
307
+
308
+    def test_107_shop_category_list(self, client, seller_token):
309
+        """店铺分类列表"""
310
+        resp = client.get("/agri/seller/shopCategory/list")
311
+        client.assert_ok("查询店铺分类失败")
312
+        data = client.extract_data() or {}
313
+        print(f"  [OK] 店铺分类列表正常")
314
+
315
+    def test_108_shop_category_scope(self, client, seller_token):
316
+        """店铺分类使用范围"""
317
+        resp = client.get("/agri/seller/shopCategory/scope")
318
+        client.assert_ok("查询店铺分类范围失败")
319
+        scope = client.extract_data() or {}
320
+        print(f"  [OK] 店铺分类范围: {scope}")
321
+
322
+
323
+# ============================================================
324
+# 商家 — 店铺管理 (SellerShopController)
325
+# ============================================================
326
+class TestSellerShop:
327
+
328
+    def test_109_shop_employee_stats(self, client, seller_token):
329
+        """店铺员工统计"""
330
+        resp = client.get("/agri/seller/shop/employeeStats")
331
+        client.assert_ok("查询员工统计失败")
332
+        stats = client.extract_data() or {}
333
+        print(f"  [OK] 员工统计: {stats}")
334
+
335
+
336
+# ============================================================
337
+# 商家 — 配送设置 (SellerDeliveryController)
338
+# ============================================================
339
+class TestSellerDelivery:
340
+
341
+    def test_110_delivery_setting_get(self, client, seller_token):
342
+        """配送设置查询"""
343
+        resp = client.get("/agri/seller/delivery")
344
+        client.assert_ok("查询配送设置失败")
345
+        setting = client.extract_data() or {}
346
+        print(f"  [OK] 配送设置: {json.dumps(setting, ensure_ascii=False)[:100]}")
347
+
348
+
349
+# ============================================================
350
+# 商家 — 财务管理 (SellerFund / PayAccount / Withdraw)
351
+# ============================================================
352
+class TestSellerFinance:
353
+
354
+    def test_111_fund_summary(self, client, seller_token):
355
+        """资金概览"""
356
+        resp = client.get("/agri/seller/finance/overview/summary")
357
+        client.assert_ok("查询资金概览失败")
358
+        summary = client.extract_data() or {}
359
+        print(f"  [OK] 商家资金概览: {json.dumps(summary, ensure_ascii=False)[:100]}")
360
+
361
+    def test_112_fund_logs(self, client, seller_token):
362
+        """资金流水"""
363
+        resp = client.get("/agri/seller/finance/overview/logs")
364
+        client.assert_ok("查询资金流水失败")
365
+        data = client.extract_data() or {}
366
+        total = data.get("total", 0) if isinstance(data, dict) else 0
367
+        print(f"  [OK] 资金流水 total={total}")
368
+
369
+    def test_113_pay_account_list(self, client, seller_token):
370
+        """收款账户列表"""
371
+        resp = client.get("/agri/seller/finance/payAccount/list")
372
+        client.assert_ok("查询收款账户失败")
373
+        accounts = client.extract_data() or []
374
+        print(f"  [OK] 收款账户 count={len(accounts) if isinstance(accounts, list) else 0}")
375
+
376
+    def test_114_pay_account_options(self, client, seller_token):
377
+        """收款账户选项"""
378
+        resp = client.get("/agri/seller/finance/payAccount/options")
379
+        client.assert_ok("查询账户选项失败")
380
+        opts = client.extract_data() or []
381
+        print(f"  [OK] 收款账户选项正常")
382
+
383
+    def test_115_withdraw_balance(self, client, seller_token):
384
+        """提现余额"""
385
+        resp = client.get("/agri/seller/finance/withdraw/balance")
386
+        client.assert_ok("查询提现余额失败")
387
+        balance = client.extract_data() or {}
388
+        print(f"  [OK] 提现余额: {json.dumps(balance, ensure_ascii=False)[:80]}")
389
+
390
+    def test_116_withdraw_list(self, client, seller_token):
391
+        """提现记录"""
392
+        resp = client.get("/agri/seller/finance/withdraw/list")
393
+        client.assert_ok("查询提现记录失败")
394
+        data = client.extract_data() or {}
395
+        total = data.get("total", 0) if isinstance(data, dict) else 0
396
+        print(f"  [OK] 提现记录 total={total}")
397
+
398
+
399
+# ============================================================
400
+# 商家 — 库存管理 (5个StockController)
401
+# ============================================================
402
+class TestSellerStock:
403
+
404
+    def test_117_stock_query_list(self, client, seller_token):
405
+        """库存查询"""
406
+        resp = client.get("/agri/seller/stock/query/list")
407
+        client.assert_ok("查询库存失败")
408
+        data = client.extract_data() or {}
409
+        total = data.get("total", 0) if isinstance(data, dict) else 0
410
+        print(f"  [OK] 库存查询 total={total}")
411
+
412
+    def test_118_stock_inbound_list(self, client, seller_token):
413
+        """入库记录"""
414
+        resp = client.get("/agri/seller/stock/inbound/list")
415
+        client.assert_ok("查询入库记录失败")
416
+        data = client.extract_data() or {}
417
+        total = data.get("total", 0) if isinstance(data, dict) else 0
418
+        print(f"  [OK] 入库记录 total={total}")
419
+
420
+    def test_119_stock_outbound_list(self, client, seller_token):
421
+        """出库记录"""
422
+        resp = client.get("/agri/seller/stock/outbound/list")
423
+        client.assert_ok("查询出库记录失败")
424
+        data = client.extract_data() or {}
425
+        total = data.get("total", 0) if isinstance(data, dict) else 0
426
+        print(f"  [OK] 出库记录 total={total}")
427
+
428
+    def test_120_stock_adjust_list(self, client, seller_token):
429
+        """库存调整记录"""
430
+        resp = client.get("/agri/seller/stock/adjust/list")
431
+        client.assert_ok("查询库存调整失败")
432
+        data = client.extract_data() or {}
433
+        total = data.get("total", 0) if isinstance(data, dict) else 0
434
+        print(f"  [OK] 库存调整 total={total}")
435
+
436
+    def test_121_stock_log_list(self, client, seller_token):
437
+        """库存流水日志"""
438
+        resp = client.get("/agri/seller/stock/log/list")
439
+        client.assert_ok("查询库存日志失败")
440
+        data = client.extract_data() or {}
441
+        total = data.get("total", 0) if isinstance(data, dict) else 0
442
+        print(f"  [OK] 库存日志 total={total}")
443
+
444
+    def test_122_stock_inbound_detail(self, client, seller_token):
445
+        """入库详情"""
446
+        resp = client.get("/agri/seller/stock/inbound/list", params={"pageSize": 1})
447
+        data = client.assert_ok()
448
+        rows = (data.get("data") or {}).get("rows") or []
449
+        if not rows:
450
+            pytest.skip("无入库记录")
451
+        iid = rows[0].get("inboundId")
452
+        resp = client.get(f"/agri/seller/stock/inbound/{iid}")
453
+        client.assert_ok("查询入库详情失败")
454
+        print(f"  [OK] 入库详情 inboundId={iid}")
455
+
456
+    def test_123_stock_goods_logs(self, client, seller_token):
457
+        """商品库存流水"""
458
+        resp = client.get("/agri/seller/stock/query/list", params={"pageSize": 1})
459
+        data = client.assert_ok()
460
+        rows = (data.get("data") or {}).get("rows") or []
461
+        if not rows:
462
+            pytest.skip("无库存数据")
463
+        gid = rows[0].get("goodsId")
464
+        resp = client.get(f"/agri/seller/stock/query/{gid}/logs")
465
+        client.assert_ok("查询商品流水失败")
466
+        print(f"  [OK] 商品库存流水 goodsId={gid}")
467
+
468
+
469
+# ============================================================
470
+# 商家 — 微信支付配置 (SellerShopWechatPayController)
471
+# ============================================================
472
+class TestSellerWechat:
473
+
474
+    def test_124_wechat_pay_get(self, client, seller_token):
475
+        """商家微信支付配置"""
476
+        resp = client.get("/agri/seller/wechat-pay")
477
+        client.assert_ok("查询支付配置失败")
478
+        config = client.extract_data() or {}
479
+        print(f"  [OK] 商家支付配置: {json.dumps(config, ensure_ascii=False)[:100]}")