feat(onboarding): add first-run membership activation

This commit is contained in:
chengma
2026-07-23 17:29:16 +08:00
parent a0862dacbd
commit 7dabeb9971
15 changed files with 855 additions and 36 deletions
+229 -12
View File
@@ -7,6 +7,7 @@ from PySide6.QtGui import QDesktopServices
from .. import subscription
from ..version import display_name
from .activation_dialog import MembershipActivationDialog
from .tabs.accounts import AccountsTab
from .tabs.apply import ApplyTab
from .tabs.collect import CollectTab
@@ -122,6 +123,8 @@ class MainWindow(QMainWindow):
self._subscription_request_token = 0
self._subscription_closed = False
self._expired_subscription_notice_shown = False
self._activation_dialog = None
self._first_use_guide_shown = False
self.tabs = QTabWidget()
self.tabs.setObjectName("mainTabs")
self.tabs.setStyleSheet(TAB_STYLE)
@@ -296,7 +299,13 @@ class MainWindow(QMainWindow):
self._subscription_worker = None
self._set_subscription_check_running(False)
def _apply_subscription_status(self, status):
def _apply_subscription_status(
self,
status,
*,
show_notice=True,
show_first_use_guide=True,
):
self._subscription_status = status
if status.allows_product_workflows:
self._expired_subscription_notice_shown = False
@@ -334,8 +343,10 @@ class MainWindow(QMainWindow):
else "会员状态已验证,当前处于观察模式"
)
self.show_status(message, level="success")
if SUBSCRIPTION_ENFORCEMENT_ENABLED:
if SUBSCRIPTION_ENFORCEMENT_ENABLED and show_notice:
self._show_subscription_notice_once(status)
if show_first_use_guide:
QTimer.singleShot(0, self._show_first_use_guide_if_pending)
return
if not SUBSCRIPTION_ENFORCEMENT_ENABLED:
self.show_status(
@@ -343,11 +354,225 @@ class MainWindow(QMainWindow):
level=self._subscription_level(status),
)
return
self.open_settings_tab()
self.show_status(status.user_message, level=self._subscription_level(status))
if status.state == subscription.STATUS_NOT_CONFIGURED:
self.show_status("请先绑定会员账号", level="warning")
self._show_membership_activation()
return
self.open_settings_tab()
if status.state == subscription.STATUS_EXPIRED:
self._show_expired_subscription_notice_once(status)
def _show_membership_activation(self):
if self._activation_dialog is not None:
self._activation_dialog.raise_()
self._activation_dialog.activateWindow()
return
dialog = MembershipActivationDialog(self)
self._activation_dialog = dialog
dialog.validationRequested.connect(self._begin_activation_validation)
dialog.memberCenterRequested.connect(self._open_activation_member_center)
dialog.finished.connect(
lambda result, current=dialog: self._on_activation_dialog_finished(
current,
result,
)
)
dialog.open()
def _begin_activation_validation(self, api_key):
dialog = self._activation_dialog
key = str(api_key or "").strip()
if dialog is None or not key:
return
self._subscription_request_token += 1
token = self._subscription_request_token
if self._subscription_worker is not None:
self._subscription_worker.cancel()
self._set_product_access(False)
self._set_subscription_check_running(True)
dialog.set_validating(True)
self.show_status("正在验证会员账号", level="info")
worker = SubscriptionCheckWorker(
config=self.config,
cmhub_config_path=self.config.get("cmhub_config_path"),
base_url=appconfig.cmhub_config(self.config).get("base_url"),
api_key=key,
)
worker.finished.connect(
lambda payload, token=token, key=key: self._on_activation_checked(
token,
key,
payload,
)
)
worker.cancelled.connect(
lambda payload, token=token, key=key: self._on_activation_checked(
token,
key,
payload,
)
)
thread = run_worker(worker, thread_name="MembershipActivationWorker", start=False)
thread.finished.connect(lambda: self._forget_subscription_thread(thread))
self._subscription_worker = worker
self._subscription_thread = thread
thread.start()
def _on_activation_checked(self, token, api_key, payload):
if self._subscription_closed or token != self._subscription_request_token:
return
dialog = self._activation_dialog
if dialog is None:
return
dialog.set_validating(False)
status = payload.get("subscription") if isinstance(payload, dict) else None
if not isinstance(status, subscription.SubscriptionStatus):
status = subscription.SubscriptionStatus(subscription.STATUS_UNAVAILABLE)
self._subscription_status = status
if not status.credentials_accepted:
self._set_product_access(False)
self._set_membership_window_title()
if status.state == subscription.STATUS_KEY_INVALID:
message = "会员 API Key 无效,请检查后重新输入。"
dialog.focus_api_key()
elif status.state == subscription.STATUS_NOT_CONFIGURED:
message = "请输入有效的会员 API Key。"
dialog.focus_api_key()
else:
message = "暂时无法验证会员账号,请检查网络后重试。"
dialog.set_status(message, level="danger")
self.show_status(message, level="warning")
return
try:
appconfig.save_cmhub_config(
{"api_key": str(api_key or "").strip()},
path=self.config.get("cmhub_config_path")
or appconfig.cmhub_config_file_path(self.config),
)
except (OSError, appconfig.ConfigError):
message = "会员账号已验证,但 API Key 保存失败,请检查数据目录后重试。"
dialog.set_status(message, level="danger")
self.show_status(message, level="danger")
return
settings_tab = self._settings_tab()
if hasattr(settings_tab, "refresh_default_gateway_credentials"):
settings_tab.refresh_default_gateway_credentials()
if not status.allows_product_workflows:
self._set_product_access(False)
self._set_membership_window_title()
message = "%s。请前往会员中心处理后重新验证。" % status.user_message
dialog.set_status(message, level="danger")
self.show_status(status.user_message, level=self._subscription_level(status))
return
self._save_first_use_state("pending")
if status.notice_id:
self._save_subscription_notice_id(status.notice_id)
self._apply_subscription_status(
status,
show_notice=False,
show_first_use_guide=False,
)
dialog.set_status("会员账号验证成功,正在进入工作区。", level="success")
dialog.accept_activation()
QTimer.singleShot(0, self._show_first_use_guide_if_pending)
def _on_activation_dialog_finished(self, dialog, result):
if self._activation_dialog is dialog:
self._activation_dialog = None
dialog.deleteLater()
if (
result != QDialog.Accepted
and not self._subscription_closed
and not self._subscription_status.allows_product_workflows
):
self.close()
def _open_activation_member_center(self):
manage_url = str(self._subscription_status.manage_url or "").strip()
if not manage_url:
manage_url = subscription.safe_manage_url(
appconfig.DEFAULT_MEMBER_CENTER_URL,
appconfig.DEFAULT_CMHUB_BASE_URL,
)
if not manage_url:
message = "会员中心地址当前不可用,请稍后重试。"
if self._activation_dialog is not None:
self._activation_dialog.set_status(message, level="danger")
self.show_status(message, level="warning")
return
opened = QDesktopServices.openUrl(QUrl(manage_url))
if opened is False:
message = "无法打开会员中心,请检查系统默认浏览器后重试。"
if self._activation_dialog is not None:
self._activation_dialog.set_status(message, level="danger")
self.show_status(message, level="warning")
def _show_first_use_guide_if_pending(self):
if self._first_use_guide_shown:
return
if appconfig.first_use_guide_state(self.config) != "pending":
return
if not self._subscription_status.allows_product_workflows:
return
self._first_use_guide_shown = True
box = QMessageBox(self)
box.setIcon(QMessageBox.Information)
box.setWindowTitle("首次使用指引")
box.setText(
"接下来按以下顺序开始使用:\n\n"
"1. 添加蝦皮店铺账号\n"
"2. 启动 Chrome 并人工完成登录\n"
"3. 导入商品 Excel\n"
"4. 开始采集、AI 生成和更新蝦皮"
)
start_button = box.addButton("开始配置店铺", QMessageBox.AcceptRole)
later_button = box.addButton("稍后提醒", QMessageBox.RejectRole)
dismiss_button = box.addButton("不再提示", QMessageBox.ActionRole)
box.setDefaultButton(start_button)
box.exec()
clicked = box.clickedButton()
if clicked is start_button:
self._save_first_use_state("completed")
self.open_accounts_tab()
elif clicked is dismiss_button:
self._save_first_use_state("dismissed")
elif clicked is later_button:
self.show_status("可稍后从账号管理开始配置店铺", level="muted")
def _save_first_use_state(self, state):
try:
saved = appconfig.save_first_use_guide_state(
state,
path=self.config_path,
)
except (OSError, appconfig.ConfigError):
self.show_status("首次使用引导状态保存失败", level="warning")
return False
self._replace_runtime_config(saved)
return True
def _save_subscription_notice_id(self, notice_id):
try:
saved = appconfig.save_subscription_notice_id(
notice_id,
path=self.config_path,
)
except (OSError, appconfig.ConfigError):
return False
self._replace_runtime_config(saved)
return True
def _replace_runtime_config(self, saved):
self.config.clear()
self.config.update(saved)
@staticmethod
def _subscription_level(status):
if status.state in {subscription.STATUS_UNAVAILABLE, subscription.STATUS_KEY_INVALID}:
@@ -441,15 +666,7 @@ class MainWindow(QMainWindow):
box.exec()
if manage_button is not None and box.clickedButton() is manage_button:
QDesktopServices.openUrl(QUrl(status.manage_url))
try:
saved = appconfig.save_subscription_notice_id(
notice_id,
path=self.config_path,
)
except Exception:
return
self.config.clear()
self.config.update(saved)
self._save_subscription_notice_id(notice_id)
def _on_tab_changed(self, index):
if self._reverting_tab_change: