This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
+240
View File
@@ -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);
}
}
+87
View File
@@ -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;
}
+48
View File
@@ -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));
}
+81
View File
@@ -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,
);
}
+59
View File
@@ -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,
);
}
+113
View File
@@ -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},
);
}
}
+139
View File
@@ -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,
};
}
}
+225
View File
@@ -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),
);
}
}
+49
View File
@@ -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(),
);
}
+145
View File
@@ -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,
);
}
+119
View File
@@ -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;
}
+46
View File
@@ -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;
}
+132
View File
@@ -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(),
);
}
+141
View File
@@ -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);
}
}