dreame-action/docs/追觅Shopify App 最终方案与上线说明.md

590 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 追觅 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 <checkout session token>
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
}
```
路由级校验失败会返回对应 HTTP 状态码和错误码,例如:
```json
{
"success": false,
"error": {
"code": "ORDER_ID_REQUIRED",
"message": "orderId is required."
}
}
```
说明:
- 前端不传 `api_secret`
- 前端传入的订单字段只作为最小定位信息,不作为可信业务数据。
- 完整订单数据由后端通过 Shopify Admin GraphQL 查询。
- `POST``OPTIONS` 响应带 `Cache-Control: no-store, max-age=0`
- 已认证的 Checkout Extension 响应使用 Shopify checkout CORS helper 包装;认证失败前无法拿到 helper 时使用最小 fallback CORS。
- 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: <api_app_id>
dreame-api-timestamp: <unix ms>
dreame-api-sign: <md5 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 <staging-client-id>
```
生产环境:
```bash
shopify app config link --config production --client-id <production-client-id>
```
非交互配置校验:
```bash
shopify app config validate --client-id <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=<production-client-id>
SHOPIFY_API_SECRET=<production-app-secret>
SHOPIFY_APP_URL=https://<production-backend-domain>
SCOPES=read_orders,read_metaobjects,write_metaobjects,write_metaobject_definitions
DATABASE_URL=<production-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://<backend-domain>/
curl -i -X OPTIONS https://<backend-domain>/api/action/activity/check
```
期望:
- 首页或健康页返回 200。
- OPTIONS 返回 204并包含 CORS headers。
- 未带 session token 的 POST 应返回 HTTP 4xx不应成功推送。
### 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=<shop>.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://<backend-domain>
```
然后在 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