feat(iap): 新增 Google Play 内购与 AI 评论举报支持
完成 Google Play 内购集成所需的全链路实现,包括: - 数据库表结构(google-play-iap.sql) - 实体、Mapper、Service 及 XML 配置 - AI 评论举报实体与业务层 - 集成文档(google-play-iap-integration.md)
This commit is contained in:
257
docs/google-play-iap-integration.md
Normal file
257
docs/google-play-iap-integration.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# Google Play IAP 服务端集成
|
||||||
|
|
||||||
|
## 系统设计
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
|
||||||
|
- 客户端购买成功后,服务端使用 `purchaseToken` 二次校验 Google Play Developer API。
|
||||||
|
- RTDN 进入后,先做 Pub/Sub 来源校验,再二次查询 Google Play Developer API,同步本地订单、token、权益状态。
|
||||||
|
- 对订阅与一次性商品统一落库,保证幂等、防重放和可审计。
|
||||||
|
|
||||||
|
### 核心流程
|
||||||
|
|
||||||
|
1. 客户端发起购买,BillingClient 中建议设置 `obfuscatedAccountId = 当前 userId`。
|
||||||
|
2. 客户端支付成功后调用 `/api/google-play/purchases/verify`。
|
||||||
|
3. 服务端调用 Google Play Developer API:
|
||||||
|
- 订阅:`purchases.subscriptionsv2.get`
|
||||||
|
- 一次性商品:`purchases.productsv2.getproductpurchasev2`
|
||||||
|
4. 服务端更新 `google_play_purchase_token`、`google_play_order`、`google_play_user_entitlement`。
|
||||||
|
5. 对需要确认的订单执行 acknowledge / consume。
|
||||||
|
6. Google Play RTDN 推送到 `/api/google-play/rtdn`。
|
||||||
|
7. webhook 校验 topic / subscription / OIDC JWT 后,再次查询 Developer API,再更新本地状态。
|
||||||
|
|
||||||
|
### 幂等策略
|
||||||
|
|
||||||
|
- RTDN 事件以 `message_id` 幂等,重复消息直接忽略。
|
||||||
|
- 订单以 `order_key` 幂等:
|
||||||
|
- 有 `google_order_id` 时用订单号。
|
||||||
|
- 没有订单号时回退为 `TOKEN:{purchaseToken}`。
|
||||||
|
- 购买 token 表以 `purchase_token` 唯一。
|
||||||
|
- 一次性商品发货前先看 `delivery_status`,已发货不重复发。
|
||||||
|
- 订阅不做“累加式延长”,而是直接把用户 VIP 到期时间同步为 Google 返回的最新 `expiryTime`,天然幂等。
|
||||||
|
|
||||||
|
### 本地权益映射
|
||||||
|
|
||||||
|
- `subscription` -> `VIP_SUBSCRIPTION`
|
||||||
|
- 一次性商品且 `duration_days > 0` 或 `unit` 包含 `vip/member/会员` -> `VIP_ONE_TIME`
|
||||||
|
- 一次性商品且 `unit` 像 `coin/quota/credit/金币/次数`,或商品名/`durationValue` 可解析数值 -> `WALLET_TOP_UP`
|
||||||
|
- 其他一次性商品 -> `NON_CONSUMABLE`
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/main/java/com/yolo/keyborad/
|
||||||
|
├── config/
|
||||||
|
│ ├── GooglePlayHttpConfig.java
|
||||||
|
│ └── GooglePlayProperties.java
|
||||||
|
├── controller/
|
||||||
|
│ └── GooglePlayController.java
|
||||||
|
├── googleplay/
|
||||||
|
│ ├── GooglePlayApiClient.java
|
||||||
|
│ ├── GooglePlayApiException.java
|
||||||
|
│ ├── GooglePlayConstants.java
|
||||||
|
│ ├── GooglePlayEntitlementApplier.java
|
||||||
|
│ ├── GooglePlayPubSubAuthService.java
|
||||||
|
│ ├── GooglePlayServiceAccountTokenProvider.java
|
||||||
|
│ ├── GooglePlayStateService.java
|
||||||
|
│ └── model/
|
||||||
|
│ ├── GooglePlayPurchaseSnapshot.java
|
||||||
|
│ ├── GooglePlaySyncCommand.java
|
||||||
|
│ └── GooglePlaySyncResult.java
|
||||||
|
├── mapper/
|
||||||
|
│ ├── GooglePlayOrderMapper.java
|
||||||
|
│ ├── GooglePlayPurchaseTokenMapper.java
|
||||||
|
│ ├── GooglePlayRtdnEventMapper.java
|
||||||
|
│ └── GooglePlayUserEntitlementMapper.java
|
||||||
|
├── model/
|
||||||
|
│ ├── dto/googleplay/
|
||||||
|
│ │ ├── GooglePlayPubSubPushRequest.java
|
||||||
|
│ │ └── GooglePlayPurchaseVerifyReq.java
|
||||||
|
│ ├── entity/googleplay/
|
||||||
|
│ │ ├── GooglePlayOrder.java
|
||||||
|
│ │ ├── GooglePlayPurchaseToken.java
|
||||||
|
│ │ ├── GooglePlayRtdnEvent.java
|
||||||
|
│ │ └── GooglePlayUserEntitlement.java
|
||||||
|
│ └── vo/googleplay/
|
||||||
|
│ └── GooglePlayPurchaseVerifyResp.java
|
||||||
|
└── service/
|
||||||
|
├── GooglePlayBillingService.java
|
||||||
|
└── impl/
|
||||||
|
└── GooglePlayBillingServiceImpl.java
|
||||||
|
```
|
||||||
|
|
||||||
|
## 建表 SQL
|
||||||
|
|
||||||
|
- 文件:`src/main/resources/sql/google-play-iap.sql`
|
||||||
|
|
||||||
|
四张表职责:
|
||||||
|
|
||||||
|
- `google_play_order`:每一笔 Google 订单/续费周期的最终状态与发货状态。
|
||||||
|
- `google_play_purchase_token`:一个 purchase token 的最新状态快照。
|
||||||
|
- `google_play_user_entitlement`:用户实际拥有的本地权益。
|
||||||
|
- `google_play_rtdn_event`:RTDN 审计与重试记录。
|
||||||
|
|
||||||
|
## 接口定义
|
||||||
|
|
||||||
|
### 1. 客户端购买校验
|
||||||
|
|
||||||
|
- `POST /api/google-play/purchases/verify`
|
||||||
|
- 需要登录态与签名
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"packageName": "com.example.app",
|
||||||
|
"productId": "vip_monthly",
|
||||||
|
"productType": "subscription",
|
||||||
|
"purchaseToken": "xxxx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {
|
||||||
|
"userId": 1001,
|
||||||
|
"productId": "vip_monthly",
|
||||||
|
"productType": "SUBSCRIPTION",
|
||||||
|
"purchaseToken": "xxxx",
|
||||||
|
"orderId": "GPA.1234-5678-9012-34567",
|
||||||
|
"orderState": "ACTIVE",
|
||||||
|
"entitlementState": "ACTIVE",
|
||||||
|
"deliveryStatus": "NOT_REQUIRED",
|
||||||
|
"accessGranted": true,
|
||||||
|
"acknowledged": true,
|
||||||
|
"consumed": false,
|
||||||
|
"expiryTime": "2026-04-18T10:00:00.000+00:00",
|
||||||
|
"lastSyncedAt": "2026-03-18T10:01:00.000+00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. RTDN Webhook
|
||||||
|
|
||||||
|
- `POST /api/google-play/rtdn`
|
||||||
|
- 不需要登录态,不走 app 签名,单独做 Pub/Sub 校验
|
||||||
|
|
||||||
|
Pub/Sub Push body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"messageId": "136969346945",
|
||||||
|
"data": "base64-encoded-json"
|
||||||
|
},
|
||||||
|
"subscription": "projects/your-project/subscriptions/google-play-rtdn-push"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
关键请求头:
|
||||||
|
|
||||||
|
- `Authorization: Bearer <OIDC JWT>`
|
||||||
|
- `X-Goog-Topic: projects/your-project/topics/google-play-rtdn`
|
||||||
|
|
||||||
|
成功响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "ok",
|
||||||
|
"data": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## RTDN 状态处理
|
||||||
|
|
||||||
|
### 订阅
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_PURCHASED`
|
||||||
|
- Developer API 状态通常为 `ACTIVE`
|
||||||
|
- 新建/更新 token、order、VIP 权益
|
||||||
|
- 如未 acknowledge,则服务端补 acknowledge
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_RENEWED`
|
||||||
|
- 再查 `subscriptionsv2.get`
|
||||||
|
- 取最新 `expiryTime` 覆盖本地 VIP 到期时间
|
||||||
|
- 不做“在旧时间上加时长”,避免 RTDN 重放导致多发
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_CANCELED`
|
||||||
|
- 若 `subscriptionState=CANCELED` 但 `expiryTime` 仍未来
|
||||||
|
- 本地仍保留 VIP,`autoRenewEnabled=false`
|
||||||
|
- 到期后等待 `EXPIRED`
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_IN_GRACE_PERIOD`
|
||||||
|
- 本地继续保留 VIP
|
||||||
|
- entitlement state 记为 `IN_GRACE_PERIOD`
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_ON_HOLD`
|
||||||
|
- 立即取消本地 VIP 可用态
|
||||||
|
- 保留 token/order 记录,等待恢复或过期
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_RESTARTED` / `SUBSCRIPTION_RECOVERED`
|
||||||
|
- 再查最新状态
|
||||||
|
- 若恢复为 `ACTIVE`,重新开启 VIP
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_PAUSED`
|
||||||
|
- 本地取消 VIP 可用态
|
||||||
|
- 记录 `autoResumeTime`
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_EXPIRED`
|
||||||
|
- 本地关闭 VIP
|
||||||
|
- entitlement state -> `EXPIRED`
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_REVOKED`
|
||||||
|
- 本地关闭 VIP
|
||||||
|
- entitlement state -> `REVOKED`
|
||||||
|
|
||||||
|
- `SUBSCRIPTION_PENDING_PURCHASE_CANCELED`
|
||||||
|
- 用当前 token 查到 `linkedPurchaseToken` 后,再同步旧 token
|
||||||
|
- 旧订阅权益继续保持
|
||||||
|
|
||||||
|
### 一次性商品
|
||||||
|
|
||||||
|
- `ONE_TIME_PRODUCT_PURCHASED`
|
||||||
|
- 若映射为钱包充值:仅当 `delivery_status != DELIVERED` 才入账
|
||||||
|
- 若映射为一次性 VIP 或非消耗型权益:同样只发一次
|
||||||
|
- 钱包型商品在发货后执行 consume
|
||||||
|
- 非消耗型商品执行 acknowledge
|
||||||
|
|
||||||
|
- `ONE_TIME_PRODUCT_CANCELED`
|
||||||
|
- 一般是 pending 订单被取消
|
||||||
|
- 本地标记为 `CANCELED`,不发货
|
||||||
|
|
||||||
|
- `VoidedPurchaseNotification`
|
||||||
|
- 先二次查询 Developer API;若 404,则结合本地 token/order 记录做最终回滚
|
||||||
|
- 订阅按 `REVOKED`
|
||||||
|
- 一次性商品按 `REFUNDED`
|
||||||
|
- 钱包余额不足以回滚时,订单标记 `MANUAL_REVIEW`,日志显式报错
|
||||||
|
|
||||||
|
## 安全校验
|
||||||
|
|
||||||
|
- RTDN 入口校验 `X-Goog-Topic`
|
||||||
|
- 校验 `subscription` 字段
|
||||||
|
- 调用 `tokeninfo` 校验 Pub/Sub OIDC JWT
|
||||||
|
- 校验 `aud`
|
||||||
|
- 校验 `email` 与 `email_verified`
|
||||||
|
- 校验 `iss` 必须是 Google Accounts
|
||||||
|
- 客户端校验接口可要求 `obfuscatedExternalAccountId == userId`
|
||||||
|
|
||||||
|
## 测试样例
|
||||||
|
|
||||||
|
建议执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -q test -Dtest=GooglePlay*
|
||||||
|
```
|
||||||
|
|
||||||
|
覆盖点:
|
||||||
|
|
||||||
|
- 订阅首次购买与续费
|
||||||
|
- 订阅取消但未过期
|
||||||
|
- 订阅宽限期
|
||||||
|
- 一次性商品发货幂等
|
||||||
|
- RTDN 重放幂等
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.yolo.keyborad.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiCommentReport;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/3/20 15:07
|
||||||
|
*/
|
||||||
|
|
||||||
|
public interface KeyboardAiCommentReportMapper extends BaseMapper<KeyboardAiCommentReport> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.yolo.keyborad.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import java.util.Date;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/3/20 15:07
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI角色评论举报记录表
|
||||||
|
*/
|
||||||
|
@Schema(description="AI角色评论举报记录表")
|
||||||
|
@Data
|
||||||
|
@TableName(value = "keyboard_ai_comment_report")
|
||||||
|
public class KeyboardAiCommentReport {
|
||||||
|
/**
|
||||||
|
* 举报记录唯一ID
|
||||||
|
*/
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@Schema(description="举报记录唯一ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 被举报的评论Id
|
||||||
|
*/
|
||||||
|
@TableField(value = "comment_id")
|
||||||
|
@Schema(description="被举报的评论Id")
|
||||||
|
private Long commentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起举报的用户ID(逻辑关联用户表)
|
||||||
|
*/
|
||||||
|
@TableField(value = "user_id")
|
||||||
|
@Schema(description="发起举报的用户ID(逻辑关联用户表)")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 举报类型:1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他
|
||||||
|
*/
|
||||||
|
@TableField(value = "report_type")
|
||||||
|
@Schema(description="举报类型:1=色情低俗, 2=政治敏感, 3=暴力恐怖, 4=侵权/冒充, 5=价值观问题, 99=其他")
|
||||||
|
private String reportType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户填写的详细举报描述
|
||||||
|
*/
|
||||||
|
@TableField(value = "report_desc")
|
||||||
|
@Schema(description="用户填写的详细举报描述")
|
||||||
|
private String reportDesc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 违规现场:举报时的评论的内容,用于审核取证
|
||||||
|
*/
|
||||||
|
@TableField(value = "comment_context")
|
||||||
|
@Schema(description="违规现场:举报时的评论的内容,用于审核取证")
|
||||||
|
private String commentContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片证据:用户上传的截图URL
|
||||||
|
*/
|
||||||
|
@TableField(value = "evidence_image_url")
|
||||||
|
@Schema(description="图片证据:用户上传的截图URL")
|
||||||
|
private String evidenceImageUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理状态:0=待处理, 1=违规确立(已处罚), 2=无效举报/已驳回, 3=已忽略
|
||||||
|
*/
|
||||||
|
@TableField(value = "\"status\"")
|
||||||
|
@Schema(description="处理状态:0=待处理, 1=违规确立(已处罚), 2=无效举报/已驳回, 3=已忽略")
|
||||||
|
private Short status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员处理备注(记录处理理由或处罚措施)
|
||||||
|
*/
|
||||||
|
@TableField(value = "admin_remark")
|
||||||
|
@Schema(description="管理员处理备注(记录处理理由或处罚措施)")
|
||||||
|
private String adminRemark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 举报提交时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_at")
|
||||||
|
@Schema(description="举报提交时间")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_at")
|
||||||
|
@Schema(description="最后更新时间")
|
||||||
|
private Date updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.yolo.keyborad.service;
|
||||||
|
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiCommentReport;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/3/20 15:07
|
||||||
|
*/
|
||||||
|
|
||||||
|
public interface KeyboardAiCommentReportService extends IService<KeyboardAiCommentReport>{
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yolo.keyborad.service.impl;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import java.util.List;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.yolo.keyborad.model.entity.KeyboardAiCommentReport;
|
||||||
|
import com.yolo.keyborad.mapper.KeyboardAiCommentReportMapper;
|
||||||
|
import com.yolo.keyborad.service.KeyboardAiCommentReportService;
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2026/3/20 15:07
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class KeyboardAiCommentReportServiceImpl extends ServiceImpl<KeyboardAiCommentReportMapper, KeyboardAiCommentReport> implements KeyboardAiCommentReportService{
|
||||||
|
|
||||||
|
}
|
||||||
24
src/main/resources/mapper/KeyboardAiCommentReportMapper.xml
Normal file
24
src/main/resources/mapper/KeyboardAiCommentReportMapper.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.yolo.keyborad.mapper.KeyboardAiCommentReportMapper">
|
||||||
|
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardAiCommentReport">
|
||||||
|
<!--@mbg.generated-->
|
||||||
|
<!--@Table keyboard_ai_comment_report-->
|
||||||
|
<id column="id" jdbcType="BIGINT" property="id" />
|
||||||
|
<result column="comment_id" jdbcType="BIGINT" property="commentId" />
|
||||||
|
<result column="user_id" jdbcType="BIGINT" property="userId" />
|
||||||
|
<result column="report_type" jdbcType="VARCHAR" property="reportType" />
|
||||||
|
<result column="report_desc" jdbcType="VARCHAR" property="reportDesc" />
|
||||||
|
<result column="comment_context" jdbcType="VARCHAR" property="commentContext" />
|
||||||
|
<result column="evidence_image_url" jdbcType="VARCHAR" property="evidenceImageUrl" />
|
||||||
|
<result column="status" jdbcType="SMALLINT" property="status" />
|
||||||
|
<result column="admin_remark" jdbcType="VARCHAR" property="adminRemark" />
|
||||||
|
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
|
||||||
|
<result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
|
||||||
|
</resultMap>
|
||||||
|
<sql id="Base_Column_List">
|
||||||
|
<!--@mbg.generated-->
|
||||||
|
id, comment_id, user_id, report_type, report_desc, comment_context, evidence_image_url,
|
||||||
|
"status", admin_remark, created_at, updated_at
|
||||||
|
</sql>
|
||||||
|
</mapper>
|
||||||
113
src/main/resources/sql/google-play-iap.sql
Normal file
113
src/main/resources/sql/google-play-iap.sql
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS google_play_order (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT,
|
||||||
|
package_name VARCHAR(128) NOT NULL,
|
||||||
|
product_id VARCHAR(255) NOT NULL,
|
||||||
|
product_type VARCHAR(32) NOT NULL,
|
||||||
|
purchase_token VARCHAR(512) NOT NULL,
|
||||||
|
order_key VARCHAR(128) NOT NULL UNIQUE,
|
||||||
|
google_order_id VARCHAR(128),
|
||||||
|
linked_purchase_token VARCHAR(512),
|
||||||
|
order_state VARCHAR(64) NOT NULL,
|
||||||
|
acknowledgement_state VARCHAR(32) NOT NULL,
|
||||||
|
consumption_state VARCHAR(32) NOT NULL,
|
||||||
|
quantity INTEGER,
|
||||||
|
refundable_quantity INTEGER,
|
||||||
|
delivery_status VARCHAR(64) NOT NULL,
|
||||||
|
granted_quantity NUMERIC(18,2) NOT NULL DEFAULT 0,
|
||||||
|
entitlement_start_time TIMESTAMP,
|
||||||
|
entitlement_end_time TIMESTAMP,
|
||||||
|
last_event_time TIMESTAMP,
|
||||||
|
last_synced_at TIMESTAMP NOT NULL,
|
||||||
|
raw_response TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_google_play_order_purchase_token
|
||||||
|
ON google_play_order (purchase_token);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_google_play_order_user_product
|
||||||
|
ON google_play_order (user_id, product_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS google_play_purchase_token (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
purchase_token VARCHAR(512) NOT NULL UNIQUE,
|
||||||
|
linked_purchase_token VARCHAR(512),
|
||||||
|
user_id BIGINT,
|
||||||
|
package_name VARCHAR(128) NOT NULL,
|
||||||
|
product_id VARCHAR(255) NOT NULL,
|
||||||
|
product_type VARCHAR(32) NOT NULL,
|
||||||
|
latest_order_key VARCHAR(128) NOT NULL,
|
||||||
|
latest_order_id VARCHAR(128),
|
||||||
|
token_state VARCHAR(64) NOT NULL,
|
||||||
|
acknowledgement_state VARCHAR(32) NOT NULL,
|
||||||
|
consumption_state VARCHAR(32) NOT NULL,
|
||||||
|
auto_renew_enabled BOOLEAN,
|
||||||
|
external_account_id VARCHAR(255),
|
||||||
|
external_profile_id VARCHAR(255),
|
||||||
|
region_code VARCHAR(16),
|
||||||
|
start_time TIMESTAMP,
|
||||||
|
expiry_time TIMESTAMP,
|
||||||
|
auto_resume_time TIMESTAMP,
|
||||||
|
canceled_state_reason VARCHAR(64),
|
||||||
|
last_event_type VARCHAR(128),
|
||||||
|
last_event_time TIMESTAMP,
|
||||||
|
last_synced_at TIMESTAMP NOT NULL,
|
||||||
|
raw_response TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_google_play_purchase_token_user
|
||||||
|
ON google_play_purchase_token (user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS google_play_user_entitlement (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
entitlement_key VARCHAR(128) NOT NULL,
|
||||||
|
product_id VARCHAR(255) NOT NULL,
|
||||||
|
product_type VARCHAR(32) NOT NULL,
|
||||||
|
source_purchase_token VARCHAR(512) NOT NULL,
|
||||||
|
current_order_key VARCHAR(128) NOT NULL,
|
||||||
|
benefit_type VARCHAR(64) NOT NULL,
|
||||||
|
state VARCHAR(64) NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL,
|
||||||
|
quantity NUMERIC(18,2) NOT NULL DEFAULT 0,
|
||||||
|
start_time TIMESTAMP,
|
||||||
|
end_time TIMESTAMP,
|
||||||
|
last_granted_at TIMESTAMP,
|
||||||
|
last_revoked_at TIMESTAMP,
|
||||||
|
metadata TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uk_google_play_user_entitlement UNIQUE (source_purchase_token, entitlement_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_google_play_user_entitlement_user
|
||||||
|
ON google_play_user_entitlement (user_id, active);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS google_play_rtdn_event (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
message_id VARCHAR(128) NOT NULL UNIQUE,
|
||||||
|
subscription_name VARCHAR(255),
|
||||||
|
package_name VARCHAR(128),
|
||||||
|
event_type VARCHAR(32) NOT NULL,
|
||||||
|
notification_type INTEGER,
|
||||||
|
notification_name VARCHAR(128),
|
||||||
|
purchase_token VARCHAR(512),
|
||||||
|
product_id VARCHAR(255),
|
||||||
|
order_id VARCHAR(128),
|
||||||
|
event_time TIMESTAMP,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
raw_envelope TEXT NOT NULL,
|
||||||
|
raw_payload TEXT NOT NULL,
|
||||||
|
error_message TEXT,
|
||||||
|
processed_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_google_play_rtdn_event_purchase_token
|
||||||
|
ON google_play_rtdn_event (purchase_token);
|
||||||
Reference in New Issue
Block a user