add
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
|
||||
/// 资产钱包(《03-接口文档》9.3,10 资产 = 1 元)
|
||||
class AssetWallet {
|
||||
const AssetWallet({
|
||||
this.assets = 0,
|
||||
this.assetsValueYuan = 0,
|
||||
this.totalEarned = 0,
|
||||
this.totalWithdrawn = 0,
|
||||
this.minWithdrawAssets = 100,
|
||||
this.hasPendingWithdraw = false,
|
||||
});
|
||||
|
||||
/// 资产余额
|
||||
final double assets;
|
||||
|
||||
/// 折合人民币(元)
|
||||
final double assetsValueYuan;
|
||||
final double totalEarned;
|
||||
final double totalWithdrawn;
|
||||
|
||||
/// 最低提现资产
|
||||
final int minWithdrawAssets;
|
||||
|
||||
/// 是否有审核中的提现申请
|
||||
final bool hasPendingWithdraw;
|
||||
|
||||
factory AssetWallet.fromJson(Map<String, dynamic> json) => AssetWallet(
|
||||
assets: (json['assets'] as num?)?.toDouble() ?? 0,
|
||||
assetsValueYuan: (json['assets_value_yuan'] as num?)?.toDouble() ?? 0,
|
||||
totalEarned: (json['total_earned'] as num?)?.toDouble() ?? 0,
|
||||
totalWithdrawn: (json['total_withdrawn'] as num?)?.toDouble() ?? 0,
|
||||
minWithdrawAssets:
|
||||
(json['min_withdraw_assets'] as num?)?.toInt() ?? 100,
|
||||
hasPendingWithdraw: json['has_pending_withdraw'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 资产流水(《03-接口文档》9.4)
|
||||
class AssetRecord {
|
||||
const AssetRecord({
|
||||
required this.id,
|
||||
required this.changeAmount,
|
||||
required this.balanceAfter,
|
||||
required this.bizType,
|
||||
this.remark = '',
|
||||
this.createdAt = '',
|
||||
});
|
||||
|
||||
final int id;
|
||||
|
||||
/// 正=收入,负=支出
|
||||
final double changeAmount;
|
||||
final double balanceAfter;
|
||||
|
||||
/// 1充值返利 2完整观看收益 3点赞收益 4提现 5提现退回 6返利扣回 7后台调整
|
||||
final int bizType;
|
||||
final String remark;
|
||||
final String createdAt;
|
||||
|
||||
static const Map<int, String> bizTypeLabels = {
|
||||
1: '充值返利',
|
||||
2: '观看收益',
|
||||
3: '点赞收益',
|
||||
4: '提现',
|
||||
5: '提现退回',
|
||||
6: '返利扣回',
|
||||
7: '后台调整',
|
||||
};
|
||||
|
||||
String get bizTypeLabel => bizTypeLabels[bizType] ?? '其他';
|
||||
bool get isExpense => changeAmount < 0;
|
||||
|
||||
factory AssetRecord.fromJson(Map<String, dynamic> json) => AssetRecord(
|
||||
id: (json['id'] as num).toInt(),
|
||||
changeAmount: (json['change_amount'] as num?)?.toDouble() ?? 0,
|
||||
balanceAfter: (json['balance_after'] as num?)?.toDouble() ?? 0,
|
||||
bizType: (json['biz_type'] as num?)?.toInt() ?? 0,
|
||||
remark: (json['remark'] as String?) ?? '',
|
||||
createdAt: (json['created_at'] as String?) ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 资产流水分页
|
||||
class AssetRecordsPage {
|
||||
const AssetRecordsPage({required this.total, required this.items});
|
||||
|
||||
final int total;
|
||||
final List<AssetRecord> items;
|
||||
|
||||
factory AssetRecordsPage.fromJson(Map<String, dynamic> json) =>
|
||||
AssetRecordsPage(
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
items: ((json['list'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(AssetRecord.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 提现申请结果(《03-接口文档》9.5)
|
||||
class WithdrawResult {
|
||||
const WithdrawResult({
|
||||
required this.withdrawNo,
|
||||
required this.assets,
|
||||
required this.amountYuan,
|
||||
required this.status,
|
||||
this.statusText = '',
|
||||
});
|
||||
|
||||
final String withdrawNo;
|
||||
final double assets;
|
||||
final double amountYuan;
|
||||
final int status;
|
||||
final String statusText;
|
||||
|
||||
factory WithdrawResult.fromJson(Map<String, dynamic> json) =>
|
||||
WithdrawResult(
|
||||
withdrawNo: (json['withdraw_no'] as String?) ?? '',
|
||||
assets: (json['assets'] as num?)?.toDouble() ?? 0,
|
||||
amountYuan: (json['amount_yuan'] as num?)?.toDouble() ?? 0,
|
||||
status: (json['status'] as num?)?.toInt() ?? 0,
|
||||
statusText: (json['status_text'] as String?) ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 提现记录(《03-接口文档》9.6)
|
||||
class WithdrawRecord {
|
||||
const WithdrawRecord({
|
||||
required this.withdrawNo,
|
||||
required this.assets,
|
||||
required this.amountYuan,
|
||||
required this.channel,
|
||||
required this.status,
|
||||
this.statusText = '',
|
||||
this.createdAt = '',
|
||||
});
|
||||
|
||||
final String withdrawNo;
|
||||
final double assets;
|
||||
final double amountYuan;
|
||||
|
||||
/// 1支付宝 2微信 3USDT
|
||||
final int channel;
|
||||
|
||||
/// 0待审核 1已打款 2已拒绝
|
||||
final int status;
|
||||
final String statusText;
|
||||
final String createdAt;
|
||||
|
||||
static const Map<int, String> channelLabels = {
|
||||
1: '支付宝',
|
||||
2: '微信',
|
||||
3: 'USDT',
|
||||
};
|
||||
|
||||
String get channelLabel => channelLabels[channel] ?? '未知';
|
||||
|
||||
factory WithdrawRecord.fromJson(Map<String, dynamic> json) =>
|
||||
WithdrawRecord(
|
||||
withdrawNo: (json['withdraw_no'] as String?) ?? '',
|
||||
assets: (json['assets'] as num?)?.toDouble() ?? 0,
|
||||
amountYuan: (json['amount_yuan'] as num?)?.toDouble() ?? 0,
|
||||
channel: (json['channel'] as num?)?.toInt() ?? 1,
|
||||
status: (json['status'] as num?)?.toInt() ?? 0,
|
||||
statusText: (json['status_text'] as String?) ?? '',
|
||||
createdAt: (json['created_at'] as String?) ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 提现记录分页
|
||||
class WithdrawRecordsPage {
|
||||
const WithdrawRecordsPage({required this.total, required this.items});
|
||||
|
||||
final int total;
|
||||
final List<WithdrawRecord> items;
|
||||
|
||||
factory WithdrawRecordsPage.fromJson(Map<String, dynamic> json) =>
|
||||
WithdrawRecordsPage(
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
items: ((json['list'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(WithdrawRecord.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 资产/提现服务(《03-接口文档》第 9 章,均需登录)
|
||||
class AssetService {
|
||||
AssetService._();
|
||||
|
||||
/// 9.3 我的资产
|
||||
static Future<AssetWallet> fetchAssets() async {
|
||||
final data =
|
||||
await ApiClient.get<Map<String, dynamic>>(Endpoints.assetsMe);
|
||||
return AssetWallet.fromJson(data);
|
||||
}
|
||||
|
||||
/// 9.4 资产流水(biz_type 筛选 + 分页)
|
||||
static Future<AssetRecordsPage> fetchRecords({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
int? bizType,
|
||||
}) async {
|
||||
final query = <String, dynamic>{'page': page, 'page_size': pageSize};
|
||||
if (bizType != null) query['biz_type'] = bizType;
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.assetsRecords,
|
||||
queryParameters: query,
|
||||
);
|
||||
return AssetRecordsPage.fromJson(data);
|
||||
}
|
||||
|
||||
/// 9.5 申请提现
|
||||
/// 错误码:2102 资产余额不足,2103 低于最低提现额度,2104 已有审核中的提现申请
|
||||
static Future<WithdrawResult> applyWithdraw({
|
||||
required double assets,
|
||||
required int channel,
|
||||
required String account,
|
||||
}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.assetsWithdraw,
|
||||
data: {'assets': assets, 'channel': channel, 'account': account},
|
||||
);
|
||||
return WithdrawResult.fromJson(data);
|
||||
}
|
||||
|
||||
/// 9.6 提现记录
|
||||
static Future<WithdrawRecordsPage> fetchWithdrawRecords({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.withdrawRecords,
|
||||
queryParameters: {'page': page, 'page_size': pageSize},
|
||||
);
|
||||
return WithdrawRecordsPage.fromJson(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/api_exception.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
import '../models/user_model.dart';
|
||||
|
||||
/// 认证服务(《03-接口文档》第 3 章)
|
||||
class AuthService {
|
||||
AuthService._();
|
||||
|
||||
/// 3.1 发送短信验证码 scene: register/login/reset
|
||||
static Future<void> sendSmsCode(String phone, String scene) async {
|
||||
await ApiClient.I.post(Endpoints.smsSend, data: {'phone': phone, 'scene': scene});
|
||||
}
|
||||
|
||||
/// 3.2 发送邮箱验证码
|
||||
static Future<void> sendEmailCode(String email, String scene) async {
|
||||
await ApiClient.I.post(Endpoints.emailSend, data: {'email': email, 'scene': scene});
|
||||
}
|
||||
|
||||
/// 3.3 注册(type: phone/email,可带邀请码),返回 Token 对与用户信息
|
||||
static Future<LoginResult> register({
|
||||
required String type,
|
||||
required String account,
|
||||
required String code,
|
||||
required String password,
|
||||
String? nickname,
|
||||
String? inviteCode,
|
||||
}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(Endpoints.register, data: {
|
||||
'type': type,
|
||||
if (type == 'phone') 'phone': account else 'email': account,
|
||||
'code': code,
|
||||
'password': password,
|
||||
if (nickname != null && nickname.isNotEmpty) 'nickname': nickname,
|
||||
if (inviteCode != null && inviteCode.isNotEmpty) 'invite_code': inviteCode,
|
||||
});
|
||||
return _fromTokenData(data);
|
||||
}
|
||||
|
||||
/// 3.4 登录(type: phone_password/phone_code/email_password)
|
||||
static Future<LoginResult> login({
|
||||
required String type,
|
||||
String? phone,
|
||||
String? email,
|
||||
String? password,
|
||||
String? code,
|
||||
}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(Endpoints.login, data: {
|
||||
'type': type,
|
||||
if (phone != null) 'phone': phone,
|
||||
if (email != null) 'email': email,
|
||||
if (password != null) 'password': password,
|
||||
if (code != null) 'code': code,
|
||||
});
|
||||
return _fromTokenData(data);
|
||||
}
|
||||
|
||||
/// 3.6 退出登录
|
||||
static Future<void> logout() async {
|
||||
try {
|
||||
await ApiClient.I.post(Endpoints.logout);
|
||||
} on ApiException catch (e) {
|
||||
// Token 已失效等情况忽略,本地照常清理
|
||||
if (!e.isUnauthorized) rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一解析 Token 响应:access/refresh 入安全存储,user 转模型
|
||||
static LoginResult _fromTokenData(Map<String, dynamic> data) {
|
||||
final user = UserModel.fromJson((data['user'] as Map<String, dynamic>?) ?? const {});
|
||||
final trial = data['trial_granted'] as Map<String, dynamic>?;
|
||||
return LoginResult(
|
||||
user: user,
|
||||
trialPlanName: trial?['plan_name'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录/注册结果
|
||||
class LoginResult {
|
||||
LoginResult({required this.user, this.trialPlanName});
|
||||
|
||||
final UserModel user;
|
||||
|
||||
/// 注册赠送的体验会员名称(关闭赠送开关时为 null)
|
||||
final String? trialPlanName;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
import '../models/comment_item.dart';
|
||||
|
||||
/// 评论服务(《03-接口文档》6.3~6.5)
|
||||
class CommentService {
|
||||
CommentService._();
|
||||
|
||||
/// 评论列表:parentId=0 查一级评论(带 reply_count),否则查该评论的二级回复
|
||||
static Future<CommentListPage> list(
|
||||
int videoId, {
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
int parentId = 0,
|
||||
}) async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.videoComments(videoId),
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
'parent_id': parentId,
|
||||
},
|
||||
);
|
||||
return CommentListPage.fromJson(data);
|
||||
}
|
||||
|
||||
/// 发表评论(需登录):回复时传 parentId(一级评论 ID)与 replyToUserId
|
||||
static Future<CommentCreateResult> create(
|
||||
int videoId,
|
||||
String content, {
|
||||
int? parentId,
|
||||
int? replyToUserId,
|
||||
}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.videoComments(videoId),
|
||||
data: {
|
||||
'content': content,
|
||||
if (parentId != null) 'parent_id': parentId,
|
||||
if (replyToUserId != null) 'reply_to_user_id': replyToUserId,
|
||||
},
|
||||
);
|
||||
return CommentCreateResult.fromJson(data);
|
||||
}
|
||||
|
||||
/// 删除评论(本人评论或本人视频下的评论)
|
||||
static Future<void> delete(int commentId) =>
|
||||
ApiClient.delete<Map<String, dynamic>>(Endpoints.commentDelete(commentId));
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
|
||||
/// 推广归因服务(三段式自建方案):
|
||||
/// 1. 分享口令(剪贴板解析,准确率 100%)——主路径
|
||||
/// 2. H5 中间页点击埋点 + 首启模糊匹配——辅助路径(本类 reportMatch)
|
||||
/// 3. 注册页邀请码手填——兜底
|
||||
class InviteService {
|
||||
InviteService._();
|
||||
|
||||
/// 口令格式:#短视频邀请码XXXXXXXX#(邀请码 8 位大写字母数字,与服务端生成规则一致)
|
||||
static final RegExp _commandRe = RegExp(r'#短视频邀请码([0-9A-Z]{8})#');
|
||||
|
||||
/// 从剪贴板文本解析邀请口令,未命中返回 null
|
||||
static String? parseInviteCommand(String? text) {
|
||||
if (text == null || text.isEmpty) return null;
|
||||
return _commandRe.firstMatch(text)?.group(1);
|
||||
}
|
||||
|
||||
/// 生成推广文案(邀请页"复制推广文案",口令包裹其中供被邀请人首启解析)
|
||||
static String buildShareText({required String inviteCode, required String inviteUrl}) {
|
||||
return '【短视频】精彩短视频刷不停,新人注册立送体验会员!\n'
|
||||
'① 点击链接下载:$inviteUrl\n'
|
||||
'② 或复制本段文字,打开 App 自动绑定我的邀请\n'
|
||||
'#短视频邀请码$inviteCode#';
|
||||
}
|
||||
|
||||
/// 首启模糊归因上报(无口令时的辅助路径);
|
||||
/// 命中结果由服务端按 device_id 缓存,注册时自动带出;失败不阻塞启动
|
||||
static Future<String?> reportMatch(String deviceId) async {
|
||||
try {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.inviteMatch,
|
||||
data: {'device_id': deviceId},
|
||||
);
|
||||
if (data['matched'] == true) return data['invite_code'] as String?;
|
||||
} catch (_) {
|
||||
// 归因失败不影响正常启动
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 我的邀请(邀请码/推广链接/统计,对应《03-接口文档》9.1)
|
||||
static Future<ReferralInfo> fetchMyReferral() async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(Endpoints.referralMe);
|
||||
return ReferralInfo.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// 我的邀请信息
|
||||
class ReferralInfo {
|
||||
ReferralInfo({
|
||||
required this.inviteCode,
|
||||
required this.inviteUrl,
|
||||
required this.eligible,
|
||||
required this.totalInvitees,
|
||||
required this.activatedCount,
|
||||
required this.rewardPointsTotal,
|
||||
required this.rebateAssetsTotal,
|
||||
});
|
||||
|
||||
final String inviteCode;
|
||||
final String inviteUrl;
|
||||
|
||||
/// 是否具备奖励资格(会员)
|
||||
final bool eligible;
|
||||
final int totalInvitees;
|
||||
final int activatedCount;
|
||||
final int rewardPointsTotal;
|
||||
final double rebateAssetsTotal;
|
||||
|
||||
factory ReferralInfo.fromJson(Map<String, dynamic> json) => ReferralInfo(
|
||||
inviteCode: (json['invite_code'] as String?) ?? '',
|
||||
inviteUrl: (json['invite_url'] as String?) ?? '',
|
||||
eligible: (json['eligible'] as bool?) ?? false,
|
||||
totalInvitees: (json['total_invitees'] as num?)?.toInt() ?? 0,
|
||||
activatedCount: (json['activated_count'] as num?)?.toInt() ?? 0,
|
||||
rewardPointsTotal: (json['reward_points_total'] as num?)?.toInt() ?? 0,
|
||||
rebateAssetsTotal: (json['rebate_assets_total'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
import '../models/membership_models.dart';
|
||||
|
||||
/// 会员服务(《03-接口文档》第 7 章)
|
||||
class MembershipService {
|
||||
MembershipService._();
|
||||
|
||||
/// 7.1 套餐列表(游客可,价格来自服务端配置)
|
||||
static Future<PlansResult> fetchPlans() async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.membershipPlans,
|
||||
);
|
||||
return PlansResult.fromJson(data);
|
||||
}
|
||||
|
||||
/// 7.2 我的会员状态(需登录)
|
||||
static Future<MyMemberState> fetchMe() async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.membershipMe,
|
||||
);
|
||||
return MyMemberState.fromJson(data);
|
||||
}
|
||||
|
||||
/// 7.3 积分开通会员(需登录)
|
||||
/// 错误码:2004 终身卡重复开通,2001 积分不足
|
||||
static Future<RedeemResult> redeem(String planCode) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.membershipRedeem,
|
||||
data: {'plan_code': planCode},
|
||||
);
|
||||
return RedeemResult.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// 开通结果
|
||||
class RedeemResult {
|
||||
const RedeemResult({
|
||||
required this.orderNo,
|
||||
required this.planName,
|
||||
required this.pointsCost,
|
||||
required this.remainingPoints,
|
||||
required this.expireAt,
|
||||
});
|
||||
|
||||
final String orderNo;
|
||||
final String planName;
|
||||
final int pointsCost;
|
||||
final int remainingPoints;
|
||||
final String expireAt;
|
||||
|
||||
factory RedeemResult.fromJson(Map<String, dynamic> json) => RedeemResult(
|
||||
orderNo: json['order_no'] as String,
|
||||
planName: json['plan_name'] as String,
|
||||
pointsCost: (json['points_cost'] as num).toInt(),
|
||||
remainingPoints: (json['remaining_points'] as num).toInt(),
|
||||
expireAt: json['expire_at'] as String,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
|
||||
/// 消息通知(《03-接口文档》10.2)
|
||||
class NotificationItem {
|
||||
const NotificationItem({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.title,
|
||||
this.content = '',
|
||||
this.bizId = '',
|
||||
this.isRead = false,
|
||||
this.createdAt = '',
|
||||
});
|
||||
|
||||
final int id;
|
||||
|
||||
/// 1审核结果 2提现进度 3系统公告 4评论回复
|
||||
final int type;
|
||||
final String title;
|
||||
final String content;
|
||||
|
||||
/// 关联业务 ID(审核结果=视频ID、提现进度=提现单号等)
|
||||
final String bizId;
|
||||
final bool isRead;
|
||||
final String createdAt;
|
||||
|
||||
static const Map<int, String> typeLabels = {
|
||||
1: '审核',
|
||||
2: '提现',
|
||||
3: '公告',
|
||||
4: '回复',
|
||||
};
|
||||
|
||||
String get typeLabel => typeLabels[type] ?? '通知';
|
||||
|
||||
/// 审核结果:biz_id 为视频 ID
|
||||
int? get videoId => type == 1 ? int.tryParse(bizId) : null;
|
||||
|
||||
NotificationItem copyWith({bool? isRead}) => NotificationItem(
|
||||
id: id,
|
||||
type: type,
|
||||
title: title,
|
||||
content: content,
|
||||
bizId: bizId,
|
||||
isRead: isRead ?? this.isRead,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
factory NotificationItem.fromJson(Map<String, dynamic> json) =>
|
||||
NotificationItem(
|
||||
id: (json['id'] as num).toInt(),
|
||||
type: (json['type'] as num?)?.toInt() ?? 3,
|
||||
title: (json['title'] as String?) ?? '',
|
||||
content: (json['content'] as String?) ?? '',
|
||||
bizId: (json['biz_id'] as String?) ?? '',
|
||||
isRead: json['is_read'] as bool? ?? false,
|
||||
createdAt: (json['created_at'] as String?) ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 通知分页
|
||||
class NotificationsPage {
|
||||
const NotificationsPage({required this.total, required this.items});
|
||||
|
||||
final int total;
|
||||
final List<NotificationItem> items;
|
||||
|
||||
factory NotificationsPage.fromJson(Map<String, dynamic> json) =>
|
||||
NotificationsPage(
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
items: ((json['list'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(NotificationItem.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 消息通知服务(《03-接口文档》10.2~10.4,均需登录)
|
||||
class NotificationService {
|
||||
NotificationService._();
|
||||
|
||||
/// 10.2 通知列表(type 筛选 + 分页)
|
||||
static Future<NotificationsPage> fetchNotifications({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
int? type,
|
||||
}) async {
|
||||
final query = <String, dynamic>{'page': page, 'page_size': pageSize};
|
||||
if (type != null) query['type'] = type;
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.notifications,
|
||||
queryParameters: query,
|
||||
);
|
||||
return NotificationsPage.fromJson(data);
|
||||
}
|
||||
|
||||
/// 10.3 未读数
|
||||
static Future<int> fetchUnreadCount() async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.notificationsUnread,
|
||||
);
|
||||
return (data['unread'] as num?)?.toInt() ?? 0;
|
||||
}
|
||||
|
||||
/// 10.4 标记已读:ids 与 all 二选一
|
||||
static Future<void> markRead({List<int>? ids, bool all = false}) async {
|
||||
await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.notificationsRead,
|
||||
data: all ? {'all': true} : {'ids': ids},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluwx/fluwx.dart';
|
||||
import 'package:tobias/tobias.dart';
|
||||
|
||||
import '../core/config/app_config.dart';
|
||||
import 'payment_service.dart';
|
||||
|
||||
/// 支付宝/微信原生 SDK 调起封装(《03-接口文档》8.2 pay_params)
|
||||
///
|
||||
/// 服务端配置了商户密钥时 pay_params 为真实调起参数,此处调起原生 SDK;
|
||||
/// 未配置(dev)时服务端返回 dev_mock 占位,调用方应以轮询兜底代替调起。
|
||||
enum PayLaunchResult {
|
||||
/// 渠道回调成功(仍需以服务端查单为准,避免客户端伪造)
|
||||
success,
|
||||
|
||||
/// 用户主动取消
|
||||
canceled,
|
||||
|
||||
/// 调起失败(未安装 App / 参数缺失 / SDK 报错)
|
||||
failed,
|
||||
|
||||
/// pay_params 无有效调起参数(服务端未配置商户密钥)
|
||||
unsupported,
|
||||
}
|
||||
|
||||
class PayLauncher {
|
||||
PayLauncher._();
|
||||
|
||||
static final _fluwx = Fluwx();
|
||||
|
||||
/// 微信调起等待回调的最长时间
|
||||
static const _wechatTimeout = Duration(minutes: 5);
|
||||
|
||||
/// 支付宝调起:order_string 直接交给 SDK
|
||||
static Future<PayLaunchResult> launchAlipay(RechargeOrderInfo order) async {
|
||||
final orderString = order.payParams['order_string'] as String?;
|
||||
if (orderString == null || orderString.isEmpty) {
|
||||
return PayLaunchResult.unsupported;
|
||||
}
|
||||
try {
|
||||
final result = await Tobias().pay(orderString);
|
||||
return _mapAlipayStatus(result['resultStatus'] as String?);
|
||||
} catch (_) {
|
||||
return PayLaunchResult.failed;
|
||||
}
|
||||
}
|
||||
|
||||
/// 微信调起:registerApi → pay → 等待 SDK 回调
|
||||
static Future<PayLaunchResult> launchWechat(RechargeOrderInfo order) async {
|
||||
final params = order.payParams;
|
||||
final appId = params['appid'] as String?;
|
||||
final partnerId = params['partnerid'] as String?;
|
||||
final prepayId = params['prepayid'] as String?;
|
||||
final packageValue = params['package'] as String?;
|
||||
final nonceStr = params['noncestr'] as String?;
|
||||
final sign = params['sign'] as String?;
|
||||
final timestamp = int.tryParse('${params['timestamp']}');
|
||||
if (appId == null ||
|
||||
appId.isEmpty ||
|
||||
partnerId == null ||
|
||||
partnerId.isEmpty ||
|
||||
prepayId == null ||
|
||||
prepayId.isEmpty ||
|
||||
packageValue == null ||
|
||||
packageValue.isEmpty ||
|
||||
nonceStr == null ||
|
||||
nonceStr.isEmpty ||
|
||||
sign == null ||
|
||||
sign.isEmpty ||
|
||||
timestamp == null) {
|
||||
return PayLaunchResult.unsupported;
|
||||
}
|
||||
|
||||
final completer = Completer<PayLaunchResult>();
|
||||
final cancelable = _fluwx.addSubscriber((response) {
|
||||
if (response is WeChatPaymentResponse && !completer.isCompleted) {
|
||||
completer.complete(_mapWechatErrCode(response.errCode));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await _fluwx.registerApi(
|
||||
appId: appId,
|
||||
universalLink: AppConfig.wechatUniversalLink.isEmpty
|
||||
? null
|
||||
: AppConfig.wechatUniversalLink,
|
||||
);
|
||||
final sent = await _fluwx.pay(
|
||||
which: Payment(
|
||||
appId: appId,
|
||||
partnerId: partnerId,
|
||||
prepayId: prepayId,
|
||||
packageValue: packageValue,
|
||||
nonceStr: nonceStr,
|
||||
timestamp: timestamp,
|
||||
sign: sign,
|
||||
),
|
||||
);
|
||||
if (!sent) return PayLaunchResult.failed;
|
||||
return await completer.future.timeout(
|
||||
_wechatTimeout,
|
||||
onTimeout: () => PayLaunchResult.failed,
|
||||
);
|
||||
} catch (_) {
|
||||
return PayLaunchResult.failed;
|
||||
} finally {
|
||||
cancelable.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// 按支付方式分发(1支付宝 2微信)
|
||||
static Future<PayLaunchResult> launch(RechargeOrderInfo order) {
|
||||
return switch (order.payMethod) {
|
||||
1 => launchAlipay(order),
|
||||
2 => launchWechat(order),
|
||||
_ => Future.value(PayLaunchResult.unsupported),
|
||||
};
|
||||
}
|
||||
|
||||
/// 支付宝 resultStatus:9000成功 6001用户取消 8000/8001处理中 其余失败
|
||||
static PayLaunchResult _mapAlipayStatus(String? status) {
|
||||
return switch (status) {
|
||||
'9000' => PayLaunchResult.success,
|
||||
'6001' => PayLaunchResult.canceled,
|
||||
'8000' || '8001' => PayLaunchResult.success, // 处理中,交由服务端查单确认
|
||||
_ => PayLaunchResult.failed,
|
||||
};
|
||||
}
|
||||
|
||||
/// 微信 errCode:0成功 -2用户取消 其余失败
|
||||
static PayLaunchResult _mapWechatErrCode(int? errCode) {
|
||||
return switch (errCode) {
|
||||
0 => PayLaunchResult.success,
|
||||
-2 => PayLaunchResult.canceled,
|
||||
_ => PayLaunchResult.failed,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
|
||||
/// 充值套餐档位(《03-接口文档》8.1,后台实时可配,前端不硬编码)
|
||||
class RechargePackage {
|
||||
const RechargePackage({
|
||||
required this.id,
|
||||
required this.amountYuan,
|
||||
required this.points,
|
||||
this.bonus = 0,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int amountYuan;
|
||||
final int points;
|
||||
|
||||
/// 赠送积分(角标「赠 x 积分」)
|
||||
final int bonus;
|
||||
|
||||
/// 到账总积分 = 基础 + 赠送
|
||||
int get totalPoints => points + bonus;
|
||||
|
||||
factory RechargePackage.fromJson(Map<String, dynamic> json) =>
|
||||
RechargePackage(
|
||||
id: (json['id'] as num).toInt(),
|
||||
amountYuan: (json['amount_yuan'] as num).toInt(),
|
||||
points: (json['points'] as num).toInt(),
|
||||
bonus: (json['bonus'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 自定义金额配置
|
||||
class CustomRechargeConfig {
|
||||
const CustomRechargeConfig({
|
||||
this.enabled = false,
|
||||
this.minYuan = 1,
|
||||
this.maxYuan = 10000,
|
||||
this.pointsPerYuan = 10,
|
||||
});
|
||||
|
||||
final bool enabled;
|
||||
final int minYuan;
|
||||
final int maxYuan;
|
||||
final int pointsPerYuan;
|
||||
|
||||
bool inRange(int yuan) => yuan >= minYuan && yuan <= maxYuan;
|
||||
|
||||
factory CustomRechargeConfig.fromJson(Map<String, dynamic> json) =>
|
||||
CustomRechargeConfig(
|
||||
enabled: json['enabled'] as bool? ?? false,
|
||||
minYuan: (json['min_yuan'] as num?)?.toInt() ?? 1,
|
||||
maxYuan: (json['max_yuan'] as num?)?.toInt() ?? 10000,
|
||||
pointsPerYuan: (json['points_per_yuan'] as num?)?.toInt() ?? 10,
|
||||
);
|
||||
}
|
||||
|
||||
/// 支付方式(1支付宝 2微信 3USDT)
|
||||
class PayMethod {
|
||||
const PayMethod({required this.method, required this.name});
|
||||
|
||||
final int method;
|
||||
final String name;
|
||||
|
||||
bool get isUsdt => method == 3;
|
||||
|
||||
factory PayMethod.fromJson(Map<String, dynamic> json) => PayMethod(
|
||||
method: (json['method'] as num).toInt(),
|
||||
name: json['name'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// 8.1 套餐列表响应
|
||||
class PackagesResult {
|
||||
const PackagesResult({
|
||||
this.packages = const [],
|
||||
this.custom = const CustomRechargeConfig(),
|
||||
this.payMethods = const [],
|
||||
this.usdtRate = 0,
|
||||
});
|
||||
|
||||
final List<RechargePackage> packages;
|
||||
final CustomRechargeConfig custom;
|
||||
final List<PayMethod> payMethods;
|
||||
final double usdtRate;
|
||||
|
||||
factory PackagesResult.fromJson(Map<String, dynamic> json) => PackagesResult(
|
||||
packages: ((json['packages'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(RechargePackage.fromJson)
|
||||
.toList(),
|
||||
custom: CustomRechargeConfig.fromJson(
|
||||
(json['custom'] as Map<String, dynamic>?) ?? const {},
|
||||
),
|
||||
payMethods: ((json['pay_methods'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(PayMethod.fromJson)
|
||||
.toList(),
|
||||
usdtRate: (json['usdt_rate'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 8.2 创建充值订单响应
|
||||
/// 支付宝/微信:pay_params 供 SDK 调起;USDT:地址 + 金额(含尾差)+ 网络
|
||||
class RechargeOrderInfo {
|
||||
const RechargeOrderInfo({
|
||||
required this.orderNo,
|
||||
required this.payMethod,
|
||||
required this.amountYuan,
|
||||
required this.points,
|
||||
this.expireAt = '',
|
||||
this.payParams = const {},
|
||||
this.usdtRate,
|
||||
this.usdtAmount,
|
||||
this.receiveAddress,
|
||||
this.network,
|
||||
});
|
||||
|
||||
final String orderNo;
|
||||
final int payMethod;
|
||||
final int amountYuan;
|
||||
final int points;
|
||||
final String expireAt;
|
||||
|
||||
/// 支付宝 order_string / 微信 appid+prepayid 等
|
||||
final Map<String, dynamic> payParams;
|
||||
final double? usdtRate;
|
||||
final String? usdtAmount;
|
||||
final String? receiveAddress;
|
||||
final String? network;
|
||||
|
||||
bool get isUsdt => payMethod == 3;
|
||||
|
||||
factory RechargeOrderInfo.fromJson(Map<String, dynamic> json) =>
|
||||
RechargeOrderInfo(
|
||||
orderNo: json['order_no'] as String,
|
||||
payMethod: (json['pay_method'] as num).toInt(),
|
||||
amountYuan: (json['amount_yuan'] as num).toInt(),
|
||||
points: (json['points'] as num).toInt(),
|
||||
expireAt: (json['expire_at'] as String?) ?? '',
|
||||
payParams:
|
||||
(json['pay_params'] as Map<String, dynamic>?) ?? const {},
|
||||
usdtRate: (json['usdt_rate'] as num?)?.toDouble(),
|
||||
usdtAmount: json['usdt_amount'] as String?,
|
||||
receiveAddress: json['receive_address'] as String?,
|
||||
network: json['network'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 8.3 查询订单响应(App 轮询用)
|
||||
class OrderStatusInfo {
|
||||
const OrderStatusInfo({
|
||||
required this.orderNo,
|
||||
required this.status,
|
||||
this.statusText = '',
|
||||
this.points = 0,
|
||||
this.paidAt,
|
||||
this.walletPoints = 0,
|
||||
});
|
||||
|
||||
final String orderNo;
|
||||
|
||||
/// 0待支付 1已支付 2已关闭 3已退款
|
||||
final int status;
|
||||
final String statusText;
|
||||
final int points;
|
||||
final String? paidAt;
|
||||
|
||||
/// 查单时服务端附带的当前钱包积分
|
||||
final int walletPoints;
|
||||
|
||||
bool get isPending => status == 0;
|
||||
bool get isPaid => status == 1;
|
||||
|
||||
factory OrderStatusInfo.fromJson(Map<String, dynamic> json) =>
|
||||
OrderStatusInfo(
|
||||
orderNo: json['order_no'] as String,
|
||||
status: (json['status'] as num).toInt(),
|
||||
statusText: (json['status_text'] as String?) ?? '',
|
||||
points: (json['points'] as num?)?.toInt() ?? 0,
|
||||
paidAt: json['paid_at'] as String?,
|
||||
walletPoints: (json['wallet_points'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 支付服务(《03-接口文档》第 8 章,均需登录)
|
||||
class PaymentService {
|
||||
PaymentService._();
|
||||
|
||||
/// 8.1 充值套餐列表
|
||||
static Future<PackagesResult> fetchPackages() async {
|
||||
final data =
|
||||
await ApiClient.get<Map<String, dynamic>>(Endpoints.payPackages);
|
||||
return PackagesResult.fromJson(data);
|
||||
}
|
||||
|
||||
/// 8.2 创建充值订单
|
||||
/// 错误码:1001 金额不在可选档位
|
||||
static Future<RechargeOrderInfo> createRecharge({
|
||||
required int amountYuan,
|
||||
required int payMethod,
|
||||
}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.recharge,
|
||||
data: {'amount_yuan': amountYuan, 'pay_method': payMethod},
|
||||
);
|
||||
return RechargeOrderInfo.fromJson(data);
|
||||
}
|
||||
|
||||
/// 8.3 查询订单状态
|
||||
/// 错误码:3001 订单不存在
|
||||
static Future<OrderStatusInfo> fetchOrder(String orderNo) async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.orderDetail(orderNo),
|
||||
);
|
||||
return OrderStatusInfo.fromJson(data);
|
||||
}
|
||||
|
||||
/// 8.4 取消订单(仅待支付可取消)
|
||||
/// 错误码:3001 订单不存在,3002 订单状态异常
|
||||
static Future<void> cancelOrder(String orderNo) async {
|
||||
await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.orderCancel(orderNo),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
import '../models/video_item.dart';
|
||||
|
||||
/// 搜索服务(《03-接口文档》5.10/5.11)
|
||||
class SearchService {
|
||||
SearchService._();
|
||||
|
||||
/// 视频搜索:sort=hot(默认)| latest;命中敏感词服务端返回空结果
|
||||
static Future<SearchPage> search(
|
||||
String keyword, {
|
||||
String sort = 'hot',
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.search,
|
||||
queryParameters: {
|
||||
'keyword': keyword,
|
||||
'sort': sort,
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
},
|
||||
);
|
||||
return SearchPage.fromJson(data);
|
||||
}
|
||||
|
||||
/// 热搜榜 Top10(Redis ZSet search:hot)
|
||||
static Future<List<String>> hotWords() async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(Endpoints.hotSearch);
|
||||
return ((data['words'] as List?) ?? const []).map((e) => e.toString()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
/// 搜索结果分页(普通 page/page_size 分页)
|
||||
class SearchPage {
|
||||
SearchPage({required this.total, required this.items});
|
||||
|
||||
final int total;
|
||||
final List<VideoItem> items;
|
||||
|
||||
factory SearchPage.fromJson(Map<String, dynamic> json) => SearchPage(
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
items: ((json['list'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(VideoItem.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
|
||||
/// 上传发布服务(《03-接口文档》5.5 / 5.6 / 5.8)
|
||||
/// 开发环境链路:upload/token(server 中转模式)→ upload/file(multipart)→ 发布
|
||||
class UploadService {
|
||||
UploadService._();
|
||||
|
||||
/// 5.8 分类列表(发布时选择)
|
||||
static Future<List<VideoCategory>> fetchCategories() async {
|
||||
final data = await ApiClient.get<List<dynamic>>(Endpoints.categories);
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(VideoCategory.fromJson)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 5.5 获取上传凭证(开发环境返回 server 中转模式)
|
||||
static Future<UploadTokenInfo> fetchUploadToken({
|
||||
required String fileName,
|
||||
required int fileSize,
|
||||
}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.uploadToken,
|
||||
data: {'file_name': fileName, 'file_size': fileSize},
|
||||
);
|
||||
return UploadTokenInfo.fromJson(data);
|
||||
}
|
||||
|
||||
/// 5.5 配套:multipart 中转上传(字段 object_key + file)
|
||||
static Future<UploadFileResult> uploadFile({
|
||||
required String objectKey,
|
||||
required String filePath,
|
||||
required String fileName,
|
||||
void Function(int sent, int total)? onProgress,
|
||||
}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.uploadFile,
|
||||
data: FormData.fromMap({
|
||||
'object_key': objectKey,
|
||||
'file': await MultipartFile.fromFile(filePath, filename: fileName),
|
||||
}),
|
||||
onSendProgress: onProgress,
|
||||
);
|
||||
return UploadFileResult.fromJson(data);
|
||||
}
|
||||
|
||||
/// 5.6 发布视频
|
||||
static Future<PublishResult> publish({
|
||||
required String objectKey,
|
||||
required String title,
|
||||
String description = '',
|
||||
required int categoryId,
|
||||
List<String> tags = const [],
|
||||
}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.publish,
|
||||
data: {
|
||||
'object_key': objectKey,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'category_id': categoryId,
|
||||
'tags': tags,
|
||||
},
|
||||
);
|
||||
return PublishResult.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// 视频分类
|
||||
class VideoCategory {
|
||||
VideoCategory({required this.categoryId, required this.name});
|
||||
|
||||
final int categoryId;
|
||||
final String name;
|
||||
|
||||
factory VideoCategory.fromJson(Map<String, dynamic> json) => VideoCategory(
|
||||
categoryId: (json['category_id'] as num?)?.toInt() ?? 0,
|
||||
name: (json['name'] as String?) ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 上传凭证(server 中转模式)
|
||||
class UploadTokenInfo {
|
||||
UploadTokenInfo({
|
||||
required this.uploadType,
|
||||
required this.objectKey,
|
||||
required this.uploadUrl,
|
||||
});
|
||||
|
||||
final String uploadType; // server / oss_sts
|
||||
final String objectKey;
|
||||
final String uploadUrl;
|
||||
|
||||
factory UploadTokenInfo.fromJson(Map<String, dynamic> json) =>
|
||||
UploadTokenInfo(
|
||||
uploadType: (json['upload_type'] as String?) ?? '',
|
||||
objectKey: (json['object_key'] as String?) ?? '',
|
||||
uploadUrl: (json['upload_url'] as String?) ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 中转上传结果
|
||||
class UploadFileResult {
|
||||
UploadFileResult({
|
||||
required this.objectKey,
|
||||
required this.url,
|
||||
required this.fileSize,
|
||||
});
|
||||
|
||||
final String objectKey;
|
||||
final String url;
|
||||
final int fileSize;
|
||||
|
||||
factory UploadFileResult.fromJson(Map<String, dynamic> json) =>
|
||||
UploadFileResult(
|
||||
objectKey: (json['object_key'] as String?) ?? '',
|
||||
url: (json['url'] as String?) ?? '',
|
||||
fileSize: (json['file_size'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 发布结果
|
||||
class PublishResult {
|
||||
PublishResult({
|
||||
required this.videoId,
|
||||
required this.status,
|
||||
required this.statusText,
|
||||
required this.isPriority,
|
||||
});
|
||||
|
||||
final int videoId;
|
||||
final int status; // 0转码中 1待审核 2已发布 3拒绝 4下架 5转码失败
|
||||
final String statusText;
|
||||
final bool isPriority; // 会员优先转码
|
||||
|
||||
factory PublishResult.fromJson(Map<String, dynamic> json) => PublishResult(
|
||||
videoId: (json['video_id'] as num?)?.toInt() ?? 0,
|
||||
status: (json['status'] as num?)?.toInt() ?? 0,
|
||||
statusText: (json['status_text'] as String?) ?? '',
|
||||
isPriority: (json['is_priority'] as bool?) ?? false,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
import '../models/user_model.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import '../models/video_item.dart';
|
||||
|
||||
/// 用户服务(《03-接口文档》第 4 章 + 6.6/6.7)
|
||||
class UserService {
|
||||
UserService._();
|
||||
|
||||
/// 4.1 获取个人信息
|
||||
static Future<UserModel> fetchMe() async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(Endpoints.me);
|
||||
return UserModel.fromJson(data);
|
||||
}
|
||||
|
||||
/// 4.2 修改资料(返回更新后的完整用户对象)
|
||||
static Future<UserModel> updateMe({
|
||||
String? nickname,
|
||||
String? bio,
|
||||
int? gender,
|
||||
}) async {
|
||||
final data = await ApiClient.put<Map<String, dynamic>>(
|
||||
Endpoints.me,
|
||||
data: {
|
||||
if (nickname != null) 'nickname': nickname,
|
||||
if (bio != null) 'bio': bio,
|
||||
if (gender != null) 'gender': gender,
|
||||
},
|
||||
);
|
||||
return UserModel.fromJson(data);
|
||||
}
|
||||
|
||||
/// 4.3 上传头像(multipart,字段名 file;仅支持 jpg/png,≤5MB)
|
||||
static Future<String> uploadAvatar({
|
||||
required String filePath,
|
||||
required String fileName,
|
||||
}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.meAvatar,
|
||||
data: FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(filePath, filename: fileName),
|
||||
}),
|
||||
);
|
||||
return (data['avatar_url'] as String?) ?? '';
|
||||
}
|
||||
|
||||
/// 4.7 用户公开主页(游客可)
|
||||
static Future<UserPublicProfile> fetchPublicProfile(int userId) async {
|
||||
final data =
|
||||
await ApiClient.get<Map<String, dynamic>>(Endpoints.userDetail(userId));
|
||||
return UserPublicProfile.fromJson(data);
|
||||
}
|
||||
|
||||
/// 4.8 他人作品列表(仅已发布)
|
||||
static Future<PagedVideos> fetchUserVideos(
|
||||
int userId, {
|
||||
int page = 1,
|
||||
int pageSize = 50,
|
||||
}) =>
|
||||
_fetchVideos(Endpoints.userVideos(userId), page: page, pageSize: pageSize);
|
||||
|
||||
/// 我的作品(需登录,含各状态)
|
||||
static Future<PagedVideos> fetchMyVideos({
|
||||
int page = 1,
|
||||
int pageSize = 50,
|
||||
}) =>
|
||||
_fetchVideos(Endpoints.meVideos, page: page, pageSize: pageSize);
|
||||
|
||||
/// 6.6 我的收藏(需登录)
|
||||
static Future<PagedVideos> fetchMyFavorites({
|
||||
int page = 1,
|
||||
int pageSize = 50,
|
||||
}) =>
|
||||
_fetchVideos(Endpoints.meFavorites, page: page, pageSize: pageSize);
|
||||
|
||||
/// 6.7 观看历史(需登录):video + watched_at + cost_type
|
||||
static Future<List<HistoryItem>> fetchMyHistory({
|
||||
int page = 1,
|
||||
int pageSize = 50,
|
||||
}) async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.meHistory,
|
||||
queryParameters: {'page': page, 'page_size': pageSize},
|
||||
);
|
||||
return ((data['list'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(HistoryItem.fromJson)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static Future<PagedVideos> _fetchVideos(
|
||||
String path, {
|
||||
required int page,
|
||||
required int pageSize,
|
||||
}) async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
path,
|
||||
queryParameters: {'page': page, 'page_size': pageSize},
|
||||
);
|
||||
return PagedVideos(
|
||||
total: (data['total'] as num?)?.toInt() ?? 0,
|
||||
items: ((data['list'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(VideoItem.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 视频分页结果(普通 page/page_size 分页)
|
||||
class PagedVideos {
|
||||
PagedVideos({required this.total, required this.items});
|
||||
|
||||
final int total;
|
||||
final List<VideoItem> items;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'dart:io';
|
||||
|
||||
import '../core/config/app_config.dart';
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
|
||||
/// 版本检查(《03-接口文档》10.1,《01-开发文档》5.7)
|
||||
class VersionService {
|
||||
VersionService._();
|
||||
|
||||
static Future<VersionInfo> check() async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.appVersion,
|
||||
queryParameters: {
|
||||
'platform': Platform.isIOS ? 'ios' : 'android',
|
||||
'version_code': AppConfig.appVersionCode,
|
||||
},
|
||||
);
|
||||
return VersionInfo(
|
||||
latestVersion: (data['latest_version'] as String?) ?? '',
|
||||
latestVersionCode: (data['latest_version_code'] as num?)?.toInt() ?? 0,
|
||||
forceUpdate: (data['force_update'] as bool?) ?? false,
|
||||
updateUrl: (data['update_url'] as String?) ?? '',
|
||||
notice: (data['notice'] as String?) ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 版本检查结果
|
||||
class VersionInfo {
|
||||
VersionInfo({
|
||||
required this.latestVersion,
|
||||
required this.latestVersionCode,
|
||||
required this.forceUpdate,
|
||||
required this.updateUrl,
|
||||
required this.notice,
|
||||
});
|
||||
|
||||
final String latestVersion;
|
||||
final int latestVersionCode;
|
||||
final bool forceUpdate;
|
||||
final String updateUrl;
|
||||
|
||||
/// 维护公告(非空时全 App 只读提示)
|
||||
final String notice;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
import '../models/video_item.dart';
|
||||
import '../models/watch_result.dart';
|
||||
|
||||
/// 视频服务:Feed / 详情 / 观看计费上报(《03-接口文档》第 5 章)
|
||||
class VideoService {
|
||||
VideoService._();
|
||||
|
||||
/// 5.1 Feed 流:type=recommend|latest|hot,游标分页
|
||||
static Future<FeedPage> fetchFeed({
|
||||
String type = 'recommend',
|
||||
String? cursor,
|
||||
int limit = 10,
|
||||
int? categoryId,
|
||||
}) async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.feed,
|
||||
queryParameters: {
|
||||
'type': type,
|
||||
if (cursor != null && cursor.isNotEmpty) 'cursor': cursor,
|
||||
'limit': limit,
|
||||
if (categoryId != null) 'category_id': categoryId,
|
||||
},
|
||||
);
|
||||
final list = ((data['list'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(VideoItem.fromJson)
|
||||
.toList();
|
||||
return FeedPage(items: list, nextCursor: data['next_cursor'] as String?);
|
||||
}
|
||||
|
||||
/// 5.2 视频详情(登录返回续播进度 my_position)
|
||||
static Future<Map<String, dynamic>> fetchDetail(int videoId) =>
|
||||
ApiClient.get<Map<String, dynamic>>(Endpoints.videoDetail(videoId));
|
||||
|
||||
/// 5.3 观看上报(计费):开始播放时调用,客户端已按 video_id+日期 本地去重
|
||||
static Future<WatchResult> reportWatch(int videoId, {int? position}) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.videoWatch(videoId),
|
||||
data: {if (position != null) 'position': position},
|
||||
);
|
||||
return WatchResult.fromJson(data);
|
||||
}
|
||||
|
||||
/// 5.4 完整观看上报:进度 ≥95% 或自然播完时调用(创作者收益计数)
|
||||
/// 返回 credited:今日首次有效为 true,其余情况 false(无需 UI 处理)
|
||||
static Future<bool> reportComplete(int videoId, int watchDuration) async {
|
||||
final data = await ApiClient.post<Map<String, dynamic>>(
|
||||
Endpoints.videoWatchComplete(videoId),
|
||||
data: {'watch_duration': watchDuration},
|
||||
);
|
||||
return (data['credited'] as bool?) ?? false;
|
||||
}
|
||||
|
||||
/// 5.29 点赞/取消
|
||||
static Future<Map<String, dynamic>> toggleLike(int videoId) =>
|
||||
ApiClient.post<Map<String, dynamic>>(Endpoints.videoLike(videoId));
|
||||
|
||||
/// 5.30 收藏/取消
|
||||
static Future<Map<String, dynamic>> toggleFavorite(int videoId) =>
|
||||
ApiClient.post<Map<String, dynamic>>(Endpoints.videoFavorite(videoId));
|
||||
|
||||
/// 5.12 编辑视频(仅作者):仅传需要修改的字段
|
||||
static Future<void> updateVideo(
|
||||
int videoId, {
|
||||
String? title,
|
||||
String? description,
|
||||
int? categoryId,
|
||||
List<String>? tags,
|
||||
}) async {
|
||||
await ApiClient.put<Map<String, dynamic>>(
|
||||
Endpoints.videoEdit(videoId),
|
||||
data: {
|
||||
if (title != null) 'title': title,
|
||||
if (description != null) 'description': description,
|
||||
if (categoryId != null) 'category_id': categoryId,
|
||||
if (tags != null) 'tags': tags,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 5.13 删除视频(仅作者,服务端软删除)
|
||||
static Future<void> deleteVideo(int videoId) =>
|
||||
ApiClient.delete<Map<String, dynamic>>(Endpoints.videoDelete(videoId));
|
||||
|
||||
/// 5.9 标签视频列表(游客可):sort=hot(默认)| latest,普通分页
|
||||
static Future<TagVideosPage> fetchTagVideos(
|
||||
String tagName, {
|
||||
String sort = 'hot',
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.tagVideos(Uri.encodeComponent(tagName)),
|
||||
queryParameters: {'sort': sort, 'page': page, 'page_size': pageSize},
|
||||
);
|
||||
return TagVideosPage.fromJson(data, fallbackTag: tagName);
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed 分页结果(游标)
|
||||
class FeedPage {
|
||||
FeedPage({required this.items, this.nextCursor});
|
||||
|
||||
final List<VideoItem> items;
|
||||
|
||||
/// null 表示没有更多
|
||||
final String? nextCursor;
|
||||
}
|
||||
|
||||
/// 标签视频分页结果(普通 page/page_size 分页)
|
||||
class TagVideosPage {
|
||||
TagVideosPage({required this.tag, required this.total, required this.items});
|
||||
|
||||
final String tag;
|
||||
final int total;
|
||||
final List<VideoItem> items;
|
||||
|
||||
factory TagVideosPage.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
String fallbackTag = '',
|
||||
}) =>
|
||||
TagVideosPage(
|
||||
tag: (json['tag'] as String?) ?? fallbackTag,
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
items: ((json['list'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(VideoItem.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/network/endpoints.dart';
|
||||
|
||||
/// 钱包信息(《03-接口文档》4.4)
|
||||
class WalletInfo {
|
||||
const WalletInfo({
|
||||
this.points = 0,
|
||||
this.totalRecharge = 0,
|
||||
this.totalEarned = 0,
|
||||
this.totalConsumed = 0,
|
||||
this.rewardProgress = const CommentRewardProgress(),
|
||||
});
|
||||
|
||||
final int points;
|
||||
final int totalRecharge;
|
||||
final int totalEarned;
|
||||
final int totalConsumed;
|
||||
final CommentRewardProgress rewardProgress;
|
||||
|
||||
factory WalletInfo.fromJson(Map<String, dynamic> json) => WalletInfo(
|
||||
points: (json['points'] as num?)?.toInt() ?? 0,
|
||||
totalRecharge: (json['total_recharge'] as num?)?.toInt() ?? 0,
|
||||
totalEarned: (json['total_earned'] as num?)?.toInt() ?? 0,
|
||||
totalConsumed: (json['total_consumed'] as num?)?.toInt() ?? 0,
|
||||
rewardProgress: CommentRewardProgress.fromJson(
|
||||
(json['comment_reward_progress'] as Map<String, dynamic>?) ??
|
||||
const {},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 评论奖励进度
|
||||
class CommentRewardProgress {
|
||||
const CommentRewardProgress({
|
||||
this.current = 0,
|
||||
this.target = 10,
|
||||
this.todayRewarded = 0,
|
||||
this.dailyLimit = 5,
|
||||
});
|
||||
|
||||
final int current;
|
||||
final int target;
|
||||
final int todayRewarded;
|
||||
final int dailyLimit;
|
||||
|
||||
factory CommentRewardProgress.fromJson(Map<String, dynamic> json) =>
|
||||
CommentRewardProgress(
|
||||
current: (json['current'] as num?)?.toInt() ?? 0,
|
||||
target: (json['target'] as num?)?.toInt() ?? 10,
|
||||
todayRewarded: (json['today_rewarded'] as num?)?.toInt() ?? 0,
|
||||
dailyLimit: (json['daily_limit'] as num?)?.toInt() ?? 5,
|
||||
);
|
||||
}
|
||||
|
||||
/// 积分明细记录(《03-接口文档》4.5)
|
||||
class PointsRecord {
|
||||
const PointsRecord({
|
||||
required this.id,
|
||||
required this.changeAmount,
|
||||
required this.balanceAfter,
|
||||
required this.bizType,
|
||||
this.remark = '',
|
||||
this.createdAt = '',
|
||||
});
|
||||
|
||||
final int id;
|
||||
|
||||
/// 正=获得,负=消耗
|
||||
final int changeAmount;
|
||||
final int balanceAfter;
|
||||
|
||||
/// 1充值 2观看 3评论奖励 4会员 5赠送 6调整 7扣回
|
||||
final int bizType;
|
||||
final String remark;
|
||||
final String createdAt;
|
||||
|
||||
static const Map<int, String> bizTypeLabels = {
|
||||
1: '充值',
|
||||
2: '观看',
|
||||
3: '评论奖励',
|
||||
4: '会员',
|
||||
5: '赠送',
|
||||
6: '调整',
|
||||
7: '扣回',
|
||||
};
|
||||
|
||||
String get bizTypeLabel => bizTypeLabels[bizType] ?? '其他';
|
||||
bool get isExpense => changeAmount < 0;
|
||||
|
||||
factory PointsRecord.fromJson(Map<String, dynamic> json) => PointsRecord(
|
||||
id: (json['id'] as num).toInt(),
|
||||
changeAmount: (json['change_amount'] as num).toInt(),
|
||||
balanceAfter: (json['balance_after'] as num).toInt(),
|
||||
bizType: (json['biz_type'] as num).toInt(),
|
||||
remark: (json['remark'] as String?) ?? '',
|
||||
createdAt: (json['created_at'] as String?) ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 积分明细分页
|
||||
class PointsRecordsPage {
|
||||
const PointsRecordsPage({required this.total, required this.items});
|
||||
|
||||
final int total;
|
||||
final List<PointsRecord> items;
|
||||
|
||||
factory PointsRecordsPage.fromJson(Map<String, dynamic> json) =>
|
||||
PointsRecordsPage(
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
items: ((json['list'] as List?) ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(PointsRecord.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 钱包服务(《03-接口文档》4.4/4.5)
|
||||
class WalletService {
|
||||
WalletService._();
|
||||
|
||||
/// 4.4 钱包信息(需登录)
|
||||
static Future<WalletInfo> fetchWallet() async {
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(Endpoints.meWallet);
|
||||
return WalletInfo.fromJson(data);
|
||||
}
|
||||
|
||||
/// 4.5 积分明细(需登录)
|
||||
static Future<PointsRecordsPage> fetchPointsRecords({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
int? bizType,
|
||||
}) async {
|
||||
final query = <String, dynamic>{'page': page, 'page_size': pageSize};
|
||||
if (bizType != null) query['biz_type'] = bizType;
|
||||
final data = await ApiClient.get<Map<String, dynamic>>(
|
||||
Endpoints.mePointsRecords,
|
||||
queryParameters: query,
|
||||
);
|
||||
return PointsRecordsPage.fromJson(data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user