版本:v2.0(統整修訂版)
日期:2026-06-07
來源整合:技術設計/活動引擎/管理臺統整方案、開發規格 v1.0、使用者故事 v1.0
狀態:評審稿(已對齊三份文件衝突並給出修訂結論)
[!NOTE]
本文件定位為開發參考用的臨時設計稿,協助團隊在正式規格文件發布前對齊設計邊界;待正式規格建立後,可由正式文件取代並移除此參考稿。
現有 Aile 配額體系已覆蓋“扣點側”能力(超額扣點、套餐/延長包購買、欠費處理、AIPool 繫結與授權),但“獲點側”尚未形成完整閉環,導致運營補償、使用者自購、活動激勵與點數收支審計分散。
本規格目標:建立 Aile 點獲取體系統一開發邊界。
springcloud-aile。aile-service-tenant 已存在配額/點數模組,主包路徑 com.aile.service.tenant.service.quota。TenantPointAuditModel,集合 aile.tenant.point.audit,欄位覆蓋 eventId、tenantId、openId、auditScene、sourceBizId、ailePointsChange、status、outTraceId 等。TenantPointAuditServiceImpl,支援審計寫入、列表查詢、成功/處理中/失敗狀態更新(當前統計口徑偏扣點側)。TenantAipoolDeductClient.increasePoint(...),可呼叫 AIPool 累點介面,需產品化封裝。TenantPointAuditController,路徑 /point/audit/v1/list,作為 Admin 審計列表基礎。結論(DRY):不新建獨立賬務底座,在現有 quota/point audit 能力上擴充套件。給點執行邏輯收斂到新的 TenantPointGrantService,審計讀寫仍復用 TenantPointAuditService。
包含:
不包含:審批流;Aile 側給點額度上限配置;AilePro 聊天介面快捷給點;AileApp 管理員給點;完整活動中心與複雜活動規則配置。
| 角色/物件 | 定義 |
|---|---|
| Aile 官方管理員 | Aile 官方租戶下具備後臺管理許可權的運營角色,可對個人租戶執行給點。Phase 1 不做更細粒度審批與額度分級。角色編碼見第 13 節待確認。 |
| 商務人士 / 個人租戶 | 一個自然人對應的個人租戶,在 AIPool 的 Aile 租戶下擁有 Aile 點賬戶。 |
| AIPool 點數中心 | Aile 點真實賬務系統,負責點數增加、扣減、支付、餘額與交易流水(賬務真相)。 |
openId | AIPool 累點/扣點目標使用者標識,給點必須明確到該標識。 |
eventId | Aile 側全域性冪等鍵,同一事件重複提交不得重複給點。 |
TenantPointAudit | Aile 側審計底稿,用於冪等、狀態追蹤、排障與報表。 |
SYSTEM_GLOBAL_TENANT | 平臺級行為(如平臺級活動)的全域性租戶常量。不可用於管理員給點的 tenantId——管理員給點的 tenantId 必須為目標個人租戶(見第 12 節衝突 C8)。 |
活動給點擴充套件架構:
TenantPointGrantService新增業務語義明確的服務 TenantPointGrantService(實現類 TenantPointGrantServiceImpl),不要把給點編排流程堆入 TenantPointAuditServiceImpl。審計讀寫仍復用 TenantPointAuditService。
核心職責:
eventId、openId、點數、場景、來源業務 ID。TenantPointAuditModel,狀態 Pending。TenantAipoolDeductClient.increasePoint(...)。Success / Failed,通訊異常保留 Processing。eventId、status、outTraceId、失敗原因。| 欄位 | 說明 |
|---|---|
eventId | 全域性冪等鍵,規則 GRANT_{Scene}_{businessId}。管理員給點 businessId=ADM_TASK_{taskId};活動給點 businessId={campaignId}_{participantId};購買到賬 businessId={orderId}。 |
tenantId | 目標租戶 ID。管理員給點/使用者自購:目標個人租戶 ID。平臺級活動:SYSTEM_GLOBAL_TENANT。常量統一管理,不硬編碼散落。 |
accountId | 可選,Aile 賬號 ID,用於排障和前端展示。 |
openId | 必填,AIPool 累點目標。 |
amount | 必填,正整數。審計 ailePointsChange 為正數。 |
auditScene | AdminGrant、ActivityGrant、UserPurchase、OrderCompensation 等。 |
sourceBizId | 來源業務主鍵(管理員任務 ID、活動參與記錄 ID、訂單 ID)。 |
operatorId | 管理員給點必填;系統/活動給點使用系統操作者。 |
reasonCode | 給點原因列舉(見 6.7)。 |
remark | 人可讀說明。原因為“其他”時必填。 |
點數審計狀態列舉統一為:Pending、Processing、Success、Failed(PascalCase)。
冪等要求:
eventId 必須唯一(Mongo 唯一約束)。eventId:Success:直接返回既有成功結果,不再呼叫 AIPool。Processing:返回處理中,避免併發重複呼叫。Failed:僅允許透過明確“重試/補發”動作再次推進。@Indexed(unique = true) 與倉庫 AGENTS 的組合索引規範存在風格差異,正式開發時評估是否遷移為 @CompoundIndex。tenant_point_audit 推薦結構{
"bsonType": "object",
"required": ["eventId", "tenantId", "openId", "auditScene", "sourceBizId", "ailePointsChange", "status", "occurredAt"],
"properties": {
"id": { "bsonType": "objectId" },
"eventId": { "bsonType": "string", "description": "全域性唯一業務冪等鍵, 規則 GRANT_{Scene}_{businessId}; 唯一索引保證資料庫層防刷" },
"tenantId": { "bsonType": "string", "description": "管理員給點/自購=目標個人租戶; 平臺級活動=SYSTEM_GLOBAL_TENANT" },
"openId": { "bsonType": "string" },
"auditScene": { "bsonType": "string", "enum": ["QuotaDeduct", "OrderPaid", "OrderCompensation", "ActivityGrant", "AdminGrant", "UserPurchase"] },
"sourceBizId": { "bsonType": "string", "description": "關聯業務主鍵: 訂單ID/客訴單ID/被邀請使用者ID/加祕書事件ID" },
"ailePointsChange": { "bsonType": "int", "description": "正數為給點, 負數為扣點" },
"status": { "bsonType": "string", "enum": ["Pending", "Processing", "Success", "Failed"] },
"occurredAt": { "bsonType": "long", "description": "毫秒時間戳" },
"operatorId": { "bsonType": "string", "description": "管理員給點操作者" },
"reasonCode": { "bsonType": "string" },
"aipoolResponseCode": { "bsonType": "string" },
"aipoolResponseMessage": { "bsonType": "string" },
"outTraceId": { "bsonType": "string", "description": "AIPool 交易流水號, 跨系統對賬" },
"remark": { "bsonType": "string" }
}
}@Slf4j
@Service
public class TenantPointGrantServiceImpl implements TenantPointGrantService {
@Resource private TenantPointAuditService tenantPointAuditService;
@Resource private TenantAipoolDeductClient tenantAipoolDeductClient;
@Resource private AilePointGrantProperties grantProperties; // 有效期/pointType 配置化
public PointGrantResult grantPoints(PointGrantCommand cmd) {
// 1. 生成 eventId
String eventId = "GRANT_" + cmd.getScene().name() + "_" + cmd.getBusinessId();
// 2. 幂等检索
TenantPointAuditModel existing = tenantPointAuditService.findAuditByEventId(eventId);
if (existing != null) {
return PointGrantResult.from(existing); // Success 直接返回; Processing/Failed 按 6.3 处理
}
// 3. 写入 Pending
TenantPointAuditModel audit = TenantPointAuditModel.superBuilder()
.eventId(eventId)
.tenantId(cmd.getTenantId()) // 管理员给点=目标个人租户; 平台级活动=SYSTEM_GLOBAL_TENANT
.openId(cmd.getOpenId())
.auditScene(cmd.getScene())
.sourceBizId(cmd.getBusinessId())
.operatorId(cmd.getOperatorId())
.reasonCode(cmd.getReasonCode())
.ailePointsChange(cmd.getAmount())
.status(TenantPointAuditStatus.Pending)
.occurredAt(System.currentTimeMillis())
.remark(cmd.getRemark())
.build();
tenantPointAuditService.insert(audit);
// 4. 组装请求 (有效期/pointType 配置化)
TenantAipoolPointIncreaseRequest request = TenantAipoolPointIncreaseRequest.builder()
.openId(cmd.getOpenId())
.point(cmd.getAmount())
.code(eventId) // AIPool 侧强幂等键
.reason(cmd.getRemark())
.fromSystem(cmd.getScene().getFromSystem()) // AILE_ADMIN_GRANT / AILE_USER_PURCHASE / AILE_ACTIVITY_GRANT
.fromInfo(cmd.getScene().name() + ":" + cmd.getBusinessId())
.startTime(System.currentTimeMillis())
.expireTime(grantProperties.resolveExpireTime(cmd.getScene())) // 不硬编码, 按场景配置
.pointType(grantProperties.resolvePointType(cmd.getScene()))
.build();
try {
tenantPointAuditService.markProcessing(eventId, cmd.getAmount());
TenantAipoolDeductResult result = tenantAipoolDeductClient.increasePoint(request);
if (result.isSuccess()) {
tenantPointAuditService.markGrantSuccess(eventId, cmd.getAmount(),
result.getResponseCode(), result.getResponseMessage(), result.getOutTraceId());
return PointGrantResult.success(eventId, result.getOutTraceId());
} else {
// 下游业务拒绝(余额超限/黑名单等): 明确失败
tenantPointAuditService.markFailed(eventId, cmd.getAmount(), result.getResponseMessage());
return PointGrantResult.failed(eventId, result.getResponseMessage());
}
} catch (Exception ex) {
// 通信超时/异常: 保留 Processing, 交由补单 Job 或手动重试, 不静默失败
log.error("[PointGrant] Communication error. eventId={}", eventId, ex);
return PointGrantResult.processing(eventId, "S2S Communication Error: " + ex.getMessage());
}
}
}複用 TenantAipoolDeductClient.increasePoint(...),產品化補齊:
fromSystem:區分 AILE_ADMIN_GRANT、AILE_USER_PURCHASE、AILE_ACTIVITY_GRANT。fromInfo:統一 {scene}:{sourceBizId}。code:使用 eventId,與 AIPool 冪等鍵對齊。reason:原因列舉 + 備註組合。startTime / expireTime:配置化,預設 365 天(1 年),可按場景(活動/管理員/購買)分別覆蓋,不硬編碼(見決議 Q2)。pointType:固定為發行租戶封閉點,首期不支援其他點種(見決議 Q3)。realIp:遵循現有配置兜底與校驗機制。TenantAipoolPointRequestInterceptor 自動注入 aile_api_key、aile_api_secret、時間戳與 SHA-256 簽名,經 API Gateway 校驗。AIPool 端以 code 為分散式防重鍵加 Redis 鎖,重複呼叫直接返回原成功流水。業務規則:
tenantId 寫入目標個人租戶 ID(非 SYSTEM_GLOBAL_TENANT),保留 operatorId 為管理員標識。taskId,後端以 ADM_TASK_{taskId} 作為 businessId,依賴 Mongo eventId 唯一索引攔截重複點選。介面(修訂示例,tenantId 取目標租戶):
@PostMapping("/admin/v1/users/{openId}/points/grant")
@PreAuthorize("hasRole('AILE_OFFICIAL_ADMIN')") // 角色编码待最终确认(Q5)
public ResponseEntity<AdminGrantResponse> manualGrantPoints(
@PathVariable String openId,
@Validated @RequestBody AdminGrantRequest body) {
PointGrantCommand cmd = PointGrantCommand.builder()
.tenantId(body.getTargetTenantId()) // 目标个人租户, 非 SYSTEM_GLOBAL_TENANT
.openId(openId)
.amount(body.getAmount())
.scene(TenantPointAuditScene.AdminGrant)
.businessId("ADM_TASK_" + body.getTaskId())
.operatorId(getLoginAdminId())
.reasonCode(body.getReasonCode())
.remark("管理员[" + getLoginAdminEmail() + "]手动指派: " + body.getReason())
.build();
PointGrantResult r = tenantPointGrantService.grantPoints(cmd);
return ResponseEntity.ok(AdminGrantResponse.from(r));
}介面能力:查詢租戶列表(型別/關鍵字/狀態篩選)、發起給點、按 eventId/審計 ID 查詢結果、審計列表(複用/擴充套件 /point/audit/v1/list,增加時間範圍、操作人、來源業務 ID、正負點數篩選)。
業務定位:Aile 不替代 AIPool 支付能力,只提供內嵌購買入口與代理/回撥對接。套餐、價格、贈送比例由 Aile Admin 配置維護(非 AIPool 動態拉取,見決議 Q4);賬務真相(餘額、交易流水)仍以 AIPool 為準,Aile 不維護獨立賬務。
Backend 支援:拉取可購買套餐/檔位;建立購買訂單或獲取 AIPool 支付引數;接收/同步支付結果;支付成功寫入 TenantPointAudit(auditScene=UserPurchase,以訂單 ID 冪等);Admin/App 查詢餘額與購買記錄。
已決議(見第 13 節):套餐由 Aile Admin 配置管理(Q4);首期支付支援 UUPon + 現金(混合支付,Q9);到賬以 AIPool 回撥為主、Aile 輪詢兜底對賬(Q5)。
邊界:活動引擎負責規則判斷、預算扣減、防刷與參與記錄;統一給點服務只接收“已判定透過”的給點事件;預算必須發點前原子扣減;活動參與記錄與 TenantPointAudit 透過 sourceBizId/eventId 關聯。
活動狀態機(統一口徑,見衝突 C3):DRAFT → ACTIVE → PAUSED / EXHAUSTED / ENDED → ARCHIVED。
活動 Schema(aile.campaign):
{
"title": "Campaign",
"type": "object",
"properties": {
"campaignId": { "type": "string", "description": "CAMP_{SCOPE}_{YYYYMMDD}_{NAME}" },
"title": { "type": "string" },
"campaignScope": { "type": "string", "enum": ["PLATFORM", "TENANT"] },
"tenantId": { "type": "string", "description": "PLATFORM 时写入 SYSTEM_GLOBAL_TENANT" },
"status": { "type": "string", "enum": ["DRAFT", "ACTIVE", "PAUSED", "EXHAUSTED", "ENDED", "ARCHIVED"] },
"startTime": { "type": "integer" },
"endTime": { "type": "integer" },
"totalBudget": { "type": "integer" },
"distributedBudget": { "type": "integer", "default": 0 },
"singleGrantAmount": { "type": "integer" },
"rules": { "type": "array", "items": { "type": "object", "properties": {
"ruleType": { "type": "string", "enum": ["EventTypeCondition", "UserEligibility", "FrequencyLimit"] },
"operator": { "type": "string", "enum": ["EQUALS", "IN_LIST", "LESS_THAN_OR_EQUAL"] },
"value": { "type": "string" }
}}}
}
}高併發預算控制(Redis Lua 原子扣減):
-- KEYS[1]: campaign:CAMP_ID:budget
-- ARGV[1]: single grant amount
local key = KEYS[1]
local decrement = tonumber(ARGV[1])
local current = tonumber(redis.call('get', key) or "0")
if current >= decrement then
redis.call('decrby', key, decrement)
return 1
else
return 0
end返回 1 安全獲取併發布給點事件;返回 0 由活動服務釋出 alert 並非同步更新狀態為 EXHAUSTED,杜絕超發。給點失敗需進入補償/重試佇列,不靜默吞預算。
入口:Aile 官方租戶 Admin 後臺租戶列表。能力:
eventId、AIPool 響應碼/資訊、失敗原因。入口:個人租戶 Admin“我的配額/點數”區域。展示餘額、“立即購買”入口、檔位/價格/贈送/支付方式彈窗、支付狀態、成功後重新整理餘額、收支記錄出現“購買充值”。
LIFETIME、受眾過濾)。campaign_record,對 Failed 記錄一鍵 Re-run,底層 Unique Index + code 雙重冪等保證不重發。ACTIVE/EXHAUSTED 活動追加預算,差額同步 INCRBY 至 Redis。核心 API(節選):
paths:
/admin/v1/campaigns:
post: { summary: 创建行销活动(PLATFORM/TENANT) }
/admin/v1/campaigns/{id}/budget:
put: { summary: 追加活动预算(Thread-Safe, 同步 INCRBY Redis) }
/admin/v1/campaigns/re-run:
post: { summary: 一键补发(携带原 eventId, 强幂等) }Phase 1 不強制開發。
eventId 防重複(Aile + AIPool 雙重)。TenantPointAudit。eventId、tenantId、openId、sourceBizId、outTraceId。tenantId 為目標個人租戶)。eventId 重複請求不重複呼叫 AIPool 或重複給點。Success 並記錄 outTraceId。Failed 並記錄失敗原因;通訊異常保留 Processing 可補單。TenantPointAuditModel、TenantAipoolDeductClient、quota 模組;給點編排收斂 TenantPointGrantService。| 編號 | 衝突點 | 各文件表述 | 修訂結論 |
|---|---|---|---|
| C1 | 給點服務歸屬/命名 | 統整方案架構圖把給點入口標為 TenantPointAuditServiceImpl,但程式碼用 TenantPointGrantServiceImpl;開發規格明確要求新建 TenantPointGrantService,不堆入審計服務 | 統一:新建 TenantPointGrantService(Impl) 負責給點編排,審計讀寫複用 TenantPointAuditService。架構圖已更正。 |
| C2 | 通訊異常時的狀態處理 | 統整方案程式碼異常分支直接 markFailed,但其註釋寫“保留 Pending/Processing 以便補單重試”——自相矛盾 | 統一:業務拒絕→Failed;通訊超時/異常→保留 Processing,交補單 Job/手動重試。程式碼已修訂。 |
| C3 | 活動狀態列舉 | 開發規格:Draft/Active/Paused/Exhausted/Ended/Archived(6 態,首字母大寫);統整方案:DRAFT/ACTIVE/PAUSED/EXHAUSTED/COMPLETED(5 態,無 Archived,用 COMPLETED);使用者故事:建立/編輯/啟用/暫停/結束 | 統一:大寫列舉 DRAFT/ACTIVE/PAUSED/EXHAUSTED/ENDED/ARCHIVED;自然結束用 ENDED,保留 ARCHIVED。 |
| C4 | 點數審計狀態大小寫 | 兩份均用 Pending/Processing/Success/Failed(PascalCase),但活動態混用大寫——風格不一致 | 統一:點數審計態 PascalCase;活動態全大寫。兩套互不混用。 |
| C5 | auditScene 列舉集合 | 統整方案含 QuotaDeduct/OrderPaid/OrderCompensation/ActivityGrant/AdminGrant/UserPurchase;開發規格僅列舉部分 | 統一採用統整方案完整集合(見 6.4)。 |
| C6 | 點有效期 expireTime | 統整方案程式碼硬編碼 1 年;開發規格明確“不應硬編碼,需產品確認”,並列為待確認問題 | 統一:expireTime 配置化,按場景分別配置,預設值待產品確認(Q2)。 |
| C7 | pointType | 統整方案硬編碼 pointType=1(封閉點);開發規格未提及 | 統一:pointType 配置化,預設值需產品確認(Q3)。 |
| C8 | 管理員給點的 tenantId | 使用者故事/開發規格:給點目標=個人租戶,tenantId=目標個人租戶 ID;統整方案管理員控制器傳 SYSTEM_GLOBAL_TENANT | 重要修訂:管理員給點 tenantId 寫目標個人租戶 ID(用 operatorId 記錄管理員);SYSTEM_GLOBAL_TENANT 僅用於平臺級活動。 |
| C9 | 許可權角色編碼 | 統整方案硬編碼 hasRole('ADMIN');開發規格列為待確認 | 統一:佔位 AILE_OFFICIAL_ADMIN,最終編碼待確認(Q5)。 |
| C10 | 活動引擎服務歸屬 | 統整方案:獨立 aipool-activity-service;開發規格:待確認(springcloud-aile vs 獨立服務) | 已決議(Q8):活動引擎獨立為 aipool-activity-service。 |
| C11 | 階段定位差異 | 統整方案完整呈現活動引擎/管理臺(易被讀作 Phase 1);使用者故事/開發規格將活動給點列為 Phase 3 | 統一按 Phase 1/2/3 劃分(見第 3 節);活動給點屬 Phase 3。 |
| C12 | eventId 活動場景格式 | 開發規格:GRANT_ActivityGrant_{campaignId}_{participantId};統整方案:GRANT_{Scene}_{businessId}(單一 businessId) | 統一:businessId={campaignId}_{participantId},套入通用規則 GRANT_{Scene}_{businessId},兩者等價。 |
| 編號 | 問題 | 回填答覆 | 設計落地 / 狀態 |
|---|---|---|---|
| 1 | AIPool 累點介面契約/成功碼/冪等碼/錯誤碼 | 需與 AIPool 確認 | ⏳ 外部依賴:待 AIPool 提供正式欄位契約與狀態碼,封裝層預留適配與錯誤碼對映 |
| 2 | Aile 點預設有效期 | 可配置,預設 1 年 | ✅ expireTime 配置化,預設 365 天,可按活動/管理員/購買分別覆蓋 |
| 3 | pointType 取值 | 只有封閉點 | ✅ 固定為發行租戶封閉點,首期不支援其他點種 |
| 4 | 使用者自購套餐來源 | Aile Admin 配置管理 | ✅ 套餐/檔位/價格在 Aile Admin 配置維護,App/Admin 讀 Aile 配置展示(非 AIPool 動態拉取) |
| 5 | 購買到賬觸發方式 | AIPool 回撥 + Aile 輪詢 | ✅ 主:AIPool 支付成功回撥寫審計;輔:Aile 定時輪詢兜底對賬 |
| 6 | 官方管理員角色編碼 | Aile 官方管理員 | ✅ 角色=Aile 官方管理員;佔位編碼 AILE_OFFICIAL_ADMIN,最終編碼值隨 Admin 許可權模型確定 |
| 7 | 個人租戶與 openId 對映 | 需與 AIPool 協商 | ⏳ 外部依賴:與 AIPool 確認個人租戶需提供哪些資訊,以唯一定位 AIPool 使用者並完成給點 |
| 8 | 活動給點服務歸屬 | 建議 aipool-activity-service | ✅ 已決議:活動引擎獨立為 aipool-activity-service |
| 9 | 支付方式 | 支援 UUPon + 現金 | ✅ 首期支援 UUPon + 現金(混合支付) |
pointType 預設值與各場景取值。openId 的穩定對映來源,以及異常缺失時的處理方式。springcloud-aile 還是獨立 aipool-activity-service。