Files

444 lines
15 KiB
Dart
Raw Permalink Normal View History

2026-04-25 16:36:34 +08:00
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,
});
}
}