余额通知
This commit is contained in:
parent
fb1789f97f
commit
20bef24a12
@ -127,6 +127,7 @@ services/notice-service/configs/config.tencent.example.yaml
|
||||
```text
|
||||
docs/语音房基础闭环实现.md
|
||||
docs/notice-service架构.md
|
||||
docs/flutter对接/Flutter App 通知与钱包余额对接.md
|
||||
```
|
||||
|
||||
## Storage Model
|
||||
|
||||
387
docs/flutter对接/Flutter App 通知与钱包余额对接.md
Normal file
387
docs/flutter对接/Flutter App 通知与钱包余额对接.md
Normal file
@ -0,0 +1,387 @@
|
||||
# Flutter App 通知与钱包余额对接
|
||||
|
||||
本文只定义 App 需要对接的 HTTP 接口和 IM 私有事件契约,不约束 Flutter 端内部状态管理实现。
|
||||
|
||||
## 通用 HTTP 约定
|
||||
|
||||
### Base URL
|
||||
|
||||
```text
|
||||
https://{api_host}
|
||||
```
|
||||
|
||||
本地开发:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:13000
|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
| Header | 必填 | 说明 |
|
||||
| -------------------------------------- | --------- | -------------------------------------------- |
|
||||
| `Authorization: Bearer {access_token}` | 是 | 登录后后端返回的 App 访问令牌 |
|
||||
| `Content-Type: application/json` | POST 必填 | 请求体为 JSON |
|
||||
| `X-App-Code` | 否 | 多 App 包场景可传;通常由 gateway 按包名解析 |
|
||||
| `X-App-Platform` | 否 | `android` / `ios`,部分钱包商品接口使用 |
|
||||
|
||||
### 响应 Envelope
|
||||
|
||||
所有 `/api/v1` 业务接口使用统一响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_xxx",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
失败时没有 `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "unauthorized",
|
||||
"request_id": "req_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
## 错误代码
|
||||
|
||||
| HTTP | code | message | 说明 |
|
||||
| ---- | ---------------------- | ------------------------ | ------------------------------- |
|
||||
| 200 | `OK` | `ok` | 成功 |
|
||||
| 400 | `INVALID_JSON` | `invalid json` | JSON 格式错误 |
|
||||
| 400 | `INVALID_ARGUMENT` | `invalid argument` | 参数缺失或非法 |
|
||||
| 401 | `UNAUTHORIZED` | `unauthorized` | 未登录、token 过期或 token 无效 |
|
||||
| 403 | `PROFILE_REQUIRED` | `profile required` | 用户资料未完成 |
|
||||
| 403 | `PERMISSION_DENIED` | `permission denied` | 无权限 |
|
||||
| 404 | `NOT_FOUND` | `not found` | 资源不存在 |
|
||||
| 409 | `INSUFFICIENT_BALANCE` | `insufficient balance` | 金币/余额不足 |
|
||||
| 409 | `LEDGER_CONFLICT` | `conflict` | 钱包幂等冲突 |
|
||||
| 409 | `IDEMPOTENCY_CONFLICT` | `conflict` | `command_id` 重复但请求内容不同 |
|
||||
| 409 | `ROOM_CLOSED` | `room closed` | 房间已关闭 |
|
||||
| 409 | `CONFLICT` | `conflict` | 当前状态不允许操作 |
|
||||
| 429 | `RATE_LIMITED` | `rate limited` | 触发限流 |
|
||||
| 500 | `INTERNAL_ERROR` | `internal error` | 服务端内部错误 |
|
||||
| 502 | `UPSTREAM_ERROR` | `upstream service error` | 内部依赖不可用 |
|
||||
|
||||
App 处理建议:`UNAUTHORIZED` 重新登录;`UPSTREAM_ERROR` 可提示重试;`INSUFFICIENT_BALANCE` 引导充值;其它错误按页面语义提示。
|
||||
|
||||
## 1. 获取腾讯 IM 登录票据
|
||||
|
||||
```text
|
||||
GET /api/v1/im/usersig
|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
| Header | 必填 |
|
||||
| --------------- | ---- |
|
||||
| `Authorization` | 是 |
|
||||
|
||||
### Query 参数
|
||||
|
||||
无。
|
||||
|
||||
### 返回值 `data`
|
||||
|
||||
```json
|
||||
{
|
||||
"sdk_app_id": 1400000000,
|
||||
"user_id": "10001",
|
||||
"user_sig": "xxx",
|
||||
"expire_at_ms": 1710000000000,
|
||||
"join_groups": [
|
||||
{
|
||||
"group_id": "hy_lalu_bc_g",
|
||||
"type": "global_broadcast",
|
||||
"region_id": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| -------------- | ------ | -------------------------------------- |
|
||||
| `sdk_app_id` | int64 | 腾讯云 IM AppID |
|
||||
| `user_id` | string | 腾讯云 IM identifier,直接用于 IM 登录 |
|
||||
| `user_sig` | string | 腾讯云 IM 登录票据 |
|
||||
| `expire_at_ms` | int64 | 票据过期时间,UTC epoch ms |
|
||||
| `join_groups` | array | App 登录 IM 后需要加入的服务端群 |
|
||||
|
||||
### 可能错误
|
||||
|
||||
`UNAUTHORIZED`、`PROFILE_REQUIRED`、`PERMISSION_DENIED`、`INTERNAL_ERROR`、`UPSTREAM_ERROR`。
|
||||
|
||||
## 2. 获取我的钱包余额
|
||||
|
||||
```text
|
||||
GET /api/v1/wallet/me/balances
|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
| Header | 必填 |
|
||||
| --------------- | ---- |
|
||||
| `Authorization` | 是 |
|
||||
|
||||
### Query 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------- | --------------- | ---- | --------------------------------------------- |
|
||||
| `asset_type` | string/repeated | 否 | 可重复传,也可逗号分隔;不传默认只返回 `COIN` |
|
||||
| `asset_types` | string/repeated | 否 | 同 `asset_type` |
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=USD_BALANCE
|
||||
```
|
||||
|
||||
### 返回值 `data`
|
||||
|
||||
```json
|
||||
{
|
||||
"balances": [
|
||||
{
|
||||
"asset_type": "COIN",
|
||||
"available_amount": 1123123,
|
||||
"frozen_amount": 0,
|
||||
"version": 18
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------------ | ------ | ------------------------------------------------- |
|
||||
| `asset_type` | string | `COIN` / `DIAMOND` / `USD_BALANCE` / `GIFT_POINT` |
|
||||
| `available_amount` | int64 | 可用余额 |
|
||||
| `frozen_amount` | int64 | 冻结余额 |
|
||||
| `version` | int64 | 该资产余额版本,用于和 IM 通知去重/防乱序 |
|
||||
|
||||
### 可能错误
|
||||
|
||||
`UNAUTHORIZED`、`PROFILE_REQUIRED`、`INVALID_ARGUMENT`、`UPSTREAM_ERROR`。
|
||||
|
||||
## 3. 获取钱包首页摘要
|
||||
|
||||
```text
|
||||
GET /api/v1/wallet/me/overview
|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
| Header | 必填 |
|
||||
| --------------- | ---- |
|
||||
| `Authorization` | 是 |
|
||||
|
||||
### Query 参数
|
||||
|
||||
无。
|
||||
|
||||
### 返回值 `data`
|
||||
|
||||
```json
|
||||
{
|
||||
"balances": [
|
||||
{
|
||||
"asset_type": "COIN",
|
||||
"available_amount": 1123123,
|
||||
"frozen_amount": 0,
|
||||
"version": 18
|
||||
},
|
||||
{
|
||||
"asset_type": "DIAMOND",
|
||||
"available_amount": 200,
|
||||
"frozen_amount": 0,
|
||||
"version": 4
|
||||
},
|
||||
{
|
||||
"asset_type": "USD_BALANCE",
|
||||
"available_amount": 1500,
|
||||
"frozen_amount": 0,
|
||||
"version": 2
|
||||
}
|
||||
],
|
||||
"feature_flags": {
|
||||
"recharge_enabled": true,
|
||||
"diamond_exchange_enabled": true,
|
||||
"withdraw_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---------------------------------------- | ----- | -------------------------- |
|
||||
| `balances` | array | 钱包首页需要展示的资产余额 |
|
||||
| `feature_flags.recharge_enabled` | bool | 是否允许充值 |
|
||||
| `feature_flags.diamond_exchange_enabled` | bool | 是否允许钻石兑换 |
|
||||
| `feature_flags.withdraw_enabled` | bool | 是否允许提现 |
|
||||
|
||||
### 可能错误
|
||||
|
||||
`UNAUTHORIZED`、`PROFILE_REQUIRED`、`UPSTREAM_ERROR`。
|
||||
|
||||
## 4. 送礼
|
||||
|
||||
```text
|
||||
POST /api/v1/rooms/gift/send
|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
| Header | 必填 |
|
||||
| -------------------------------- | ---- |
|
||||
| `Authorization` | 是 |
|
||||
| `Content-Type: application/json` | 是 |
|
||||
|
||||
### Body 参数
|
||||
|
||||
```json
|
||||
{
|
||||
"room_id": "lalu_xxx",
|
||||
"command_id": "gift_1700000000000_10001",
|
||||
"target_type": "user",
|
||||
"target_user_ids": [10002],
|
||||
"gift_id": "car_001",
|
||||
"gift_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------- | ---- | --------------------------------------------------------- |
|
||||
| `room_id` | string | 是 | 房间 ID |
|
||||
| `command_id` | string | 是 | 本次用户动作幂等键;HTTP 重试必须复用同一个值 |
|
||||
| `target_type` | string | 否 | 当前支持 `user`;后续扩展 `all_mic`、`all_room`、`couple` |
|
||||
| `target_user_ids` | int64[] | 是 | 收礼用户 ID;`target_type=user` 时必须且只能 1 个 |
|
||||
| `target_user_id` | int64 | 否 | 单目标兼容字段;新代码优先用 `target_user_ids` |
|
||||
| `gift_id` | string | 是 | 礼物 ID |
|
||||
| `gift_count` | int32 | 是 | 礼物数量 |
|
||||
|
||||
### 返回值 `data`
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"applied": true,
|
||||
"room_version": 14,
|
||||
"server_time_ms": 1710000000000
|
||||
},
|
||||
"billing_receipt_id": "br_xxx",
|
||||
"room_heat": 123456,
|
||||
"gift_rank": [],
|
||||
"room": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------------------- | ------ | ------------------------------------------ |
|
||||
| `result.applied` | bool | 本次命令是否实际落地;幂等重放可能为 false |
|
||||
| `result.room_version` | int64 | 房间版本 |
|
||||
| `result.server_time_ms` | int64 | 服务端时间 |
|
||||
| `billing_receipt_id` | string | 钱包扣费回执 |
|
||||
| `room_heat` | int64 | 房间热度 |
|
||||
| `gift_rank` | array | 房间礼物榜快照 |
|
||||
| `room` | object | 房间快照 |
|
||||
|
||||
送礼成功不代表 App 必须从该响应里更新钱包余额。金币余额以 `WalletBalanceChanged` IM 私有事件或 `/wallet/me/balances` 刷新结果为准。
|
||||
|
||||
### 可能错误
|
||||
|
||||
`UNAUTHORIZED`、`PROFILE_REQUIRED`、`INVALID_ARGUMENT`、`INSUFFICIENT_BALANCE`、`ROOM_CLOSED`、`LEDGER_CONFLICT`、`IDEMPOTENCY_CONFLICT`、`UPSTREAM_ERROR`、`INTERNAL_ERROR`。
|
||||
|
||||
## IM 私有事件:钱包余额变更
|
||||
|
||||
### 事件名称
|
||||
|
||||
```text
|
||||
WalletBalanceChanged
|
||||
```
|
||||
|
||||
### IM 通道
|
||||
|
||||
腾讯云 IM C2C 自定义消息。
|
||||
|
||||
| 字段 | 值 |
|
||||
| ------------------- | ------------------------------------------ |
|
||||
| `MsgType` | `TIMCustomElem` |
|
||||
| `Desc` | `WalletBalanceChanged` |
|
||||
| `Ext` / `extension` | `wallet_notice` |
|
||||
| `To_Account` | 当前登录用户的 IM identifier |
|
||||
| `CloudCustomData` | 与 `TIMCustomElem.Data` 相同的 JSON 字符串 |
|
||||
|
||||
### Headers
|
||||
|
||||
无。IM 私有事件不是 HTTP 请求,没有 HTTP header。
|
||||
|
||||
### 参数 / Payload
|
||||
|
||||
`TIMCustomElem.Data` 是 JSON 字符串:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "WalletBalanceChanged",
|
||||
"event_id": "wev_xxx",
|
||||
"app_code": "lalu",
|
||||
"transaction_id": "tx_xxx",
|
||||
"command_id": "gift_1700000000000_10001",
|
||||
"user_id": "10001",
|
||||
"asset_type": "COIN",
|
||||
"available_delta": -9900,
|
||||
"frozen_delta": 0,
|
||||
"available_after": 1123123,
|
||||
"frozen_after": 0,
|
||||
"balance_version": 18,
|
||||
"created_at_ms": 1710000000000,
|
||||
"source_created_at_ms": 1710000000000,
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------ | ---- | -------------------------------- |
|
||||
| `event_type` | string | 是 | 固定 `WalletBalanceChanged` |
|
||||
| `event_id` | string | 是 | 事件幂等键 |
|
||||
| `app_code` | string | 是 | App 租户 |
|
||||
| `transaction_id` | string | 是 | 钱包交易 ID |
|
||||
| `command_id` | string | 是 | 触发该余额变化的业务命令 ID |
|
||||
| `user_id` | string | 是 | 当前用户 ID 字符串 |
|
||||
| `asset_type` | string | 是 | 资产类型 |
|
||||
| `available_delta` | int64 | 是 | 可用余额变化量 |
|
||||
| `frozen_delta` | int64 | 是 | 冻结余额变化量 |
|
||||
| `available_after` | int64 | 是 | 变化后的可用余额 |
|
||||
| `frozen_after` | int64 | 是 | 变化后的冻结余额 |
|
||||
| `balance_version` | int64 | 是 | 该用户该资产的新版本 |
|
||||
| `created_at_ms` | int64 | 是 | 钱包事件创建时间 |
|
||||
| `source_created_at_ms` | int64 | 是 | 源 outbox 创建时间 |
|
||||
| `metadata` | object | 否 | 业务附加信息,不作为余额展示依赖 |
|
||||
|
||||
### 返回值
|
||||
|
||||
无。该事件由服务端单向投递给 App,App 不需要回调后端确认。
|
||||
|
||||
### 错误代码
|
||||
|
||||
无 HTTP 错误代码。App 本地处理规则:
|
||||
|
||||
| 场景 | 处理 |
|
||||
| ------------------------------------------------- | -------------------------------------------- |
|
||||
| `Desc` 不是 `WalletBalanceChanged` | 忽略 |
|
||||
| `Ext/extension` 不是 `wallet_notice` | 忽略 |
|
||||
| JSON 解析失败 | 忽略并记录客户端日志 |
|
||||
| `event_id` 已处理 | 忽略 |
|
||||
| 本地 `asset_type` 的 `version >= balance_version` | 忽略 |
|
||||
| 长时间未收到通知 | 调 `GET /api/v1/wallet/me/balances` 兜底刷新 |
|
||||
|
||||
## App 最小对接顺序
|
||||
|
||||
1. 登录后调用 `GET /api/v1/im/usersig`。
|
||||
2. 用返回的 `user_id/user_sig/sdk_app_id` 登录腾讯云 IM。
|
||||
3. 调 `GET /api/v1/wallet/me/balances` 拉取初始余额。
|
||||
4. 监听 C2C 自定义消息 `WalletBalanceChanged`。
|
||||
5. 送礼调用 `POST /api/v1/rooms/gift/send`。
|
||||
6. 送礼后的余额以 IM 私有事件或余额刷新接口为准。
|
||||
92
docs/flutter对接/版本强更接口App对接.md
Normal file
92
docs/flutter对接/版本强更接口App对接.md
Normal file
@ -0,0 +1,92 @@
|
||||
# 版本强更接口 App 对接
|
||||
|
||||
## 接口地址
|
||||
|
||||
`GET /api/v1/app/version`
|
||||
|
||||
公开接口,不需要 `Authorization`。
|
||||
|
||||
## Header
|
||||
|
||||
| Header | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `X-App-Code` | 否 | App 编码,默认 `lalu`。 |
|
||||
| `X-App-Package` | 否 | App 包名;当前 `com.org.laluparty` 可解析为 `lalu`。 |
|
||||
| `X-App-Platform` | 条件必填 | `android` 或 `ios`;query `platform` 为空时读取该 header。 |
|
||||
| `X-App-Build-Number` | 条件必填 | 当前客户端编译版本号;query `build_number` 为空时读取该 header。 |
|
||||
| `X-App-Version` | 否 | 当前客户端展示版本号,只用于日志排查。 |
|
||||
|
||||
## Query 参数
|
||||
|
||||
| 参数 | 必填 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `platform` | 条件必填 | string | `android` 或 `ios`;优先级高于 `X-App-Platform`。 |
|
||||
| `build_number` | 条件必填 | int64 | 当前客户端编译版本号;优先级高于 `X-App-Build-Number`。 |
|
||||
|
||||
`platform` 必须通过 query 或 header 传入。`build_number` 对 App 端必须传;服务端缺省时按 `0` 处理,会导致已配置版本都被判定为有更新。
|
||||
|
||||
请求示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/app/version?platform=android&build_number=100
|
||||
X-App-Code: lalu
|
||||
X-App-Platform: android
|
||||
X-App-Build-Number: 100
|
||||
X-App-Version: 1.1.0
|
||||
```
|
||||
|
||||
## 成功返回
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_abc",
|
||||
"data": {
|
||||
"app_code": "lalu",
|
||||
"platform": "android",
|
||||
"version": "1.2.0",
|
||||
"build_number": 120,
|
||||
"force_update": true,
|
||||
"download_url": "https://example.com/lalu.apk",
|
||||
"description": "修复已知问题",
|
||||
"has_update": true,
|
||||
"current_build_number": 100,
|
||||
"updated_at_ms": 1710000000000,
|
||||
"server_time_ms": 1710000001234
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
返回字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `app_code` | string | 服务端解析后的 App 编码。 |
|
||||
| `platform` | string | `android` 或 `ios`。 |
|
||||
| `version` | string | 后台配置的最新展示版本号,只用于展示。 |
|
||||
| `build_number` | int64 | 后台当前最高编译版本号。 |
|
||||
| `current_build_number` | int64 | 服务端收到的客户端编译版本号。 |
|
||||
| `has_update` | bool | `build_number > current_build_number` 时为 `true`。 |
|
||||
| `force_update` | bool | 当前客户端落后且后台最新版本标记强更时为 `true`。 |
|
||||
| `download_url` | string | 更新跳转地址;Android 通常是 APK 下载页,iOS 通常是 App Store 链接。 |
|
||||
| `description` | string | 更新说明,可为空。 |
|
||||
| `updated_at_ms` | int64 | 后台版本配置更新时间,Unix epoch milliseconds。 |
|
||||
| `server_time_ms` | int64 | gateway 返回时间,Unix epoch milliseconds。 |
|
||||
|
||||
## 错误码
|
||||
|
||||
| HTTP 状态码 | `code` | 触发条件 |
|
||||
| --- | --- | --- |
|
||||
| `400` | `INVALID_ARGUMENT` | `platform` 为空或不是 `android` / `ios`。 |
|
||||
| `502` | `UPSTREAM_ERROR` | gateway 读取后台版本配置失败。 |
|
||||
|
||||
错误返回仍使用统一 envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "INVALID_ARGUMENT",
|
||||
"message": "platform is invalid",
|
||||
"request_id": "req_abc"
|
||||
}
|
||||
```
|
||||
@ -159,3 +159,27 @@ wallet_notice_worker:
|
||||
3. 活动/系统私有通知:新增 `activitynotice` 模块,读取 activity outbox,复用 `notice_delivery_events` 或新增同语义位点表。
|
||||
4. 站内 inbox:新增 inbox 模块,写 notice-service 自己的 inbox 表,再按客户端分页接口读取。
|
||||
5. Push:新增 push 模块,和 IM 投递使用不同 `channel`,独立重试和死信。
|
||||
|
||||
## 真实库验证
|
||||
|
||||
默认测试不会访问本地 MySQL。需要验证真实表链路时,显式传入 DSN:
|
||||
|
||||
```bash
|
||||
NOTICE_REAL_MYSQL_DSN='hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC' \
|
||||
NOTICE_REAL_WALLET_DATABASE=hyapp_wallet \
|
||||
go test ./services/notice-service/internal/modules/walletnotice \
|
||||
-run TestMySQLRepositoryProcessesRealWalletOutbox -count=1 -v
|
||||
```
|
||||
|
||||
如果需要保留本次验证行用于查库,加:
|
||||
|
||||
```bash
|
||||
NOTICE_REAL_MYSQL_KEEP_ROWS=true
|
||||
```
|
||||
|
||||
验证内容:
|
||||
|
||||
- 往 `hyapp_wallet.wallet_outbox` 插入一条真实 `WalletBalanceChanged`。
|
||||
- `walletnotice` 从真实 MySQL 抢占该事件。
|
||||
- fake publisher 代替腾讯云 IM,避免误发真实消息。
|
||||
- `hyapp_notice.notice_delivery_events` 写入 `delivered`,并保存最终客户端 payload。
|
||||
|
||||
@ -38,7 +38,7 @@ func TestMySQLRepositoryProcessesRealWalletOutbox(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("NewMySQLRepository: %v", err)
|
||||
}
|
||||
nowMs := time.Now().UnixMilli()
|
||||
insertedAtMS := int64(0)
|
||||
suffix := fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
appCode := "notice_real"
|
||||
userID := int64(990000000)
|
||||
@ -58,7 +58,7 @@ func TestMySQLRepositoryProcessesRealWalletOutbox(t *testing.T) {
|
||||
"metadata": map[string]any{
|
||||
"validation": "notice-service-real-mysql",
|
||||
},
|
||||
"created_at_ms": nowMs,
|
||||
"created_at_ms": insertedAtMS,
|
||||
}
|
||||
payloadJSON, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
@ -70,7 +70,7 @@ func TestMySQLRepositoryProcessesRealWalletOutbox(t *testing.T) {
|
||||
available_delta, frozen_delta, payload, status, retry_count, last_error, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, 'WalletBalanceChanged', ?, ?, ?, 'COIN', 188, 0, ?, 'pending', 0, '', ?, ?)`,
|
||||
quoteDB(walletDatabase),
|
||||
), appCode, eventID, transactionID, commandID, userID, string(payloadJSON), nowMs, nowMs); err != nil {
|
||||
), appCode, eventID, transactionID, commandID, userID, string(payloadJSON), insertedAtMS, insertedAtMS); err != nil {
|
||||
t.Fatalf("insert wallet_outbox: %v", err)
|
||||
}
|
||||
if !keepRealRows() {
|
||||
@ -84,7 +84,7 @@ func TestMySQLRepositoryProcessesRealWalletOutbox(t *testing.T) {
|
||||
service := New(Config{NodeID: "notice-real-test"}, repository, publisher)
|
||||
processed, err := service.ProcessWalletBalanceNotices(ctx, WalletNoticeWorkerOptions{
|
||||
WorkerID: "notice-real-test-worker",
|
||||
BatchSize: 10,
|
||||
BatchSize: 1,
|
||||
LockTTL: 30 * time.Second,
|
||||
PublishTimeout: time.Second,
|
||||
MaxRetryCount: 3,
|
||||
|
||||
@ -3,6 +3,7 @@ package walletnotice
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@ -273,13 +274,15 @@ func (r *MySQLRepository) claimDeliveryEvent(ctx context.Context, tx *sql.Tx, ev
|
||||
}
|
||||
|
||||
// MarkWalletBalanceDelivered 标记一条私有余额通知已投递到腾讯云 IM。
|
||||
func (r *MySQLRepository) MarkWalletBalanceDelivered(ctx context.Context, event WalletBalanceEvent, nowMs int64) error {
|
||||
// payload_json 更新成最终客户端负载,便于后台排查和死信重放使用同一消息形态。
|
||||
func (r *MySQLRepository) MarkWalletBalanceDelivered(ctx context.Context, event WalletBalanceEvent, deliveredPayload json.RawMessage, nowMs int64) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE notice_delivery_events
|
||||
SET status = ?, locked_by = '', lock_until_ms = 0, next_retry_at_ms = 0,
|
||||
last_error = '', delivered_at_ms = ?, updated_at_ms = ?
|
||||
payload_json = ?, last_error = '', delivered_at_ms = ?, updated_at_ms = ?
|
||||
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
|
||||
deliveryStatusDelivered,
|
||||
string(deliveredPayload),
|
||||
nowMs,
|
||||
nowMs,
|
||||
sourceWalletOutbox,
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
// Repository 是 notice-service 需要的持久化能力。
|
||||
type Repository interface {
|
||||
ClaimWalletBalanceEvents(ctx context.Context, workerID string, limit int, lockTTL time.Duration) ([]WalletBalanceEvent, error)
|
||||
MarkWalletBalanceDelivered(ctx context.Context, event WalletBalanceEvent, nowMs int64) error
|
||||
MarkWalletBalanceDelivered(ctx context.Context, event WalletBalanceEvent, deliveredPayload json.RawMessage, nowMs int64) error
|
||||
MarkWalletBalanceRetryable(ctx context.Context, event WalletBalanceEvent, retryCount int, nextRetryAtMS int64, lastErr string, nowMs int64) error
|
||||
MarkWalletBalanceFailed(ctx context.Context, event WalletBalanceEvent, retryCount int, lastErr string, nowMs int64) error
|
||||
}
|
||||
@ -81,7 +81,7 @@ func (s *Service) publishWalletBalanceEvent(ctx context.Context, event WalletBal
|
||||
})
|
||||
nowMs := time.Now().UnixMilli()
|
||||
if err == nil {
|
||||
if markErr := s.repository.MarkWalletBalanceDelivered(ctx, event, nowMs); markErr != nil {
|
||||
if markErr := s.repository.MarkWalletBalanceDelivered(ctx, event, payload, nowMs); markErr != nil {
|
||||
return markErr
|
||||
}
|
||||
logx.Info(ctx, "notice_wallet_balance_delivered",
|
||||
|
||||
@ -97,7 +97,7 @@ func (r *fakeRepository) ClaimWalletBalanceEvents(context.Context, string, int,
|
||||
return r.events, nil
|
||||
}
|
||||
|
||||
func (r *fakeRepository) MarkWalletBalanceDelivered(_ context.Context, event WalletBalanceEvent, _ int64) error {
|
||||
func (r *fakeRepository) MarkWalletBalanceDelivered(_ context.Context, event WalletBalanceEvent, _ json.RawMessage, _ int64) error {
|
||||
r.delivered = append(r.delivered, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user