Files
chat/client/flutter/lib/services/api_service.dart
2026-04-25 16:36:34 +08:00

444 lines
15 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:sales_chat/models/user.dart';
import 'package:sales_chat/models/group.dart';
import 'package:sales_chat/models/message.dart';
import 'package:sales_chat/models/invite.dart';
import 'package:sales_chat/models/stats.dart';
/// 与 Tailchat 后端通信的 API 服务。
///
/// 关键约定:
/// - 所有 API 路由都在 `/api/`(网关路径)下。
/// - Tailchat 核心用户路由:`/api/user/<action>`
/// - 插件路由:`/api/plugin:com.msgbyte.saleschat/<action>`(冒号用于分隔 Moleculer 服务名段)
/// - 认证使用 `X-Token` 请求头(非 Authorization: Bearer
/// - 网关响应格式为 `{ "code": <int>, "data": <payload> }`。
class ApiService {
late final Dio _dio;
final FlutterSecureStorage _storage;
static const String _baseUrl = 'http://localhost:3000/api';
static const String _tokenKey = 'auth_token';
static const String _userKey = 'user_data';
ApiService() : _storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
) {
_dio = Dio(BaseOptions(
baseUrl: _baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
},
));
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
// Tailchat 使用 X-Token 请求头进行认证
final token = await _storage.read(key: _tokenKey);
if (token != null) {
options.headers['X-Token'] = token;
}
return handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
await _storage.delete(key: _tokenKey);
await _storage.delete(key: _userKey);
}
return handler.next(error);
},
));
}
Future<String?> get token => _storage.read(key: _tokenKey);
Future<bool> get isAuthenticated async =>
await _storage.read(key: _tokenKey) != null;
/// 解包网关响应:{ "code": 200, "data": ... }
dynamic _unwrapResponse(Response response) {
final body = response.data;
if (body is Map<String, dynamic> && body.containsKey('data')) {
return body['data'];
}
return body;
}
// ============================================================
// 认证 API - Tailchat 核心用户服务
// ============================================================
/// 使用邮箱或用户名 + 密码登录。
/// 后端接口POST /api/user/login {email|username, password}
/// 响应(已解包):{ ...userFields, token: "jwt..." }
Future<AuthResponse> login({
String? email,
String? username,
required String password,
}) async {
final data = <String, dynamic>{
'password': password,
};
if (email != null && email.isNotEmpty) {
data['email'] = email;
} else if (username != null && username.isNotEmpty) {
data['username'] = username;
}
final response = await _dio.post('/user/login', data: data);
final unwrapped = _unwrapResponse(response);
// 登录响应是用户对象附加了 `token`
final authResponse = AuthResponse.fromJson({
'data': unwrapped,
});
await _storage.write(key: _tokenKey, value: authResponse.token);
await _storage.write(
key: _userKey, value: jsonEncode(authResponse.user.toJson()));
return authResponse;
}
/// 注册新用户。
/// 后端接口POST /api/user/register {username, email, nickname, password}
/// 响应(已解包):{ ...userFields, token: "jwt..." }
Future<AuthResponse> register({
String? username,
String? email,
String? nickname,
required String password,
}) async {
final data = <String, dynamic>{
'password': password,
};
if (username != null && username.isNotEmpty) data['username'] = username;
if (email != null && email.isNotEmpty) data['email'] = email;
if (nickname != null && nickname.isNotEmpty) data['nickname'] = nickname;
final response = await _dio.post('/user/register', data: data);
final unwrapped = _unwrapResponse(response);
final authResponse = AuthResponse.fromJson({
'data': unwrapped,
});
await _storage.write(key: _tokenKey, value: authResponse.token);
await _storage.write(
key: _userKey, value: jsonEncode(authResponse.user.toJson()));
return authResponse;
}
/// 创建临时(访客)用户。
/// 后端接口POST /api/user/createTemporaryUser {nickname}
Future<AuthResponse> createTemporaryUser({required String nickname}) async {
final response = await _dio.post('/user/createTemporaryUser', data: {
'nickname': nickname,
});
final unwrapped = _unwrapResponse(response);
final authResponse = AuthResponse.fromJson({
'data': unwrapped,
});
await _storage.write(key: _tokenKey, value: authResponse.token);
await _storage.write(
key: _userKey, value: jsonEncode(authResponse.user.toJson()));
return authResponse;
}
/// 登出(仅本地 - JWT 是无状态的)
Future<void> logout() async {
await _storage.delete(key: _tokenKey);
await _storage.delete(key: _userKey);
}
/// 从本地缓存获取当前用户
Future<User?> getCurrentUser() async {
final userData = await _storage.read(key: _userKey);
if (userData == null) return null;
try {
return User.fromJson(jsonDecode(userData));
} catch (e) {
return null;
}
}
// ============================================================
// 群组 API - Tailchat 核心群组服务
// ============================================================
/// 获取当前用户加入的群组。
/// 后端接口GET /api/group/getUserGroups
Future<List<Group>> getGroups() async {
final response = await _dio.get('/group/getUserGroups');
final data = _unwrapResponse(response);
return (data as List).map((e) => Group.fromJson(e)).toList();
}
/// 获取单个群组的基本信息。
/// 后端接口GET /api/group/getGroupBasicInfo {groupId}
Future<GroupBasicInfo> getGroupBasicInfo(String groupId) async {
final response =
await _dio.get('/group/getGroupBasicInfo', queryParameters: {
'groupId': groupId,
});
final data = _unwrapResponse(response);
return GroupBasicInfo.fromJson(data);
}
/// 获取完整群组信息。
/// 后端接口GET /api/group/getGroupInfo {groupId}
Future<Group> getGroupInfo(String groupId) async {
final response = await _dio.get('/group/getGroupInfo', queryParameters: {
'groupId': groupId,
});
final data = _unwrapResponse(response);
return Group.fromJson(data);
}
/// 创建新群组。
/// 后端接口POST /api/group/createGroup {name, panels}
Future<Group> createGroup(String name, {String? description}) async {
// Tailchat 创建群组时至少需要一个文本面板
final response = await _dio.post('/group/createGroup', data: {
'name': name,
'panels': [
{
'id': 'default',
'name': 'General',
'type': 0, // GroupPanelType.TEXT文本面板类型
}
],
});
final data = _unwrapResponse(response);
return Group.fromJson(data);
}
// ============================================================
// 消息 API - Tailchat 核心 chat.message 服务
// ============================================================
/// 获取会话消息。
/// 后端接口GET /api/chat.message/fetchConverseMessage {converseId, startId?}
Future<List<Message>> getMessages(String converseId,
{int limit = 50, String? before}) async {
final response = await _dio.get(
'/chat.message/fetchConverseMessage',
queryParameters: {
'converseId': converseId,
if (before != null) 'startId': before,
});
final data = _unwrapResponse(response);
return (data as List).map((e) => Message.fromJson(e)).toList();
}
/// 发送消息。
/// 后端接口POST /api/chat.message/sendMessage {converseId, content, groupId?, meta?}
Future<Message> sendMessage(String converseId, String content,
{String? groupId}) async {
final response =
await _dio.post('/chat.message/sendMessage', data: {
'converseId': converseId,
'content': content,
if (groupId != null) 'groupId': groupId,
});
final data = _unwrapResponse(response);
return Message.fromJson(data);
}
/// 撤回消息2 分钟内)。
/// 后端接口POST /api/chat.message/recallMessage {messageId}
Future<void> recallMessage(String messageId) async {
await _dio.post('/chat.message/recallMessage', data: {
'messageId': messageId,
});
}
/// 删除消息(管理员)。
/// 后端接口POST /api/chat.message/deleteMessage {messageId}
Future<void> deleteMessage(String messageId) async {
await _dio.post('/chat.message/deleteMessage', data: {
'messageId': messageId,
});
}
// ============================================================
// 插件 API - plugin:com.msgbyte.saleschat
// Tailchat 网关路由:/api/plugin:<pluginName>/<action>
// ============================================================
/// 创建邀请。
Future<Invite> createInvite({
required String groupId,
int? expiresInDays,
}) async {
final data = <String, dynamic>{
'groupId': groupId,
};
if (expiresInDays != null) {
data['expiresIn'] = expiresInDays;
}
final response = await _dio.post(
'/plugin:com.msgbyte.saleschat/inviteCreate',
data: data);
final unwrapped = _unwrapResponse(response);
return Invite.fromJson(unwrapped);
}
/// 获取我的邀请。
Future<List<Invite>> getMyInvites() async {
final response = await _dio
.get('/plugin:com.msgbyte.saleschat/inviteGetMyInvites');
final data = _unwrapResponse(response);
return (data as List).map((e) => Invite.fromJson(e)).toList();
}
/// 通过邀请码获取邀请(公开,无需认证)。
Future<Invite> getInviteByCode(String code) async {
final response = await _dio.get(
'/plugin:com.msgbyte.saleschat/inviteGetByCode',
queryParameters: {'code': code});
final data = _unwrapResponse(response);
return Invite.fromJson(data);
}
/// 获取邀请统计。
Future<InviteStats> getInviteStats(String code) async {
final response = await _dio.get(
'/plugin:com.msgbyte.saleschat/inviteGetStats',
queryParameters: {'code': code});
final data = _unwrapResponse(response);
return InviteStats.fromJson(data);
}
/// 停用邀请。
Future<void> deactivateInvite(String code) async {
await _dio.post(
'/plugin:com.msgbyte.saleschat/inviteDeactivate',
data: {'code': code});
}
/// 通过邀请码加入。
Future<Map<String, dynamic>> joinInvite(String code) async {
final response = await _dio.post(
'/plugin:com.msgbyte.saleschat/inviteJoin',
data: {'code': code});
return _unwrapResponse(response);
}
/// 获取我的统计数据。
Future<MyStats> getMyStats() async {
final response =
await _dio.get('/plugin:com.msgbyte.saleschat/statsGetMyStats');
final data = _unwrapResponse(response);
return MyStats.fromJson(data);
}
/// 获取排行榜。
Future<List<RankingEntry>> getRanking({String period = 'monthly'}) async {
final response = await _dio.get(
'/plugin:com.msgbyte.saleschat/statsGetRanking',
queryParameters: {'period': period});
final data = _unwrapResponse(response);
return (data as List).map((e) => RankingEntry.fromJson(e)).toList();
}
/// 获取趋势数据。
Future<List<Map<String, dynamic>>> getTrend({
required String salesId,
String period = 'daily',
}) async {
final response = await _dio.get(
'/plugin:com.msgbyte.saleschat/statsGetTrend',
queryParameters: {
'salesId': salesId,
'period': period,
});
final data = _unwrapResponse(response);
return (data as List).cast<Map<String, dynamic>>();
}
/// 获取团队统计(仅管理员)。
Future<List<Map<String, dynamic>>> getTeamStats() async {
final response = await _dio
.get('/plugin:com.msgbyte.saleschat/statsGetTeamStats');
final data = _unwrapResponse(response);
return (data as List).cast<Map<String, dynamic>>();
}
/// 获取平台统计(仅超级管理员)。
Future<PlatformStats> getPlatformStats() async {
final response = await _dio
.get('/plugin:com.msgbyte.saleschat/statsGetPlatformStats');
final data = _unwrapResponse(response);
return PlatformStats.fromJson(data);
}
/// 获取用户列表(仅管理员)。
Future<List<Map<String, dynamic>>> getUsers() async {
final response = await _dio
.get('/plugin:com.msgbyte.saleschat/adminGetUserList');
final data = _unwrapResponse(response);
return (data as List).cast<Map<String, dynamic>>();
}
/// 更新用户角色(仅超级管理员)。
Future<void> updateUserRole(String userId, String role) async {
await _dio.post(
'/plugin:com.msgbyte.saleschat/adminUpdateUserRole',
data: {'userId': userId, 'role': role});
}
/// 删除用户(仅超级管理员)。
Future<void> deleteUser(String userId,
{String type = 'soft', String reason = ''}) async {
await _dio.post(
'/plugin:com.msgbyte.saleschat/adminDeleteUser',
data: {
'userId': userId,
'type': type,
if (reason.isNotEmpty) 'reason': reason,
});
}
/// 获取群组列表(仅管理员)。
Future<List<Map<String, dynamic>>> getAdminGroups() async {
final response = await _dio
.get('/plugin:com.msgbyte.saleschat/adminGetGroupList');
final data = _unwrapResponse(response);
return (data as List).cast<Map<String, dynamic>>();
}
/// 获取群组统计(仅管理员)。
Future<Map<String, dynamic>> getGroupStats(String groupId) async {
final response = await _dio.get(
'/plugin:com.msgbyte.saleschat/adminGetGroupStats',
queryParameters: {'groupId': groupId});
return _unwrapResponse(response);
}
/// 踢出群组成员(仅管理员)。
Future<void> kickGroupMember(String groupId, String userId,
{String? reason}) async {
await _dio.post(
'/plugin:com.msgbyte.saleschat/adminKickUser',
data: {
'groupId': groupId,
'userId': userId,
if (reason != null) 'reason': reason,
});
}
}