Survey API and receipt embed
ReceiptRoller 的問卷功能讓店鋪透過 QR/連結/數位收據嵌入收集回答,並可選擇性地回饋優惠券。本文涵蓋公開消費者 API、收據嵌入酬載,以及店鋪可附加至活動的觀眾鎖定規則。
公開 URL
每個問卷活動都會取得一個 10 字元的 URL-safe 權杖。公開的著陸頁是:
https://{host}/s/{token}
host 來自 config 中的 Survey:UniversalLinkHost;退回時使用請求的 host。原生應用程式透過 Universal Link / App Link 認領 /s/*(項目會被加入 apple-app-site-association 與 assetlinks.json)。LINE 內建瀏覽器的使用者會被 302 導向附加 ?t={token} 的 LIFF URL;其他所有人則落在內嵌的 web 退回表單。
消費者 API
三個端點,皆位於 /api/v1/surveys 之下。preview 與 submit 是 [AllowAnonymous],因此 LIFF 或 web 退回可在不帶 bearer 權杖的情況下呼叫;伺服器以 auth_required 狀態把關非匿名活動。
GET /api/v1/surveys/campaign/{token}
回傳活動中繼資料與完整的提問清單。用戶端在使用者開始回答前用它渲染表單。
{
"status": "ok",
"campaign": {
"campaignId": "...",
"storeId": "...",
"title": "...",
"description": "...",
"validFrom": "2026-04-01T00:00:00Z",
"validUntil": "2026-05-31T23:59:59Z",
"isAnonymous": true,
"maxResponsesPerUser": 1,
"hasReward": true
},
"questions": [
{ "questionId": "...", "order": 0, "type": "single_choice",
"text": "...", "isRequired": true, "optionsJson": "[\"A\",\"B\"]" }
]
}
可能的 status 值:ok、expired、inactive、unknown_token。
POST /api/v1/surveys/responses
提交回答。匿名呼叫者必須送出穩定的 deviceId;非匿名活動需要 bearer 權杖。
POST /api/v1/surveys/responses
Content-Type: application/json
{
"token": "...",
"answers": [
{ "questionId": "...", "value": "A" },
{ "questionId": "...", "value": "5" },
{ "questionId": "...", "value": "OptionA,OptionC" }
],
"deviceId": "anon-..."
}
回應使用 status 判別欄位,用戶端據以分歧:
| Status | 意義 |
|---|---|
recorded | 儲存成功。當活動有獎勵且優惠券驗證通過時,couponIssuedId 會被填入。 |
response_limit | 使用者已提交 maxResponsesPerUser 次。 |
expired | 在有效期間之外。 |
inactive | 活動已封存。 |
unknown_token | 找不到權杖。 |
validation_error | 必填欄位為空或超出範圍。validationErrors 字典以 questionId 為鍵。 |
auth_required | 活動需要已登入的使用者;用戶端應啟動登入。 |
複選回答的編碼
對於 multi_choice 提問,用戶端可送出以逗號連接的字串("A,C")或 JSON 陣列字串("[\"A\",\"C\"]")。兩者都被分析彙總器接受。
GET /api/v1/surveys/me
僅限 bearer。回傳呼叫者自己的回答歷史(response id、campaign id、store id、completed-at、coupon-issued id)。
數位收據嵌入
當店鋪有 showOnDigitalReceipt = true 的有效活動,且其鎖定規則符合收據的情境時,交易收據 API 會將 survey 物件附加至收據酬載:
GET /api/v1/rx/{claimToken}
{
"claimToken": "...",
"storeName": "...",
"transaction": { ... },
"survey": {
"campaignId": "...",
"title": "...",
"description": "...",
"url": "https://{host}/s/{token}",
"hasReward": true
}
}
當沒有活動符合時,survey 欄位為 null — 用戶端只需隱藏該區段。嵌入查找的失敗絕不會破壞收據回應。
觀眾鎖定規則
每個活動可附加一份 JSON 文件,用以過濾哪些收據會取得嵌入。文件是一份扁平的 AND 清單 — 收據只有在每條規則都符合時才符合。空/缺少的文件 = 永遠符合。
{
"rules": [
{ "field": "total_amount", "op": "gte", "value": 1000 },
{ "field": "item_count", "op": "gte", "value": 3 },
{ "field": "category", "op": "eq", "value": "drinks" },
{ "field": "product_name", "op": "contains", "value": "coffee" },
{ "field": "time_of_day", "op": "gte", "value": 18 },
{ "field": "day_of_week", "op": "eq", "value": "fri" },
{ "field": "customer_visit_count", "op": "gte", "value": 2 }
]
}
欄位
| Field | 來源 | Type |
|---|---|---|
total_amount | 收據合計 | decimal |
item_count | 明細行數 | int |
category | 任一明細行的類別 | string |
product_name | 任一明細行的商品名 | string |
time_of_day | 交易的時刻(0-23) | int |
day_of_week | 3 字母英文縮寫(mon...sun) | string |
customer_visit_count | 此使用者的累計來店次數(延遲載入) | int |
運算子
eq— 完全一致(字串或數字)gt/gte/lt/lte— 數值比較contains— 不分大小寫的子字串(字串欄位與明細行陣列)
Fail-closed 語意
格式錯誤的 JSON、未知欄位、未知運算子或缺少值,會使該規則(因而整個匹配)失敗。這是刻意的:靜默地不嵌入問卷,好過把它嵌入到錯誤的收據上。
優惠券獎勵
在活動上設定 RewardCouponId,會讓 SubmitResponseAsync 在提交時解析優惠券。優惠券必須是有效的且在其自身的有效期間內;若如此,回答列會被蓋上優惠券 id,結果 DTO 會將其作為 couponIssuedId 回傳。ReceiptRoller 的優惠券是店鋪層級的範本 — 沒有依使用者的授予模型。消費者用戶端將回傳的 id 視為可兌換的代碼。
依使用者去重
每個活動有 maxResponsesPerUser 設定(預設 1)。服務以 userId(已登入的提交)或 deviceId(匿名提交)為鍵計數回答來執行上限。web 退回表單在 localStorage 生成穩定的 device id;LIFF/原生用戶端應送出自己的。
Universal Link / App Link
/s/* 路徑與既有的端末 QR(/r/*)、交易 QR(/rx/*)、check-in(/c/*)路徑一起被包含在 apple-app-site-association 與 assetlinks.json 中。認領這些路徑的原生應用程式會直接從 OS 收到問卷 URL,並可在不經過 web 著陸頁的情況下呼叫消費者 API。
工程參考
完整的設計規格,包含各階段的 PR 清單與已知的 v2 延後項目,位於工程 wiki:Engineering / Survey Feature — Design Spec。