Commit 31fb1489 authored by wushanjing's avatar wushanjing

新增丸掌柜丸友集授权简化自动化用例

parent 84caf17f
from __future__ import annotations
from pathlib import Path
from typing import Iterable
from playwright.sync_api import Frame, Page, expect
from e2e.pages.tenant_auth_page import TenantAuthPage
from e2e.pages.wanzhanggui_order_page import WANYOUJI_CONSOLE_URL
from e2e.pages.wanzhanggui_push_page import WANZHANGGUI_CONSOLE_URL
from e2e.utils.bug_reporter import BugReporter
class AuthorizationSimplificationPage(TenantAuthPage):
"""Interactions shared by the new Wanyouji/Wanzhanggui authorization pages."""
def __init__(self, page: Page, tenant_home_url: str, bug_reporter: BugReporter | None = None):
super().__init__(page, tenant_home_url, bug_reporter)
def open_wanyouji_merchant_recommendation(self) -> Frame:
required = ["发单商户推荐", "发单商户授权", "待我审核"]
last_error: AssertionError | None = None
for _ in range(3):
try:
self.goto(WANYOUJI_CONSOLE_URL)
self.wait_for_enterprise_console_shell()
self.page.wait_for_timeout(1200)
self.click_sidebar_text("工作室基础应用", right_edge=True)
self.click_sidebar_text("发单商户推荐")
return self._business_frame(required)
except AssertionError as error:
last_error = error
self.page.wait_for_timeout(1200)
raise last_error or AssertionError("未能加载发单商户推荐页面")
def open_wanzhanggui_workshop_authorization_new(self) -> Frame:
required = ["工作室推荐", "工作室授权", "待我审核"]
last_error: AssertionError | None = None
for _ in range(3):
try:
self.goto(WANZHANGGUI_CONSOLE_URL)
self.wait_for_enterprise_console_shell()
self.page.wait_for_timeout(1200)
self.click_sidebar_text("商家管理", right_edge=True)
self.click_sidebar_text("工作室授权新")
return self._business_frame(required)
except AssertionError as error:
last_error = error
self.page.wait_for_timeout(1200)
raise last_error or AssertionError("未能加载工作室授权新页面")
def _business_frame(self, required_texts: Iterable[str]) -> Frame:
required = list(required_texts)
for _ in range(36):
for frame in reversed(self.page.frames):
try:
body = frame.locator("body").inner_text(timeout=1200)
except Exception:
continue
if all(text in body for text in required):
return frame
self.page.wait_for_timeout(800)
snapshots: list[str] = []
for frame in self.page.frames:
try:
snapshots.append(f"{frame.url}: {frame.locator('body').inner_text(timeout=600)[:600]}")
except Exception:
continue
raise AssertionError(f"未找到授权简化业务 iframe;required={required}; frames={snapshots}")
def click_tab(self, frame: Frame, tab_text: str) -> None:
clicked = frame.evaluate(
"""text => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const compact = value => (value || '').replace(/\s+/g, '').trim();
const nodes = Array.from(document.querySelectorAll('[role=tab], .ant-tabs-tab, button, a, span, div'))
.filter(visible)
.filter(el => {
const value = compact(el.innerText || el.textContent || '');
return value === compact(text) || value.startsWith(compact(text));
});
const target = nodes.sort((a, b) => {
const ar = a.getBoundingClientRect(), br = b.getBoundingClientRect();
return ar.width * ar.height - br.width * br.height;
})[0];
if (!target) return false;
(target.closest('[role=tab], .ant-tabs-tab, button, a') || target).click();
return true;
}""",
tab_text,
)
assert clicked, f"授权页未找到Tab {tab_text};text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
self.page.wait_for_timeout(900)
def search_by_name(self, frame: Frame, label: str, value: str) -> None:
found = frame.evaluate(
"""({ label, value }) => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const inputs = Array.from(document.querySelectorAll('input')).filter(visible);
const target = inputs.find(input => {
const placeholder = input.getAttribute('placeholder') || '';
const scope = input.parentElement?.parentElement?.innerText || '';
return placeholder.includes(label) || scope.includes(label);
}) || inputs[0];
if (!target) return false;
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
setter?.call(target, value);
target.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' }));
target.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}""",
{"label": label, "value": value},
)
assert found, f"授权页未找到 {label} 搜索框"
self._click_text(frame, ["查询", "查 询"])
self.page.wait_for_timeout(1400)
def row_text(self, frame: Frame, name: str) -> str:
return str(
frame.evaluate(
"""name => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const rows = Array.from(document.querySelectorAll('tr, .ant-table-row, [class*=table-row]'))
.filter(visible)
.map(row => (row.innerText || row.textContent || '').trim());
return rows.find(text => text.includes(name)) || '';
}""",
name,
)
)
def require_row(self, frame: Frame, name: str, expected_text: str = "") -> str:
for _ in range(5):
text = self.row_text(frame, name)
if text and (not expected_text or expected_text in text):
return text
self.page.wait_for_timeout(900)
body = frame.locator("body").inner_text(timeout=5000)
raise AssertionError(f"授权页未找到记录或状态不匹配;name={name}; expected={expected_text}; text={body[:1800]}")
def click_row_action(self, frame: Frame, name: str, actions: list[str]) -> None:
clicked = frame.evaluate(
"""({ name, actions }) => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const rows = Array.from(document.querySelectorAll('tr, .ant-table-row, [class*=table-row]'))
.filter(visible)
.filter(row => (row.innerText || row.textContent || '').includes(name));
for (const row of rows) {
const controls = Array.from(row.querySelectorAll('button, a, [role=button], span, div')).filter(visible);
const target = controls.find(el => actions.some(action => (el.innerText || el.textContent || '').trim() === action));
if (target) { (target.closest('button, a, [role=button]') || target).click(); return true; }
}
return false;
}""",
{"name": name, "actions": actions},
)
assert clicked, f"授权页记录未找到操作;name={name}; actions={actions}; row={self.row_text(frame, name)}"
self.page.wait_for_timeout(700)
def confirm_visible_modal(self, frame: Frame, actions: list[str] | None = None) -> None:
action_texts = actions or ["同意", "确认", "确定", "保存"]
clicked = frame.evaluate(
"""actions => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const roots = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer, [role=dialog]')).filter(visible);
const root = roots[roots.length - 1] || document;
const controls = Array.from(root.querySelectorAll('button, [role=button], span, div')).filter(visible);
const compact = value => (value || '').replace(/\s+/g, '').trim();
const target = controls.find(el => actions.some(action => compact(el.innerText || el.textContent || '') === compact(action)));
if (!target) return false;
(target.closest('button, [role=button]') || target).click();
return true;
}""",
action_texts,
)
assert clicked, f"未找到确认操作 {action_texts};modal={frame.locator('body').inner_text(timeout=5000)[:1500]}"
self.page.wait_for_timeout(1200)
def click_display_config(self, frame: Frame) -> None:
for _ in range(3):
self._click_text(frame, ["对外展示配置"])
dialog = frame.locator(".ant-modal:visible, [role=dialog]:visible").last
try:
expect(dialog).to_be_visible(timeout=3000)
return
except AssertionError:
self.page.wait_for_timeout(500)
raise AssertionError("未能打开对外展示配置弹窗")
def save_display_config(self, frame: Frame, data: dict[str, str], qr_code: Path | None = None) -> None:
root = frame.locator(".ant-modal:visible, [role=dialog]:visible").last
expect(root).to_be_visible(timeout=5000)
self._fill_modal_field(root, ["简介"], data["intro"], textarea=True)
self._select_modal_option(frame, root, "擅长游戏", data["game"])
self._select_modal_option(frame, root, "结算周期", data["settlement"])
self._fill_modal_field(root, ["联系人"], data["contact"])
self._fill_modal_field(root, ["联系手机", "手机号"], data["phone"])
self._fill_modal_field(root, ["微信号"], data["wechat"])
self._fill_modal_field(root, ["QQ号", "QQ 号"], data["qq"])
if qr_code:
upload = root.locator("input[type=file]").first
if upload.count():
upload.set_input_files(str(qr_code))
uploading = root.get_by_text("Uploading...", exact=False)
if uploading.count():
expect(uploading).to_be_hidden(timeout=30000)
self.confirm_visible_modal(frame, ["保存"])
expect(root).to_be_hidden(timeout=8000)
def toggle_external_display(self, frame: Frame, enabled: bool = True) -> None:
result = frame.evaluate(
"""enabled => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const label = Array.from(document.querySelectorAll('span, div, label')).find(el => visible(el) && (el.innerText || el.textContent || '').trim() === '对外展示');
if (!label) return false;
const scope = label.parentElement?.parentElement || label.parentElement || document;
const input = scope.querySelector('input[type=checkbox]');
if (input && Boolean(input.checked) !== enabled) { input.click(); return true; }
const control = scope.querySelector('.ant-switch, [role=switch]');
if (control) { control.click(); return true; }
return Boolean(input?.checked) === enabled;
}""",
enabled,
)
assert result, "未找到或未能切换对外展示开关"
self.page.wait_for_timeout(900)
def _fill_modal_field(self, root, labels: list[str], value: str, *, textarea: bool = False) -> None:
selector = "textarea" if textarea else "input"
for label in labels:
candidate = root.locator(f"{selector}[placeholder*='{label}']").first
if candidate.count():
candidate.fill(value)
return
inputs = root.locator(selector)
for index in range(inputs.count()):
candidate = inputs.nth(index)
try:
belongs_to_label = candidate.evaluate(
"""(element, labels) => {
for (let node = element.parentElement; node && node !== document.body; node = node.parentElement) {
const text = node.innerText || node.textContent || '';
const controls = node.querySelectorAll('input, textarea');
if (controls.length === 1 && labels.some(label => text.includes(label))) return true;
}
return false;
}""",
labels,
)
except Exception:
belongs_to_label = False
if belongs_to_label:
candidate.fill(value)
return
controls = root.evaluate(
"""node => Array.from(node.querySelectorAll('input, textarea, [contenteditable=true]'))
.map(el => ({ tag: el.tagName, placeholder: el.getAttribute('placeholder') || '', name: el.getAttribute('name') || '' }))"""
)
raise AssertionError(
f"展示配置弹窗未找到字段 {labels}; text={root.inner_text(timeout=3000)[:1200]}; controls={controls}"
)
def _select_modal_option(self, frame: Frame, root, label: str, value: str) -> None:
comboboxes = root.get_by_role("combobox")
index = 0 if label == "擅长游戏" else 1
if comboboxes.count() > index:
combobox = comboboxes.nth(index)
combobox.click(timeout=3000, force=True)
self.page.wait_for_timeout(500)
option = self.page.locator(f'[role="option"][aria-label="{value}"]:visible').first
try:
expect(option).to_be_visible(timeout=1500)
option.click()
except AssertionError:
combobox.press("ArrowDown")
combobox.press("Enter")
combobox.press("Escape")
self.page.wait_for_timeout(300)
return
selected = root.evaluate(
r"""(root, { label, value }) => {
const norm = text => String(text || '').replace(/\s+/g, '').trim();
const labels = Array.from(root.querySelectorAll('*'))
.map(el => ({ el, text: norm(el.innerText || el.textContent || '') }))
.filter(item => item.text === norm(label))
.sort((a, b) => a.text.length - b.text.length);
const target = labels[0]?.el;
if (!target) return false;
for (let node = target.parentElement; node; node = node.parentElement) {
const selector = node.querySelector('.ant-select-selector, [role=combobox], input:not([type=file])');
if (selector) {
selector.click();
return true;
}
}
return false;
}""",
{"label": label, "value": value},
)
if not selected:
structure = root.evaluate(
r"""(root, label) => Array.from(root.querySelectorAll('*'))
.filter(el => (el.innerText || el.textContent || '').trim() === label)
.slice(0, 3)
.map(el => {
const items = [];
for (let node = el; node && items.length < 5; node = node.parentElement) {
items.push({ tag: node.tagName, className: String(node.className || ''), text: (node.innerText || node.textContent || '').trim().slice(0, 300) });
}
return items;
})""",
label,
)
raise AssertionError(f"展示配置弹窗未找到下拉字段 {label}; structure={structure}")
self.page.wait_for_timeout(500)
option = frame.locator(".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content").filter(has_text=value).first
expect(option).to_be_visible(timeout=4000)
option.click()
self.page.wait_for_timeout(300)
def _click_text(self, frame: Frame, texts: list[str]) -> None:
clicked = frame.evaluate(
"""texts => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const nodes = Array.from(document.querySelectorAll('button, a, [role=button], span, div')).filter(visible);
const target = nodes.find(el => texts.some(text => (el.innerText || el.textContent || '').trim() === text));
if (!target) return false;
(target.closest('button, a, [role=button]') || target).click();
return true;
}""",
texts,
)
assert clicked, f"未找到操作 {texts};text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
from __future__ import annotations
import os
from datetime import datetime
from pathlib import Path
import pytest
from playwright.sync_api import Page
from e2e.pages.auth_simplification_page import AuthorizationSimplificationPage
from e2e.pages.admin_page import AdminPage
from e2e.pages.product_solution_admin_page import ProductSolutionAdminPage
from e2e.pages.tenant_registration_page import AccountAlreadyRegistered, TenantRegistrationPage
from e2e.pages.tenant_subscription_page import TenantSubscriptionPage
from e2e.pages.wanzhanggui_order_page import ManualOrderData, WanyoujiFulfillmentPage, WanzhangguiOrderPage
from e2e.pages.wanzhanggui_push_page import WanzhangguiPushPage
from e2e.utils.account_store import E2EAccount, account_store
from e2e.utils.data_factory import EnterpriseRegistrationData, enterprise_data, manual_order_receive_price
from e2e.utils.image_captcha import ImageCaptchaSolver
from e2e.utils.verification_code import FeishuConfig, VerificationCodeProvider
CASE_PREFIX = "E2E-AUTH-SIMPLIFY"
FIXED_PHONE = os.getenv("E2E_AUTH_SIMPLIFICATION_FIXED_PHONE", "19159770995")
FIXED_PASSWORD = os.getenv("E2E_AUTH_SIMPLIFICATION_FIXED_PASSWORD", "test123456")
FIXED_MERCHANT_NAME = os.getenv("E2E_AUTH_SIMPLIFICATION_FIXED_MERCHANT_NAME", "个体工商户1378")
RUN_OPENING = os.getenv("E2E_AUTH_SIMPLIFICATION_ENABLE_OPENING", "0").strip().lower() in {"1", "true", "yes"}
FORCE_NEW_ENTERPRISE = os.getenv("E2E_AUTH_SIMPLIFICATION_FORCE_NEW_ENTERPRISE", "0").strip().lower() in {"1", "true", "yes"}
def pytest_configure(config):
config.addinivalue_line("markers", "e2e_auth_simplification: Wanyouji/Wanzhanggui authorization simplification flows")
@pytest.mark.e2e_auth_simplification
@pytest.mark.e2e_seed
@pytest.mark.e2e_ledger
def test_auth_simplify_001_new_wanyouji_display_is_visible_to_wanzhanggui_and_admin(
page: Page, settings, bug_reporter, dual_account_pages
) -> None:
"""Create one enterprise, open Wanyouji, and verify its display profile across three surfaces."""
account = create_certified_enterprise(page, settings, bug_reporter)
ensure_product_opened(page, settings, bug_reporter, account, "丸友集")
account = account_store().mark_wanyouji_opened(account)
profile = display_profile(account)
# This page is later reused for SaaS-admin verification. Keep the headed
# pytest window useful while the dual-account contexts perform tenant work.
page.goto(settings.admin_url, wait_until="domcontentloaded")
wanyouji_page, wanzhanggui_page = dual_account_pages.pair(account.phone, FIXED_PHONE)
wanyouji = AuthorizationSimplificationPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
wanyouji.login_main_account(account.phone, account.password)
if os.getenv("E2E_AUTH_SIMPLIFICATION_SKIP_DISPLAY_CONFIG", "0").strip().lower() not in {"1", "true", "yes"}:
frame = wanyouji.open_wanyouji_merchant_recommendation()
wanyouji.click_display_config(frame)
wanyouji.save_display_config(frame, profile, qr_code=ensure_qr_placeholder())
wanyouji.toggle_external_display(frame)
wanzhanggui = AuthorizationSimplificationPage(wanzhanggui_page, settings.tenant_home_url, bug_reporter)
wanzhanggui.login_main_account(FIXED_PHONE, FIXED_PASSWORD)
verify_recommendation_detail(wanzhanggui, account.enterprise_name, "工作室名称", profile)
if os.getenv("E2E_AUTH_SIMPLIFICATION_SKIP_ADMIN_RECOMMENDATION_CHECK", "0").strip().lower() not in {"1", "true", "yes"}:
verify_admin_recommendation(page, settings, account.enterprise_name, profile)
save_flow_artifact(account, profile, "wanyouji_display_ready")
@pytest.mark.e2e_auth_simplification
@pytest.mark.e2e_chain
@pytest.mark.e2e_ledger
def test_auth_simplify_002_same_enterprise_wanzhanggui_display_and_supply_initialization(
page: Page, settings, bug_reporter, dual_account_pages
) -> None:
"""Open Wanzhanggui for the seeded enterprise and validate merchant recommendation/admin supply records."""
account, profile = latest_flow_account("wanyouji_display_ready")
ensure_product_opened(page, settings, bug_reporter, account, "丸掌柜")
account = account_store().mark_wanzhanggui_opened(account)
profile = {**profile, "intro": f"丸掌柜-{profile['intro']}"}
wanzhanggui_page, wanyouji_page = dual_account_pages.pair(account.phone, FIXED_PHONE)
wanzhanggui = AuthorizationSimplificationPage(wanzhanggui_page, settings.tenant_home_url, bug_reporter)
wanzhanggui.login_main_account(account.phone, account.password)
frame = wanzhanggui.open_wanzhanggui_workshop_authorization_new()
wanzhanggui.click_display_config(frame)
wanzhanggui.save_display_config(frame, profile, qr_code=ensure_qr_placeholder())
wanzhanggui.toggle_external_display(frame)
wanyouji = AuthorizationSimplificationPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
wanyouji.login_main_account(FIXED_PHONE, FIXED_PASSWORD)
verify_recommendation_detail(wanyouji, account.enterprise_name, "商户名称", profile)
verify_admin_recommendation(page, settings, account.enterprise_name, profile)
verify_admin_merchant_supply(page, settings, account.enterprise_name)
save_flow_artifact(account, profile, "wanzhanggui_display_ready")
@pytest.mark.e2e_auth_simplification
@pytest.mark.e2e_chain
@pytest.mark.e2e_ledger
def test_auth_simplify_003_wanyouji_apply_wanzhanggui_approve_and_dispatch(
page: Page, settings, bug_reporter, dual_account_pages
) -> None:
"""Wanyouji applies to the fixed merchant, then approval produces an enabled strategy and a hall order."""
account, _ = latest_flow_account("wanyouji_display_ready")
wanyouji_page, wanzhanggui_page = dual_account_pages.pair(account.phone, FIXED_PHONE)
wanyouji = AuthorizationSimplificationPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
wanzhanggui = AuthorizationSimplificationPage(wanzhanggui_page, settings.tenant_home_url, bug_reporter)
wanyouji.login_main_account(account.phone, account.password)
wanzhanggui.login_main_account(FIXED_PHONE, FIXED_PASSWORD)
wanyouji_frame = wanyouji.open_wanyouji_merchant_recommendation()
try:
wanyouji.require_row(wanyouji_frame, FIXED_MERCHANT_NAME)
target_row = wanyouji_frame.locator("tr, .ant-table-row, [class*=table-row]").filter(has_text=FIXED_MERCHANT_NAME).first
target_row.get_by_role("button", name="申请发单", exact=True).click(timeout=5000)
submit = wanyouji_frame.get_by_role("button", name="提交申请", exact=True).last
submit.click(timeout=5000)
wanyouji_page.wait_for_timeout(1200)
except AssertionError:
wanyouji.click_tab(wanyouji_frame, "发单商户授权")
wanyouji.require_row(wanyouji_frame, FIXED_MERCHANT_NAME)
wanzhanggui_frame = wanzhanggui.open_wanzhanggui_workshop_authorization_new()
wanzhanggui.click_tab(wanzhanggui_frame, "待我审核")
wanzhanggui.search_by_name(wanzhanggui_frame, "工作室名称", account.enterprise_name)
review_row = wanzhanggui_frame.locator("tr, .ant-table-row, [class*=table-row]").filter(has_text=account.enterprise_name).first
review_row.get_by_role("button", name="审核", exact=True).click(timeout=5000)
wanzhanggui.confirm_visible_modal(wanzhanggui_frame, ["同意"])
assert_authorized_and_dispatch(wanzhanggui_page, wanyouji_page, settings, bug_reporter, account)
save_flow_artifact(account, {}, "authorized_and_dispatched")
@pytest.mark.e2e_auth_simplification
@pytest.mark.e2e_chain
@pytest.mark.e2e_ledger
def test_auth_simplify_004_close_reapply_approve_and_dispatch_again(
page: Page, settings, bug_reporter, dual_account_pages
) -> None:
"""Close the established relation, exercise re-application, then dispatch again after approval."""
account, _ = latest_flow_account("authorized_and_dispatched")
wanyouji_page, fixed_wanzhanggui_page = dual_account_pages.pair(account.phone, FIXED_PHONE)
wanyouji = AuthorizationSimplificationPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
wanzhanggui = AuthorizationSimplificationPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
wanyouji.login_main_account(account.phone, account.password)
wanzhanggui.login_main_account(account.phone, account.password)
frame = wanyouji.open_wanyouji_merchant_recommendation()
wanyouji.click_tab(frame, "发单商户授权")
wanyouji.search_by_name(frame, "商户名称", FIXED_MERCHANT_NAME)
wanyouji.click_row_action(frame, FIXED_MERCHANT_NAME, ["关闭授权"])
wanyouji.confirm_visible_modal(frame, ["确认", "确定"])
frame = wanzhanggui.open_wanzhanggui_workshop_authorization_new()
wanzhanggui.search_by_name(frame, "工作室名称", FIXED_MERCHANT_NAME)
wanzhanggui.click_row_action(frame, FIXED_MERCHANT_NAME, ["申请发单", "申请推单"])
wanzhanggui.confirm_visible_modal(frame, ["确认", "确定"])
frame = wanyouji.open_wanyouji_merchant_recommendation()
wanyouji.click_tab(frame, "待我审核")
wanyouji.search_by_name(frame, "商户名称", FIXED_MERCHANT_NAME)
wanyouji.click_row_action(frame, FIXED_MERCHANT_NAME, ["审核"])
wanyouji.confirm_visible_modal(frame, ["同意"])
assert_authorized_and_dispatch(fixed_wanzhanggui_page, wanyouji_page, settings, bug_reporter, account)
def create_certified_enterprise(page: Page, settings, bug_reporter) -> E2EAccount:
resume_phone = os.getenv("E2E_AUTH_SIMPLIFICATION_RESUME_PHONE", "").strip()
if resume_phone:
account = account_store().find_main(resume_phone)
assert account, f"未在账号台账找到授权简化恢复账号:{resume_phone}"
assert account.certification_status == "approved", f"恢复账号未完成认证:{resume_phone}"
return account
if not RUN_OPENING:
pytest.skip("Set E2E_AUTH_SIMPLIFICATION_ENABLE_OPENING=1 to create a new enterprise and open products.")
resumable = None if FORCE_NEW_ENTERPRISE else next(
(
item
for item in reversed(account_store().load())
if item.account_type == "main"
and item.enterprise_name.startswith("E2E授权简化")
and item.certification_status == "approved"
and item.wanyouji_status != "opened"
),
None,
)
if resumable:
return resumable
provider = VerificationCodeProvider(
mode=settings.verification_code_mode,
static_code=settings.verification_code_static,
feishu_config=FeishuConfig(
app_id=settings.feishu_app_id, app_secret=settings.feishu_app_secret, chat_id=settings.feishu_chat_id,
timeout_seconds=settings.feishu_timeout_seconds, poll_interval_seconds=settings.feishu_poll_interval_seconds,
),
manual_code_file=settings.manual_code_file,
manual_timeout_seconds=settings.manual_code_timeout_seconds,
manual_poll_interval_seconds=settings.manual_code_poll_interval_seconds,
)
tenant = TenantRegistrationPage(page, settings.tenant_home_url, provider, ImageCaptchaSolver(mode=settings.image_captcha_mode), bug_reporter)
for _ in range(settings.max_register_attempts):
data = enterprise_data(settings.register_password, settings.phone_prefix, "E2E授权简化")
try:
tenant.register_enterprise(data)
admin = AdminPage(page, settings.admin_url)
admin.login(settings.admin_username, settings.admin_password)
admin.approve_enterprise_certification(data.contact_name)
account = account_store().mark_certified(data)
return account
except AccountAlreadyRegistered:
tenant.logout_if_needed()
raise AssertionError("无法创建新的授权简化测试企业")
def ensure_product_opened(page: Page, settings, bug_reporter, account: E2EAccount, product_name: str) -> None:
if product_name == "丸友集" and account.wanyouji_status == "opened":
return
if product_name == "丸掌柜" and account.wanzhanggui_status == "opened":
return
if not RUN_OPENING:
pytest.skip(f"{product_name}未开通;设置E2E_AUTH_SIMPLIFICATION_ENABLE_OPENING=1后运行开通链路。")
tenant_login = TenantRegistrationPage(
page, settings.tenant_home_url,
VerificationCodeProvider(mode=settings.verification_code_mode), ImageCaptchaSolver(mode=settings.image_captcha_mode), bug_reporter,
)
subscription = TenantSubscriptionPage(page, settings.tenant_home_url, tenant_login, bug_reporter)
if product_name == "丸友集":
open_wanyouji_by_admin_assign(page, settings, subscription, account)
else:
subscription.login_certified_tenant(account.phone, account.password)
subscription.apply_wanzhanggui(account.phone)
assert subscription.wanzhanggui_available(), "丸掌柜未进入已开通状态;当前环境可能要求支付/专用指定开通。"
def open_wanyouji_by_admin_assign(page: Page, settings, subscription: TenantSubscriptionPage, account: E2EAccount) -> None:
"""Open the Wanyouji internal-test version through SaaS-admin's merchant subscription panel."""
from e2e.tests.SaaS订阅邀请登录.test_prodsol_assign_001_admin_assign_open import (
click_assign_open_entry,
close_visible_modals,
open_merchant_list_new,
open_merchant_subscription_modal_strict,
read_subscription_modal_products,
read_subscription_row_text,
search_and_find_merchant_row,
submit_assign_open,
visible_modal_text,
)
admin = ProductSolutionAdminPage(page, settings.admin_url)
admin.login(settings.admin_username, settings.admin_password)
close_visible_modals(page)
open_merchant_list_new(page)
merchant_row = search_and_find_merchant_row(page, account)
open_merchant_subscription_modal_strict(page, merchant_row)
existing_row = read_subscription_row_text(page, "丸友集")
if "丸友集" not in read_subscription_modal_products(page) and "丸友集" not in existing_row:
click_assign_open_entry(page)
selected_product, selected_version = select_wanyouji_internal_assign_version(page)
assert selected_product == "丸友集", f"指定开通选择的产品错误:{selected_product}"
assert selected_version == "内测版", f"指定开通选择的版本错误:{selected_version}"
if os.getenv("E2E_AUTH_SIMPLIFICATION_PAUSE_BEFORE_ASSIGN_SUBMIT", "0").strip().lower() in {"1", "true", "yes"}:
page.pause()
submit_assign_open(page)
close_visible_modals(page)
open_merchant_list_new(page)
merchant_row = search_and_find_merchant_row(page, account)
open_merchant_subscription_modal_strict(page, merchant_row)
row_text = read_subscription_row_text(page, "丸友集")
if "内测版" not in row_text:
switch_wanyouji_subscription_to_internal_test(page, account.enterprise_name)
close_visible_modals(page)
open_merchant_list_new(page)
merchant_row = search_and_find_merchant_row(page, account)
open_merchant_subscription_modal_strict(page, merchant_row)
row_text = read_subscription_row_text(page, "丸友集")
assert "丸友集" in row_text and "内测版" in row_text, (
f"后台指定开通后订阅未展示丸友集内测版:merchant={account.enterprise_name}; row={row_text}; "
f"modal={visible_modal_text(page)[:1200]}"
)
close_visible_modals(page)
if os.getenv("E2E_AUTH_SIMPLIFICATION_SKIP_TENANT_AVAILABILITY_CHECK", "0").strip().lower() in {"1", "true", "yes"}:
return
availability_attempts = int(os.getenv("E2E_AUTH_SIMPLIFICATION_AVAILABILITY_ATTEMPTS", "40"))
for _ in range(availability_attempts):
subscription.login_certified_tenant(account.phone, account.password)
try:
if subscription.wanyouji_available():
return
except AssertionError:
pass
page.wait_for_timeout(3000)
raise AssertionError(
f"后台指定开通丸友集内测版后{availability_attempts * 3}秒内未显示已开通"
)
def switch_wanyouji_subscription_to_internal_test(page: Page, enterprise_name: str) -> None:
"""Correct a default-offered Wanyouji subscription through the admin version-change action."""
from e2e.tests.SaaS订阅邀请登录.test_prodsol_wyjadjust_001_wanyouji_adjust_default import (
click_subscription_product_action,
fill_adjust_reason,
)
from e2e.tests.SaaS订阅邀请登录.test_prodsol_wyjmenu_001_wanyouji_menu_button_config import (
select_internal_test_version,
submit_internal_test_version_adjustment,
)
click_subscription_product_action(page, "丸友集", ["调整版本"])
select_internal_test_version(page)
fill_adjust_reason(page, f"{CASE_PREFIX} {enterprise_name} 切换内测版")
submit_internal_test_version_adjustment(page)
def select_wanyouji_internal_assign_version(page: Page) -> tuple[str, str]:
"""Select exactly 丸友集 / 内测版 in the active 指定开通 modal."""
selected_product = str(page.evaluate("""({ expected, side }) => {
const root = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
.reverse().find(el => el.offsetParent !== null) || document.body;
const compact = value => (value || '').replace(/\\s+/g, '').trim();
const rootRect = root.getBoundingClientRect();
const candidates = Array.from(root.querySelectorAll('*'))
.filter(el => el.offsetParent !== null)
.filter(el => compact(el.innerText || el.textContent) === expected)
.filter(el => {
const rect = el.getBoundingClientRect();
return side === 'left' ? rect.x < rootRect.x + rootRect.width * 0.55 : rect.x >= rootRect.x + rootRect.width * 0.38;
})
.sort((a, b) => {
const ar = a.getBoundingClientRect(); const br = b.getBoundingClientRect();
return (ar.width * ar.height) - (br.width * br.height);
});
const target = candidates[0];
if (!target) return '';
const clickable = target.closest('label, .ant-radio-wrapper, .ant-checkbox-wrapper, li, .ant-list-item, .ant-card, .ant-table-row, tr, button, a, [role="button"]') || target;
clickable.scrollIntoView({ block: 'center', inline: 'center' });
clickable.click();
return expected;
}""", {"expected": "丸友集", "side": "left"}))
assert selected_product == "丸友集", "指定开通弹框未找到丸友集产品候选"
page.wait_for_timeout(900)
selected_version = str(page.evaluate("""({ expected }) => {
const root = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
.reverse().find(el => el.offsetParent !== null) || document.body;
const compact = value => (value || '').replace(/\\s+/g, '').trim();
const rootRect = root.getBoundingClientRect();
const candidates = Array.from(root.querySelectorAll('*'))
.filter(el => el.offsetParent !== null)
.filter(el => compact(el.innerText || el.textContent) === expected)
.filter(el => el.getBoundingClientRect().x >= rootRect.x + rootRect.width * 0.38)
.sort((a, b) => {
const ar = a.getBoundingClientRect(); const br = b.getBoundingClientRect();
return (ar.width * ar.height) - (br.width * br.height);
});
const target = candidates[0];
if (!target) return '';
const clickable = target.closest('label, .ant-radio-wrapper, .ant-checkbox-wrapper, li, .ant-list-item, .ant-card, .ant-table-row, tr, button, a, [role="button"]') || target;
clickable.scrollIntoView({ block: 'center', inline: 'center' });
clickable.click();
return expected;
}""", {"expected": "内测版"}))
return selected_product, selected_version
def assert_authorized_and_dispatch(
wanzhanggui_page: Page, wanyouji_page: Page, settings, bug_reporter, workshop: E2EAccount
) -> None:
push = WanzhangguiPushPage(wanzhanggui_page, settings.tenant_home_url, bug_reporter)
push.login_main_account(FIXED_PHONE, FIXED_PASSWORD)
authorization_frame = push.open_workshop_authorization()
assert push.workshop_authorized(authorization_frame, workshop.enterprise_name), "工作室授权未启用"
strategy_frame = push.open_dispatch_strategy()
assert workshop.enterprise_name in strategy_frame.locator("body").inner_text(timeout=5000), "未找到自动生成的工作室策略"
stamp = datetime.now().strftime("%m%d%H%M%S")
order = ManualOrderData(
source_order_no=f"AUTH{stamp}", order_title=f"授权简化{stamp}", strategy_name=workshop.enterprise_name,
strategy_type="单平台指定", receive_price=manual_order_receive_price(), dispatch_price="1", role_name=f"E2E角色{stamp[-4:]}",
)
order_page = WanzhangguiOrderPage(wanzhanggui_page, settings.tenant_home_url, bug_reporter)
order_page.login_main_account(FIXED_PHONE, FIXED_PASSWORD)
order_page.create_manual_order(order)
fulfillment = WanyoujiFulfillmentPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
fulfillment.login_main_account(workshop.phone, workshop.password)
fulfillment.verify_order_reached_hall(order.source_order_no, order.order_title, "私单")
account_store().mark_workshop_authorized(workshop, FIXED_PHONE)
account_store().mark_dispatch_strategy_created(workshop, FIXED_PHONE, workshop.enterprise_name, "单平台指定")
account_store().mark_order_dispatched(
wanzhanggui_phone=FIXED_PHONE, source_order_no=order.source_order_no, order_title=order.order_title,
strategy_name=order.strategy_name, strategy_type=order.strategy_type, wanyouji_phones=[workshop.phone],
)
def verify_recommendation_detail(app: AuthorizationSimplificationPage, name: str, label: str, profile: dict[str, str]) -> None:
last_error: AssertionError | None = None
for attempt in range(2):
frame = app.open_wanzhanggui_workshop_authorization_new() if label == "工作室名称" else app.open_wanyouji_merchant_recommendation()
app.search_by_name(frame, label, name)
try:
text = app.require_row(frame, name)
for value in profile.values():
assert value in text, f"推荐记录未展示配置值 {value};text={text[:1800]}"
return
except AssertionError as exc:
last_error = exc
if attempt == 0:
app.page.reload(wait_until="domcontentloaded")
app.page.wait_for_timeout(1200)
raise last_error or AssertionError(f"推荐页未找到工作室:{name}")
def verify_admin_recommendation(page: Page, settings, name: str, profile: dict[str, str]) -> None:
admin_search(page, settings, "推荐评分配置", name)
text = page.locator("body").inner_text(timeout=5000)
assert name in text, f"Admin推荐评分配置未找到对象 {name}"
for value in [profile["intro"], profile["contact"], profile["phone"], profile["wechat"], profile["qq"]]:
assert value in text, f"Admin推荐评分配置未展示 {value}"
def verify_admin_merchant_supply(page: Page, settings, name: str) -> None:
admin_search(page, settings, "商户供应链管理", name)
assert name in page.locator("body").inner_text(timeout=5000), f"商户供应链管理未搜索到 {name}"
def admin_search(page: Page, settings, menu: str, keyword: str) -> None:
from e2e.pages.admin_page import AdminPage
admin = AdminPage(page, settings.admin_url)
admin.login(settings.admin_username, settings.admin_password)
admin.click_exact_text("供应链管理", timeout=5000)
admin.click_exact_text(menu, timeout=5000)
page.wait_for_timeout(1200)
field = page.locator("#objectName") if menu == "推荐评分配置" else page.locator("input[placeholder*='名称'], input[placeholder*='商户']").first
if not field.is_visible(timeout=1500):
field = page.locator("input:not([type='hidden']):not([disabled]):not([readonly])").first
field.fill(keyword)
if menu == "推荐评分配置":
clicked = page.evaluate(
"""() => {
const wanted = '\\u67e5\\u8be2';
const visible = el => {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const nodes = Array.from(document.querySelectorAll('button, a, [role="button"], span, div'))
.filter(visible)
.filter(el => (el.innerText || el.textContent || '').replace(/\\s+/g, '') === wanted)
.sort((left, right) => {
const a = left.getBoundingClientRect();
const b = right.getBoundingClientRect();
return a.width * a.height - b.width * b.height;
});
const target = nodes[0];
if (!target) return false;
(target.closest('button, a, [role="button"]') || target).click();
return true;
}"""
)
assert clicked, "推荐评分配置未找到查询按钮"
else:
try:
admin.click_exact_text("搜索", timeout=1500)
except AssertionError:
field.press("Enter")
page.wait_for_timeout(1200)
def display_profile(account: E2EAccount) -> dict[str, str]:
suffix = account.phone[-4:]
return {
"intro": f"E2E授权展示{suffix}", "game": "三角洲行动", "settlement": "周结",
"contact": f"联系人{suffix}", "phone": f"1390000{suffix}", "wechat": f"e2e-wx-{suffix}", "qq": f"9{suffix}",
}
def ensure_qr_placeholder() -> Path:
path = Path("runtime/auth_simplification_qr.png")
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_bytes(bytes.fromhex("89504e470d0a1a0a0000000d4948445200000001000000010802000000907753de0000000c4944415408d763f8ffff3f0005fe02fea73581e80000000049454e44ae426082"))
return path.resolve()
def save_flow_artifact(account: E2EAccount, profile: dict[str, str], stage: str) -> None:
path = Path("runtime/auth_simplification_flow.json")
path.write_text(
__import__("json").dumps({"phone": account.phone, "enterprise_name": account.enterprise_name, "profile": profile, "stage": stage}, ensure_ascii=False),
encoding="utf-8",
)
def latest_flow_account(required_stage: str) -> tuple[E2EAccount, dict[str, str]]:
import json
path = Path("runtime/auth_simplification_flow.json")
if not path.exists():
pytest.skip("未找到授权简化前序流程产物,请先执行AUTO-FLOW-AUTH-001/002/003。")
payload = json.loads(path.read_text(encoding="utf-8"))
if payload.get("stage") != required_stage:
pytest.skip(f"前序流程未达到 {required_stage},当前={payload.get('stage')}")
account = account_store().find_main(str(payload["phone"]))
assert account, f"台账未找到前序企业账号 {payload['phone']}"
return account, dict(payload.get("profile") or {})
from __future__ import annotations
import os
import pytest
from playwright.sync_api import Page
from e2e.tests.履约验收.test_e2e_delivery_001_player_submit_acceptance import (
test_打手确认上号上传首图并提交验收后主账号查看 as run_delivery_001,
)
from e2e.tests.履约验收.test_e2e_delivery_002_upstream_acceptance_settlement import (
test_丸友集验收打手子单并校验打手收入 as run_delivery_002,
)
from e2e.tests.订单状态流转.order_flow_member_helpers import ensure_wanyouji_player_member
from e2e.tests.订单流转.test_e2e_task_001_external_order_dispatch_to_player import (
test_丸掌柜外部派单由丸友集接单并发到打手抢单大厅 as run_task_001,
)
from e2e.tests.结算.test_e2e_settle_player_account_001_payment_account import (
test_打手完善打款账户信息 as run_settle_player_account_001,
)
from e2e.tests.结算.test_e2e_settle_player_apply_001_create_bill import (
test_打手在我的收入中点击一键结算并提交账单 as run_settle_player_apply_001,
)
from e2e.utils.account_store import account_store
WORKSHOP_PHONE = "19041265882"
WANZHANGGUI_PHONE = "19159770995"
PLAYER_PHONE = "19012864298"
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_auth_simplification_player_to_settlement(page: Page, settings, bug_reporter, monkeypatch) -> None:
store = account_store()
workshop = store.find_main(WORKSHOP_PHONE)
assert workshop and workshop.wanyouji_status == "opened", "授权简化工作室未处于丸友集已开通状态"
monkeypatch.setenv("E2E_PLAYER_PHONE", PLAYER_PHONE)
player = ensure_wanyouji_player_member(page=page, settings=settings, bug_reporter=bug_reporter, parent=workshop)
latest_order = next(
(
order
for order in reversed(store.load_payload().get("dispatched_orders", []))
if order.get("wanzhanggui_phone") == WANZHANGGUI_PHONE
and WORKSHOP_PHONE in order.get("wanyouji_phones", [])
and order.get("status") == "dispatched"
),
None,
)
assert latest_order, "未找到授权简化策略最近分发且尚未被打手处理的订单"
monkeypatch.setenv("E2E_TASK_WANZHANGGUI_PHONE", WANZHANGGUI_PHONE)
monkeypatch.setenv("E2E_TASK_WANYOUJI_PHONE", WORKSHOP_PHONE)
monkeypatch.setenv("E2E_TASK_PLAYER_PHONE", player.phone)
monkeypatch.setenv("E2E_TASK_SOURCE_ORDER_NO", latest_order["source_order_no"])
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "抢单大厅")
monkeypatch.setenv("E2E_DELIVERY_SOURCE_ORDER_NO", latest_order["source_order_no"])
monkeypatch.setenv("E2E_REUSE_TENANT_LOGIN", "1")
monkeypatch.setenv("E2E_REUSE_PLAYER_LOGIN", "1")
monkeypatch.setenv("E2E_SETTLE_PLAYER_APPLY_SKIP_OWNER_SUMMARY", "1")
monkeypatch.setenv("E2E_PLAYER_SETTLEMENT_FAST_SEARCH", "1")
run_task_001(page, settings, bug_reporter)
run_delivery_001(page, settings, bug_reporter)
run_delivery_002(page, settings, bug_reporter)
run_settle_player_account_001(page, settings, bug_reporter)
run_settle_player_apply_001(page, settings, bug_reporter)
from pathlib import Path
from openpyxl import Workbook
from openpyxl.formatting.rule import FormulaRule
from openpyxl.styles import Alignment, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
OUTPUT = Path("e2e/tests/丸掌柜丸友集授权简化/丸掌柜丸友集授权简化_测试点及自动化用例.xlsx")
FEATURE_POINTS = [
("FP-001", "菜单与迁移", "旧授权页下线与数据迁移", "旧页面入口不可访问;原生效中授权同步到新授权列表且状态为已授权", "P0", "自动化"),
("FP-002", "丸掌柜授权页", "菜单与Tab", "工作室授权菜单可访问,包含工作室推荐、工作室授权、待我审核三个Tab", "P0", "自动化"),
("FP-003", "丸友集授权页", "菜单与Tab", "发单商户授权菜单可访问,包含发单商户推荐、发单商户授权、待我审核三个Tab", "P0", "自动化"),
("FP-004", "推荐列表", "展示范围", "仅展示开启对外展示的合作方;推荐Tab中展示可申请记录", "P0", "自动化"),
("FP-005", "授权列表", "展示范围", "授权Tab仅展示发生过授权关系的审核中、已授权、已拒绝、已关闭记录,不展示可申请", "P0", "自动化"),
("FP-006", "待我审核", "展示范围", "仅展示需要当前登录方审核的对向申请;无关申请不可见", "P0", "自动化"),
("FP-007", "推荐列表", "排序", "Admin 配置置顶、推荐、评分后,前台按置顶优先、推荐优先、评分倒序排序", "P1", "自动化"),
("FP-008", "推荐列表", "评分展示", "用户侧仅展示评分,不展示置顶/推荐后台标识", "P1", "自动化"),
("FP-009", "丸掌柜申请", "申请推单", "丸掌柜可对可申请工作室发起申请推单,目标工作室进入待我审核,申请方状态为审核中", "P0", "自动化"),
("FP-010", "丸友集申请", "申请接单", "丸友集可对可申请发单商户发起申请接单,目标商户进入待我审核,申请方状态为审核中", "P0", "自动化"),
("FP-011", "审核", "同意申请", "待我审核中同意后,双方授权列表均更新为已授权", "P0", "自动化"),
("FP-012", "审核", "拒绝申请", "待我审核中拒绝后,双方授权列表均更新为已拒绝,对方可再次申请", "P0", "自动化"),
("FP-013", "授权状态", "关闭授权", "已授权关系可关闭;关闭后双方状态为已关闭,且不可作为可发单协作关系", "P0", "自动化"),
("FP-014", "授权状态", "状态隔离", "审核中/已拒绝/已关闭关系均不可用于供应链默认发单和自动推单策略", "P0", "自动化"),
("FP-015", "联系配置", "入口与弹窗", "授权列表右上角存在联系方法配置入口;点击打开配置弹窗", "P1", "自动化"),
("FP-016", "联系配置", "保存与展示", "双方保存联系方法后,对方在申请、审核及协作场景可查看最新配置", "P1", "自动化"),
("FP-017", "联系配置", "保存交互", "联系人、联系手机、微信号、服务时段编辑后可保存、取消不保存且再次打开正确回显", "P1", "自动化"),
("FP-018", "列表检索", "空态与加载", "筛选无结果时正确展示空态;多条结果的分页/加载不丢失筛选条件和记录", "P1", "自动化"),
("FP-019", "供应链", "授权同步", "授权成功的全部丸友集自动出现在供应链授权列表,默认可发单,无需二次授权", "P0", "自动化"),
("FP-020", "供应链", "名称同步", "供应链列表展示工作室名称和主体;工作室改名后列表名称同步更新", "P1", "自动化"),
("FP-021", "供应链筛选", "默认值", "供应链筛选默认选中丸友集", "P1", "自动化"),
("FP-022", "供应链筛选", "前置校验", "未选择供应链即按工作室名称筛选时提示请先选择供应链,且供应链筛选框标红", "P1", "自动化"),
("FP-023", "推单策略", "自动生成", "每个已授权工作室自动生成一条指定工作室推单策略,策略名称为工作室名称", "P0", "自动化"),
("FP-024", "推单策略", "授权变更联动", "新增授权后创建对应策略;关闭授权后策略可用性按产品规则校验", "P1", "需产品确认关闭后策略规则"),
("FP-025", "推单策略", "工作室选择", "支持一键全选;列表最多显示5行,悬停浮层可查看更多", "P1", "自动化"),
("FP-026", "Admin", "默认供应链", "开通丸掌柜时默认开通所有供应链", "P0", "自动化"),
("FP-027", "Admin", "关闭供应链", "运营可关闭某个供应链;前台可用范围与后台关闭结果一致", "P0", "自动化"),
("FP-028", "权限与异常", "重复/并发申请", "同一合作关系重复提交、双方同时提交时无重复授权记录,状态一致", "P1", "自动化"),
("FP-029", "权限与异常", "访问控制", "未开通对应产品、无菜单权限或非主体账号不可越权查看/审核/关闭授权", "P1", "自动化"),
("FP-030", "对外展示配置", "展示名称规则", "提测文档要求可保存展示名称;旧PRD要求移除展示名称/标题并直接取商户名称,二者冲突,测试前必须确认最终规则", "P0", "待需求确认"),
("FP-031", "丸掌柜授权列表", "列表字段", "工作室名称、工作室主体、联系方式、授权状态、最近申请、操作字段均正确展示", "P0", "自动化"),
("FP-032", "丸友集授权列表", "列表字段", "商户名称、商户主体、联系方式、授权状态、最近申请、操作字段均正确展示", "P0", "自动化"),
("FP-033", "授权列表", "最近申请", "最近申请展示最近一次申请方向及申请时间;新一次申请后按最新记录更新", "P1", "自动化"),
("FP-034", "授权列表筛选", "丸掌柜多条件检索", "支持按工作室名称、工作室主体、手机号、联系人搜索,并支持按授权状态筛选", "P1", "自动化"),
("FP-035", "授权列表筛选", "丸友集多条件检索", "支持按商户名称、商户主体、手机号、联系人搜索,并支持按授权状态筛选", "P1", "自动化"),
("FP-036", "联系配置", "配置字段", "联系方法配置弹窗包含联系人、联系手机、微信号、服务时段四个字段,并按保存结果回显", "P0", "自动化"),
("FP-037", "联系配置", "手机格式", "联系手机为空、合法手机号、非法长度/字符等输入按产品校验,保存失败不污染已保存数据", "P1", "自动化"),
("FP-038", "授权状态", "审核中防重", "当前账号发起的审核中申请仅显示等待审核,不可重复申请", "P0", "自动化"),
("FP-039", "授权状态", "待我审核隔离", "对方发起的待我审核记录仅出现在待我审核Tab,不在授权Tab展示", "P0", "自动化"),
("FP-040", "授权状态", "重新开启权限", "已关闭关系可重新开启授权,且仅最初发起申请的一方有此操作权限", "P0", "自动化"),
("FP-041", "授权状态", "重新开启流转", "发起方重新开启已关闭授权后,双方关系恢复已授权并恢复协作资格", "P0", "自动化"),
("FP-042", "供应链", "二次授权豁免范围", "仅丸友集的已授权合作关系免填写信息二次授权;代练丸子及其他外部代练平台仍按原规则填写信息授权", "P0", "自动化"),
("FP-043", "供应链", "授权范围一致性", "供应链列表包含所有且仅包含授权成功的丸友集;审核中、拒绝、关闭和未授权工作室不出现/不可发单", "P0", "自动化"),
("FP-044", "推单策略", "策略类型与对象", "自动生成策略为指定工作室策略,供应链为丸友集,授权商户号为对应单个工作室", "P0", "自动化"),
("FP-045", "推单策略", "创建幂等", "对同一工作室的授权同步/重复事件不产生重复同名自动策略", "P1", "自动化"),
("FP-046", "推单策略", "选项范围", "授权商户号下拉仅可选已授权工作室;选择全部后与全部可用工作室一致", "P1", "自动化"),
("FP-047", "Admin推荐配置", "配置项生效", "Admin 的评分编辑、推荐状态、置顶状态保存后立即影响前台推荐排序", "P1", "自动化"),
("FP-048", "Admin供应链", "后台入口变更", "商户供应链管理保留,供应链授权入口下线;默认开通和单项关闭在保留入口中生效", "P1", "自动化"),
("FP-049", "展示边界", "对外展示开关", "关闭对外展示的商户/工作室不进入对方推荐列表;重新开启后可按排序规则出现", "P0", "自动化"),
("FP-050", "状态与数据一致性", "双端一致性", "每次申请、审核、关闭、重开后,两端记录的关系状态、联系人信息、最近申请方向与时间一致", "P0", "自动化"),
("FP-051", "对外展示配置", "丸掌柜资料维护", "首次进入可查看当前配置;未配置为空态;保存、修改、重新进入均正确回显;不能修改其他商户数据", "P0", "自动化"),
("FP-052", "对外展示配置", "展示资料字段", "按最终确认规则校验展示名称/商户名称、简介、擅长游戏、结算方式、联系人和联系方式的保存、修改、回显及对方可见", "P0", "自动化"),
("FP-053", "对外展示配置", "丸友集候选影响", "工作室关闭对外展示后不再进入丸掌柜候选列表;已有已授权关系保留,仅影响新候选与新申请", "P0", "自动化"),
("FP-054", "关闭授权", "关联配置失效", "关闭已授权关系后,对应供应链授权和发单策略同步失效,不能再用于新发单", "P0", "自动化"),
("FP-055", "历史授权迁移", "迁移范围", "仅原授权管理中已生效授权迁入新模型,并同步工作室授权和供应链配置", "P0", "自动化"),
("FP-056", "历史授权迁移", "迁移结果对照", "迁移完成生成结果表;源数据、新授权、供应链配置逐条可对照,失败数据可定位", "P0", "自动化"),
("FP-057", "历史授权迁移", "迁移幂等", "重复调用迁移接口不产生重复授权关系或重复供应链配置", "P0", "自动化"),
("FP-058", "策略配置", "授权对象可选性", "授权通过后可在策略配置选择对应工作室、保存并回显;未授权工作室不可选", "P0", "自动化"),
("FP-059", "WYJ新发单", "授权状态拦截", "仅已授权工作室允许创建WYJ新订单;审核中、已拒绝、已关闭、未申请均拦截且提示清晰", "P0", "自动化"),
("FP-060", "WYJ新发单", "多发子单校验", "多工作室/多子单推送时,每个实际子单均依据其目标工作室的当前授权状态独立校验", "P0", "自动化"),
("FP-061", "WYJ新发单", "新旧授权切换", "WYJ新发单只依赖新双边授权模型,不依赖旧授权码、license或appSecret", "P0", "自动化"),
("FP-062", "发单回归", "非WYJ供应链不受影响", "丸子及其他非WYJ供应链仍可按原规则发单,不能被WYJ新授权校验误拦截", "P0", "自动化"),
("FP-063", "存量商户补偿", "供应链服务初始化", "存量丸掌柜调用服务补偿后,丸友集、丸掌柜、一起玩供应链/服务均开通,且生成结果表", "P0", "自动化"),
("FP-064", "存量商户补偿", "补偿可追溯与幂等", "补偿结果可逐条对照、失败可追踪;重复调用不产生重复服务或供应链配置", "P0", "自动化"),
("FP-065", "订阅自动初始化", "订阅触发", "新商户订阅开通丸掌柜成功后自动触发供应链和服务初始化,无需人工补偿", "P0", "自动化"),
("FP-066", "订阅自动初始化", "初始化可靠性", "重复订阅消息不产生重复配置;初始化失败日志可追踪并支持重试", "P0", "自动化"),
("FP-067", "影响范围", "旧授权页面下线", "原license授权信息页面/后台耦合路径下线或不再作为新发单依据;新双边授权页面承接业务", "P1", "自动化"),
("FP-068", "范围控制", "非本次范围回归", "订阅取消、订阅到期不影响现有双边授权;存量订单售后/撤销/验收/申诉/结算与大厅多工作室模式不纳入本次功能变更", "P2", "手工回归"),
]
CASES = [
("AUTO-AUTH-001", "历史生效授权迁移", "丸掌柜、丸友集存在旧授权页生效中关系", "发布新版本后进入双方新授权页", "双方授权Tab均存在对应关系且状态已授权;旧入口下线", "FP-001", "P0", "双账号"),
("AUTO-AUTH-002", "丸掌柜申请推单-工作室同意", "丸掌柜与对外展示工作室无授权关系", "丸掌柜推荐Tab申请推单;工作室待我审核同意;双方回查", "双方状态由可申请/审核中变为已授权;出现供应链记录和同名指定工作室策略", "FP-004,FP-006,FP-009,FP-011,FP-019,FP-023", "P0", "双账号"),
("AUTO-AUTH-003", "丸友集申请接单-商户同意", "丸友集与对外展示商户无授权关系", "丸友集推荐Tab申请接单;商户待我审核同意;双方回查", "双方状态变已授权;供应链与策略自动同步", "FP-003,FP-004,FP-006,FP-010,FP-011,FP-019,FP-023", "P0", "双账号"),
("AUTO-AUTH-004", "申请拒绝与重新申请", "存在待审核申请", "审核方拒绝;申请方确认已拒绝;再次发起申请", "拒绝后双方状态已拒绝,可重新申请并重新进入审核中", "FP-012", "P0", "双账号"),
("AUTO-AUTH-005", "关闭授权及协作隔离", "双方已授权且供应链、策略已生成", "任一方关闭授权;回查双方授权、供应链和策略可用性", "双方已关闭;关闭关系不再用于默认可发单或自动化协作", "FP-013,FP-014,FP-024", "P0", "双账号"),
("AUTO-AUTH-006", "Tab数据隔离", "准备可申请、审核中、已授权、已拒绝、已关闭及非当前方审核数据", "分别打开推荐、授权、待我审核Tab", "两端菜单及三个Tab均可访问;各Tab仅展示规定数据集合,无关待审数据不可见", "FP-002,FP-003,FP-004,FP-005,FP-006", "P0", "双账号"),
("AUTO-AUTH-007", "推荐排序与前台隐藏配置", "Admin配置多条记录的置顶、推荐、评分", "打开两端推荐列表,记录顺序和展示字段", "顺序为置顶、推荐、评分倒序;仅评分对用户可见", "FP-007,FP-008", "P1", "Admin+双账号"),
("AUTO-AUTH-008", "联系方法配置及对方可见", "双方可进入授权列表", "丸掌柜保存联系方法;取消修改后重开确认未保存;丸友集在申请/审核详情查看;反向执行", "入口与弹窗可用,保存后对方展示最新信息;取消不改变已保存数据", "FP-015,FP-016,FP-017", "P1", "双账号"),
("AUTO-AUTH-009", "供应链名称同步与筛选", "已授权工作室存在,且可改名", "改工作室名称;进入供应链列表;验证默认筛选;不选供应链直接搜索", "名称和主体同步;默认丸友集;提示请先选择供应链且筛选框标红", "FP-020,FP-021,FP-022", "P1", "双账号"),
("AUTO-AUTH-010", "推单策略展示与全选", "存在6家以上已授权工作室", "打开策略配置,触发全选并查看列表第6项", "全选生效;列表最多5行;悬停可查看更多;策略名称与工作室名一致", "FP-023,FP-025", "P1", "单账号"),
("AUTO-AUTH-011", "丸掌柜开通默认供应链与后台关闭", "待开通丸掌柜的商户;Admin可操作供应链", "开通丸掌柜;检查供应链;Admin关闭其中一项;回查前台", "开通后默认全开;关闭项在前台不可用/不可见,其他项正常", "FP-026,FP-027", "P0", "Admin+单账号"),
("AUTO-AUTH-012", "重复与并发申请防重", "双方无授权关系,可分别发起申请", "同方重复点击申请;双方近同时申请;回查记录", "仅保留一条关系,不产生重复待审/授权记录,双方状态一致", "FP-028", "P1", "双账号"),
("AUTO-AUTH-013", "权限控制", "准备无菜单权限、未开通产品、非主体账号", "尝试访问授权页、申请、审核和关闭", "无权限用户不能执行越权操作,接口和页面均受控", "FP-029", "P1", "多账号"),
("AUTO-AUTH-014", "配置名称简化与列表字段", "准备丸掌柜及丸友集主体,并配置对外展示", "进入对外展示配置和两端授权列表;修改商户名称后回查", "无展示名称/标题字段;对外名称取商户名称;双方列表字段完整且名称同步", "FP-030,FP-031,FP-032", "P0", "双账号"),
("AUTO-AUTH-015", "授权列表五维筛选与最近申请", "存在名称、主体、联系人、手机号、状态不同的多条关系,且数量超过单页", "两端逐一按4个文本条件和授权状态筛选;执行无结果查询和翻页;产生新申请后回查最近申请", "每项筛选仅返回匹配记录;无结果正确空态;翻页后筛选条件保持;最近申请方向与时间更新且双端一致", "FP-018,FP-033,FP-034,FP-035,FP-050", "P1", "双账号"),
("AUTO-AUTH-016", "联系方法字段与校验", "双方可打开联系方法配置", "依次填联系人、手机、微信、服务时段保存;再输入非法手机保存", "四字段均回显给对方;非法输入被拦截且不覆盖旧数据", "FP-036,FP-037", "P0", "双账号"),
("AUTO-AUTH-017", "审核中与待我审核Tab边界", "A 向 B 发起申请", "A 查看授权Tab和推荐Tab;B 查看授权Tab与待我审核Tab;A 再点击申请", "A 显示审核中且不可重复申请;B 待审记录只在待我审核Tab", "FP-005,FP-006,FP-038,FP-039", "P0", "双账号"),
("AUTO-AUTH-018", "关闭后重开授权的发起方权限", "A 发起并获批的已授权关系", "A 关闭;B 尝试重开;A 重开;双方回查供应链和策略", "仅 A 可重开;重开后状态已授权并恢复协作资格", "FP-040,FP-041,FP-050", "P0", "双账号"),
("AUTO-AUTH-019", "丸友集供应链二次授权豁免边界", "准备已授权丸友集、外部代练平台及未授权丸友集", "进入供应链授权/发单配置,分别选择三类对象", "仅已授权丸友集默认可发单且无需填授权信息;外部平台仍需填写;未授权对象不可用", "FP-019,FP-042,FP-043", "P0", "单账号"),
("AUTO-AUTH-020", "自动策略属性、幂等与授权范围", "新建一个可授权工作室且无同名策略", "完成授权;读取新策略属性;重复触发同步;打开策略授权商户号下拉", "生成一条指定工作室/丸友集/对应工作室的策略;不重复生成;下拉仅有已授权工作室", "FP-023,FP-044,FP-045,FP-046", "P0", "双账号"),
("AUTO-AUTH-021", "对外展示开关与Admin推荐配置", "Admin 可配置目标的对外展示、置顶、推荐、评分", "依次关闭/开启对外展示,设置评分、推荐、置顶后查看前台", "关闭时前台不可见;开启后出现;排序按置顶、推荐、评分倒序,配置变更即时生效", "FP-004,FP-007,FP-047,FP-049", "P0", "Admin+双账号"),
("AUTO-AUTH-022", "Admin供应链入口保留与单项关闭", "丸掌柜已开通,Admin可管理供应链", "检查后台菜单;在商户供应链管理关闭一条丸友集供应链并访问旧供应链授权入口", "商户供应链管理可用;旧供应链授权入口下线;关闭项前台不可发单", "FP-026,FP-027,FP-048", "P1", "Admin+单账号"),
("AUTO-AUTH-023", "双端对外展示配置全流程", "丸掌柜和丸友集工作室均可进入展示配置", "验证空态;保存资料;刷新回显;修改;关闭/开启展示;尝试跨主体访问", "资料可保存、回显、修改;关闭后从对方候选移除但既有授权保留;不可跨主体操作", "FP-051,FP-052,FP-053", "P0", "双账号"),
("AUTO-AUTH-024", "关闭授权后配置及新单失效", "双方已授权,供应链和策略均可用", "关闭授权;检查供应链、策略;用原策略创建WYJ新单;重新申请并审核通过后重试", "关闭后关联配置失效且新单被拦截;重新授权成功后恢复可用", "FP-013,FP-054,FP-059", "P0", "双账号"),
("AUTO-AUTH-025", "历史授权迁移与幂等", "准备旧模型已生效、非生效和异常授权数据;可调用迁移接口", "调用迁移;读取新关系、供应链配置和结果表;再次调用迁移", "仅已生效数据迁入;结果可对照并可追踪失败;重复迁移不产生重复数据", "FP-001,FP-055,FP-056,FP-057", "P0", "接口+双账号"),
("AUTO-AUTH-026", "策略配置授权范围", "存在一个已授权和一个未授权丸友集工作室", "新建指定工作室策略;查看下拉;选择已授权工作室保存并重新进入;尝试选择未授权工作室", "已授权工作室可选且策略正确回显;未授权工作室不能选择;关闭后策略不可新发单", "FP-058,FP-054", "P0", "单账号"),
("AUTO-AUTH-027", "WYJ新发单授权状态矩阵", "准备已授权、审核中、已拒绝、已关闭、未申请的工作室及策略", "分别针对五种状态创建WYJ新单,记录页面与接口结果", "仅已授权成功;其余四种均失败且提示清晰", "FP-059,FP-061", "P0", "双账号"),
("AUTO-AUTH-028", "WYJ多子单与非WYJ回归", "准备多工作室发单数据,其中至少一方非已授权;同时有非WYJ供应链", "发起多子单推送;再通过非WYJ供应链发单", "每个子单独立按目标授权判断;非WYJ发单不受新校验影响", "FP-060,FP-062", "P0", "单账号"),
("AUTO-AUTH-029", "存量丸掌柜服务补偿", "准备历史已开通丸掌柜商户;可调用initMerchantSupply接口", "调用补偿;查询商户服务、供应链配置和结果表;重复调用", "丸友集、丸掌柜、一起玩服务均完整;结果可对照;重复调用无重复配置", "FP-063,FP-064", "P0", "接口+Admin"),
("AUTO-AUTH-030", "新订阅自动初始化与失败追踪", "准备未开通丸掌柜的新商户;可观察订阅消息和日志", "完成订阅;等待异步任务;检查服务/供应链;重放消息;构造失败并检查日志与重试", "订阅自动初始化;重放幂等;失败可追踪并可恢复", "FP-065,FP-066", "P0", "订阅链路+Admin"),
("AUTO-AUTH-031", "新旧页面及非改造范围回归", "存在旧授权入口、现有已授权关系和订阅状态变化条件", "访问旧页面和新页面;验证新订单不读取旧凭据;执行取消/到期后回查双边授权", "旧路径不承接新发单;新模型生效;取消/到期不影响现有双边授权", "FP-067,FP-068", "P1", "单账号+接口"),
]
DETAILED_CASES = [
("DT-001", "丸掌柜对外展示配置保存、回显与隔离", "丸掌柜商户A、商户B均可登录;A有配置入口", "展示名称规则以产品最终确认结论为准;简介、游戏、结算方式、联系人、手机、微信、服务时段", "1. 以A登录,进入工作室授权-展示配置。\n2. 首次未配置时核对空态和保存入口。\n3. 填写全部展示资料并保存。\n4. 刷新或退出后重进,逐项核对回显。\n5. 修改两项资料并保存,再次重进核对最新值。\n6. 切换为B登录,尝试通过页面/接口查看或修改A配置。", "A资料保存、修改、回显一致;B无法读取或修改A私有配置;展示名称字段是否出现按需求确认结论验收。", "保存前后截图、接口请求/响应、A/B主体标识"),
("DT-002", "展示开关对候选与既有授权的影响", "A与工作室W已授权;另有未授权且开启展示的工作室W2", "A、W、W2的展示开关和授权状态", "1. A进入工作室推荐,确认W2可见且可申请。\n2. W2关闭对外展示。\n3. A刷新推荐列表并按名称搜索W2。\n4. A回查与W的已授权记录和发单能力。\n5. W2重新开启展示,回查推荐列表。", "关闭展示后W2不在候选/不可申请;既有W授权不丢失;重新开启后W2按排序规则恢复候选。", "推荐列表截图、授权列表截图、筛选结果"),
("DT-003", "丸掌柜发起申请及审核中防重", "丸掌柜A和开启展示的丸友集工作室W无授权关系", "W名称、联系方式、主体", "1. A进入工作室推荐,按名称/主体/联系人/手机号筛选W。\n2. 打开W详情,核对展示资料和联系方式。\n3. 点击申请推单并确认。\n4. 刷新页面,核对状态和最近申请方向/时间。\n5. 再次点击申请,或并发提交同一申请。\n6. W登录,进入待我审核查看申请。", "A显示审核中/待审核且不能重复生成记录;W仅在待我审核看到该申请;双方最近申请信息一致。", "申请ID、双端列表截图、重复点击提示、接口记录数"),
("DT-004", "丸友集审核通过后的授权、供应链和策略联动", "DT-003申请处于待我审核", "申请ID、目标工作室W", "1. W进入待我审核,核对申请方展示资料。\n2. 点击同意并确认。\n3. A和W分别进入授权Tab,核对状态已授权。\n4. A进入供应链列表,核对W已同步且默认可发单。\n5. A进入策略配置,核对W可选及自动策略/指定工作室策略属性。\n6. 用该策略创建WYJ新订单。", "双端均已授权;供应链和策略可用;使用W的WYJ新单创建成功。", "双端状态、供应链记录、策略详情、订单号"),
("DT-005", "审核拒绝与重新申请", "存在A对W的待审核申请", "申请ID", "1. W在待我审核点击拒绝。\n2. A、W分别刷新授权列表。\n3. A核对状态已拒绝及可重新申请按钮。\n4. A重新发起申请。\n5. W核对新待审记录和最近申请时间。", "双方状态一致为已拒绝;拒绝状态不可发单;重新申请产生新的待审流程而不覆盖历史追溯信息。", "拒绝操作记录、重申请记录、订单创建拦截截图"),
("DT-006", "关闭授权、失效与重新授权", "A和W已授权,且有可用供应链和策略", "授权关系、策略ID", "1. 任选有权限的一方关闭授权并确认。\n2. 双端回查状态已关闭。\n3. A检查供应链和策略状态。\n4. A用原策略创建WYJ新单。\n5. 按最终规则重新申请/重新开启并由对方审核通过。\n6. 再次创建WYJ新单。", "关闭后供应链/策略失效,新单被拦截;重新审核通过后恢复可发单。重开权限以产品确认规则验收。", "状态变更、策略状态、失败提示、恢复后的订单号"),
("DT-007", "历史有效授权迁移与结果对照", "准备旧授权模型:已生效、未生效、异常数据;具备migrateLicense接口权限", "旧授权主键、商户、工作室、预期供应链配置", "1. 记录迁移前源数据基线。\n2. 调用migrateLicense迁移接口。\n3. 查询新双边授权、供应链配置和迁移结果表。\n4. 逐条对照已生效源授权与新关系。\n5. 检查未生效源数据未误迁。\n6. 再次调用接口并比较记录数。", "仅有效授权迁入并可发单;供应链同步;结果表可追踪失败;重复迁移不产生重复记录。", "接口响应、迁移结果表、源/目标对照、重复调用前后计数"),
("DT-008", "策略配置可选范围与回显", "A至少有一个已授权工作室W和一个未授权工作室U", "W、U、策略名称", "1. A进入发单策略配置,新建指定工作室策略。\n2. 选择丸友集供应链。\n3. 打开授权商户号下拉,记录选项。\n4. 选择W,填写必填项后保存。\n5. 重新打开策略核对名称、供应链、工作室和状态。\n6. 尝试通过UI和接口选择U。", "W可选且保存后完整回显;U不可选择/保存被拦截;关闭授权后该策略不可新发单。", "下拉选项、策略详情、非法提交响应"),
("DT-009", "WYJ新发单授权状态矩阵", "为五个工作室准备已授权、审核中、已拒绝、已关闭、未申请状态和相应订单数据", "五个工作室、统一订单模板", "1. 分别指定五个工作室创建WYJ新订单。\n2. 记录页面提示、请求响应和订单落库结果。\n3. 对已授权工作室继续完成推单。\n4. 对四个非已授权状态核对未产生有效订单/子单。", "仅已授权成功;审核中、拒绝、关闭、未申请全部拦截且提示明确。", "五组结果表、订单号/子单号、拦截提示"),
("DT-010", "WYJ多子单独立授权校验与非WYJ回归", "多发策略含多个目标工作室,其中至少一个非已授权;同时有丸子或其他非WYJ供应链", "多子单策略、非WYJ订单", "1. 通过多发策略发起订单。\n2. 逐个检查子单的目标工作室和创建结果。\n3. 通过非WYJ供应链创建相同类型订单。\n4. 检查WYJ新单请求未传/未校验旧授权码、license、appSecret。", "每个WYJ子单独立校验目标授权;非WYJ链路不受影响;WYJ新单不依赖旧凭据。", "子单明细、非WYJ订单号、请求日志/链路日志"),
("DT-011", "存量丸掌柜服务补偿", "存量已开通丸掌柜商户M;具备initMerchantSupply接口权限", "商户M、补偿前服务清单", "1. 记录补偿前供应链和服务。\n2. 调用initMerchantSupply。\n3. 查询M的商户服务、供应链配置及初始化结果表。\n4. 核对丸友集、丸掌柜、一起玩均已开通。\n5. 重复调用并检查数据计数。\n6. 人为准备失败样本并核对结果可追踪。", "三类服务/供应链完整;结果可对照;重复调用幂等;失败可定位。", "接口响应、服务清单、初始化结果表、前后计数"),
("DT-012", "新订阅自动初始化、幂等与失败恢复", "新商户N未开通丸掌柜;可查看订阅消息/初始化日志", "商户N、订阅单号、消息ID", "1. 完成N的丸掌柜订阅开通。\n2. 等待异步初始化完成。\n3. 登录N,核对供应链和服务。\n4. 执行一次授权申请、策略配置和发单冒烟。\n5. 重放同一订阅消息。\n6. 模拟初始化失败,检查日志并执行重试。", "无需人工补偿即可初始化;后续链路可用;消息重放无重复配置;失败日志含可检索关联信息且重试可恢复。", "订阅单、消息ID、初始化日志、服务清单、冒烟订单号"),
("DT-013", "Admin推荐排序与用户侧可见性", "至少三条展示记录,Admin可设置评分、推荐和置顶", "三个对象及评分/推荐/置顶组合", "1. 在Admin设置不同评分、推荐、置顶组合并保存。\n2. 进入丸掌柜和丸友集推荐列表。\n3. 记录排序和用户可见字段。\n4. 修改一条配置后刷新前台。", "前台按置顶、推荐、评分倒序;用户仅见允许展示的评分/资料;配置变更正确生效。", "Admin配置截图、两端列表排序截图"),
("DT-014", "新旧入口切换与订阅生命周期不影响授权", "已有双边已授权关系,且可模拟订阅取消/到期", "授权关系、订阅状态", "1. 访问旧license授权入口及新双边授权入口。\n2. 验证新WYJ发单只通过新关系判断。\n3. 对订阅执行取消或到期模拟。\n4. 回查双边授权状态、列表和发单资格。", "旧页面不再作为新发单授权依据;取消/到期不改变现有双边授权模型。", "页面访问结果、发单请求、订阅状态与授权状态对照"),
]
AUTOMATION_FLOW_CASES = [
(
"AUTO-FLOW-AUTH-001",
"新建丸友集工作室对外展示配置在丸掌柜与SaaS-admin双端可见",
"P0",
"已生成脚本,待测试环境首跑",
"测试环境;SMS/图形验证码可用;SaaS-admin账号可用;固定丸掌柜账号19159770995可登录且具备工作室授权新菜单权限。",
"企业名称:E2E授权展示_{时间戳};注册手机号:运行时新生成;简介:E2E自动化展示简介;擅长游戏:至少选择1项;结算周期:月结;联系人/手机/微信号/QQ号:运行时唯一值;二维码:使用运行时PNG测试文件。",
"1. 使用企业注册与认证既有流程创建新的主账号,并记录手机号、企业名称、商户ID。\n2. 登录SaaS-admin,进入商户管理-商户列表新,查询该企业并打开查看订阅;点击指定开通,精确选择产品丸友集和版本内测版后提交。\n3. 重新打开查看订阅,断言当前生效订阅展示丸友集-内测版;租户端登录后断言丸友集已开通。\n4. 登录新建企业,进入丸友集-工作室基础应用-发单商户推荐。\n5. 点击右上角对外展示配置,填写简介、擅长游戏、结算周期、联系人、联系手机、微信号、QQ号并上传二维码。\n6. 点击保存,断言弹窗关闭;重新打开配置,断言全部字段回显与本次输入一致。\n7. 开启对外展示开关,刷新页面并断言开关为开启状态。\n8. 通过dual_account_pages创建新企业丸友集页与19159770995丸掌柜页的隔离上下文。\n9. 在19159770995侧进入商家管理-工作室授权新-工作室推荐,按新企业名称查询。\n10. 打开查询结果的查看详情,逐项核对工作室名称/主体、简介、擅长游戏、结算周期、联系人、联系手机、微信号、QQ号和二维码。\n11. 打开独立SaaS-admin页,进入供应链管理-推荐评分配置,按对象名称查询新企业。\n12. 核对Admin展示信息和联系方式与第5步提交的数据完全一致。\n13. 保存三端截图、关键接口响应、企业标识和测试数据;将账号写入台账,供后续授权申请用例复用。",
"对外展示配置保存后弹窗关闭且可完整回显;开关开启后,新工作室出现在固定丸掌柜的工作室推荐搜索结果;详情页和SaaS-admin推荐评分配置的展示资料、联系方式、二维码均与新建企业配置一致;三个端的对象名称/主体一致。",
"注册认证:e2e/tests/test_e2e_saas_001_enterprise_certification.py;丸友集专用开通:e2e/tests/SaaS订阅邀请登录/test_prodsol_wyjtrial_001_wanyouji_free_trial.py;双账号fixture:e2e/conftest.py;账号台账:e2e/utils/account_store.py。",
"新建:WanyoujiDisplayConfigPage(配置、开关、回显);WanzhangguiWorkshopAuthorizationPage(搜索、详情断言);AdminRecommendationScorePage(对象搜索、详情断言)。测试函数使用dual_account_pages,Admin使用独立Page;标记e2e_seed、e2e_chain。",
"注册账号、开通版本/订阅记录、配置提交值、两端查询结果、Admin查询结果、截图路径、接口响应摘要。",
),
(
"AUTO-FLOW-AUTH-002",
"复用新建企业开通丸掌柜并验证商户展示、Admin推荐及供应链初始化",
"P0",
"已生成脚本,待测试环境首跑",
"AUTO-FLOW-AUTH-001已成功执行并将新企业主账号、企业名称、商户ID写入台账;该企业已开通丸友集;固定账号19159770995可登录丸友集且具备发单商户推荐菜单权限;SaaS-admin账号可用。",
"复用AUTO-FLOW-AUTH-001的企业名称、主账号、商户ID;丸掌柜展示配置使用与丸友集不同的运行时唯一简介、联系人、手机、微信号、QQ号和二维码;擅长游戏至少选择1项;结算周期:月结。",
"1. 从账号台账读取AUTO-FLOW-AUTH-001创建的企业主账号、企业名称和商户ID,先断言该账号已开通丸友集。\n2. 复用丸掌柜专用订阅/支付开通链路为同一企业开通丸掌柜,并回查企业概览或订阅记录为已开通。\n3. 以该企业主账号登录丸掌柜,进入商家管理-工作室授权新。\n4. 点击右上角对外展示配置,填写简介、擅长游戏、结算周期、联系人、联系手机、微信号、QQ号并上传二维码。\n5. 点击保存,断言配置弹框关闭;重新打开弹框,断言全部字段回显正确。\n6. 勾选对外展示开关,刷新后断言开关仍为开启。\n7. 通过dual_account_pages创建新企业丸掌柜页与19159770995丸友集页的隔离上下文。\n8. 在19159770995侧进入丸友集-工作室基础应用-发单商户推荐,按新企业商户名称查询。\n9. 打开查询结果的查看详情,逐项核对商户名称/主体、简介、擅长游戏、结算周期、联系人、联系手机、微信号、QQ号和二维码。\n10. 打开独立SaaS-admin页,进入供应链管理-推荐评分配置,按对象名称查询该商户,核对展示信息和联系方式与第4步完全一致。\n11. 在SaaS-admin进入供应链管理-商户供应链管理,按商户名称搜索。\n12. 断言至少返回一条与新企业名称匹配的商户记录;记录商户ID及其支持的供应链。\n13. 保存三端截图、关键接口响应和台账链接,供授权申请、策略和发单用例复用。",
"丸掌柜开通成功;展示配置保存后弹框关闭且可完整回显;开启对外展示后,固定丸友集账号19159770995可在发单商户推荐中搜索到该商户;详情页和推荐评分配置的展示资料、联系方式、二维码与丸掌柜配置一致;商户供应链管理按商户名至少返回一条对应记录。",
"前置账号与企业标识:AUTO-FLOW-AUTH-001台账产物;丸掌柜开通应复用其专用订阅/支付开通链路;双账号fixture:e2e/conftest.py;账号台账:e2e/utils/account_store.py。通用指定开通用例排除丸掌柜,不能直接调用。",
"新建:WanzhangguiDisplayConfigPage(配置、开关、回显);WanyoujiMerchantAuthorizationPage(商户搜索、详情断言);AdminRecommendationScorePage(对象搜索、详情断言);AdminMerchantSupplyPage(商户搜索、至少一条结果断言)。测试函数使用dual_account_pages,Admin使用独立Page;标记e2e_chain。",
"开通前后订阅状态、丸掌柜配置提交值、丸友集详情结果、推荐评分配置结果、商户供应链管理搜索结果、商户ID/供应链清单、截图路径、接口响应摘要。",
),
(
"AUTO-FLOW-AUTH-003",
"新建丸友集申请固定丸掌柜并审核后自动生成策略发单至抢单大厅",
"P0",
"已生成脚本,待测试环境首跑",
"AUTO-FLOW-AUTH-001与AUTO-FLOW-AUTH-002均成功;新企业已同时开通丸友集和丸掌柜且已开启两端对外展示;固定账号19159770995已开通丸掌柜、丸友集且具备审核、策略配置、手工录单和抢单大厅权限;测试数据满足手工录单必填条件。",
"复用前两条用例的新企业主账号、企业名称和商户ID;固定商户:19159770995(个体工商户1378);自动生成策略名称:新企业名称;订单标题、来源单号、联系电话使用运行时唯一值。",
"1. 从台账读取新企业账号和名称,使用dual_account_pages创建新企业丸友集页及19159770995丸掌柜页。\n2. 新企业登录丸友集,进入工作室基础应用-发单商户推荐。\n3. 按商户名称搜索19159770995对应的个体工商户1378,断言唯一/目标结果存在。\n4. 如需核对展示资料,先点击查看详情;随后点击申请推单并确认。\n5. 断言新企业丸友集侧申请状态变为审核中;记录申请时间和关联商户。\n6. 19159770995丸掌柜侧进入商家管理-工作室授权新,切换待我审核,按新企业名称查询。\n7. 点击审核,在审核弹窗点击同意并确认;分别刷新两端授权页,断言状态均为已授权。\n8. 19159770995进入履约工作室/平台-工作室授权,按新企业名称查询,断言工作室记录状态为启用。\n9. 进入履约工作室/平台-推单配置,按新企业名称查询,断言自动新增/可用策略名称为新企业名称、状态为启用、策略类型为单平台指定,且目标工作室为新企业。\n10. 进入商家管理-订单管理-手工录单,填写运行时唯一订单数据,选择第9步策略并提交推单。\n11. 断言丸掌柜侧订单创建/推单成功,记录主单号、子单号、来源单号和策略名称。\n12. 新企业丸友集进入工作室履约应用-抢单大厅,使用来源单号或订单标题查询。\n13. 断言大厅存在对应订单,且订单标题、来源单号、上游商户、目标工作室与丸掌柜侧记录一致。\n14. 保存申请、审核、工作室授权、策略详情、订单创建和抢单大厅的截图/接口响应;更新台账授权关系、策略和订单产物。",
"新企业可向个体工商户1378发起申请;19159770995审核同意后双方已授权;新企业名称对应的工作室授权为启用;自动生成或可用的推单策略为启用且类型为单平台指定;使用该策略手工录单推单成功;新企业丸友集抢单大厅可查询到完全匹配的订单。",
"双账号fixture:e2e/conftest.py;工作室授权/策略:e2e/pages/wanzhanggui_push_page.py、e2e/tests/推单配置/test_e2e_push_001_wanzhanggui_workshop_authorization.py、test_e2e_push_003_dispatch_strategy.py;手工录单与大厅回查:e2e/pages/wanzhanggui_order_page.py、e2e/tests/订单流转/test_e2e_order_001_manual_dispatch.py(如文件路径以当前仓库为准)。",
"新建:WanyoujiMerchantRecommendationPage(搜索、详情、申请推单);WanzhangguiWorkshopAuthorizationNewPage(待审、同意、状态);WanzhangguiSupplyAndStrategyPage(工作室启用、自动策略类型);复用WanzhangguiOrderPage创建订单与WanyoujiFulfillmentPage抢单大厅查询。测试函数标记e2e_chain,输入依赖前两条台账产物。",
"申请ID与时间、双端授权状态、工作室授权记录、策略ID/类型/状态、主单号/子单号/来源单号、抢单大厅查询结果、截图路径、接口响应摘要。",
),
(
"AUTO-FLOW-AUTH-004",
"关闭既有授权后重新申请审核并恢复单平台策略发单闭环",
"P0",
"已生成脚本,待测试环境首跑",
"AUTO-FLOW-AUTH-003已成功,且新企业与19159770995间当前关系为已授权;新企业已开通丸友集和丸掌柜;19159770995具备丸掌柜工作室授权、策略及手工录单权限;新企业丸友集具备待我审核和抢单大厅权限。",
"复用新企业账号、企业名称和商户ID;固定商户:19159770995(个体工商户1378);重新申请记录ID;恢复后的工作室名称/策略名称:新企业名称;手工录单来源单号、订单标题、联系电话均使用运行时唯一值。",
"1. 新企业登录丸友集,进入工作室基础应用-发单商户推荐,切换发单商户授权Tab。\n2. 按19159770995对应企业商户搜索,确认当前状态为已授权后点击关闭授权并确认。\n3. 刷新两端授权列表,断言该关系状态均为已关闭,并记录关闭前的授权关系ID、策略ID和状态。\n4. 新企业切换进入丸掌柜-商家管理-工作室授权新,按19159770995对应企业商户查询。\n5. 点击页面操作【申请发单】并确认,记录本次重新申请的申请ID、申请方和被申请方。\n6. 新企业切换进入丸友集-工作室基础应用-发单商户推荐,切换待我审核Tab,按关联商户/申请ID查询。\n7. 断言存在第5步产生的待审核记录,点击审核,在弹窗点击同意并确认。\n8. 刷新新企业丸掌柜与丸友集授权页,断言双方状态恢复为已授权;同时核对申请方向、最近申请时间和审核人记录与步骤5至7一致。\n9. 19159770995进入丸掌柜-履约工作室/平台-工作室授权,按新企业名称查询,断言对应工作室状态为启用。\n10. 进入履约工作室/平台-推单配置,按新企业名称查询,断言对应策略状态为启用、策略类型为单平台指定、目标工作室为新企业。\n11. 进入商家管理-订单管理-手工录单,填写唯一订单数据,选择第10步单平台策略并提交推单。\n12. 断言19159770995丸掌柜侧发单成功,记录主单号、子单号、来源单号和策略名称。\n13. 新企业丸友集进入工作室履约应用-抢单大厅,按来源单号或订单标题查询。\n14. 断言大厅存在本次订单,核对订单标题、来源单号、上游商户、目标工作室、订单状态与丸掌柜侧一致。\n15. 保存关闭授权、重新申请、审核、授权恢复、工作室授权、策略、订单和抢单大厅的截图/接口响应,并更新台账关系和订单产物。",
"关闭后关系为已关闭;重新申请产生唯一待审记录且由新企业丸友集待我审核可见;同意后双方恢复已授权;19159770995侧工作室授权和新企业名称的单平台指定策略均为启用;使用该策略再次发单成功,新企业丸友集抢单大厅可检索并核对到对应订单。",
"前置授权与策略产物:AUTO-FLOW-AUTH-003;双账号fixture:e2e/conftest.py;授权/策略复用:e2e/pages/wanzhanggui_push_page.py与e2e/pages/wanzhanggui_order_page.py;手工录单与抢单大厅复用既有订单发单脚本。",
"新建:AuthorizationStateTransitionPage(关闭、按申请ID追踪、待审/同意、双端状态断言);复用WanzhangguiSupplyAndStrategyPage、WanzhangguiOrderPage和WanyoujiFulfillmentPage。测试函数标记e2e_chain,执行前校验申请方向与待审归属。",
"关闭授权前后状态、关系ID、重新申请ID/方向/时间、审核记录、策略ID/类型/状态、主单号/子单号/来源单号、抢单大厅结果、截图路径、接口响应摘要。",
),
]
def style_sheet(ws, widths):
header_fill = PatternFill("solid", fgColor="1F4E78")
header_font = Font(color="FFFFFF", bold=True)
thin = Side(style="thin", color="D9E2F3")
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
for row in ws.iter_rows(min_row=2):
for cell in row:
cell.alignment = Alignment(vertical="top", wrap_text=True)
cell.border = __import__("openpyxl").styles.Border(bottom=thin)
for index, width in enumerate(widths, 1):
ws.column_dimensions[get_column_letter(index)].width = width
ws.freeze_panes = "A2"
ws.auto_filter.ref = ws.dimensions
ws.row_dimensions[1].height = 30
def main():
wb = Workbook()
ws = wb.active
ws.title = "测试点"
ws.append(["测试点ID", "模块", "测试主题", "测试点/预期", "优先级", "自动化建议"])
for row in FEATURE_POINTS:
ws.append(row)
style_sheet(ws, [15, 16, 20, 68, 10, 22])
ws.conditional_formatting.add("E2:E200", FormulaRule(formula=['E2="P0"'], fill=PatternFill("solid", fgColor="F4CCCC")))
ws = wb.create_sheet("自动化用例")
ws.append(["用例ID", "用例名称", "前置条件", "自动化步骤", "预期结果", "覆盖测试点", "优先级", "执行模型"])
for row in CASES:
ws.append(row)
style_sheet(ws, [18, 30, 38, 62, 54, 34, 10, 16])
ws.conditional_formatting.add("G2:G200", FormulaRule(formula=['G2="P0"'], fill=PatternFill("solid", fgColor="F4CCCC")))
ws = wb.create_sheet("自动化流程明细")
ws.append([
"流程用例ID", "用例名称", "优先级", "实现状态", "环境与前置条件", "运行时测试数据",
"自动化详细步骤", "预期结果", "可复用现有资产", "建议新增自动化资产", "执行证据",
])
for row in AUTOMATION_FLOW_CASES:
ws.append(row)
style_sheet(ws, [18, 36, 10, 12, 42, 50, 96, 58, 55, 66, 46])
ws.conditional_formatting.add("C2:C200", FormulaRule(formula=['C2="P0"'], fill=PatternFill("solid", fgColor="F4CCCC")))
ws = wb.create_sheet("详细测试步骤")
ws.append(["详细用例ID", "用例名称", "前置条件", "测试数据", "详细测试步骤", "预期结果", "建议保留证据"])
for row in DETAILED_CASES:
ws.append(row)
style_sheet(ws, [15, 30, 30, 30, 72, 52, 34])
ws = wb.create_sheet("需求追溯")
ws.append(["需求编号", "需求摘要", "来源页", "覆盖测试点", "覆盖自动化用例", "备注"])
rows = [
("REQ-01", "旧授权页下线,生效中授权同步新列表", "3", "FP-001", "AUTO-AUTH-001", ""),
("REQ-02", "展示名称规则冲突", "旧PRD第1页;新提测第4页", "FP-030", "DT-001", "旧PRD要求去掉展示名称/标题;新提测要求保存展示名称。测试前须由产品确认最终规则。"),
("REQ-03", "两端推荐/授权/待审核Tab范围、字段、筛选与推荐排序", "1-5", "FP-002~FP-008,FP-031~FP-035,FP-047,FP-049", "AUTO-AUTH-002,003,006,007,014,015,021", "页面结构按7月10日变更日志采用三Tab口径"),
("REQ-04", "双向申请、审核、拒绝、关闭、重开的状态流转与权限", "3-6", "FP-009~FP-014,FP-038~FP-041,FP-050", "AUTO-AUTH-002~006,012,017,018", "已关闭仅发起方可重新开启;待我审核从授权Tab移除"),
("REQ-05", "联系方法配置及字段", "5-6", "FP-015~FP-018,FP-036~FP-037", "AUTO-AUTH-008,016", "联系人、联系手机、微信号、服务时段"),
("REQ-06", "授权成功同步供应链及二次授权豁免边界", "6-7", "FP-019~FP-022,FP-042~FP-043", "AUTO-AUTH-002,003,009,019", "外部代练平台仍沿用填写信息授权"),
("REQ-07", "授权工作室自动生成指定工作室推单策略", "7-8", "FP-023~FP-025,FP-044~FP-046", "AUTO-AUTH-002,003,005,010,020", "关闭授权后的策略处置待产品确认"),
("REQ-08", "Admin默认开通全部供应链、单项关闭及入口简化", "9", "FP-026~FP-027,FP-048", "AUTO-AUTH-011,022", "商户供应链管理保留,供应链授权入口去掉"),
("REQ-09", "权限、重复与并发保护", "补充质量保障", "FP-028~FP-029", "AUTO-AUTH-012,013", "非PDF显式条目,作为授权关系核心风险补充"),
("REQ-10", "双端展示配置保存、开关及数据隔离", "新提测4-5", "FP-051~FP-053", "AUTO-AUTH-023;DT-001~002", ""),
("REQ-11", "关闭后供应链/策略失效与重授权", "新提测6-7", "FP-054", "AUTO-AUTH-024;DT-006", "新提测明确要求关闭后失效,替代旧表中的待确认表述。"),
("REQ-12", "历史授权迁移及迁移结果表", "新提测7-10", "FP-055~FP-057", "AUTO-AUTH-025;DT-007", "接口migrateLicense"),
("REQ-13", "策略配置与WYJ新发单授权校验", "新提测11-13", "FP-058~FP-062", "AUTO-AUTH-026~028;DT-008~010", "WYJ新单不依赖旧授权码/license/appSecret;非WYJ需回归。"),
("REQ-14", "存量商户补偿和订阅自动初始化", "新提测13-15", "FP-063~FP-066", "AUTO-AUTH-029~030;DT-011~012", "接口initMerchantSupply和订阅异步链路。"),
("REQ-15", "旧入口切换、范围边界", "新提测15-16", "FP-067~FP-068", "AUTO-AUTH-031;DT-014", "存量订单售后等不属于本次改造范围。"),
]
for row in rows:
ws.append(row)
style_sheet(ws, [14, 42, 12, 28, 38, 42])
ws = wb.create_sheet("说明")
ws.append(["项目", "内容"])
ws.append(["需求来源", "当前主依据:丸掌柜 × 丸友集授权简化提测文档.pdf(16页);历史参照:丸掌柜丸友集授权简化.pdf。"])
ws.append(["自动化实现建议", "双账号场景使用仓库 dual_account_pages fixture,分别创建丸掌柜和丸友集 Page Object;不在同一页面切换登录账号。"])
ws.append(["范围说明", "已逐页核对当前提测PDF。关闭授权后策略/供应链失效已由新提测明确。仅展示名称是否保留与历史PRD冲突,已在需求追溯和FP-030标记为P0待确认。"])
ws.append(["建议执行顺序", "AUTO-AUTH-001 -> 002/003 -> 004/005/006 -> 009/010 -> 007/008/011 -> 012/013"])
style_sheet(ws, [24, 110])
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
wb.save(OUTPUT)
print(OUTPUT.resolve())
if __name__ == "__main__":
main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment