444 lines
15 KiB
Dart
444 lines
15 KiB
Dart
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,
|
||
});
|
||
}
|
||
}
|