Files
gaokao-volunteer-system/admin/tests/test_web_public.py

525 lines
19 KiB
Python
Raw Normal View History

"""用户端 Web 自助入口核心测试。"""
from __future__ import annotations
import pytest
from fastapi import HTTPException, Request
from admin.routes.web_public import (
alipay_sim_payment_page,
complete_alipay_sim_payment,
complete_mock_payment,
mock_payment_page,
mock_payment_webhook,
)
from fastapi.testclient import TestClient
from data.orders.dao import OrdersDAO
def test_public_landing_page_served(client):
resp = client.get("/")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
body = resp.text
assert "高考志愿填报智能规划服务" in body
assert "新高考志愿填报 · 志愿决策支持" in body
assert "湖南新高考志愿填报" not in body
assert "为什么选择我们" in body
assert 'href="/pricing"' in body
assert "先做快速审核" not in body
assert "先做方案复核" in body
assert "查看套餐" in body
assert 'href="/review/start?source=home"' in body
assert 'btn-primary" href="/pricing"' in body
fix(landing): align copy to '复核免费 / 方案付费' (no 免费咨询 promise) 用户最新反馈: 首页不应承诺'免费咨询'; 复核现有方案是免费的, 完整方案 生成和深度辅导需在支付后启动。咨询入口只用于快速问询, 不是承诺 免费生成方案。 文案改动 (admin/routes/web_public.py): - hero-note: '咨询本身免费' → '复核现有方案本身免费; 新方案生成与深度辅导在支付后启动' - 咨询表单标题: '先告诉我们你的基本情况' → '告诉我们你的基本情况' - 咨询表单副标题: '判断 + 适合路径' → '判断 + 说明后续可走步骤, 复核本身免费; 新方案生成与深度辅导在支付后启动' - 当前目标 placeholder: '先审计现有方案' → '先复核现有方案' - 补充说明 placeholder: '想先看风险' → '想先看有没有明显风险' - 主按钮: '获取推荐路径' → '获取复核与推荐' - 次按钮: '直接看套餐' → '直接看付费套餐' - hero-trust 1 号卡: '先审计后规划' → '复核免费 / 方案付费' - hero-points lead: '先做审核' → '先做免费复核'; '再决定是否进入完整规划' → '再决定是否进入付费的完整规划' - 服务流程 01 步: '选择服务深度' → '先判断入口' ('方案复核(免费) vs 完整方案/深度辅导(付费)') - 服务流程 02 步: '避免在支付前被长表单劝退' → '选完整方案或深度辅导时, 先填最小信息后再支付' - 服务流程导语: '比空泛地说智能系统已接入更重要' → '复核免费, 方案生成和深度辅导需付费' - 底部 CTA 标题: '先看套餐, 再决定是否立即下单' → '已有方案? 先免费复核; 明确要做? 看付费套餐' - 底部 CTA 副标题: '如果你已经明确要做完整方案, 可以直接进入 99 元方案页' → '如果已经拿到一版方案, 可先做一次免费复核; 明确要完整方案或深度辅导, 可直接进入套餐页' 测试更新 (admin/tests/test_web_public.py): - 移除对'咨询本身免费' / '先审计后规划' / '先告诉我们你的基本情况' / '获取推荐路径' / '先看套餐' 的正向断言 - 改为反向断言 (确保旧文案已清除) + 新口径的正向断言 - 新增 9 条新口径断言: '复核现有方案本身免费' / '新方案生成与深度辅导在支付后启动' / '复核免费 / 方案付费' / '获取复核与推荐' / '直接看付费套餐' / '方案复核(免费)' / '深度辅导(付费)' / '已有方案? 先免费复核' / '告诉我们你的基本情况' 验证: - pytest 22 passed (test_web_public + test_order_status_page + test_web_public_alipay_sim_e2e) - 真实 HTTP 19/19 文案断言全部 OK (旧文案已清除, 新文案已上线) - Playwright 截图 vision 验证: 5/5 触点对齐口径 (hero CTA / hero-note / 咨询表单 / hero-trust 1 号卡 / 服务流程 + 底部 CTA)
2026-06-17 22:11:31 +08:00
assert "咨询本身免费" not in body
assert "咨询入口" not in body
fix(landing): align copy to '复核免费 / 方案付费' (no 免费咨询 promise) 用户最新反馈: 首页不应承诺'免费咨询'; 复核现有方案是免费的, 完整方案 生成和深度辅导需在支付后启动。咨询入口只用于快速问询, 不是承诺 免费生成方案。 文案改动 (admin/routes/web_public.py): - hero-note: '咨询本身免费' → '复核现有方案本身免费; 新方案生成与深度辅导在支付后启动' - 咨询表单标题: '先告诉我们你的基本情况' → '告诉我们你的基本情况' - 咨询表单副标题: '判断 + 适合路径' → '判断 + 说明后续可走步骤, 复核本身免费; 新方案生成与深度辅导在支付后启动' - 当前目标 placeholder: '先审计现有方案' → '先复核现有方案' - 补充说明 placeholder: '想先看风险' → '想先看有没有明显风险' - 主按钮: '获取推荐路径' → '获取复核与推荐' - 次按钮: '直接看套餐' → '直接看付费套餐' - hero-trust 1 号卡: '先审计后规划' → '复核免费 / 方案付费' - hero-points lead: '先做审核' → '先做免费复核'; '再决定是否进入完整规划' → '再决定是否进入付费的完整规划' - 服务流程 01 步: '选择服务深度' → '先判断入口' ('方案复核(免费) vs 完整方案/深度辅导(付费)') - 服务流程 02 步: '避免在支付前被长表单劝退' → '选完整方案或深度辅导时, 先填最小信息后再支付' - 服务流程导语: '比空泛地说智能系统已接入更重要' → '复核免费, 方案生成和深度辅导需付费' - 底部 CTA 标题: '先看套餐, 再决定是否立即下单' → '已有方案? 先免费复核; 明确要做? 看付费套餐' - 底部 CTA 副标题: '如果你已经明确要做完整方案, 可以直接进入 99 元方案页' → '如果已经拿到一版方案, 可先做一次免费复核; 明确要完整方案或深度辅导, 可直接进入套餐页' 测试更新 (admin/tests/test_web_public.py): - 移除对'咨询本身免费' / '先审计后规划' / '先告诉我们你的基本情况' / '获取推荐路径' / '先看套餐' 的正向断言 - 改为反向断言 (确保旧文案已清除) + 新口径的正向断言 - 新增 9 条新口径断言: '复核现有方案本身免费' / '新方案生成与深度辅导在支付后启动' / '复核免费 / 方案付费' / '获取复核与推荐' / '直接看付费套餐' / '方案复核(免费)' / '深度辅导(付费)' / '已有方案? 先免费复核' / '告诉我们你的基本情况' 验证: - pytest 22 passed (test_web_public + test_order_status_page + test_web_public_alipay_sim_e2e) - 真实 HTTP 19/19 文案断言全部 OK (旧文案已清除, 新文案已上线) - Playwright 截图 vision 验证: 5/5 触点对齐口径 (hero CTA / hero-note / 咨询表单 / hero-trust 1 号卡 / 服务流程 + 底部 CTA)
2026-06-17 22:11:31 +08:00
assert "复核现有方案本身免费" in body
assert "新方案生成与深度辅导在支付后启动" in body
assert "先免费复核一次你的现有方案" in body
# hero 不再保留"了解服务流程"文字链
assert "了解服务流程" not in body
fix(landing): align copy to '复核免费 / 方案付费' (no 免费咨询 promise) 用户最新反馈: 首页不应承诺'免费咨询'; 复核现有方案是免费的, 完整方案 生成和深度辅导需在支付后启动。咨询入口只用于快速问询, 不是承诺 免费生成方案。 文案改动 (admin/routes/web_public.py): - hero-note: '咨询本身免费' → '复核现有方案本身免费; 新方案生成与深度辅导在支付后启动' - 咨询表单标题: '先告诉我们你的基本情况' → '告诉我们你的基本情况' - 咨询表单副标题: '判断 + 适合路径' → '判断 + 说明后续可走步骤, 复核本身免费; 新方案生成与深度辅导在支付后启动' - 当前目标 placeholder: '先审计现有方案' → '先复核现有方案' - 补充说明 placeholder: '想先看风险' → '想先看有没有明显风险' - 主按钮: '获取推荐路径' → '获取复核与推荐' - 次按钮: '直接看套餐' → '直接看付费套餐' - hero-trust 1 号卡: '先审计后规划' → '复核免费 / 方案付费' - hero-points lead: '先做审核' → '先做免费复核'; '再决定是否进入完整规划' → '再决定是否进入付费的完整规划' - 服务流程 01 步: '选择服务深度' → '先判断入口' ('方案复核(免费) vs 完整方案/深度辅导(付费)') - 服务流程 02 步: '避免在支付前被长表单劝退' → '选完整方案或深度辅导时, 先填最小信息后再支付' - 服务流程导语: '比空泛地说智能系统已接入更重要' → '复核免费, 方案生成和深度辅导需付费' - 底部 CTA 标题: '先看套餐, 再决定是否立即下单' → '已有方案? 先免费复核; 明确要做? 看付费套餐' - 底部 CTA 副标题: '如果你已经明确要做完整方案, 可以直接进入 99 元方案页' → '如果已经拿到一版方案, 可先做一次免费复核; 明确要完整方案或深度辅导, 可直接进入套餐页' 测试更新 (admin/tests/test_web_public.py): - 移除对'咨询本身免费' / '先审计后规划' / '先告诉我们你的基本情况' / '获取推荐路径' / '先看套餐' 的正向断言 - 改为反向断言 (确保旧文案已清除) + 新口径的正向断言 - 新增 9 条新口径断言: '复核现有方案本身免费' / '新方案生成与深度辅导在支付后启动' / '复核免费 / 方案付费' / '获取复核与推荐' / '直接看付费套餐' / '方案复核(免费)' / '深度辅导(付费)' / '已有方案? 先免费复核' / '告诉我们你的基本情况' 验证: - pytest 22 passed (test_web_public + test_order_status_page + test_web_public_alipay_sim_e2e) - 真实 HTTP 19/19 文案断言全部 OK (旧文案已清除, 新文案已上线) - Playwright 截图 vision 验证: 5/5 触点对齐口径 (hero CTA / hero-note / 咨询表单 / hero-trust 1 号卡 / 服务流程 + 底部 CTA)
2026-06-17 22:11:31 +08:00
assert "获取复核与推荐" in body
assert "方案复核(免费)" in body
assert "深度辅导(付费)" in body
assert "已有方案?先免费复核" in body
assert "先告诉我们你的基本情况" not in body
fix(landing): align copy to '复核免费 / 方案付费' (no 免费咨询 promise) 用户最新反馈: 首页不应承诺'免费咨询'; 复核现有方案是免费的, 完整方案 生成和深度辅导需在支付后启动。咨询入口只用于快速问询, 不是承诺 免费生成方案。 文案改动 (admin/routes/web_public.py): - hero-note: '咨询本身免费' → '复核现有方案本身免费; 新方案生成与深度辅导在支付后启动' - 咨询表单标题: '先告诉我们你的基本情况' → '告诉我们你的基本情况' - 咨询表单副标题: '判断 + 适合路径' → '判断 + 说明后续可走步骤, 复核本身免费; 新方案生成与深度辅导在支付后启动' - 当前目标 placeholder: '先审计现有方案' → '先复核现有方案' - 补充说明 placeholder: '想先看风险' → '想先看有没有明显风险' - 主按钮: '获取推荐路径' → '获取复核与推荐' - 次按钮: '直接看套餐' → '直接看付费套餐' - hero-trust 1 号卡: '先审计后规划' → '复核免费 / 方案付费' - hero-points lead: '先做审核' → '先做免费复核'; '再决定是否进入完整规划' → '再决定是否进入付费的完整规划' - 服务流程 01 步: '选择服务深度' → '先判断入口' ('方案复核(免费) vs 完整方案/深度辅导(付费)') - 服务流程 02 步: '避免在支付前被长表单劝退' → '选完整方案或深度辅导时, 先填最小信息后再支付' - 服务流程导语: '比空泛地说智能系统已接入更重要' → '复核免费, 方案生成和深度辅导需付费' - 底部 CTA 标题: '先看套餐, 再决定是否立即下单' → '已有方案? 先免费复核; 明确要做? 看付费套餐' - 底部 CTA 副标题: '如果你已经明确要做完整方案, 可以直接进入 99 元方案页' → '如果已经拿到一版方案, 可先做一次免费复核; 明确要完整方案或深度辅导, 可直接进入套餐页' 测试更新 (admin/tests/test_web_public.py): - 移除对'咨询本身免费' / '先审计后规划' / '先告诉我们你的基本情况' / '获取推荐路径' / '先看套餐' 的正向断言 - 改为反向断言 (确保旧文案已清除) + 新口径的正向断言 - 新增 9 条新口径断言: '复核现有方案本身免费' / '新方案生成与深度辅导在支付后启动' / '复核免费 / 方案付费' / '获取复核与推荐' / '直接看付费套餐' / '方案复核(免费)' / '深度辅导(付费)' / '已有方案? 先免费复核' / '告诉我们你的基本情况' 验证: - pytest 22 passed (test_web_public + test_order_status_page + test_web_public_alipay_sim_e2e) - 真实 HTTP 19/19 文案断言全部 OK (旧文案已清除, 新文案已上线) - Playwright 截图 vision 验证: 5/5 触点对齐口径 (hero CTA / hero-note / 咨询表单 / hero-trust 1 号卡 / 服务流程 + 底部 CTA)
2026-06-17 22:11:31 +08:00
assert "先审计后规划" not in body
fix(landing+pricing): A) consult form privacy note + B) pricing page copy sync A) 首页咨询表单加隐私说明 (admin/routes/web_public.py): - 在表单标题下方加 .consult-privacy 框: '🔒 这些输入只用于判断要不要复核你的方案, 不会留底、不会用于生成方案、不会发邮件推销。 如果你决定不进入付费方案, 提交的资料不会保存到我们的数据库。' - 在表单下方加 .consult-privacy-tail: '不会收到营销短信, 提交后你也可以随时要求删除已填资料。' - 新增 CSS: .consult-privacy (薄荷绿底+边框) / .consult-privacy-tail (灰色脚注) - 设计意图: 不抢眼但能看见, 用浅色背景与表单区分, 不影响主 CTA 视觉权重 B) 套餐页 /pricing 同步'复核免费 / 方案付费' 口径: - lead 文案加 <strong>复核现有方案本身免费</strong> (放在最前面) - consult-summary 内加 consult-reassure 框: '💡 如果你还没决定, 先做一次免费复核, 再决定要不要进入付费方案。' - summary 副卡加 summary-reassure: '🔒 还没下单前, 提交的基本情况仅用于判断是否需要复核, 不会留底。' - trust-proof 1 号卡: '先审计再决定' → '复核免费 / 方案付费' - trust-band 3 号卡: '合规入口可见' → '复核免费 / 方案付费' (新增回流入口) - 49元档 eyebrow: '快速校验' → '复核 / 风险' (强调只审不改) - 99元档 eyebrow: '完整规划' → '生成 / 完整' (强调生成动作) - 199元档 eyebrow: '深度辅导' → '生成 / 深度' - 49元档 desc: 加'不会重新生成志愿表 (生成需选 99 元)' 边界 - 99元档 desc: 加'方案生成与详细报告在支付后启动' 边界 - 199元档 desc: 加'在 99 元完整方案基础上提供多轮修订和深度解释' - 49元档 CTA: '先做快速审核' → '先做付费审核' (用'付费'反衬'免费复核') - 99元档 CTA: '立即开始完整规划' → '支付并启动方案生成' - 199元档 CTA: '了解深度辅导' (保持) - 49元档 features: 加'不重新生成志愿表(生成需选 99 元)' 边界 - 199元档 features: 加'包含 99 元完整方案的所有交付' - FAQ 加 '复核是免费的吗?包含什么?' (明确边界: 复核免费/审核报告 49/完整方案 99起) - 99元 FAQ 加 '99 元是生成一份完整方案的价格, 不是查看价格' 避免误解 - notice 提示: '先做一次免费复核' 引导 + 保留 49/99 路径建议 新增测试断言: - 首页: '不会留底' / '不会用于生成方案' / '不会发邮件推销' / '不会收到营销短信' (4 条) - 套餐页: '复核现有方案本身免费' / '复核免费 / 方案付费' / 'href="/#consult-box"' / '先做一次免费复核' / '先做付费审核' / '支付并启动方案生成' / '复核 / 风险' / '生成 / 完整' / '生成 / 深度' / '不会重新生成志愿表' / '支付后启动' / '复核是免费的吗?包含什么?' / '还没决定' (13 条) - 套餐页反向断言: '先做快速审核' / '立即开始完整规划' / '先审计再决定' / '快速校验' (4 条) 验证: - pytest 22 passed (test_web_public + test_order_status_page + test_web_public_alipay_sim_e2e) - ruff check: All checks passed - 真实 HTTP 端到端 17/17 断言 OK (A 6 条 + B 20 条旧移除+新命中) - Playwright vision 验证: A) 首页隐私说明: 上下双向布局, 浅色克制, 覆盖'用途/营销/删除'三要素 ✅ B) 套餐页: lead/eyebrow/CTA/FAQ 5 处引导回首页 全部对齐'复核免费/方案付费' ✅
2026-06-17 22:38:42 +08:00
assert "不会留底" in body
assert "不会用于生成方案" in body
assert "不会发邮件推销" in body
assert "不会收到营销短信" in body
assert "了解服务流程" not in body
# hero 压缩后的新文案
assert "最常见的不是“不会选”,而是先选错方向" in body
assert "先看你的方案值不值得继续" in body
assert "服务流程" in body
assert "把风险解释清楚" in body
assert "家长决策支持" not in body
assert "家长联系方式" not in body
assert "把风险解释给家长听懂" not in body
# 首页省份必须使用下拉,不再是自由文本
assert 'name="province"' in body
assert 'placeholder="例如:湖南"' not in body
# hero 已移除三张说明卡
assert "复核免费 / 方案付费" not in body
assert "风险重点可解释" not in body
assert "资料与交付站内可追踪" not in body
# 流程编号已改为文字版
assert ">01<" not in body
assert ">04<" not in body
assert "/static/portal-ui.css" in body
def test_public_pricing_page_served(client):
resp = client.get("/pricing")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
body = resp.text
assert "服务套餐" in body
assert "49元 AI方案审核" in body
assert "99元 完整志愿方案" in body
assert "199元 深度辅导版" in body
assert 'data-package="audit"' in body
assert 'data-package="standard"' in body
assert 'data-package="premium"' in body
assert "推荐方案" in body
assert "你可以先从 99 元完整志愿方案开始" in body
assert "支付接入建设中" not in body
fix(landing+pricing): A) consult form privacy note + B) pricing page copy sync A) 首页咨询表单加隐私说明 (admin/routes/web_public.py): - 在表单标题下方加 .consult-privacy 框: '🔒 这些输入只用于判断要不要复核你的方案, 不会留底、不会用于生成方案、不会发邮件推销。 如果你决定不进入付费方案, 提交的资料不会保存到我们的数据库。' - 在表单下方加 .consult-privacy-tail: '不会收到营销短信, 提交后你也可以随时要求删除已填资料。' - 新增 CSS: .consult-privacy (薄荷绿底+边框) / .consult-privacy-tail (灰色脚注) - 设计意图: 不抢眼但能看见, 用浅色背景与表单区分, 不影响主 CTA 视觉权重 B) 套餐页 /pricing 同步'复核免费 / 方案付费' 口径: - lead 文案加 <strong>复核现有方案本身免费</strong> (放在最前面) - consult-summary 内加 consult-reassure 框: '💡 如果你还没决定, 先做一次免费复核, 再决定要不要进入付费方案。' - summary 副卡加 summary-reassure: '🔒 还没下单前, 提交的基本情况仅用于判断是否需要复核, 不会留底。' - trust-proof 1 号卡: '先审计再决定' → '复核免费 / 方案付费' - trust-band 3 号卡: '合规入口可见' → '复核免费 / 方案付费' (新增回流入口) - 49元档 eyebrow: '快速校验' → '复核 / 风险' (强调只审不改) - 99元档 eyebrow: '完整规划' → '生成 / 完整' (强调生成动作) - 199元档 eyebrow: '深度辅导' → '生成 / 深度' - 49元档 desc: 加'不会重新生成志愿表 (生成需选 99 元)' 边界 - 99元档 desc: 加'方案生成与详细报告在支付后启动' 边界 - 199元档 desc: 加'在 99 元完整方案基础上提供多轮修订和深度解释' - 49元档 CTA: '先做快速审核' → '先做付费审核' (用'付费'反衬'免费复核') - 99元档 CTA: '立即开始完整规划' → '支付并启动方案生成' - 199元档 CTA: '了解深度辅导' (保持) - 49元档 features: 加'不重新生成志愿表(生成需选 99 元)' 边界 - 199元档 features: 加'包含 99 元完整方案的所有交付' - FAQ 加 '复核是免费的吗?包含什么?' (明确边界: 复核免费/审核报告 49/完整方案 99起) - 99元 FAQ 加 '99 元是生成一份完整方案的价格, 不是查看价格' 避免误解 - notice 提示: '先做一次免费复核' 引导 + 保留 49/99 路径建议 新增测试断言: - 首页: '不会留底' / '不会用于生成方案' / '不会发邮件推销' / '不会收到营销短信' (4 条) - 套餐页: '复核现有方案本身免费' / '复核免费 / 方案付费' / 'href="/#consult-box"' / '先做一次免费复核' / '先做付费审核' / '支付并启动方案生成' / '复核 / 风险' / '生成 / 完整' / '生成 / 深度' / '不会重新生成志愿表' / '支付后启动' / '复核是免费的吗?包含什么?' / '还没决定' (13 条) - 套餐页反向断言: '先做快速审核' / '立即开始完整规划' / '先审计再决定' / '快速校验' (4 条) 验证: - pytest 22 passed (test_web_public + test_order_status_page + test_web_public_alipay_sim_e2e) - ruff check: All checks passed - 真实 HTTP 端到端 17/17 断言 OK (A 6 条 + B 20 条旧移除+新命中) - Playwright vision 验证: A) 首页隐私说明: 上下双向布局, 浅色克制, 覆盖'用途/营销/删除'三要素 ✅ B) 套餐页: lead/eyebrow/CTA/FAQ 5 处引导回首页 全部对齐'复核免费/方案付费' ✅
2026-06-17 22:38:42 +08:00
assert "复核现有方案本身免费" in body
assert "复核免费 / 方案付费" in body
assert 'href="/#consult-box"' in body
assert "先做一次免费复核" in body
assert "选择付费审核" in body
assert "支付并启动方案生成" in body
assert "选择深度辅导" in body
fix(landing+pricing): A) consult form privacy note + B) pricing page copy sync A) 首页咨询表单加隐私说明 (admin/routes/web_public.py): - 在表单标题下方加 .consult-privacy 框: '🔒 这些输入只用于判断要不要复核你的方案, 不会留底、不会用于生成方案、不会发邮件推销。 如果你决定不进入付费方案, 提交的资料不会保存到我们的数据库。' - 在表单下方加 .consult-privacy-tail: '不会收到营销短信, 提交后你也可以随时要求删除已填资料。' - 新增 CSS: .consult-privacy (薄荷绿底+边框) / .consult-privacy-tail (灰色脚注) - 设计意图: 不抢眼但能看见, 用浅色背景与表单区分, 不影响主 CTA 视觉权重 B) 套餐页 /pricing 同步'复核免费 / 方案付费' 口径: - lead 文案加 <strong>复核现有方案本身免费</strong> (放在最前面) - consult-summary 内加 consult-reassure 框: '💡 如果你还没决定, 先做一次免费复核, 再决定要不要进入付费方案。' - summary 副卡加 summary-reassure: '🔒 还没下单前, 提交的基本情况仅用于判断是否需要复核, 不会留底。' - trust-proof 1 号卡: '先审计再决定' → '复核免费 / 方案付费' - trust-band 3 号卡: '合规入口可见' → '复核免费 / 方案付费' (新增回流入口) - 49元档 eyebrow: '快速校验' → '复核 / 风险' (强调只审不改) - 99元档 eyebrow: '完整规划' → '生成 / 完整' (强调生成动作) - 199元档 eyebrow: '深度辅导' → '生成 / 深度' - 49元档 desc: 加'不会重新生成志愿表 (生成需选 99 元)' 边界 - 99元档 desc: 加'方案生成与详细报告在支付后启动' 边界 - 199元档 desc: 加'在 99 元完整方案基础上提供多轮修订和深度解释' - 49元档 CTA: '先做快速审核' → '先做付费审核' (用'付费'反衬'免费复核') - 99元档 CTA: '立即开始完整规划' → '支付并启动方案生成' - 199元档 CTA: '了解深度辅导' (保持) - 49元档 features: 加'不重新生成志愿表(生成需选 99 元)' 边界 - 199元档 features: 加'包含 99 元完整方案的所有交付' - FAQ 加 '复核是免费的吗?包含什么?' (明确边界: 复核免费/审核报告 49/完整方案 99起) - 99元 FAQ 加 '99 元是生成一份完整方案的价格, 不是查看价格' 避免误解 - notice 提示: '先做一次免费复核' 引导 + 保留 49/99 路径建议 新增测试断言: - 首页: '不会留底' / '不会用于生成方案' / '不会发邮件推销' / '不会收到营销短信' (4 条) - 套餐页: '复核现有方案本身免费' / '复核免费 / 方案付费' / 'href="/#consult-box"' / '先做一次免费复核' / '先做付费审核' / '支付并启动方案生成' / '复核 / 风险' / '生成 / 完整' / '生成 / 深度' / '不会重新生成志愿表' / '支付后启动' / '复核是免费的吗?包含什么?' / '还没决定' (13 条) - 套餐页反向断言: '先做快速审核' / '立即开始完整规划' / '先审计再决定' / '快速校验' (4 条) 验证: - pytest 22 passed (test_web_public + test_order_status_page + test_web_public_alipay_sim_e2e) - ruff check: All checks passed - 真实 HTTP 端到端 17/17 断言 OK (A 6 条 + B 20 条旧移除+新命中) - Playwright vision 验证: A) 首页隐私说明: 上下双向布局, 浅色克制, 覆盖'用途/营销/删除'三要素 ✅ B) 套餐页: lead/eyebrow/CTA/FAQ 5 处引导回首页 全部对齐'复核免费/方案付费' ✅
2026-06-17 22:38:42 +08:00
assert "先做快速审核" not in body
assert "立即开始完整规划" not in body
assert "先审计再决定" not in body
assert "快速校验" not in body
assert "复核是免费的吗?包含什么?" in body
assert "还没决定" in body
assert "/static/portal-ui.css" in body
def test_public_checkout_page_served(client):
resp = client.get("/checkout/standard")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
body = resp.text
assert "完整志愿方案" in body
assert "订单摘要" in body
assert "退款政策" in body
assert "立即支付" in body
assert "先下单再补资料" in body
assert "当前建议" in body
assert "交付方式" in body
assert "最小下单" not in body
assert "/static/portal-ui.css" in body
def test_public_create_order_endpoint(client, app):
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"customer_email": "parent@example.com",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 201, resp.text
body = resp.json()
assert body["order_id"].startswith("GKO-")
assert body["source"] == "web"
assert body["status"] == "pending"
assert body["service_version"] == "standard"
assert body["amount_cents"] == 9900
assert body["next_step"] == "payment"
assert body["checkout_url"].startswith("/pay/mock/")
assert "/portal/" in body["portal_status_url"]
with OrdersDAO.connect(app.state.settings.orders_db_path) as dao:
created = dao.get(body["order_id"])
assert created.customer_email == "parent@example.com"
assert created.candidate_name == "张三"
def test_public_create_order_returns_503_with_friendly_message_when_encryption_key_missing(
tmp_path, monkeypatch
):
admin_db = tmp_path / "admin.db"
orders_db = tmp_path / "orders.db"
share_db = tmp_path / "share.db"
share_reports = tmp_path / "share_reports"
share_reports.mkdir()
monkeypatch.setenv("GAOKAO_ENV", "dev")
monkeypatch.setenv("GAOKAO_DB_PATH", str(admin_db))
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(orders_db))
monkeypatch.setenv("GAOKAO_SHARE_DB_PATH", str(share_db))
monkeypatch.setenv("GAOKAO_SHARE_REPORT_DIR", str(share_reports))
monkeypatch.delenv("GAOKAO_ORDERS_FERNET_KEY", raising=False)
monkeypatch.setenv("GAOKAO_JWT_SECRET", "x" * 64)
monkeypatch.setenv("GAOKAO_JWT_EXP_MIN", "5")
monkeypatch.setenv("GAOKAO_ADMIN_USER", "admin")
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "test-pass-123")
from admin.app import create_app
from admin.config import load_settings
from data.orders import crypto
crypto.get_fernet.cache_clear()
settings = load_settings()
app = create_app(settings)
with TestClient(app) as client:
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 503, resp.text
body = resp.json()
assert "当前暂时无法创建订单" in body["detail"]["reason"]
def test_public_create_order_rejects_missing_minimal_fields(client):
resp = client.post(
"/api/public/orders",
json={
"service_version": "audit",
"amount_cents": 4900,
"customer_name": "李家长",
"candidate_province": "广东",
},
)
assert resp.status_code == 422
body = resp.json()
assert body["message"] == "请求数据未通过校验"
assert any(
field["field"] in {"body.customer_phone", "body.candidate_name"}
for field in body["detail"]["fields"]
)
def test_public_create_order_rejects_price_tampering(client):
resp = client.post(
"/api/public/orders",
json={
"service_version": "audit",
"amount_cents": 4999,
"customer_name": "张家长",
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 422
body = resp.json()
assert body["message"] == "请求数据未通过校验"
assert any(
"amount_cents 与套餐价格不一致" in field["reason"]
for field in body["detail"]["fields"]
)
def test_public_create_order_persists_candidate_province(client, app):
resp = client.post(
"/api/public/orders",
json={
"service_version": "audit",
"amount_cents": 4900,
"customer_name": "李家长",
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "广东",
},
)
assert resp.status_code == 201, resp.text
body = resp.json()
with OrdersDAO.connect(app.state.settings.orders_db_path) as dao:
created = dao.get(body["order_id"])
assert created.candidate_province == "广东"
def test_public_create_order_rejects_unsupported_province(client):
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_name": "李家长",
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "内蒙古",
},
)
assert resp.status_code == 422
body = resp.json()
assert body["message"] == "请求数据未通过校验"
assert "candidate_province" in resp.text
def test_public_create_order_supports_optional_contact_email(client, app):
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_name": "李家长",
"customer_phone": "13800138000",
"customer_email": "guardian@example.com",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 201, resp.text
body = resp.json()
with OrdersDAO.connect(app.state.settings.orders_db_path) as dao:
created = dao.get(body["order_id"])
assert created.customer_email == "guardian@example.com"
def test_public_checkout_page_renders_audit_package(client):
resp = client.get("/checkout/audit")
assert resp.status_code == 200, resp.text
body = resp.text
assert "49元 AI方案审核" in body
assert "适合已经拿到其他方案、希望先快速校验风险的家庭。" in body
assert "当前建议" in body
assert "当前这一步只收会影响下单与后续联系的必要信息" in body
assert "支付成功后,再进入资料向导补充分数、位次、偏好和已有方案附件。" in body
assert "service_version: 'audit'" in body
assert "amount_cents: 4900" in body
def test_public_checkout_page_renders_premium_package(client):
resp = client.get("/checkout/premium")
assert resp.status_code == 200, resp.text
body = resp.text
assert "199元 深度辅导版" in body
assert "适合需要更多沟通、补充说明与深度修订支持的家庭。" in body
assert "199元 深度辅导版" in body
assert "service_version: 'premium'" in body
assert "amount_cents: 19900" in body
def test_public_create_order_rejects_unknown_service_version(client):
resp = client.post(
"/api/public/orders",
json={
"service_version": "unknown",
"amount_cents": 9999,
"customer_name": "李家长",
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 422
body = resp.json()
assert body["message"] == "请求数据未通过校验"
assert any(
field["field"] == "body.service_version" for field in body["detail"]["fields"]
)
def test_public_payment_flow_returns_payment_success_page(client, settings):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_name": "张家长",
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
body = create_resp.json()
payment_id = body["checkout_url"].split("/pay/mock/")[1].split("?")[0]
complete = client.post(f"/pay/mock/{payment_id}/complete", follow_redirects=False)
assert complete.status_code == 303, complete.text
assert complete.headers["location"].endswith("/payment-success")
token = (
complete.headers["location"].split("/portal/")[1].split("/payment-success")[0]
)
page = client.get(f"/portal/{token}/payment-success")
assert page.status_code == 200, page.text
assert "订单已创建,下一步继续补资料" in page.text
assert "支付状态paid" in page.text
assert "立即补充资料" in page.text
assert "查看订单进度" in page.text
def test_payment_success_page_shows_audit_copy_for_audit_orders(client, settings):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "audit",
"amount_cents": 4900,
"customer_phone": "13800138000",
"candidate_name": "李同学",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
body = create_resp.json()
payment_id = body["checkout_url"].split("/pay/mock/")[1].split("?")[0]
complete = client.post(f"/pay/mock/{payment_id}/complete", follow_redirects=False)
assert complete.status_code == 303, complete.text
assert complete.headers["location"].endswith("/payment-success")
token = (
complete.headers["location"].split("/portal/")[1].split("/payment-success")[0]
)
page = client.get(f"/portal/{token}/payment-success")
assert page.status_code == 200, page.text
assert "订单已创建,下一步继续补资料" in page.text
assert "补充基础信息" in page.text
assert "填写偏好目标" in page.text
assert "持续查看进度" in page.text
def test_prod_hides_simulated_payment_entrypoints(tmp_path, monkeypatch):
admin_db = tmp_path / "admin.db"
orders_db = tmp_path / "orders.db"
share_db = tmp_path / "share.db"
share_reports = tmp_path / "share_reports"
share_reports.mkdir()
monkeypatch.setenv("GAOKAO_ENV", "prod")
monkeypatch.setenv("GAOKAO_DB_PATH", str(admin_db))
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(orders_db))
monkeypatch.setenv("GAOKAO_SHARE_DB_PATH", str(share_db))
monkeypatch.setenv("GAOKAO_SHARE_REPORT_DIR", str(share_reports))
monkeypatch.setenv("GAOKAO_ORDERS_FERNET_KEY", "test-secret-for-web-self-service")
monkeypatch.setenv("GAOKAO_JWT_SECRET", "x" * 64)
monkeypatch.setenv("GAOKAO_PAYMENT_WEBHOOK_SECRET", "prod-secret-ok-123456")
monkeypatch.setenv("GAOKAO_PORTAL_TOKEN_SECRET", "prod-portal-secret-ok-1234567890")
monkeypatch.setenv("GAOKAO_JWT_EXP_MIN", "5")
monkeypatch.setenv("GAOKAO_ADMIN_USER", "admin")
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "test-pass-123")
monkeypatch.setenv("GAOKAO_PAYMENT_PROVIDER", "alipay")
monkeypatch.setenv("GAOKAO_PAYMENT_BASE_URL", "http://testserver")
from admin.config import load_settings
settings = load_settings()
request = Request({
"type": "http",
"method": "POST",
"path": "/api/public/payments/mock/webhook",
"headers": [],
"query_string": b"",
"server": ("testserver", 80),
"client": ("127.0.0.1", 12345),
"scheme": "http",
})
for route_call in (
lambda: mock_payment_page("pay_123", settings),
lambda: complete_mock_payment("pay_123", settings),
lambda: alipay_sim_payment_page("pay_123", settings),
lambda: complete_alipay_sim_payment("pay_123", settings),
lambda: mock_payment_webhook({}, request, settings),
):
with pytest.raises(HTTPException) as exc_info:
route_call()
assert exc_info.value.status_code == 404
def test_public_create_order_returns_503_without_creating_orphan_order_when_provider_unavailable(
tmp_path, monkeypatch
):
admin_db = tmp_path / "admin.db"
orders_db = tmp_path / "orders.db"
share_db = tmp_path / "share.db"
share_reports = tmp_path / "share_reports"
share_reports.mkdir()
monkeypatch.setenv("GAOKAO_ENV", "dev")
monkeypatch.setenv("GAOKAO_DB_PATH", str(admin_db))
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(orders_db))
monkeypatch.setenv("GAOKAO_SHARE_DB_PATH", str(share_db))
monkeypatch.setenv("GAOKAO_SHARE_REPORT_DIR", str(share_reports))
monkeypatch.setenv("GAOKAO_ORDERS_FERNET_KEY", "test-secret-for-web-self-service")
monkeypatch.setenv("GAOKAO_JWT_SECRET", "x" * 64)
monkeypatch.setenv("GAOKAO_JWT_EXP_MIN", "5")
monkeypatch.setenv("GAOKAO_ADMIN_USER", "admin")
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "test-pass-123")
monkeypatch.setenv("GAOKAO_PAYMENT_PROVIDER", "alipay")
monkeypatch.setenv("GAOKAO_PAYMENT_BASE_URL", "http://testserver")
monkeypatch.setenv("GAOKAO_PAYMENT_WEBHOOK_SECRET", "missing-real-provider-env")
from admin.app import create_app
from admin.config import load_settings
settings = load_settings()
app = create_app(settings)
with TestClient(app) as client:
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_name": "张家长",
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 503, resp.text
assert "payment provider unavailable" in resp.text
with OrdersDAO.connect(settings.orders_db_path) as dao:
assert dao.count() == 0
def test_my_orders_page_served(client):
"""我的订单页必须存在且可访问。"""
resp = client.get("/my-orders")
assert resp.status_code == 200, resp.text
assert "我的订单" in resp.text
assert "手机号" in resp.text
def test_my_orders_lookup_by_phone(client, settings):
"""C 方案:手机号直查已废弃,凭手机号访问 /my-orders 应显示引导说明而非订单列表。"""
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
# 手机号查询不再返回订单列表,而是显示引导说明
resp = client.get("/my-orders?phone=13800138000")
assert resp.status_code == 200, resp.text
assert "请通过订单链接进入" in resp.text
def test_my_reports_page_served(client):
"""C 方案:我的报告页存在但不再显示手机号查询表单。"""
resp = client.get("/my-reports")
assert resp.status_code == 200, resp.text
assert "我的报告" in resp.text
assert "请通过订单链接进入" in resp.text