# 追觅 Shopify App 最终方案与上线说明 > 本文件是当前代码目录的最终版方案和上线主口径。 > 历史材料包括 `README.md`、`docs/shopify-deployment-runbook.md` 以及 `初始化需求/` 下多份需求澄清文档;后续对业务、开发、测试和上线沟通时,以本文为准。 ## 1. 当前结论 追觅 Shopify App 按 **Shopify Remix App + Checkout UI Extension + 后端同步触发** 实现。 当前版本可以进入开发/测试环境验证,但还不能直接生产上线。生产上线前必须完成真实 Shopify app link、真实店铺安装、生产数据库方案、第三方接口联调和真实测试订单验收。 锁定口径: | 项目 | 最终口径 | | --- | --- | | App 类型 | Custom app,当前按单店交付 | | 页面范围 | v1 只做 Thank You Page | | Extension target | `purchase.thank-you.block.render` | | 默认展示位置 | `default_placement = "ORDER_STATUS1"`,商家仍可在 checkout and accounts editor 移动 | | 前端职责 | 只取 `orderId/orderNumber/locale` 和 session token,调用后端 | | 后端职责 | 校验 token、查订单、读配置、签名、调第三方、写日志和幂等 | | 配置方式 | app-owned Metaobject `$app:dreame_activity` | | 第三方成功码 | 通过 Metaobject 可配置,默认 `0` | | 幂等 | `shop + orderId + activityId` 唯一,不重复推送 | | 失败行为 | 返回 `success=false`,前端不显示按钮,不影响原生 Thank You Page | ## 2. 最终业务流程 1. 顾客完成支付后进入 Shopify Thank You Page。 2. Checkout UI Extension `purchase.thank-you.block.render` 加载。 3. 前端读取订单确认信息中的 `orderId/orderNumber/locale`。 4. 前端通过 `sessionToken.get()` 获取 Shopify checkout session token。 5. 前端调用后端 `POST /api/action/activity/check`。 6. 后端校验 session token,并通过 Admin GraphQL 查询完整订单。 7. 后端读取 `$app:dreame_activity` Metaobject entry。 8. 后端检查订单级幂等;已成功推送则直接返回按钮数据,不重复调用第三方。 9. 后端签名调用第三方接口①活动状态接口。 10. 活动有效后,后端生成 UUID v4 `event_id`,签名调用第三方接口②订单完成事件推送。 11. 推送成功后写日志和幂等记录。 12. 后端返回 `success=true + redirectUrl + buttonText`。 13. 前端显示按钮,点击后跳转到配置的 `redirect_url`。 ## 3. 系统组件 | 组件 | 位置 | 职责 | | --- | --- | --- | | Shopify App 后端 | `app/` | Remix 后端、Shopify OAuth/session、业务 API、订单查询、配置读取、第三方调用 | | Thank You Extension | `extensions/thank-you-activity/` | Thank You Page 按钮展示和后端调用 | | 配置服务 | `app/services/config.server.ts` | 读取 `$app:dreame_activity` Metaobject | | 主流程服务 | `app/services/activity.server.ts` | 状态检查、推送、幂等、返回按钮数据 | | 第三方 client | `app/services/dreame-client.server.ts` | mock/live 第三方接口调用 | | 签名服务 | `app/services/signature.server.ts` | `MD5(bodyString + timestamp + api_secret)` | | 日志服务 | `app/services/logger.server.ts` | 写入调用日志并避免敏感信息外泄 | | 幂等服务 | `app/services/idempotency.server.ts` | 防止同一订单重复推送 | | Prisma 模型 | `prisma/schema.prisma` | `Session`、`ActivityPushLog`、`ActivityPushIdempotency` | ## 4. 后端接口契约 接口: ```text POST /api/action/activity/check ``` 请求头: ```text Authorization: Bearer Content-Type: application/json ``` 请求体: ```json { "orderId": "gid://shopify/Order/1234567890", "orderNumber": "#1001", "locale": "zh-CN" } ``` 成功响应: ```json { "success": true, "redirectUrl": "https://activity.example.com", "buttonText": "领取奖励" } ``` 失败响应: ```json { "success": false } ``` 说明: - 前端不传 `api_secret`。 - 前端传入的订单字段只作为最小定位信息,不作为可信业务数据。 - 完整订单数据由后端通过 Shopify Admin GraphQL 查询。 - CORS 对 Checkout Extension 放开,来源可信由 session token 校验负责。 ## 5. Shopify 配置 ### 5.1 App scopes 当前 scopes: ```text read_orders,read_metaobjects,write_metaobjects,write_metaobject_definitions ``` 用途: | Scope | 用途 | | --- | --- | | `read_orders` | 后端查询订单 email、customerId、currency、order number 等字段 | | `read_metaobjects` | 后端读取活动配置 | | `write_metaobjects` | setup 脚本写入/更新配置 entry | | `write_metaobject_definitions` | Shopify CLI 部署 app-owned Metaobject definition | ### 5.2 Extension target 文件: ```text extensions/thank-you-activity/shopify.extension.toml ``` 当前配置: ```toml [[extensions.targeting]] module = "./src/Checkout.tsx" target = "purchase.thank-you.block.render" default_placement = "ORDER_STATUS1" ``` 说明: - `purchase.thank-you.block.render` 是当前最终 target。 - `ORDER_STATUS1` 是默认推荐位置,不等于强制固定位置。 - 商家可在 Shopify checkout and accounts editor 中移动该 block。 - 若业务要求固定 header 位置,需要改 target、改代码并重新部署,不是业务配置项。 ## 6. Metaobject 配置 当前采用 app-owned Metaobject: | 项目 | 值 | | --- | --- | | TOML key | `[metaobjects.app.dreame_activity]` | | Admin API type | `$app:dreame_activity` | | Handle | `dreame-activity-config` | | Admin access | `merchant_read_write` | | Storefront access | `none` | 字段: | Key | Type | Required | 默认值 | 说明 | | --- | --- | --- | --- | --- | | `activity_id` | `number_integer` | yes | - | 追觅活动 ID | | `api_app_id` | `single_line_text_field` | yes | - | 第三方接口 app id | | `api_secret` | `single_line_text_field` | yes | - | 第三方签名 secret,仅后端读取 | | `status_api_url` | `url` | yes | - | 接口①活动状态地址 | | `push_api_url` | `url` | yes | - | 接口②推送地址 | | `status_success_code` | `number_integer` | no | `0` | 接口①成功 code | | `status_active_required` | `boolean` | no | `true` | 接口①是否必须 `data.status=true` | | `push_success_code` | `number_integer` | no | `0` | 接口②成功 code | | `redirect_url` | `url` | yes | - | 前端按钮跳转地址 | | `button_text` | `json` | yes | - | 多语言按钮文案 | | `shop` | `single_line_text_field` | no | myshopify domain | 覆盖传给第三方的 `properties.shop` | `button_text` 示例: ```json { "en": "Claim Your Reward", "zh-CN": "领取奖励", "zh-TW": "領取獎勵", "de": "Belohnung erhalten", "fr": "Réclamer votre récompense" } ``` 兼容说明: - 新环境默认使用 `$app:dreame_activity`。 - 若目标店铺已经存在旧的 merchant-owned type `dreame_activity`,可通过环境变量 `METAOBJECT_TYPE=dreame_activity` 兼容。 - 不建议新环境继续创建 merchant-owned `dreame_activity`。 ## 7. 第三方接口与签名 所有第三方请求都由后端发起,前端不直接调用第三方。 请求头: ```text dreame-api-app-id: dreame-api-timestamp: dreame-api-sign: ``` 签名算法: ```text sign = MD5(bodyString + timestamp + api_secret) ``` 实现要求: - `bodyString` 必须是实际发送的 JSON 字符串。 - 同一个 `bodyString` 同时用于签名和 `fetch` body。 - 禁止签名后再次序列化 payload。 - `api_secret` 不进入前端、不进入响应体、不进入日志。 接口①活动状态: ```json { "activity_id": 12 } ``` 默认继续条件: ```text HTTP 2xx && code == status_success_code && data.status == true ``` 如果 `status_active_required=false`,则不再要求 `data.status == true`。 接口②推送订单完成事件: ```json { "activity_id": 12, "event_type": "order_end", "created_at": 1776901800000, "user_id": "99999", "email": "customer@example.com", "event_id": "uuid-v4", "properties": { "shop": "demo-store.myshopify.com", "currency": "USD", "order_id": "123456", "order_number": "#1001" } } ``` 默认成功条件: ```text HTTP 2xx && code == push_success_code ``` ## 8. 幂等和日志 幂等规则: | 项目 | 口径 | | --- | --- | | 幂等粒度 | `shop + orderId + activityId` | | 命中后行为 | 不重复调用第三方接口②,仍返回 `success=true + redirectUrl + buttonText` | | event_id | 首次成功推送时生成 UUID v4 并落库 | 数据库表: | 表 | 用途 | | --- | --- | | `ActivityPushLog` | 记录成功/失败调用、请求体、响应体、错误信息 | | `ActivityPushIdempotency` | 记录成功推送的订单和活动,防止重复推送 | 日志要求: - 不保存 `api_secret`。 - 不保存 `dreame-api-sign`。 - 失败需要记录可排查的 `errorMessage`。 - 日志保留天数由 `LOG_RETENTION_DAYS` 控制,默认 `7`。 ## 9. 本地开发说明 前置: - Node.js 20 或更高版本。 - pnpm/corepack 可用。 - Shopify CLI 可用。 安装和验证: ```bash cd "/Users/jason/Downloads/myproject/Shopify 插件开发/dreame-action" corepack pnpm install corepack pnpm prisma:generate corepack pnpm prisma:migrate corepack pnpm test corepack pnpm typecheck corepack pnpm build ``` 本地环境变量: ```env SHOPIFY_API_KEY= SHOPIFY_API_SECRET= SHOPIFY_APP_URL= SCOPES=read_orders,read_metaobjects,write_metaobjects,write_metaobject_definitions DATABASE_URL="file:./dev.sqlite" THIRD_PARTY_MODE=mock THIRD_PARTY_TIMEOUT_MS=5000 LOG_RETENTION_DAYS=7 METAOBJECT_TYPE=$app:dreame_activity METAOBJECT_HANDLE=dreame-activity-config DREAME_MOCK_STATUS_CODE=0 DREAME_MOCK_STATUS_ACTIVE=true DREAME_MOCK_PUSH_CODE=0 ``` 启动开发: ```bash corepack pnpm dev ``` 说明: - 默认 `THIRD_PARTY_MODE=mock`,不会真实调用第三方。 - 切换真实第三方接口前,必须先配置 Metaobject entry 和第三方密钥。 ## 10. 上线前置条件 上线前必须准备: | 类别 | 前置项 | | --- | --- | | Shopify | 真实测试/生产 Shopify app、Client ID、App secret | | 店铺 | 目标店铺安装权限、测试订单能力、非 Starter 套餐 | | 域名 | 后端公网 HTTPS 域名 | | 数据库 | 生产数据库连接串和迁移窗口 | | 第三方 | 接口①②正式/测试地址、`api_app_id`、`api_secret`、成功码规则、可验签样例 | | 配置 | `$app:dreame_activity` definition 和 `dreame-activity-config` entry | | Extension | Checkout UI Extension 已部署,`app_url` setting 指向后端域名 | | 验收数据 | 成功订单、失败订单、活动关闭、重复刷新等测试场景 | 生产数据库特别说明: - 当前 `prisma/schema.prisma` datasource provider 是 `sqlite`,适合本地开发。 - 生产建议使用 Postgres。 - SQLite 切 Postgres 不是只改 `DATABASE_URL`,需要改 Prisma datasource provider、生成 migration,并在目标数据库执行迁移。 - 未完成生产数据库迁移前,不建议生产上线。 ## 11. 上线步骤 ### 11.1 安装依赖和基础质量门禁 ```bash corepack pnpm install corepack pnpm prisma:generate corepack pnpm test corepack pnpm typecheck corepack pnpm build ``` 通过标准: - 单测全部通过。 - TypeScript 无错误。 - Remix production build 成功。 ### 11.2 链接 Shopify app 测试环境: ```bash shopify app config link --config staging --client-id ``` 生产环境: ```bash shopify app config link --config production --client-id ``` 非交互配置校验: ```bash shopify app config validate --client-id --json ``` 已链接环境校验: ```bash shopify app config validate --config staging --json shopify app config validate --config production --json ``` 通过标准: - validate 返回有效结果。 - app URL、redirect URLs、scopes 与目标环境一致。 - 不把 `.shopify/`、个人 `client_id`、个人 extension `uid` 当作通用配置提交。 ### 11.3 配置后端环境变量 生产环境建议: ```env SHOPIFY_API_KEY= SHOPIFY_API_SECRET= SHOPIFY_APP_URL=https:// SCOPES=read_orders,read_metaobjects,write_metaobjects,write_metaobject_definitions DATABASE_URL= THIRD_PARTY_MODE=live THIRD_PARTY_TIMEOUT_MS=5000 LOG_RETENTION_DAYS=7 METAOBJECT_TYPE=$app:dreame_activity METAOBJECT_HANDLE=dreame-activity-config ``` 禁止提交到 Git: - `.env` - `.env.staging` - `.env.production` - Admin access token - Shopify app secret - 第三方 `api_secret` - 数据库密码 ### 11.4 部署后端服务 后端服务部署到托管平台后,先检查: ```bash curl -i https:/// curl -i -X OPTIONS https:///api/action/activity/check ``` 期望: - 首页或健康页返回 200。 - OPTIONS 返回 204,并包含 CORS headers。 - 未带 session token 的 POST 不应成功推送。 ### 11.5 部署 Shopify 配置和 Extension 测试环境: ```bash shopify app deploy --config staging --allow-updates --message "staging deployment" ``` 生产候选: ```bash shopify app deploy --config production --no-release --message "production candidate" ``` 生产发布: ```bash shopify app deploy --config production --allow-updates --message "production release" ``` 生产环境不要使用 `--allow-deletes`,除非已经明确确认要删除远端扩展或配置。 ### 11.6 创建或更新 Metaobject entry 脚本: ```bash SHOPIFY_SHOP_DOMAIN=.myshopify.com \ SHOPIFY_ADMIN_ACCESS_TOKEN=shpat_xxx \ corepack pnpm tsx scripts/setup-metaobject.ts ``` 说明: - 脚本使用 `metaobjectUpsert`。 - 重复执行会更新同一个 handle,不会创建重复 entry。 - 脚本里的默认值是示例值,上线前必须替换为真实业务配置。 ### 11.7 配置 Extension setting 在 Shopify app 的 Checkout UI Extension 设置中配置: ```text app_url = https:// ``` 然后在 checkout and accounts editor 中确认 block 已添加到 Thank You Page。 ## 12. 上线验收 基础验收: | 验收项 | 通过标准 | | --- | --- | | Extension 加载 | Thank You Page 中可以看到扩展区域 | | 后端调用 | `/api/action/activity/check` 被调用 | | Token 校验 | 未授权请求不能触发成功推送 | | Metaobject 读取 | 后端读取到 `$app:dreame_activity` 配置 | | 接口① | 签名正确,活动开启时继续 | | 接口② | 只在接口①满足条件后调用 | | 按钮展示 | 成功时显示本地化按钮 | | 跳转 | 点击按钮跳转到 `redirect_url` | | 幂等 | 同一订单重复刷新不重复推送 | | 日志 | 成功/失败均有日志,不含密钥和签名 | 测试场景: 1. 成功订单:活动开启、接口②成功,页面显示按钮。 2. 活动关闭:接口①返回 inactive,页面不显示按钮。 3. 接口②失败:页面不显示按钮,日志记录失败。 4. 缺 email/customerId:不推送,返回 `success=false`。 5. 重复刷新 Thank You Page:不重复调用接口②,仍显示按钮。 6. 多语言:`zh-CN` 命中文案,未知 locale 回退 `en`。 7. 超时:第三方超时不阻塞 Shopify 原生页面。 ## 13. 回滚方案 | 问题 | 回滚方式 | | --- | --- | | 后端发布异常 | 回滚托管平台 web app release | | Shopify Extension 异常 | 在 Shopify Developer Dashboard 回滚到上一 app version | | Metaobject 配置错误 | 修正 `dreame-activity-config` entry 后重新验证 | | 第三方接口异常 | 测试环境可临时切回 `THIRD_PARTY_MODE=mock`;生产不建议长期 mock | | 数据库迁移异常 | 停止发布,恢复数据库备份或回滚 migration | ## 14. 仍需确认后再迭代的事项 这些事项不能只靠业务配置解决,需要确认后改代码、改部署或单独排期: | 事项 | 当前默认 | 后续动作 | | --- | --- | --- | | 是否支持 Order Status Page | v1 不做 | 若要支持,单独设计 Customer Account/Order Status 实现 | | 是否改成 header 固定位置 | 不改,继续 block target | 若要固定顶部,改 extension target 并重新部署 | | 按钮是否必须新窗口打开 | 当前使用现有 Button `to` 行为 | 若必须 `_blank`,评估 UI extension 组件升级 | | 生产数据库 | 建议 Postgres | 上线前确认 provider、连接串、migration | | 是否增加 webhook 兜底 | v1 不做 | 若要求“未进入 Thank You Page 也推送”,二期加 `orders/paid` webhook | | 接口②是否增加 `lineItems/totalPrice` | 当前不传 | 第三方确认字段格式后再改 payload | | `trigger` 无 `order_end` 时是否推送 | 当前主线按 `order_end` | 若第三方强依赖 trigger,需明确规则 | | 是否做后台配置页 | v1 不做 | 二期用 Polaris App Home 管理配置 | ## 15. 最终检查清单 上线前逐项勾选: - [ ] `corepack pnpm test` 通过。 - [ ] `corepack pnpm typecheck` 通过。 - [ ] `corepack pnpm build` 通过。 - [ ] Shopify app 已 link 到正确 staging/production app。 - [ ] `shopify app config validate --json` 通过。 - [ ] 后端 HTTPS 域名可访问。 - [ ] Shopify app URL 和 redirect URLs 已改成目标域名。 - [ ] 生产数据库方案和 migration 已完成。 - [ ] `$app:dreame_activity` definition 已部署。 - [ ] `dreame-activity-config` entry 已填真实配置。 - [ ] Extension `app_url` setting 指向目标后端域名。 - [ ] 已使用真实测试订单完成成功/失败/重复刷新验证。 - [ ] 日志不含 `api_secret`、`dreame-api-sign`、Admin token。 - [ ] Git 中没有 `.env`、`.shopify/`、个人 app 绑定文件或密钥。 ## 16. 参考资料 - Shopify Thank You block target: https://shopify.dev/docs/api/checkout-ui-extensions/latest/targets/thank-you/block - Shopify Checkout UI Extension targets: https://shopify.dev/docs/api/checkout-ui-extensions/latest/targets - Shopify Metaobject definitions: https://shopify.dev/docs/apps/build/metaobjects/manage-metaobject-definitions - Shopify `metaobjectUpsert`: https://shopify.dev/docs/api/admin-graphql/latest/mutations/metaobjectUpsert - Shopify CLI: https://shopify.dev/docs/api/shopify-cli