Files
customer-im-client-dev/apps/im_app/lib/data/remote/red_envelope_request.dart
pp-bot d9539d391c feat(redpacket): 红包与游戏横幅全量实现 (#19~#24)
- #19 fix: SendRedEnvelopeUseCase 动态取 currencyType(workspaceId>0 取
  workspace.currency,修复 iOS 硬编码 PEA → 150001 错误)
- #20: RedEnvelopeBubble typ=8,四态(橙色领取/已领/过期/抢完)+ 领取按钮
- #21: ReceiveRedEnvelopeUseCase POST /app/api/wallet/rp/receive,
  typed JSON body(避免 code=30007),SnackBar 反馈
- #22: SendRedEnvelopeSheet BottomSheet,STANDARD_RP + LUCKY_RP,
  发送成功后构建 typ=8 content JSON 回调给 ChatPage
- #23: BannerViewModel Notifier,Group.topic 双格式解析(JSON object/string),
  FetchBannerUseCase + Timer 倒计时 + applyNewRound WS 接口
- #24: BannerView 游戏横幅条(状态/倒计时/上期结果),
  MiniAppFloatButton 悬浮按钮(hasGame 显示/隐藏,onTap TODO #25)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:11:29 +09:00

293 lines
7.9 KiB
Dart
Raw 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.
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/core/foundation/api_paths.dart';
// ── 发送红包 ──────────────────────────────────────────────────────────────────
/// 发送红包响应
class SendRpData {
final String rpId;
const SendRpData({required this.rpId});
factory SendRpData.fromJson(Map<String, dynamic> json) {
return SendRpData(
rpId: (json['rpID'] ?? json['rp_id'] ?? '').toString(),
);
}
}
/// 发送红包请求
///
/// 对应 Gitea issue #19 + #22
///
/// ## currencyType 规则(#19 bug fix
///
/// iOS 硬编码 `"PEA"` → `code=150001` 错误workspace 群使用 USDT
/// Flutter 修复:`currencyType` 由调用方传入UseCase 层根据 workspaceId 动态决定:
/// - workspaceId > 0 → 从 `/workspace/workspace/get` 取 workspace.currency
/// - workspaceId == 0 → 默认 `"PEA"`
class SendRpRequest extends ApiRequestable<SendRpData> {
final String amount;
final String currencyType;
final int chatId;
final int chatType;
final String rpType;
final List<int> recipientIds;
final int rpNum;
final String remark;
final int msgSendTime;
const SendRpRequest({
required this.amount,
required this.currencyType,
required this.chatId,
required this.chatType,
required this.rpType,
required this.recipientIds,
required this.rpNum,
required this.remark,
required this.msgSendTime,
});
@override
String get path => ApiPaths.rpSend;
@override
HttpMethod get method => HttpMethod.post;
@override
Map<String, dynamic> get parameters => {
'amount': amount,
'currencyType': currencyType,
'chatID': chatId,
'chatType': chatType,
'rpType': rpType,
'recipientIDs': recipientIds,
'rpNum': rpNum,
'remark': remark,
'msgSendTime': msgSendTime,
};
@override
SendRpData? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data is! Map<String, dynamic>) return null;
return SendRpData.fromJson(data);
}
}
// ── 领取红包 ──────────────────────────────────────────────────────────────────
/// 领取红包响应
class ReceiveRpData {
final bool grabFlag;
final String amount;
final int rpStatus;
const ReceiveRpData({
required this.grabFlag,
required this.amount,
required this.rpStatus,
});
factory ReceiveRpData.fromJson(Map<String, dynamic> json) {
return ReceiveRpData(
grabFlag: json['grabFlag'] as bool? ?? false,
amount: (json['amount'] ?? '0').toString(),
rpStatus: json['rpStatus'] as int? ?? 0,
);
}
}
/// 领取红包请求
///
/// 对应 Gitea issue #21
///
/// ⚠️ 必须 JSON typed body非 form 字符串),否则 server 返回 code=30007。
/// `supportMask: true` + `supportHideTail: true` 为必填标志位。
class ReceiveRpRequest extends ApiRequestable<ReceiveRpData> {
final String rpId;
final int chatId;
final String rpType;
final int sendRpMsgId;
const ReceiveRpRequest({
required this.rpId,
required this.chatId,
required this.rpType,
required this.sendRpMsgId,
});
@override
String get path => ApiPaths.rpReceive;
@override
HttpMethod get method => HttpMethod.post;
@override
Map<String, dynamic> get parameters => {
'rpID': rpId,
'chatID': chatId,
'rpType': rpType,
'sendRpMsgID': sendRpMsgId,
'supportMask': true,
'supportHideTail': true,
};
@override
ReceiveRpData? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data is! Map<String, dynamic>) return null;
return ReceiveRpData.fromJson(data);
}
}
// ── 游戏横幅 ──────────────────────────────────────────────────────────────────
/// 游戏横幅响应
class GameBannerData {
final String gameId;
final String gameName;
final String? appId;
final GameCurrentRound? currentRound;
final GameLastRound? lastRound;
const GameBannerData({
required this.gameId,
required this.gameName,
this.appId,
this.currentRound,
this.lastRound,
});
factory GameBannerData.fromJson(Map<String, dynamic> json) {
return GameBannerData(
gameId: json['gameId'] as String? ?? '',
gameName: json['gameName'] as String? ?? '',
appId: json['appid'] as String?,
currentRound: json['currentRound'] is Map<String, dynamic>
? GameCurrentRound.fromJson(json['currentRound'] as Map<String, dynamic>)
: null,
lastRound: json['lastCompletedRound'] is Map<String, dynamic>
? GameLastRound.fromJson(json['lastCompletedRound'] as Map<String, dynamic>)
: null,
);
}
}
class GameCurrentRound {
final String round;
final int? startTime;
final int? closureTime;
final int? drawTime;
final int? serverTime;
const GameCurrentRound({
required this.round,
this.startTime,
this.closureTime,
this.drawTime,
this.serverTime,
});
factory GameCurrentRound.fromJson(Map<String, dynamic> json) {
return GameCurrentRound(
round: json['round'] as String? ?? '',
startTime: json['startTime'] as int?,
closureTime: json['closureTime'] as int?,
drawTime: json['drawTime'] as int?,
serverTime: json['serverTime'] as int?,
);
}
}
class GameLastRound {
final String round;
final String result;
final String? simple;
const GameLastRound({
required this.round,
required this.result,
this.simple,
});
factory GameLastRound.fromJson(Map<String, dynamic> json) {
return GameLastRound(
round: json['round'] as String? ?? '',
result: json['result'] as String? ?? '',
simple: json['simple'] as String?,
);
}
}
/// 获取游戏横幅请求
class FetchBannerRequest extends ApiRequestable<GameBannerData> {
final String gameId;
const FetchBannerRequest({required this.gameId});
@override
String get path => ApiPaths.bannerGet;
@override
HttpMethod get method => HttpMethod.post;
@override
Map<String, dynamic> get parameters => {'game_id': gameId};
@override
GameBannerData? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data is! Map<String, dynamic>) return null;
return GameBannerData.fromJson(data);
}
}
// ── Workspace ─────────────────────────────────────────────────────────────────
/// Workspace 信息响应(用于获取 currency
class WorkspaceData {
final int id;
final String currency;
final String name;
const WorkspaceData({
required this.id,
required this.currency,
required this.name,
});
factory WorkspaceData.fromJson(Map<String, dynamic> json) {
final ws = json['workspace'] as Map<String, dynamic>? ?? json;
return WorkspaceData(
id: ws['id'] as int? ?? 0,
currency: ws['currency'] as String? ?? 'PEA',
name: ws['name'] as String? ?? '',
);
}
}
/// 获取 Workspace 信息请求
class GetWorkspaceRequest extends ApiRequestable<WorkspaceData> {
final int workspaceId;
const GetWorkspaceRequest({required this.workspaceId});
@override
String get path => ApiPaths.workspaceGet;
@override
HttpMethod get method => HttpMethod.get;
@override
Map<String, dynamic> get parameters => {'id': workspaceId.toString()};
@override
WorkspaceData? decodeResponse(dynamic response) {
final data = (response as dynamic).data;
if (data is! Map<String, dynamic>) return null;
return WorkspaceData.fromJson(data);
}
}