跳至内容
Micro Frontend

原生嵌入票务商店

微前端将 ADITUS 票务商店直接渲染到您的页面中,无需 iframe。没有后端的网站加载脚本,并同时设置 public-key 和 cart-scope="tab"。有后端的网站在自己的服务器上创建会话并传入会话令牌,由您的服务器决定购买者是谁。没有会话时,微前端保持未激活状态。

初次了解?概览页面不涉及技术细节,介绍 Micro Frontend 能做什么。

快速开始

有两种方式可以让商店运行起来。两者使用相同的脚本和相同的元素,区别在于由谁创建会话。

无后端

元素会为您的 publicKey 自行创建浏览器会话,仅限于您允许的域名。请设置 cart-scope="tab":没有服务器身份时,默认值 "shared" 会以明确的错误结束。此时每个浏览器标签页都有自己的购物车。该会话是匿名的:访客可以浏览并填充购物车。注册前,element.config.onUserRequired 必须返回由服务器签发的绑定用户的会话令牌;否则商店会在购物车处显示提示并停止。

index.html
<!-- Site without a backend: load the script once, place the element. -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>

<aditus-shop
  public-key="pk_your_site"
  cart-scope="tab"
></aditus-shop>

有后端

您的服务器使用 mint secret 创建会话,并决定购买者是谁。将返回的令牌传给元素。提供购买者邮箱时,默认的 cart-scope "shared" 会在所有标签页中保持同一个购物车。

server.js
// 1. On your server: create a session. The mint secret never leaves the server.
const res = await fetch("https://developers.aditus.com/api/shop/session", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
    "Content-Type": "application/json",
  },
  // email comes from your own login session, never from the browser request.
  body: JSON.stringify({ publicKey: "pk_your_site", email: user.email }),
});
const { sessionToken } = await res.json();
index.html
<!-- 2. In your page: pass the token from your server to the element. -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>

<aditus-shop
  public-key="pk_your_site"
  session-token="SESSION_TOKEN_FROM_YOUR_SERVER"
></aditus-shop>

将 pk_your_site 替换为您在 ADITUS 管理后台中的可发布 key。developers.aditus.com 是脚本和 API 的生产地址。两种方式在下文中都有详细说明。

微前端背后的技术

微前端将完整的购买流程(活动、商品、购物车、注册、支付、完成)渲染到您页面的 DOM 中。它使用您页面的布局,您的 CSS 可以为其每个部分设置样式。

React 19,兼容 React 18

完整流程作为一个类型安全的 React 流程运行。它使用 React 19 构建和测试,只使用 React 18 的功能集(useState、useEffect、useMemo 等),因此源码包可在 React 18 及更高版本上运行(peerDependency react >=18)。除 React 外,它有一个运行时依赖:用于校验电话号码的 libphonenumber-js。没有 UI 库、数据获取框架或状态工具包。ADITUS 托管的脚本已包含 React,因此您的页面不需要自己的 React。

轻量 DOM 而不是 iframe

没有 iframe,没有 Shadow DOM。商店直接渲染到您页面的 DOM 中,融入您的布局,具备响应式和无障碍特性。字体和颜色来自 --aditus-* 令牌;默认字体为 system-ui。设置 --aditus-font 即可使用您页面的字体。

与框架无关:一个 mount(),无处不在

一次调用(mount(container, config))即可将商店挂载到任意 DOM 元素。无论 React、Vue、Angular 还是纯 HTML:适配器与框架无关,在任何地方的行为都相同。

造型是你的

每个元素都带有稳定的 CSS 钩子(BEM 类 aditus-shop__* 加 data-state)。您可以用自己的 CSS 设置样式,或通过少量 --aditus-* 设计令牌设置品牌颜色、圆角和间距。可选的基础样式位于 CSS @layer aditus-shop 中,因此您未分层的规则始终优先。这同样适用于您网站的通用规则,例如来自 WordPress 或 Bootstrap 主题的 button {}、input {} 或 a {}:它们也会改变商店的外观。如果不希望商店继承这些规则,请将其限定在您自己的页面区域内。

多语言,可实时切换

DE/EN 是内置的。语言切换通过 handle.update() 直接推送到正在运行的实例中,无需重置用户的旅程。购物车和进度被保留。

设计安全

没有有效会话时,商店保持未激活状态,不发出任何商店请求。会话要么来自您的服务器(使用 mint secret 创建,该密钥从不进入浏览器),要么在没有后端的网站上来自限定于您 publicKey 域名的浏览器会话。所有商店调用都经过 ADITUS API,由其在服务器端添加商店凭据。

分三层设置样式

使用最通用的合适层级:设计令牌建立品牌,通用语义 BEM 钩子让重复角色保持一致,现有组件专用钩子则留给专家级覆盖。

通用语义钩子

.aditus-shop__section, .aditus-shop__panel, .aditus-shop__list, .aditus-shop__list-item, .aditus-shop__actions 和 .aditus-shop__action. 这些附加钩子描述角色,同时每个元素仍保留其专用钩子。 Styling Studio 的 URL 与截图分析正是在这一层级提出规则(边框还是阴影、按钮字重、区块间距),绝不涉及组件专用钩子。

操作与例外

在已实现的位置中,data-variant="primary" 或 data-variant="secondary" 用于记录操作的角色。不要从 DOM 嵌套关系推断语义角色:reg-form 并不是已文档化的 panel。仅当该表单确实需要例外时,才定位其专用钩子。

shop-micro-frontend.css
/* 1. Tokens: the broad brand foundation. */
.onsite-shop .aditus-shop {
  --aditus-accent: #c2410c;
  --aditus-surface: #ffffff;
  --aditus-radius: 12px;
}

/* 2. General hooks: consistent roles throughout the journey. */
.onsite-shop .aditus-shop__section { margin-block: 2rem; }
.onsite-shop .aditus-shop__panel {
  background: var(--aditus-surface);
  border: 1px solid var(--aditus-border);
  border-radius: var(--aditus-radius);
  box-shadow: var(--aditus-shadow);
}
.onsite-shop .aditus-shop__list { gap: 0.75rem; }
.onsite-shop .aditus-shop__list-item { padding-block: 0.75rem; }
.onsite-shop .aditus-shop__actions { gap: 0.5rem; }
.onsite-shop .aditus-shop__action[data-variant="primary"] { font-weight: 700; }
.onsite-shop .aditus-shop__action[data-variant="secondary"] { box-shadow: none; }

/* 3. Expert override: only the named component changes. */
.onsite-shop .aditus-shop__article-card {
  border-inline-start: 4px solid var(--aditus-accent);
}

对您网站的要求

要求取决于交付方式。ADITUS 托管的脚本只需要一个当前的浏览器;源码包还需要您的应用中有 React 18+。两者都需要一个会话,其 publicKey 的白名单中包含您的域名。

需要什么

  • 仅限源码包:您的应用包中需要 React 18+ 和 react-dom 18+(peerDependency react >=18);mount() 使用 react-dom/client 中的 createRoot。托管脚本自带 React。
  • 具有 DOM(客户端)的浏览器。适配器安装到真实的 DOM 元素(轻量 DOM)中。只要 mount() 在客户端上运行,SSR 应用就可以。
  • 需要当前的常青浏览器。适配器使用标准 Web API(fetch、AbortController、ResizeObserver、CSS @layer),基线自 2022 年左右开始。没有IE。
  • 一个属于您 publicKey 的会话。没有后端的网站同时设置 public-key 和 cart-scope="tab";元素随后通过限定域名的 /shop/embed-session 自行创建浏览器会话。有后端的网站在服务器上使用 mint secret 创建会话并传入令牌。没有会话时,微前端保持未激活状态。您的域名必须在该 publicKey 的白名单中。

不需要什么

  • 无需特定框架。mount(container, config) 适用于 React、Vue、Angular 或纯 HTML。
  • 没有 iframe,没有 Shadow DOM。它会本地渲染到您的页面中。
  • 您的网站无需特定 CSS。基础样式位于 @layer aditus-shop 中,采用单类选择器优先级,因此您网站上任何未分层的规则都会优先。这也包括 button、input 或 a 等通用元素规则:它们同样会作用于商店的控件;如果不希望生效,请将其限定在您自己的内容范围内。box-sizing、内边距和间距还额外受到保护,不受 * { margin:0; padding:0 } 之类全局重置的影响。baseStyles: false 可关闭基础样式。
  • 您的网站无需其他库。除 React peerDependencies 外,源码包自带一个运行时依赖(用于校验电话号码的 libphonenumber-js);托管脚本包含全部内容。
  • 托管脚本无需任何构建配置;源码包只需一个 ESM 导入。

您的网站是可信的:在创建已识别会话之前,由您的服务器验证用户。微前端有意在您页面的 Light DOM 中渲染,不使用 iframe 隔离。您页面上的脚本可以访问 DOM 和浏览器存储,因此请保护好您的网站及其第三方脚本。基于公钥的来源授权并不是用户身份验证。支付凭据只在支付服务商的外部页面上输入。

支付安全、PCI 和 CSP

购买准备在您的页面中渲染。支付本身在支付服务商的页面上进行,因此没有任何支付数据经过您的页面。

您的页面中没有支付数据

完整的购买准备工作(活动、门票、附加组件、购物车、注册)在您的页面中本地呈现。当用户开始重定向支付时,敏感流程将完全离开您的页面:正常的顶级导航移交给支付提供商专用的外部支付页面,回程将恢复旅程并完成订单。卡或银行数据仅在此处输入,微前端绝不会在您的页面中呈现、传输或存储支付凭证。

最小 PCI 范围

由于您的网站中不会处理、传输或存储任何信用卡或银行数据,因此您的环境停留在经典重定向模型的最小 PCI-DSS 范围内(通常为 SAQ A;绑定分类始终由您的收单机构或 QSA 进行)。没有重定向的支付方式,例如发票,完全服务器端,浏览器中根本没有支付数据。

内容安全策略(CSP)

如果您的网站设置了内容安全策略(CSP),请添加:ADITUS 源的 script-src(托管脚本)、ADITUS API 源的 connect-src(所有商店调用都经过它)、ADITUS API 源(旗帜、支付方式标志、钱包徽章)以及您的活动和商品图片所在主机的 img-src,还有 style-src 'unsafe-inline',因为基础样式以 <style> 元素注入(使用 baseStyles: false 和您自己的样式表时不需要)。无需为支付服务商添加 script-src 或 frame-src 条目,因为支付步骤是顶层重定向。

与 iframe 和 Shadow DOM 的比较

iframe

iframe 是一个独立的文档:您页面的样式不会作用于其中,其高度需要同步,并且它有自己的焦点顺序。微前端则将购买准备以 HTML 形式渲染到您页面的 DOM 中。只有支付会离开您的页面,通过顶层重定向转到支付服务商的页面。

Shadow DOM

许多 Web Components 将自身封装在 Shadow DOM 中。您页面的样式只能通过自定义属性和 ::part 作用于它们。微前端则使用普通的 CSS 层叠:其基础样式位于 @layer aditus-shop 中,因此您页面上任何未分层的规则都会优先,无需更高的优先级或 !important。

方法比较

标准经典 iframeShadow-DOM 小部件ADITUS 微前端
无障碍独立文档,有自己的焦点顺序和页面结构。基于 ID 的 ARIA 引用(aria-labelledby、aria-describedby)无法跨越 Shadow 边界。位于您页面 DOM 中的 HTML:与您自己的标记共享同一焦点顺序和页面结构。
造型与品牌刚性:没有 CSS 继承,只能通过 postMessage API 进行定制。全局样式无法到达内部;仅通过传递的属性和部分进行主题化。您未分层的 CSS 优先于 @layer 中的基础样式;基础样式可覆盖或关闭。
捆绑与性能通常会加载完整的第二个应用程序,包括其在框架内的运行时。通常会提供自己的运行时和重复的依赖项。托管脚本:一个包含 React 的包。源码包:使用您应用的 React,外加一个运行时依赖(libphonenumber-js)。
支付安全和 PCI 范围高度隔离,但代价是用户体验、设计和响应能力。取决于组件:组件内的银行卡字段属于您的页面。在您的页面中选择,在支付服务商的页面上支付。

我们的建议:直接通过 ADITUS URL 加载 JavaScript

直接从 ADITUS 加载 JavaScript 包。这是我们对所有网站的标准建议,包括 React 和其他框架,而不仅是 HTML 或 CMS 网站。ADITUS 集中改进商店内部逻辑,兼容的改进会更新到您的集成中,无需您下载新包、重新构建或部署您的网站。

查看推荐的脚本集成方式 /api/embed/v1/aditus-shop.js

更新如何生效

v1 URL 提供兼容版本,并保持接口稳定。新版本会在后续页面加载时生效,前提是当前一小时的 HTTP 缓存已过期或重新验证,因此并非每次刷新都会立即更新。已打开的结账流程继续使用已加载的代码。

备选方案:自行管理托管和更新

只有在您明确希望自行管理分发时,才选择源码包、通过 npm 安装 tarball,或自行托管 JavaScript 副本。ADITUS 无法更新您的副本。您需要自行检查新版本、下载或安装、测试、按需重新构建并部署。此包未发布到公共 npm 注册表。

替代方案:源码包(自行分发)

作为替代方案,可从 ADITUS 实例下载源码包并打包进您的应用(Vite、webpack、Next.js 或 Nuxt)。由您自行分发:更新需重新安装、构建和部署。该包不在公共 npm 上。

交货内容

完整的适配器作为可读的 TypeScript 源,带有每个配置选项、回调和事件的类型声明。 React 和 React-dom 保持对等依赖关系,你的页面提供了它们,所以你的包中不会发生任何冲突,也没有第二个 React。

获取包裹

每个网站的软件包都是相同的,并直接从此实例下载,无需凭据:aditus-shop-embed.tgz。可从此 URL 安装(npm install 接受 tarball URL,路径 /api/embed/v1/aditus-shop-embed.tgz)或解压至仓库。入驻时会提供 publicKey 和域名白名单;没有 publicKey,微前端保持未启用。联系:联系页面。仅在自行分发和更新构建时使用此包。

保持最新:v1 轨道会就地接收兼容更新。请定期检查 manifest.json(在您的应用每次发布前,最好作为计划任务),并将 package.sourceHash 与已安装 package.json 中的 aditus.sourceHash 进行比较。若不同,请重新安装 tarball 并运行构建和测试;hooks、tokens 和事件在 v1 内保持稳定。Web 组件包通过一小时的 HTTP 缓存自行更新。

通过 ADITUS URL 集成脚本

预构建包 https://developers.aditus.com/api/embed/v1/aditus-shop.js 适用于所有网站,包括需要构建的应用、WordPress、Typo3 和纯 HTML。一个 script 标签即可注册 <aditus-shop>,包含 React,无需 npm 或自行构建。此公开、带版本号的 URL 无需凭据;商店仍需列入白名单的 publicKey 和会话。正在进行的结账不会热更新;浏览器缓存最多可能延迟一小时获取新包。

index.html
<!-- 1. Include the bundle hosted by ADITUS (once per page). -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>

<!-- 2. Explicit tab isolation for anonymous sessions created automatically in the browser (not the default).
     For shared: session-token from your backend; anonymous ones also need a server-side sharedCartKey. -->
<aditus-shop
  public-key="pk_live_yourkey"
  api-base="https://developers.aditus.com/api"
  cart-scope="tab"
  culture="en-GB"
></aditus-shop>

<!-- 3. Optional: consume journey events as plain DOM events. -->
<script>
  document.querySelector("aditus-shop").addEventListener("cart:update", (e) => {
    console.log("Items in cart:", e.detail.itemCount);
  });
</script>

同一个托管 JS URL 自动注册 <aditus-shop> 并公开 window.AditusShop:mount、themeToCss、clearJourney、mintEmbedSession 和 onEmbedSessionRenewed,签名与源码包相同。您的网站无需 React。托管 mount、element.config 和源码包接受相同的 AditusShopConfig,包括主题、eventTheme: false、articleContent 和回调。只有纯 HTML 属性无法传递函数或对象。源码包仍适用于打包工具及共享现有 React 运行时。

购物车范围、信任与存储

纯 JS:显式令牌生命周期

mount(container, config) 使用相同的源码 mount:没有 sessionToken 时保持未激活,不会自行创建会话。请显式设置完整的 /api/shop URL 作为 apiBase。在您的网站完成异步登录后,后端通过 S2S 创建会话并仅传递令牌。host.renewSession 和 host.revokeSession 是您自己的函数/路由,必须在已认证的后端实现,并非可直接使用的 ADITUS 浏览器端点。秘密不得进入浏览器。所有 JavaScript 方案都支持现有异步 onUserRequired 登录及 S2S 创建流程,返回令牌或 null。

index.html / your own login
<div id="ticketshop-slot"></div>
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js"></script>
<script>
  // Called after your async login and server-side session creation, with the returned sessionToken.
  // host.renewSession(staleToken): Promise<string | null>
  // host.revokeSession(token): Promise<void>
  // These are HOST-SPECIFIC backend functions, not built-in ADITUS routes.
  // Your backend authenticates the user; mint secrets never enter the browser.
  window.mountKnownSessionShop = function (sessionToken, host) {
    if (!sessionToken) throw new Error("A sessionToken from your backend is required");
    const slot = document.getElementById("ticketshop-slot");
    if (!slot) throw new Error("Missing mount container");
    let disposed = false;
    let config = {
      apiBase: "https://developers.aditus.com/api/shop",
      publicKey: "pk_your_site",
      sessionToken,
      cartScope: "shared", // default; trusted S2S identity required; immutable after mount
      culture: "en-GB",
      refreshSession: async (staleToken) => {
        const token = await host.renewSession(staleToken);
        if (disposed) return null;
        if (token) config = { ...config, sessionToken: token };
        return token; // Adapter swaps token and retries the failed request once.
      },
      onError: (error) => console.error("Ticketshop:", error.message),
    };
    const handle = window.AditusShop.mount(slot, config);
    return {
      updateCulture(culture) {
        if (disposed) return;
        config = { ...config, culture };
        handle.update(config); // full replacement, not a patch
      },
      async logout() {
        disposed = true;
        handle();
        window.AditusShop.clearJourney(); // local resume pointer only
        await host.revokeSession(config.sessionToken); // HOST-SPECIFIC S2S revoke
      },
      teardown() {
        disposed = true;
        handle(); // SPA route cleanup, not server-side revocation
      },
    };
  };
  // Your site supplies the real token and implementations:
  // const shop = window.mountKnownSessionShop(sessionToken, host);
  // Logout: await shop.logout(); route change: shop.teardown();
</script>

纯 JS:浏览器创建及续期

对于白名单 publicKey,mintEmbedSession(publicKey, apiRoot = "/api", { fresh?, staleToken? }) 无需秘密并返回 Promise<string | null>。辅助函数与 api-base HTML 属性使用 /api 根路径;config.apiBase 使用 /api/shop 商店路径。onEmbedSessionRenewed(publicKey, apiRoot, listener) 传递令牌字符串,返回取消订阅函数。示例处理初始失败、过期刷新、主动续期和清理。取消订阅仅移除该监听器,不清除共享缓存、续期计时器或服务端会话;辅助函数通过 sessionStorage 维持重新加载连续性。

index.html / browser session
<div id="ticketshop-slot"></div>
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js"></script>
<script>
  window.mountBrowserSessionShop = async function () {
    const { mount, mintEmbedSession, onEmbedSessionRenewed } = window.AditusShop;
    const publicKey = "pk_your_site"; // your whitelisted key, not a secret
    const apiRoot = "https://developers.aditus.com/api"; // no /shop for helpers
    const slot = document.getElementById("ticketshop-slot");
    if (!slot) throw new Error("Missing mount container");
    const sessionToken = await mintEmbedSession(publicKey, apiRoot);
    if (!sessionToken) throw new Error("Browser session failed: check key/domain");
    let disposed = false;
    let config = {
      apiBase: apiRoot + "/shop", // mount uses the shop base
      publicKey,
      sessionToken,
      cartScope: "tab", // explicit isolation opt-in: browser-created sessions cannot safely share
      culture: "en-GB",
      refreshSession: async (staleToken) => {
        const token = await mintEmbedSession(publicKey, apiRoot, {
          fresh: true, staleToken,
        });
        if (disposed) return null;
        if (token) config = { ...config, sessionToken: token };
        return token;
      },
      onError: (error) => console.error("Ticketshop:", error.message),
    };
    const handle = mount(slot, config);
    const offRenewed = onEmbedSessionRenewed(publicKey, apiRoot, (token) => {
      if (disposed) return;
      config = { ...config, sessionToken: token };
      handle.update(config); // preserve callbacks and all other options
    });
    return function teardown() {
      disposed = true;
      offRenewed();
      handle();
    };
  };
  // const teardown = await window.mountBrowserSessionShop();
  // Start now; your SPA calls window.teardownTicketshop() on route change.
  window.mountBrowserSessionShop().then((teardown) => {
    window.teardownTicketshop = teardown;
  }).catch((error) => {
    document.getElementById("ticketshop-slot").textContent = error.message;
  });
</script>

属性与完整 JavaScript 配置

element.config 接受完整 AditusShopConfig,包括函数。已定义键覆盖对应 HTML 属性,包括 false;缺省/undefined 键保留属性。每次赋值替换所有覆盖,并非补丁:element.config = {} 清除覆盖。支持自定义元素升级前赋值。api-base 是 /api 根路径,而 config.apiBase 以 /api/shop 结尾。themeToCss(theme, selector = ".aditus-shop") 返回令牌 CSS,可替代主题对象。

index.html / element.config
<aditus-shop id="shop" public-key="pk_your_site"
  api-base="https://developers.aditus.com/api" cart-scope="tab" culture="de-DE"></aditus-shop>
<script>
  const element = document.getElementById("shop");
  // Supported before upgrade by the following hosted script, too.
  element.config = {
    apiBase: "https://developers.aditus.com/api/shop",
    culture: "en-GB", // overrides the HTML culture attribute
    cartScope: "tab", // explicit isolation opt-in for this anonymous browser-created example
    theme: { accent: "#c2410c", fontSize: "16px" },
    eventTheme: false,
    onComplete: (order) => console.log("Order completed:", order),
    onError: (error) => console.error("Ticketshop:", error.message),
  };
  // No sessionToken: effective publicKey enables browser session creation.
  // Optional refresh by your site: refreshSession: (staleToken) => host.renewSession(staleToken)
  // host.renewSession is HOST-SPECIFIC; your refresh always takes precedence.
  // Later: element.config = {}; removes overrides, attributes apply again.
  // Cleanup: element.remove();
</script>
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>
属性意义
cart-scopeAditusShopConfig.cartScope?: 'tab' | 'shared' 默认为 'shared',省略时也一样。'tab' 是显式选择的独立流程模式。中央托管的 window.AditusShop.mount()、自定义元素 element.config 以及框架/源码包 mount() 接受同一选项;纯 HTML 使用 cart-scope="tab" 或 cart-scope="shared"。请在挂载前选择。已挂载结账实例的范围不可变:handle.update() 会拒绝更改,而不会迁移正在使用的购物车。完整替换配置时请保留原范围。
public-key您的可发布 key(适用域名白名单)。未设置 session-token 时,元素会自行创建浏览器会话。没有后端的网站使用此模式,并且必须同时设置 cart-scope="tab";使用默认值 "shared" 时,由于没有可信的服务器身份,浏览器会话会以明确的错误结束。
session-token由您的后端使用 mint secret 创建的会话令牌(见下文)。设置后,元素永远不会自行创建会话,会话生命周期由您的页面管理。
api-baseADITUS API 的基本 URL。默认为加载捆绑脚本的来源,通常您从不设置它。
culture商店 culture/locale 会转发给 ADITUS 以选择资源;it-IT 等有效值也会传递。Voucher 和 upsell 资源文本保持 ADITUS 提供的原文;其他界面使用德语/英语资源及回退。这不代表完整意大利语界面。更改后会原位更新语言而不重置流程。
article-layout / article-group-layout / event-layout / article-select-mode / show-header平面呈现选项,与 mount() 配置相同的值(cards、grid、quantity……)。入口点(事件选择器与固定事件)不是一个属性:它在创建会话时在服务器端固定。
include-shop-assortments可选分类合并;对应 mount() 的 includeShopAssortments。默认为 false,仅加载默认分类。设为 true 时还加载 showInShop=true 的其他分类。明确指定的起始分类始终优先。省略属性时使用客户端默认设置;明确的 false 会覆盖该设置。
survey-columnsRadioButtonList/CheckBoxList 调查问题答案选项的列数(1–4),与 mount() 配置中的 surveyColumns 相同。未设置时保持单列默认值(或服务器端存储的客户端默认值);窄视口会折叠回单列。
hide-house-number在所有国家版本中隐藏调查地址块的门牌号字段,与 mount() 配置中的 hideHouseNumber 相同。街道和门牌号随后在一个名为“街道和门牌号”的字段中一起填写,该字段占用空出的宽度;地址建议会按该国常用顺序将街道和门牌号填入此字段(例如法国为 "12 Rue Cler"),预填的门牌号会被清空,单独的门牌号答案始终以不可见的零宽空格(U+200B)提交,即使调查将其设为必填也不会出现验证错误。账单地址表单遵循同一规则。默认关闭;省略属性时使用客户端默认设置,明确的 "false" 会覆盖该设置。
show-jump-button个性化步骤中的跳转按钮;对应 mount() 配置中的 showJumpButton。默认开启:提交失败后,组件底部会固定显示一个小按钮,显示仍未填写的项数并跳转到下一项;全部填写后会引导回到该按钮,但绝不会自动提交。当该按钮或目标可见以及正在输入时会隐藏。设为 "false" 可关闭;省略属性时使用客户端默认设置。
voucher-code-param您的页面 URL 中携带代金券/票码的查询参数名称(例如 "code",用于 …/tickets?code=ED24 这样的链接),与 mount() 配置中的 voucherCodeParam 相同。元素在挂载时读取一次,将其从地址栏移除并自动兑换代码(代金券表单或第一个购物车)。仅适用于固定到活动的会话。
base-styles设置为“false”可删除中性默认皮肤并自行设计每个钩子的样式。
autoScroll / articleIconVisible / classPrefix没有对应属性:请通过 element.config 设置这些选项(名称与 mount() 配置相同)。autoScroll(默认 true)在步骤切换后将商店开头滚动回视野;如果您的页面自行管理滚动,请设为 false。articleIconVisible(默认 false)在列表布局中为每个商品显示图片或字母标识。classPrefix(默认 "aditus-shop")更改所有 CSS 钩子的前缀。

会话和自我修复

有效 sessionToken(来自配置或属性)禁用浏览器自动创建会话。有有效 publicKey 但无令牌时,元素仍自行创建浏览器会话。您的网站提供的 refreshSession 始终优先于内置刷新;未提供时,自行创建的会话使用共享辅助函数刷新及主动续期。Light DOM 不变:无 iframe 或 shadow DOM。

事件和版本控制

元素的 DOM CustomEvents 冒泡,载荷位于 event.detail:cart:update、phase:change、checkout:complete 和 analytics,名称及载荷与 handle.on() 相同。配置回调也可使用。

刻意重置旅程

element.clearJourney() 和 window.AditusShop.clearJourney() 仅移除本页本地恢复指针。重新插入或挂载前调用即可重新开始。不会删除服务端购物车或撤销会话;登出撤销必须由后端执行。

无需重新安装即可更新

mount/update 配置为完整替换而非补丁:保留完整配置并向 handle.update() 传递 { ...config, culture }。element.config 同样替换所有覆盖;未被覆盖的属性仍原地更新。展示变更保留旅程,身份变更重新建立会话。handle() 卸载;移除元素清理内部及续期监听器。保留元素时需自行移除您的页面注册的 DOM 监听器。

SSR 应用:Next.js 和 Nuxt

微前端特意是仅限客户端的。它的内容是实时的并且受会话限制:可用性、价格和用户购物车仅在请求时经过身份验证的会话中存在,因此您的服务器没有任何有意义的内容可以预渲染。 mount() 创建一个新的客户端 React 根 (createRoot);容器内服务器渲染的标记未水化。这使得 SSR 集成变得简单且可预测:在服务器上渲染占位符,安装在客户端上,并保留空间,这样就不会发生任何跳跃。

ticketshop-section.tsx
// Next.js (App Router): the micro-frontend is deliberately client-only.
"use client";
import { useEffect, useRef } from "react";
import { mount, type ShopHandle } from "@aditus/shop-embed";

export function TicketshopSection({ sessionToken }: { sessionToken: string }) {
  const el = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!el.current) return;
    const handle: ShopHandle = mount(el.current, {
      publicKey: "pk_your_site",
      sessionToken, // issued server-side, passed to the client component as a prop
      cartScope: "shared", // the default in frameworks too; trusted server identity
    });
    return () => handle(); // unmount on route change
  }, [sessionToken]);

  // Server and client render the SAME placeholder and so avoid a hydration mismatch.
  // min-height reserves the space so the page does not jump on mount (CLS).
  return <div id="aditus-shop" ref={el} style={{ minHeight: 560 }} />;
}

避免布局偏移 (CLS)

为安装容器指定一个与第一个商店视图大致匹配的最小高度,并可选择在其中渲染您自己的骨架,服务器和客户端生成相同的占位符,因此不会出现水合作用不匹配的情况。适配器在安装后立即渲染并替换一种绘制中的占位符。不要尝试在容器上使用 HydroRoot:没有服务器渲染的商店树可以附加。

SSR 应用中的会话

SSR 应用有一个天然的优势:呈现页面的服务器运行时也可以使用key创建会话,服务器到服务器,与下面的创建流程完全相同(Next.js:路由处理程序或服务器组件;Nuxt:服务器路由)。将生成的不透明token作为 prop 或有效负载传递给客户端组件。该秘密永远不会出现在客户端代码中,并且token保留在内存中。

Next.js 细节

将安装组件标记为“use client”并安装在 useEffect 中(参见代码片段)。对于页面路由器,使用 ssr: false 的 next/dynamic 可以实现相同的效果。从效果中返回句柄作为清理,以便客户端路由更改干净地卸载。正在运行的实例上的文化切换会通过handle.update()进行,而无需重置旅程;一个新的会话token重新安装(如代码片段中所示)。购物车也能幸存下来,因为微前端恢复了安装过程。

Nuxt / Vue 细节

将挂载目标包装在 <ClientOnly> 中或挂载在 onMounted 中,适配器需要浏览器中的真实 DOM 元素。请记住包要求:react 和react-dom 18+ 必须作为 Nuxt 应用程序的依赖项安装;商店渲染到自己的容器中,并且不会干扰 Vue 的虚拟 DOM。

购物车范围、信任与存储

cartScope 决定同一访客的多个标签页是否共享一个购物车。默认值为 'shared';'tab' 会有意将标签页分开。请在挂载前选择,挂载后无法更改。

方面'shared' (默认)'tab'
购物车相同来源、publicKey、身份和活动的所有标签页共用一个购物车每个标签页都有自己的购物车
会话由您的后端签发:已识别用户,或使用来自您自己服务器会话的 sharedCartKey 的匿名访客。切勿从浏览器获取身份或密钥。任何会话,包括浏览器端签发
多个标签页一个标签页可编辑,其他标签页只读并提供“在此继续编辑”。新打开或重新加载的标签页会自动接管,但支付期间不会。相互独立
不满足条件时明确报错,即使省略 cartScope 也是如此;不会静默切换为 'tab'。因此,没有后端的页面应设置 'tab'。–
  • 配置: 在 mount() 和 element.config 中使用 cartScope,在 HTML 中使用 cart-scope。handle.update() 会拒绝更改,因此请在每个替换配置中保留该值。 属性 · 签发字段
  • 存储: 表单草稿按标签页保存在 sessionStorage 中,最后一次编辑 2 小时后失效。令牌绝不写入 localStorage。已确认的订单和注册不受影响。
  • 退出登录: 退出登录或切换用户时,您的后端会撤销旧会话;随后更新令牌,或卸载嵌入组件并调用 clearJourney()。新用户绝不会继承上一位用户的购物车。
  • 安全: 嵌入组件不使用 iframe,直接渲染到您的页面中。页面上的脚本(包括第三方脚本)都可以访问它,因此只引入可信脚本。支付数据仅在支付服务商处输入。
shared-cart.js
// Load the recommended centrally hosted aditus-shop.js first.
// sessionToken comes from your trusted backend's server-side session, not a browser-created one.
// Identified user: backend authenticates user and derives email/externalUserId.
// Anonymous visitor: backend omits email and MUST derive sharedCartKey
// from its own trusted visitor session. Neither identity nor key is browser input.
const slot = document.getElementById("ticketshop-slot");
if (!slot) throw new Error("Missing mount container");
const config = {
  apiBase: "https://developers.aditus.com/api/shop",
  publicKey: "pk_your_site",
  sessionToken,
  cartScope: "shared",
};
const handle = window.AditusShop.mount(slot, config);
// Full replacement config retains the chosen scope:
handle.update({ ...config, culture: "en-GB" });
// Do not change cartScope on this handle. Route teardown: handle();

微前端需要一个会话

无token

无会话token:微前端保持不活动状态

如果没有会话token,微前端会呈现中立的通知,并且根本不会启动任何 API 调用。无法填充购物车、无法注册、无法结账。在商店激活之前,会话是强制性的,但它不必与已知用户绑定:访客会话足以浏览和填充购物车,并且可以在稍后结帐时建立真正的用户(请参阅下面的可选用户流程)。

mount.ts
import { mount } from "@aditus/shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");

// Without a sessionToken the micro-frontend stays inactive: it only shows a
// neutral notice and starts no API calls: no cart, no registration,
// no checkout. Only a session activates the shop.
mount(el, {
  publicKey: "pk_your_site",
  // sessionToken missing -> the shop is not activated.
});
您网站的用户

面向特定用户的会话token

您的后端会签发一个与一个用户绑定的短期令牌。然后,微前端充当该用户:他们的购物车、他们的数据。该token是不透明的、过期的、单用户的,并且绑定到您的公钥,并且您的秘密永远不会到达浏览器。

mount.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@aditus/shop-embed";

// getElementById can return null. Check it properly in TypeScript instead of using "!".
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");

// AditusShopConfig types every option and gives you autocompletion.
const config: AditusShopConfig = {
  apiBase: "https://developers.aditus.com/api/shop",
  publicKey: "pk_your_site",   // required as soon as a session is involved
  sessionToken,                // issued by your backend, keep it in memory only
  cartScope: "shared",          // default; requires a trusted server identity
};

const handle: ShopHandle = mount(el, config);

// The micro-frontend sends the X-Aditus-Session header with every shop call.
// On logout, revoke the session on the server and remount/update.

重新加载时,有效流程指针可让 embed 在相同授权范围和可信身份下请求服务器购物车。指针不是授权,也不保证会话/购物车过期、存储清除或用户切换后的恢复。显式 tab 不合并其他标签页的流程;默认 shared 仍执行单写入者结账锁。

支付返回使用 aditus-return(不透明、短期、服务器绑定的返回令牌)和 aditus-pay。可在同一或新标签页返回,但服务器要求授权客户端/来源及匹配的可信身份。返回查询参数不是购买证明。服务器核实支付状态并完成令牌绑定的尝试;结果不明时保持阻止并进行核对,绝不盲目重试为新购买。过期或未授权返回会产生明确错误,而不是允许创建替代订单。

分配支付服务商仅为准备支付:摘要仍可编辑。仅在获得必要同意并实际跳转时才开始锁定。cancel/error 返回参数不能证明失败:后端重新检查购物车;Completed 且有 order.id 才确认完成。只有确认成功移除同一购物车的服务商分配后才允许重试;结果不明时保持阻止并可重新核查。这不保证退款,也不排除延迟结算。

重定向支付:您的页面需要为会话做什么

  • 请在配置的返回 URL 挂载 embed。路由或必要的登录重定向应保留 aditus-return 和 aditus-pay;绝不将 aditus-pay=success 当作授权。提供同一可信身份及 publicKey 的有效会话。保留所选 cartScope,不要清除流程或发起第二次结账来绕过待处理、过期或不确定的返回。

想要一个刻意的新开始,也就是一个真正的“重新开始”按钮?该包导出clearJourney():在挂载(或重新挂载)之前调用它,并且当前页面的本地持久旅程记录将被删除,因此下一次挂载将在入口点重新开始。仅移除本地恢复指针;任何服务器端购物车状态均保持不变。

入口:活动首页、门票或代金券

入口由服务器在创建会话时决定,而不是浏览器。没有 eventSlug 时显示活动列表;有 eventSlug 时,startMode 选择商品(默认)、活动首页或兑换表单;assortment 限制商品列表。

商品选择和代金券兑换后的继续操作沿用原商店基于推荐的路由。没有可用的升级优惠且没有交叉销售时,通过正常的登录/会话检查直接进入门票个性化;有交叉销售时显示购物车。可用的升级优惠会先打开对话框。跳过时,若没有交叉销售则进入个性化,否则进入购物车。确认升级后进入购物车;关闭对话框则保持当前选择。路由绝不会在后台写入注册。

商店可见的分类可有直接入口。若会话固定了某个分类,首页只显示该分类的门票入口,不显示完整列表或兑换入口。

startMode: "home" 打开活动首页。访客在此选择购买门票或兑换凭证;仅打开页面不会创建购物车或兑换代码。购买门票进入商品列表(如设置 assortment 则受其限制);兑换凭证进入代码表单。没有 eventSlug 时先显示活动列表,再显示所选活动首页。已准备的凭证购物车或可恢复的购物流程优先于全新入口。

默认

文章入口

仅 eventSlug:微前端打开该活动的文章选择,显示商店中所有可见分类。

assortment

仅一个分类

assortment 将文章入口限制为活动的一个分类:传入其 id 或确切名称(不区分大小写)。允许隐藏分类,因此可以在专属页面上提供例如免费媒体票。没有匹配项的值会显示明确错误,而不是静默列出全部。

startMode: "voucher"

兑换代金券 / 登记门票

旅程打开的是代码表单而非文章列表。有效代码会预留门票。成功兑换后,ADITUS 活动配置 configuration.redirectAfterRegistration 决定访客停留在成功页面还是自动转发。继续操作与商品选择采用相同的推荐路由,并在门票个性化之前进行正常的登录/会话检查。返回会保留预留。“添加更多代金券”会在 voucher 页面为同一购物车重新打开代码输入;“选择门票”会打开文章选择。被拒绝的代码留在表单并显示原因。全额覆盖的门票会跳过付款。

在该表单中输入促销码(为文章打折而非抵扣门票的代码)同样可以处理:微前端切换到文章选择,显示该代码产生的价格,列出每个变动的价格(旧价和新价),并将代码保留在购物车上,使折扣同样适用于所选文章。「移除代码」返回表单并显示常规价格。

在哪里设置

服务器会话或客户端记录

服务器到服务器创建会话时在请求体中与 eventSlug 一起传入 startMode / assortment。浏览器创建的会话(仅可发布密钥)从 ADITUS 联系人配置的客户端记录中获取这三个值;请求体中的值在那里会被忽略。只有 assortment 需要 eventSlug(400 start_option_requires_event_slug);startMode 也可用于活动选择;未知值返回 400 invalid_start_mode / invalid_assortment。GET /shop/session-context 返回会话的 startMode 和 assortment。

mint-entry.ts
// Server side: use the same session route as above, only the body varies.
// You build user and your backend's login/session route yourself.
// eventSlug sets the event. startMode also works without a slug (event
// selection first, then the mode); only assortment needs the slug
// (otherwise 400 start_option_requires_event_slug).

// 1) Article start (default): the event's article selection
const bodyArticles = { publicKey, email: user.email, eventSlug: "ExperienceDaysv52024" };

// 2) Assortment start: only one assortment (id or exact name, case-insensitive)
const bodyAssortment = {
  publicKey, email: user.email,
  eventSlug: "ExperienceDaysv52024",
  assortment: "Pressetickets",
};

// 3) Voucher start: "Redeem voucher / register ticket" first
const bodyVoucher = {
  publicKey, email: user.email,
  eventSlug: "ExperienceDaysv52024",
  startMode: "voucher" as const,
};

const res = await fetch("https://developers.aditus.com/api/shop/session", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`, "Content-Type": "application/json" },
  body: JSON.stringify(bodyVoucher),
});
const { sessionToken }: { sessionToken: string } = await res.json();
voucher-hooks.css
/* Voucher form: the same tokens and role hooks as the rest of the journey */
.aditus-shop__voucher-panel  { /* panel around hints + form (general panel hook) */ }
.aditus-shop__voucher-hints  { /* the two hint lines */ }
.aditus-shop__voucher-form   { /* form */ }
.aditus-shop__voucher-input  { /* code field (also .aditus-shop__field-input) */ }
.aditus-shop__voucher-redeem { /* "Redeem" button (data-variant="primary") */ }
.aditus-shop__field-error    { /* rejection reason, role="alert" */ }
/* Event home page and event actions: structured expert hooks */
.aditus-shop__home             { /* home page; root data-step="home" */ }
.aditus-shop__home-info        { /* event information */ }
.aditus-shop__home-info-head   { /* heading and logo */ }
.aditus-shop__home-info-name   { /* event name */ }
.aditus-shop__home-info-date   { /* date */ }
.aditus-shop__home-info-venue  { /* venue */ }
.aditus-shop__home-info-desc   { /* description */ }
.aditus-shop__home-actions[data-variant="tiles"] { /* tiles, alternatively compact */ }
.aditus-shop__home-action[data-kind="articles"] { /* buy tickets; also home / voucher / assortment */ }
.aditus-shop__home-action-icon    { /* icon */ }
.aditus-shop__home-action-body    { /* text group */ }
.aditus-shop__home-action-title   { /* label */ }
.aditus-shop__home-action-desc    { /* optional description */ }
.aditus-shop__home-action-chevron { /* arrow */ }
/* Phase hook: [data-step="voucher"] on the root */

代金券通常以指向你页面的链接形式发送,代码作为查询参数,例如 …/tickets?code=ED24-ADITUS。微前端可以接管该代码并兑换,访客无需输入任何内容。有两种传递方式;两者最终都调用同一个兑换请求,之后的行为完全相同。

voucherCodeParam: "code"

A) 在浏览器中:微前端读取 URL

在 mount() 配置中(voucherCodeParam)或元素上(voucher-code-param)指定参数名。挂载时适配器读取一次,通过 history.replaceState 将其从地址栏移除(不重新加载,URL 中的其他内容保留)并兑换代码。因此刷新、书签或转发的 URL 永远不会重复兑换。适用于静态页面和浏览器创建的会话。

voucherCode 在会话请求体中

B) 在服务器端:随会话传递代码

你的后端从传入请求中读取参数,并作为 voucherCode 放入会话请求体(eventSlug 可选)。代码成为会话的一部分,仅通过经过身份验证的 GET /shop/session-context 到达微前端,该接口只交付一次随后清除;会话响应会回显已接受的代码以便核对。请同时设置 voucherCodeParam,使该参数也从地址栏消失。适用于页面本来就在服务器端渲染,或你想自行校验、记录或限流入站链接的情况。若两种方式都提供了代码,以服务器端为准。

代码会怎样

两种方式行为相同

代码仅在新旅程中兑换一次(重新加载或支付返回后恢复的旅程会忽略它)。startMode 为 "voucher" 时,代码表单的行为与“兑换代金券 / 登记门票”中所述相同。以文章为入口时,代码应用于第一个购物车,同时保留文章视图、滚动位置和数量。“继续”会将这些数量加入同一个购物车。微前端不会将代码传给 onEvent。没有 eventSlug 时,访客选择活动后即兑换。

voucher-link.ts
// Voucher links point to your site: https://your-site.com/tickets?code=ED24-ADITUS
// The route, request and authentication below are examples for your own website.
// The parameter name is up to you: here "code".

// Option A (browser): the micro-frontend reads the parameter itself.
import { mount } from "@aditus/shop-embed";

mount(document.getElementById("aditus-shop")!, {
  publicKey: "pk_your_site",
  sessionToken,                 // session with eventSlug (article or voucher start)
  voucherCodeParam: "code",     // read ?code=… once, remove it from the address bar, redeem it
});
// Web Component: <aditus-shop public-key="pk_your_site" voucher-code-param="code"></aditus-shop>

// Option B (server): your backend hands the code over when it creates the session.
// (Express example; the code never goes into the session response, only into the session context.)
app.get("/tickets", async (req, res) => {
  const code = typeof req.query.code === "string" ? req.query.code : undefined;
  const r = await fetch("https://developers.aditus.com/api/shop/session", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      publicKey: "pk_your_site",
      email: req.user?.email,
      eventSlug: "ExperienceDaysv52024",
      startMode: "voucher",       // or omit: article start, the code lands in the first cart
      voucherCode: code,          // undefined = normal start without a code
    }),
  });
  const { sessionToken }: { sessionToken: string } = await r.json();
  // The page additionally mounts with voucherCodeParam: "code" so that ?code= disappears
  // from the address bar (the session code wins, it is redeemed only once).
  res.render("tickets", { sessionToken });
});

可选:在服务器端创建会话时预留代金券

如果您的网站需要在打开微前端前处理代金券拒绝,请在受信任的 POST /api/shop/session 请求中设置 redeemVoucherOnMint: true。不同于通常的代码移交,服务器会为已认证用户创建购物车,并在签发会话前实际预留代金券,而不是增加一次预检请求。成功时仅将 sessionToken 交给浏览器。无论 voucher 还是 articles 模式,微前端都接管同一个购物车,不会重复创建或兑换。不设置该选项时,使用常规的 voucherCode 移交方式。

此模式要求 voucherCode、固定的 eventSlug、已识别的用户(email,非匿名)以及 Idempotency-Key 请求头。仅可在带有 mint secret 的后端调用,不能由浏览器 POST /shop/embed-session 调用。仅支持代金券/票码,不支持 PromotionCode。代码在购物车中预留,门票尚未注册或签发;仍须完成注册和结算,后续检查可能失败。

prepare-voucher.ts
// Server side of your website: one voucher attempt, no additional check call.
// You implement the authentication and storage of this attempt yourself.
// attemptKey is a random id your backend stores durably per logical redemption
// attempt (e.g. crypto.randomUUID()). On network errors, resend the same id
// and the SAME body; never generate a new one before a retry.
async function prepareVoucherSession(
  user: { email: string },
  code: string,
  attemptKey: string,
) {
  const response = await fetch("https://developers.aditus.com/api/shop/session", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
      "Content-Type": "application/json",
      "Idempotency-Key": attemptKey,
    },
    body: JSON.stringify({
      publicKey: "pk_your_site",
      email: user.email, // only from your authenticated server session
      eventSlug: "ExperienceDaysv52024",
      startMode: "voucher", // or "articles"
      voucherCode: code,
      redeemVoucherOnMint: true,
    }),
  });
  const result: {
    sessionToken?: string; expiresAt?: number; voucherRedeemed?: boolean;
    error?: string; reason?: string; upstreamStatus?: number;
  } = await response.json();

  if (response.ok && result.sessionToken && result.voucherRedeemed === true) {
    // Only now pass it to the browser and mount the embed.
    return { sessionToken: result.sessionToken, expiresAt: result.expiresAt };
  }
  if (response.status === 422 && result.error === "voucher_rejected") {
    // Your own UI: handle the business rejection using reason, do not open the embed.
    // Translate only documented reason values into your own texts.
    return { rejected: result.reason };
  }
  if (response.status === 409 && result.error === "voucher_preparation_uncertain") {
    // Uncertain stays uncertain; a repeated POST never turns it into a success.
    // If the Shop API cannot provide the cart id, manual clarification is needed.
    throw new Error("Redemption uncertain; clarify with ADITUS, do not book again.");
  }
  if (response.status === 410 && result.error === "voucher_preparation_expired") {
    throw new Error("Prepared session no longer valid; no blind new session.");
  }
  // 400 = invalid request/key, 409 = conflict/attempt in progress,
  // 502/503 voucher_upstream_error = infrastructure, not a business error.
  throw new Error(`Session creation not completed (${response.status})`);
}

422 voucher_rejected(包含已净化的 reason)表示业务拒绝:在您的网站上显示,未收到令牌时不要挂载微前端。400 表示请求/密钥无效;409 idempotency_conflict 表示同一密钥用于不同输入。挂起或结果不明的操作、502/503 和网络超时并不代表代码无效。在服务器上为每次逻辑尝试持久保存随机 Idempotency-Key,仅使用相同密钥和完全相同的输入重试;可能已存在预留。不要记录代码、邮箱、原始错误或令牌。

成功创建会话后,即使用户从未打开页面,购物车也可能仍被预留。拒绝或撤销后,已知购物车的清理仅尽力而为;启动时及每 60 秒运行的清理程序每次最多重试 25 条记录。相同密钥和请求可重放仍有效的完成结果。若原始令牌过期、被撤销、完成、删除或转移,则返回 410 voucher_preparation_expired;不要盲目重新预留。经授权的后继会话可在首次访问购物车时延长或转移准备状态。从您的页面 URL 中移除代码不会释放预留。

准确的会话创建错误:400 invalid_redeem_voucher_on_mint / voucher_preparation_requires_user_event_code / invalid_idempotency_key(或普通校验);422 voucher_rejected 包含 reason 和 upstreamStatus,不包含商店 API 的原始代码或消息。可能的原因: unknown · articleLimit · exhausted · ownership · used · reserved · limit · cartLimit · locked · canceled · otherEvent · legitimation · rejected · unsupported_code_type.

409 idempotency_conflict 表示输入发生变化;voucher_preparation_pending 附带 Retry-After: 2 秒,voucher_preparation_uncertain 即使重试也会保持未知状态(不会自动转为成功)。410 voucher_preparation_expired 表示原准备不可重放。502/503 voucher_upstream_error 表示 ADITUS 票务系统故障。如果商店 API 创建了购物车但未返回其 ID,现有 API 无法自动核对或释放:未知记录会保留,重试时服务器不会创建第二个购物车。开始新尝试前须与 ADITUS 人工核对。session-context 加载失败时微前端显示错误/重试,不会默默创建新购物车。

在集成测试器中实时尝试这三种方式:用 eventSlug 创建会话,选择 startMode 或分类,挂载并观察请求日志。演示系统在 ExperienceDaysv52024 上接受测试代金券 ED24-ADITUS(每次兑换预留门票约 20 分钟;返回会释放它)。 打开集成测试器

可选:匿名浏览,结账时登录

您不必预先知道用户是谁。通过访客会话安装,微前端允许匿名访客浏览事件、选择文章并填充购物车。仅当他们经过购物车(进行注册和结帐)时,它才会要求您通过 onUserRequired 回调建立真正的用户。然后流程的其余部分将继续,并保留购物车。

  1. 1

    通过访客会话安装

    激活商店仍然需要会话token,但它可以是访客会话:只需在没有电子邮件字段的情况下创建它,不需要占位符地址。访客浏览并填充其下方的购物车。

  2. 2

    离开购物车会触发 onUserRequired

    当访问者经过购物车时,微前端会调用您的回调一次并显示中立的待处理状态。对用户进行身份验证(登录/SSO),然后以与第一个会话完全相同的方式创建一个新的、用户绑定的会话:使用您的key进行服务器到服务器。后来的用户也是通过机器对机器的薄荷建立的。秘密永远不会到达浏览器,并且用户永远不会在客户端代码中设置。您的回调仅中继生成的不透明token。至关重要的是,您的后端从其自己的经过身份验证的会话(cookie/JWT)中获取该身份;它决不能接受来自浏览器的电子邮件或用户 ID,否则恶意访问者可能会请求其他人的会话并通过预填充读取他们的个人数据。

  3. 3

    返回新的token,购物车结转

    返回新的会话token,微前端将其交换并将已识别的用户绑定到现有的购物车,购物车、其文章和任何时间段都保持不变。然后就进入注册了。返回 null 以中止,访问者只是停留在购物车上,没有错误。

  4. 4

    对于固定用户省略 onUserRequired

    如果您忽略回调,则经典流程不会发生任何变化:使用用户绑定的会话进行挂载,微前端从第一步起就充当该用户。

page.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@aditus/shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");

// A guest session token is enough to mount: created server-to-server as
// usual, just without the email field (no placeholder address needed). The
// visitor may browse and fill the cart before a specific user is known.
const config: AditusShopConfig = {
  apiBase: "https://developers.aditus.com/api/shop",
  publicKey: "pk_your_site",
  sessionToken: guestSessionToken,

  // Called once as soon as the visitor leaves the cart towards registration.
  // This is where you authenticate the user (login/SSO) and create a new,
  // user-bound session server-to-server.
  // Return the fresh token -> the micro-frontend adopts it and rebuilds the
  // cart under the identified user (articles are kept). Return null to
  // cancel -> the visitor stays in the cart, without an error.
  onUserRequired: async () => {
    // Calls only your backend, with no identity in the payload. Your backend reads
    // the signed-in user from its own session (cookie/JWT) and creates the session
    // server-side for exactly this identity. Never pass the email from the browser,
    // otherwise an attacker could insert someone else's address here and reach
    // that person's personal data through the prefill.
    // host.loginAndMintSession(): Promise<string | null> is your own function.
    // Implement login/SSO + authenticated backend call + server-side session creation on your website.
    // On cancel the function returns null; on failure it throws explicitly.
    return host.loginAndMintSession();
  },
};

const handle: ShopHandle = mount(el, config);

身份验证在您的网站上进行

ADITUS 不会对您的最终用户进行身份验证。您的网站是用户身份的唯一真实来源,您运行自己的登录、SSO 或会话。 ADITUS 仅信任后端使用key创建的会话token。信任链是:您的身份验证证明身份,您用密钥签发的令牌为其提供担保,微前端呈现它。

  1. 1

    您的网站对用户进行身份验证

    登录、SSO、会员会话,完全属于您自己。 ADITUS 在此不涉及。

  2. 2

    你的后端创建一个会话

    使用key的服务器到服务器。您传递微前端应充当的角色(电子邮件)并将其绑定到您的公钥。

  3. 3

    页面挂载微前端

    不透明token被传递给浏览器并作为 sessionToken 传递给 mount()。秘密保留在您的服务器上。

  4. 4

    每次调用都会携带会话

    微前端在每个请求上发送 X-Aditus-Session;商店 API 从会话中解析购物车的用户。

  5. 5

    无token → 微前端保持不活动状态

    如果没有会话,微前端永远不会激活:它会显示中立的通知,并且不会发出任何呼叫,没有购物车,没有结账。创建一个会话以将其打开。

1. 在您的后端创建会话

server.ts
// Server side of your website. The secret key stays on the server and never reaches the browser.
// You build user and the login/session route between the browser and your server yourself; ADITUS does not provide them.
// Your backend has already authenticated the user itself (login/SSO/session).
// Important: user comes from this server session, never from the browser's
// request body. Otherwise an attacker could slip in someone else's email.
const res = await fetch("https://developers.aditus.com/api/shop/session", {
  method: "POST",
  headers: {
    // Your client's mint secret (generated in the ADITUS admin), server-side only.
    Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    publicKey: "pk_your_site",        // binds the session to your key (required)
    email: user.email,                 // who the micro-frontend acts as (omit = anonymous guest session)
    externalUserId: user.id,           // optional metadata (your user id)
    // sharedCartKey: visitor.stableRef, // required for anonymous shared carts (default):
    // opaque reference from your trusted server session, never browser input
    ttlSeconds: 1200,                  // optional: lifetime (default 20 min, max 24 h)
    eventSlug: "ExperienceDaysv52024", // optional: start directly in this event (otherwise the event overview)
    // startMode: "voucher",           // optional: start with the voucher/ticket code form (without eventSlug after the event selection)
    // assortment: "Pressetickets",    // optional (with eventSlug): only this assortment (id or name)
    // voucherCode: req.query.code,    // optional: redeem a code from a voucher link in the embed
    // redeemVoucherOnMint: true,      // optional: reserve it here instead (requires email, eventSlug and Idempotency-Key)
  }),
});

// Choose the response type freely and pass sessionToken to the browser (inline or via fetch).
const { sessionToken }: { sessionToken: string; expiresAt: number } = await res.json();

2. 使用token挂载

page.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@aditus/shop-embed";

// getElementById can return null. Check it properly in TypeScript instead of using "!".
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");

// AditusShopConfig types every option and gives you autocompletion.
const config: AditusShopConfig = {
  apiBase: "https://developers.aditus.com/api/shop",
  publicKey: "pk_your_site",   // required as soon as a session is involved
  sessionToken,                // issued by your backend, keep it in memory only
  cartScope: "shared",          // default; requires a trusted server identity
};

const handle: ShopHandle = mount(el, config);

// The micro-frontend sends the X-Aditus-Session header with every shop call.
// On logout, revoke the session on the server and remount/update.

3.注销时撤销

server.ts
// Server side, e.g. when your user logs out.
// You build the logout route between the browser and your server yourself; only your backend calls ADITUS.
await fetch(`https://developers.aditus.com/api/shop/session/${sessionToken}`, {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}` },
});
// { revoked: true }

完成的订单返回到您的页面

门票购买完成后,您不需要单独查找订单。因为微前端在您的页面中本地运行(轻量 DOM,而不是 iframe),所以它通过 onComplete 回调将完成的订单直接返回给您的代码,直接 JS 调用,没有 postMessage。微前端已经为您解决了订单,因此您收到订单号和票证链接,而不仅仅是 ID。

mount.ts
import {
  mount,
  type ShopHandle,
  type CompletedOrder,
} from "@aditus/shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_your_site",
  sessionToken,
  // Called exactly once as soon as the order is placed.
  // The micro-frontend has already resolved the order for you. You get
  // the resolved order, not just an id.
  onComplete: (order: CompletedOrder) => {
    order.id;       // order GUID for further processing (server, reconciliation)
    order.number;   // order number for display
    order.status;   // order status
    order.buyer;    // buyer (name / email), if present
    order.tickets;  // [{ name, links: [{ kind: "pdf" | "apple" | "google", url }] }]

    // -> store order.id, show a confirmation, trigger DOI / newsletter ...
  },
  // Called on every unrecoverable error in the flow.
  onError: (err: Error) => {
    console.error("Shop error:", err.message);
  },
});

您收到什么

每次成功结账时,onComplete 都会触发一次并处理已解决的订单:id(订单 GUID,用于后续处理的键)、number(用于显示的订单号)、状态、买家(姓名和电子邮件(如果存在))和门票,每张门票都带有类型为“pdf”、“apple”或“google”的键入链接。 onError 在流程中发生任何不可恢复的错误时触发。

重定向付款方式

留在页面上的方法(发票、预付款、内联卡)会立即触发 onComplete。将浏览器发送到外部支付页面(例如 Saferpay)的方法由微前端端到端处理:它从其运行的页面派生返回 URL,将用户发送出去,并在返回时完成订单并触发 onComplete,实现一个零代码的完整往返。仅当返回必须到达不同页面时,您才覆盖redirectUrls.successUrl / cancelUrl / errorUrl。

GA4/电子商务活动(可选)

微前端可以向您的分析报告购物渠道,但前提是您要求这样做。它不捆绑任何 gtag、GTM 和 GA SDK,不加载任何内容,不设置 cookie,也不自行发送任何内容。您添加一个可选回调 onEvent,并接收已在 Google 的 GA4 增强型电子商务架构中形成的类型化事件。省略 onEvent 则不会发生任何变化。您提供哪种工具以及您是否同意发送,完全取决于您。

mount.ts
import {
  mount,
  type ShopHandle,
  type ShopAnalyticsEvent,
} from "@aditus/shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_your_site",
  sessionToken,
  // Optional. If you leave out onEvent, nothing changes: the micro-frontend
  // sends nothing, loads no gtag/GTM/GA SDK and sets no cookies.
  // Tool, consent and mapping stay entirely with you. This example uses GA4 via gtag.
  onEvent: (event: ShopAnalyticsEvent) => {
    // Consent is yours: only send once the user has agreed.
    if (!hasAnalyticsConsent()) return;
    // The events already follow the GA4 schema (name + items/value/currency ...),
    // so pass them to gtag 1:1.
    const { name, ...params } = event;
    window.gtag?.("event", name, params);
  },
});

已处于 GA4 形状

每个事件都带有 GA4 事件名称及其标准参数:商品(包含 item_id、item_name、价格、数量、item_category,以及折扣和优惠券(如果适用))、价值和货币; add_ payment_info 添加 payment_type ,purchase 添加 transaction_id 。解构 name 并将其余部分直接传递给 gtag("event", name, params),无需重新映射。

同意权归您所有

onEvent 只是我们直接调用的页面中的一个普通 JavaScript 函数。微前端本身不会在任何地方发送分析,在您的处理程序发送之前,没有任何内容离开页面,因此您的同意管理保持完全控制:同意转发,路由到 GTM 的 dataLayer 而不是 gtag、批处理或完全删除事件。您的处理程序抛出的任何错误都会被捕获并隔离,因此它永远不会中断用户流程,保持处理程序轻量级(gtag/dataLayer 推送),因为它内联运行。

事件触发时
view_item_list用户看到文章列表(每个显示的集合触发一次)。
add_to_cart文章或附加组件已添加到购物车。
remove_from_cart购物车系列或附加组件被删除。
view_cart显示购物车视图(每个购物车一次)。
begin_checkout用户离开购物车进行注册/结帐。
add_payment_info选择付款方式(携带 payment_type)。
purchase订单已下达(包含 transaction_id、value、items)。

事件总线:handle.on/handle.off(可选)

除了配置回调之外,mount() 返回的句柄还带有一个小型的、完全可选的事件总线。使用handle.on(event,listener)订阅(它返回匹配的取消订阅函数),并使用handle.off(event,listener)分离。如果您从不调用 on(),则不会发生任何变化:总线不会添加任何依赖项,不会发送任何内容,也不会产生任何费用。它是对您的页面的纯粹观察,迷你购物车徽章、确认横幅、您自己的日志记录,它补充了回调而不是取代它们。

mount.ts
import {
  mount,
  type ShopHandle,
  type ShopCartUpdate,
} from "@aditus/shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount target #aditus-shop not found");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_your_site",
  sessionToken,
});

// Optional. Without on() the micro-frontend works completely normally.
// The bus is pure observation; no event is required.
// on() returns the unsubscribe function; alternatively handle.off(name, fn).
const offCart = handle.on("cart:update", (cart: ShopCartUpdate) => {
  // On every cart change: count, total, line items (GA4 item shape).
  updateMiniCartBadge(cart.itemCount); // e.g. a badge in the page header
});

handle.on("checkout:complete", ({ order }) => {
  // The order is placed. You receive the same resolved order as in onComplete.
  showConfirmationBanner(order.number);
});

handle.on("analytics", (event) => {
  // Mirror of the GA4 funnel (identical payloads to config.onEvent).
  console.debug("Funnel:", event.name);
});

// Unsubscribe selectively later; the rest stays active:
offCart();
事件触发时
cart:update购物车发生变化:创建、添加或删除项目、或清空。包含 cartId、itemCount、价值、货币和行作为 GA4 形状的项目。
phase:change可见步骤已更改。事件包含 phase(events · home · articles · voucher · cart · registration · legitimation · buyer · payment · summary · done,与 data-step 词汇相同)、previous(入口步骤时为 null)和 startPhase(会话的入口步骤)。您的页面可据此驱动自己的“返回/重新开始”按钮,而无需读取 CSS hook。
checkout:complete订单已下达。在 config.onComplete 之后立即触发,携带同一个已解析的订单对象,且发生在微前端显示其完成页面之前。同步调用 preventDefault() 即可改为显示您自己的完成页面(参见“您自己的完成页面”)。
analytics上面 GA4 漏斗的镜像:每个 onEvent 有效负载也会在总线上发出,保持不变。

监听器在 update() 中幸存

订阅存在于句柄上,而不是渲染上:handle.update({ ... }) 重新渲染微前端,但保持每个侦听器附加。通过handle()卸载会自动分离所有监听器。当你想在商店继续运行时停止监听时,你只需要off()(或返回的取消订阅)。

隔离,永不阻塞

抛出异常的监听器会被捕获并隔离,它不会破坏用户流程,也不会影响其他监听器。 “analytics”事件镜像 GA4 漏斗,其有效负载与 config.onEvent 相同,因此您可以通过总线、回调或两者使用漏斗;同样的同意规则适用:在您的代码发送页面之前,不会有任何内容离开页面。

您自己的完成页面

checkout:complete 在微前端显示其自身完成页面之前触发。如果您的页面显示自己的确认内容,请在监听器中同步调用 preventDefault():在 <aditus-shop> 元素上调用 event.preventDefault(),使用 handle.on() 时调用载荷中的 preventDefault(),或调用 config.onComplete 第二个参数中的 completion.preventDefault()。此后微前端不显示完成页面、不滚动,也不再渲染任何步骤,由您的页面接管。订单在任何情况下都已下达。调用必须在监听器内直接进行,不能在 await 之后。未调用时,微前端照常显示其完成页面。这同样适用于从外部支付页面返回的情况。

随后结束会话。订单已完成,因此您的服务器可以在同一步骤中撤销会话:使用您的密钥调用 DELETE /api/shop/session/:token(参见“撤销会话”)。已下达的订单以及订单对象中的票券链接仍然有效,因为这些链接不依赖于会话。如需再次购买,请创建新会话并重新挂载微前端。微前端不会自行结束会话,因为撤销需要您的密钥。只有当您的页面有意在同一会话中继续销售时,才保留该会话。

命令:goTo 和 back

除了观察之外,您的页面还可以向微前端发送两个范围很窄的请求。商店切换了步骤时两者都返回 true,否则返回 false;它们从不重新挂载,也不会丢失购买流程。goTo("cart") 为您自己的迷你购物车显示购物车步骤;仅在登记步骤且尚未写入任何内容时才会被接受。back() 后退一步,与商店自身的返回按钮完全相同。凡是该按钮不存在或被禁用的地方都会返回 false:入口步骤(startPhase)、完成页面,以及支付或其他更改进行期间。两者都可在挂载句柄和 <aditus-shop> 上使用(element.back()、element.goTo("cart"))。

browser-back.ts
import type { ShopHandle } from "@aditus/shop-embed";

// Optional: the browser's back gesture goes one step back in the shop.
// The micro-frontend itself never writes to the browser history. Your page
// keeps its own history entry while the shop is past its entry step.
export function connectBrowserBack(shop: ShopHandle): () => void {
  let inside = false; // after the entry step and before the completion page
  let entry = false; // our history entry is on top
  const offPhase = shop.on("phase:change", ({ phase, startPhase }) => {
    inside = phase !== startPhase && phase !== "done";
    if (inside && !entry) {
      history.pushState({ aditusShop: true }, "");
      entry = true;
    } else if (!inside && entry) {
      entry = false;
      history.back(); // entry no longer needed
    }
  });
  const onPopState = () => {
    if (!entry) return; // normal navigation of your page
    entry = false;
    // false: the shop's back button is missing or locked (e.g. payment in progress).
    if (!shop.back() && inside) {
      history.pushState({ aditusShop: true }, "");
      entry = true;
    }
  };
  window.addEventListener("popstate", onPopState);
  return () => {
    offPhase();
    window.removeEventListener("popstate", onPopState);
  };
}

// Your own mini cart: back to the cart step, otherwise scroll to the shop.
export function showCart(shop: ShopHandle, container: HTMLElement): void {
  if (!shop.goTo("cart")) container.scrollIntoView({ behavior: "smooth" });
}

微前端从不自行写入浏览器历史记录:URL 和路由器属于您的网站。示例在商店越过入口步骤期间恰好保留一条额外的历史记录,因此浏览器的返回手势(包括 iPhone 上的滑动)会先在商店内后退一步,只有在入口步骤时才离开页面。如果您的网站使用客户端路由器(React Router、Next.js、Vue Router),请通过路由器而不是 history.pushState 创建该记录,以免路由器将其视为外部导航。

公钥与会话token

将两者分开。他们回答不同的问题并且是配对的,不能互换。

publicKey: 起源信托

可发布。它包含在您的客户端代码中,因此它不是秘密,并且本身不会授予任何内容。后端强制执行每个key域白名单:key只能在其注册域中使用。它回答“这是哪个网站?”只有运营方为单个客户端显式设置的(有意不安全的)选项才能解除该白名单;即便如此,激活状态、会话要求和密钥仍然有效。

sessionToken: 身份

在您的后端使用密钥创建,不透明且短暂。它回答“用户是谁?”会话绑定到为其创建的公钥:会话请求也必须发送该key,并且不匹配将被拒绝(403)。

安全规则

  • key仅存在于您的服务器上,而不会存在于发送到浏览器的客户端代码、捆绑包或环境文件中。
  • 不要自行将令牌保存在 localStorage 或 Cookie 中;请将其保存在内存中,并通过创建新会话来续期。为了支持页面重新加载和支付往返,微前端自身会将其保存在该标签页的 sessionStorage 中。
  • token的有效期很短(默认 20 分钟,最长 24 小时)并且是单用户的。
  • 注销时撤销会话:被撤销的令牌会被立即拒绝(401)。在过期或撤销之前,有效令牌就是 bearer 令牌,请如此对待。
  • 始终将会话与其公钥配对,丢失或不匹配的key将被拒绝 (403)。
  • 无效或过期的会话会返回 401,不会回退到其他用户。

会话令牌可以做什么

会话令牌是一种 bearer 令牌。以下是 ADITUS API 对其强制执行的确切范围,可用于你的威胁模型和安全评审。

令牌与有效期
256 位随机、不透明、不携带任何数据且无法解码。默认 20 分钟,创建会话时可配置为 1 分钟到 24 小时。会话永不延长。通过创建新会话来续期。
绑定
绑定到创建会话时所用的 publicKey。每个会话请求必须发送完全相同的 key,且请求来源会与该 key 的域名白名单比对。其他 key 或外部来源将被拒绝(403)。
匿名会话
不带用户创建:可浏览活动、创建并编辑自己的购物车。注册、时段、支付分配和结账在服务器端被拒绝(403),直到您的页面换入已识别的会话。
已识别会话
额外可:注册、时段、支付分配和结账,始终限于本会话拥有的购物车。启用 protectUserData 时,为该用户存储的购买者数据受保护:会更改这些数据的写入将被拒绝(403)。
购物车所有权
购物车通过带密钥的哈希(HMAC)绑定到其所有者:匿名会话绑定令牌本身,已识别会话绑定你后端创建会话时设定的用户身份,均按 publicKey 划分。其他用户的会话无法访问,对同一购物车的写入按顺序执行。
个人数据
会话上下文返回标志、ID 以及你自己后端创建会话时设定的用户标识,没有任何您的网站原本不知道的内容。购买者数据仅可通过经过所有权检查的购物车和注册路由获取。
速率限制
默认值:创建与撤销每客户端每 IP 每分钟 300 次;地址、公司和城市自动补全每会话每分钟 60 次,且每客户端(所有会话合计)每分钟 3000 次。每个限制都是您客户端的独立配额,其他客户端不会影响它。如有需要,ADITUS 可为您的客户端单独设置限制。
审计日志
发往商店 API 的请求会被记录并保留 90 天:客户端、规范化路由、状态、来源主机、会话令牌的缩短哈希以及购物车或订单 ID。绝不记录令牌本身、电子邮件或 IP 地址。
撤销
DELETE /api/shop/session/:token 会立即使会话失效,中间没有缓存;使用该令牌的下一个请求即返回 401。请在注销时调用。

端点和响应参考

POST/api/shop/session使用 Bearer 密钥创建会话
DELETE/api/shop/session/:token使用 Bearer 密钥撤销会话

这两个调用都来自您的后端:后端使用您客户端的 mint secret(在 ADITUS 管理控制台中生成)作为 Bearer 令牌进行身份验证。每次登录创建一个会话是常规做法;浏览器只会看到生成的令牌。没有后端的网站不使用这些端点:元素会通过限定域名的 POST /api/shop/embed-session 自行创建浏览器会话(public-key 配合 cart-scope="tab")。

您可以使用您自己的凭据尝试这个确切的流程集成测试仪。创建会话、挂载商店并体验购买流程。

创建会话:请求

标头价值意义
AuthorizationBearer <mint-secret>您客户的薄荷秘密,在 ADITUS 管理控制台中生成。仅限服务器端,绝不能将其发送到浏览器或应用程序包。
Content-Typeapplication/json正文是一个 JSON 对象。
Idempotency-Keystring · 8–200 visible ASCII chars · required with redeemVoucherOnMintredeemVoucherOnMint 为 true 时必填:8–200 个可见 ASCII 字符。为一次逻辑兑换尝试在服务器上保存一个稳定密钥;重试时使用相同的密钥和完全相同的请求。同一密钥搭配不同请求会返回 409 idempotency_conflict。不要从浏览器发送 mint secret 或此密钥。
体场类型意义
sharedCartKeystring · 可选服务器到服务器创建的已识别会话通常通过可信身份共享。匿名共享购物车必须由您的后端使用 sharedCartKey 创建会话:它是从您自己的可信访客会话获取的不透明、稳定访客引用,不是电子邮件。它仅为服务器会话字段,不是浏览器配置或 HTML 属性。绝不接受浏览器任意提交的电子邮件、身份或 sharedCartKey。单靠浏览器自动创建会话无法安全共享;shared 缺少可信身份时返回明确的不支持错误,省略范围时也如此。显式 'tab' 隔离不需要 sharedCartKey;不会静默降级。
publicKeystring · 必填会话创建的可发布key。它必须与微前端安装的key相同,以后的每个商店调用都会根据它进行检查(如果不匹配则为 403)。承载key必须属于该key的客户端。
emailstring · 必填会话充当的商店用户。从您经过身份验证的服务器会话(登录/SSO cookie 或 JWT)中获取它,切勿从浏览器请求中获取,否则访问者可能会获取其他人的会话。省略它以创建匿名会话:访问者可以浏览并填充购物车,但微前端会阻止经过购物车的步骤,直到您的 onUserRequired 回调提供用户绑定会话(请参阅可选用户流程)。存在但格式错误的电子邮件仍会被拒绝 (400)。
externalUserIdstring · 可选来自您的网站已验证服务器会话的稳定用户引用。保存在会话中,并在提供时用于身份范围;续期时不要更改,也不要接受浏览器任意输入。它本身不是身份验证。
ttlSecondsstring · 必填会话生命周期(以秒为单位)。默认 20 分钟,上限为 24 小时。当它过期时,shop 调用返回 401 session_expired。此时请签发新令牌;在配置中设置 refreshSession 后,微前端会自动请求新令牌。
eventSlugstring · 必填在服务器端固定旅程的入口:设置后,微前端直接在该事件的文章选择上启动;省略,从事件概述开始。因为它是创建会话的一部分,所以浏览器无法操作它。无效字符产生 400 invalid_event_slug;slug 不匹配任何在线活动(404 event_not_found)或活动已关闭/未在售(409 event_not_sellable)时,创建会故意失败,不签发会话,也不回退到活动概述。
startModestring · 必填旅程入口视图。"articles"(默认)打开文章选择;"home" 打开活动首页及其入口(门票、代金券、已发布的商品组);"voucher" 先打开代金券/票码表单。代金券表单的行为与“兑换代金券 / 登记门票”中所述相同。没有 eventSlug 时先显示活动选择,随后该模式应用于所选活动。
assortmentstring · 必填将文章入口限制为固定活动的一个分类(需要 eventSlug;startMode 为 "voucher" 时无效)。提供分类 id 或其确切名称(不区分大小写);允许隐藏分类。没有匹配项的值会在微前端中显示明确错误,而不是静默回退到完整列表。最多 200 个字符,否则返回 400 invalid_assortment。
voucherCodestring · 必填随会话传递的代金券/票码(两种启动模式)。默认在新的流程中由微前端兑换;如果未固定活动,先选择活动。拒绝原因显示在表单或首个购物车通知中。设为 redeemVoucherOnMint: true 时,对于固定活动和已识别用户,服务器会在响应前预留该代码。长度为 1–100 个字符,不含空白或 URL 分隔符,否则返回 400 invalid_voucher_code。
redeemVoucherOnMintstring · 必填仅在服务器到服务器的会话创建请求同时提供 voucherCode、固定 eventSlug、已识别的 email 和 Idempotency-Key 时设为 true。服务器会先创建用户绑定的购物车并实际预留代金券,再签发会话。微前端直接接管该购物车,不重复创建或兑换;两种启动模式均适用。仅支持代金券/票码,不支持 PromotionCode。不设置该选项时,使用常规的 voucherCode 移交方式。仍需完成注册和结算。

创建会话:响应 (200)

场地类型意义
sessionTokenstring不透明token(sess_…)。交给浏览器,作为sessionToken传给mount();微前端在每次商店调用时将其作为 X-Aditus-Session 发送。它不包含用户数据并且无法解码。
expiresAtnumber过期时间为 Unix 时间戳(以毫秒为单位)。纯粹为您自己的调度提供信息。微前端自行对 401 做出反应。
voucherRedeemedboolean · only with redeemVoucherOnMint可选的代金券预留会话创建成功时为 true。返回的会话已经拥有包含预留的购物车,微前端直接复用。门票尚未完成注册。

撤销会话

删除具有相同承载key的 /api/shop/session/:token。该key必须属于为其创建会话的客户端。在注销时调用它,以便token随着您自己的会话而消失。响应:{ "revoked": true } (如果会话已过期,则为 false)。撤销是幂等的并且可以安全地“一劳永逸”。

服务器到服务器读取

驱动微前端的后端(自助终端外壳、现场商店)通常在创建会话之前就需要租户信息:有哪些活动、哪些在售、商店支持哪些语言、适用哪些查找列表和品牌。四个读取端点使用与创建会话相同的凭据(Bearer mint secret + X-Aditus-Public-Key 标头,相同的速率限制,Cache-Control: no-store)回答这些问题。它们是 Shop API 的白名单投影,绝非原样转发;?language=(或 Accept-Language,默认 de)选择名称的语言。错误:400 invalid_public_key、401 unauthorized、503 shop_api_not_configured、502 upstream_error。

端点返回
GET /api/shop/events?language=de租户的所有活动:id、slug、name、year、start/end、location、saleState(open · registration_only · closed)、原始标志、showInOverview、languages、图标/徽标 URL。创建会话所需的 slug 从这里选取。
GET /api/shop/configuration租户信息:mandator、查找列表名称(称谓、国家、头衔、Cookie 同意)和 websiteMetricTracking。租户的登录提供商凭据有意不包含在内。
GET /api/shop/languagessystemLanguage 加上商店语言列表({ code, name }),即微前端 culture 属性接受的值。
GET /api/shop/branding?images=ONSITE_SHOP_LOGO,…租户的 Shop 主题:styles 为扁平的键→值映射、带 URL 的主题图片,以及(对于你指定的最多 40 个图片键(A–Z、0–9、_、-))每个键是否存在、其公开 URL 和内容类型。图片 URL 无需认证,可直接在外壳中使用。 加上 &eventSlug=<slug> 或 &event=<GUID> 可将主题和键限定到一个活动(活动级覆盖、仅活动拥有的键如 ONSITE_SHOP_HEADER / ONSITE_SHOP_ATTRACT_*);响应会注明解析到的活动。未知 slug → 404 event_not_found,非 GUID → 400 invalid_event,Shop API 不可用 → 503 event_unavailable。

错误是明确的

地位代码意义
503session_not_configured此客户端未配置嵌入会话。
401unauthorized用于创建会话的 secret 无效或缺失。
400invalid_public_key提供的 public key 无效。
400invalid_email提供的电子邮件地址无效。
400invalid_token提供的会话 token 无效。
429rate_limited请求过多,请稍后重试。
403public_key_invalid提供的 public key 格式无效。
403public_key_unknown无法识别提供的 public key。
403public_key_origin_unresolved无法确定此请求的来源。
403public_key_domain_not_allowed此 public key 不允许来自该域名的请求。
401invalid_session提供的会话无效。
401session_expired提供的会话已过期。
403session_requires_public_key此会话需要 public key。
403session_key_mismatchpublic key 与会话不匹配。
403anonymous_session匿名会话无法执行此操作。

现在让它成为你的

身份已确定。接下来,为微前端打造品牌。样式工具生成一个可立即粘贴的主题配置,并通过实时预览解释每个参数。

打开样式工具

正在使用编程代理?请把机器可读的微前端集成指南交给它。复制下方的规范 URI 并粘贴给你的代理。

https://developers.aditus.com/ai/micro-frontend.md