feat: 增加接口封装、获取验证码接口

This commit is contained in:
liyi 2025-09-01 18:20:05 +08:00
parent 4e8e860810
commit 1adeb429cc
18 changed files with 394 additions and 88 deletions

View File

@ -1,3 +1,3 @@
class ApiPath { class ApiPath {
static const String login = "/auth/login"; static const String sendValidationCode = "/v1/common/sendValidationCode";
} }

View File

@ -1,51 +1,70 @@
class ApiResponse<T> { class ApiResponse<T> {
final bool success; final String? description;
final T? data; final T? data;
final String? message; final String? errorMsg;
final int? statusCode; final int? errorCode;
//
const ApiResponse({ const ApiResponse({
required this.success, this.description,
this.data, this.data,
this.message, this.errorMsg,
this.statusCode, this.errorCode,
}); });
// // JSON ApiResponse
factory ApiResponse.success(T data, factory ApiResponse.fromJson(
{String message = 'Success', int? statusCode}) { Map<String, dynamic> json,
T Function(dynamic)? dataFromJson, // data null
) {
final dataJson = json['data'];
final T? parsedData = dataJson != null && dataFromJson != null
? dataFromJson(dataJson)
: null;
return ApiResponse<T>(
errorCode: json['errorCode'],
errorMsg: json['errorMsg'],
description: json['description'],
data: parsedData,
);
}
//
factory ApiResponse.success(T data, {
String? errorMsg,
int? errorCode,
String? description,
}) {
return ApiResponse<T>( return ApiResponse<T>(
success: true,
data: data, data: data,
message: message, errorMsg: errorMsg ?? 'success',
statusCode: statusCode, errorCode: errorCode,
description: description ?? 'success',
); );
} }
// //
factory ApiResponse.error(String message, {int? statusCode, T? data}) { factory ApiResponse.error(
String errorMsg, {
int? errorCode,
T? data,
String? description,
}) {
return ApiResponse<T>( return ApiResponse<T>(
success: false, description: description,
message: message, errorMsg: errorMsg,
statusCode: statusCode, errorCode: errorCode,
data: data, // data: data,
); );
} }
// bool get isSuccess {
factory ApiResponse.loading() { return errorCode == 0;
return ApiResponse<T>(
success: false,
message: 'Loading...',
);
} }
@override
List<Object?> get props => [success, data, message, statusCode];
@override @override
String toString() { String toString() {
return 'ApiResponse(success: $success, message: $message, data: $data, statusCode: $statusCode)'; return 'ApiResponse(description: $description, errorMsg: $errorMsg, data: $data, errorCode: $errorCode)';
} }
} }

View File

@ -1,7 +1,9 @@
// api/service/user_api_service.dart
import 'package:dio/dio.dart' as dioAlias; import 'package:dio/dio.dart' as dioAlias;
import 'package:starwork_flutter/api/api_response.dart'; import 'package:starwork_flutter/api/api_response.dart';
import 'package:starwork_flutter/common/constant/http_constant.dart';
import 'package:starwork_flutter/flavors.dart'; import 'package:starwork_flutter/flavors.dart';
import 'package:flutter/foundation.dart'; // debugPrint
class BaseApiService { class BaseApiService {
final dioAlias.Dio dio = dioAlias.Dio(); final dioAlias.Dio dio = dioAlias.Dio();
@ -10,37 +12,62 @@ class BaseApiService {
dio.options.baseUrl = F.apiHost; dio.options.baseUrl = F.apiHost;
dio.options.connectTimeout = const Duration(seconds: 30); dio.options.connectTimeout = const Duration(seconds: 30);
dio.options.receiveTimeout = const Duration(seconds: 30); dio.options.receiveTimeout = const Duration(seconds: 30);
// token
// 🔥
if (kDebugMode) {
dio.interceptors.add(dioAlias.LogInterceptor(
request: true,
requestHeader: true,
requestBody: true,
responseHeader: true,
responseBody: true,
error: true,
logPrint: (obj) {
debugPrint('[DIO] $obj');
},
));
}
} }
/// ///
Future<ApiResponse<T>> makeRequest<T>({ Future<ApiResponse<T>> makeRequest<T>({
required String path, required String path,
String method = 'GET', String method = HttpConstant.post,
dynamic data, dynamic data,
Map<String, dynamic>? queryParameters, Map<String, dynamic>? queryParameters,
required T Function(dynamic) fromJson, required T Function(dynamic) fromJson,
}) async { }) async {
try { try {
// 🔍
if (kDebugMode) {
final uri = Uri.parse('${dio.options.baseUrl}$path');
final queryString =
queryParameters != null ? '?${uri.queryParameters.toString()}' : '';
debugPrint('🟦 API Request: $method ${uri.toString()}$queryString');
if (data != null) {
debugPrint('🟦 Request Body: $data');
}
}
dioAlias.Response response; dioAlias.Response response;
switch (method.toUpperCase()) { switch (method.toUpperCase()) {
case 'GET': case HttpConstant.get:
response = await dio.get(path, queryParameters: queryParameters); response = await dio.get(path, queryParameters: queryParameters);
break; break;
case 'POST': case HttpConstant.post:
response = await dio.post(path, response = await dio.post(path,
data: data, queryParameters: queryParameters); data: data, queryParameters: queryParameters);
break; break;
case 'PUT': case HttpConstant.put:
response = response =
await dio.put(path, data: data, queryParameters: queryParameters); await dio.put(path, data: data, queryParameters: queryParameters);
break; break;
case 'DELETE': case HttpConstant.delete:
response = await dio.delete(path, response = await dio.delete(path,
data: data, queryParameters: queryParameters); data: data, queryParameters: queryParameters);
break; break;
case 'PATCH': case HttpConstant.patch:
response = await dio.patch(path, response = await dio.patch(path,
data: data, queryParameters: queryParameters); data: data, queryParameters: queryParameters);
break; break;
@ -48,20 +75,46 @@ class BaseApiService {
return ApiResponse.error('Unsupported method: $method'); return ApiResponse.error('Unsupported method: $method');
} }
//
if (kDebugMode) {
debugPrint(
'🟩 API Response [${response.statusCode}] ${response.requestOptions.path}');
debugPrint('🟩 Response Data: ${response.data}');
}
if (response.statusCode! >= 200 && response.statusCode! < 300) { if (response.statusCode! >= 200 && response.statusCode! < 300) {
final parsedData = fromJson(response.data); final jsonMap = response.data as Map<String, dynamic>;
return ApiResponse.success(
parsedData, // ApiResponse.fromJson
statusCode: response.statusCode, final apiResponse = ApiResponse<T>.fromJson(jsonMap, fromJson);
message: 'Success',
); // errorCode
if (apiResponse.errorCode == 0) {
return apiResponse; // data
} else {
// data
return ApiResponse.error(
apiResponse.errorMsg ?? 'Request failed',
errorCode: apiResponse.errorCode,
data: apiResponse.data,
);
}
} else { } else {
return ApiResponse.error( return ApiResponse.error(
response.statusMessage ?? 'Request failed', response.statusMessage ?? 'Request failed',
statusCode: response.statusCode, errorCode: response.statusCode,
); );
} }
} on dioAlias.DioException catch (e) { } on dioAlias.DioException catch (e) {
//
if (kDebugMode) {
debugPrint('🟥 API Error: ${e.type} - ${e.message}');
if (e.response != null) {
debugPrint('🟥 Error Response: ${e.response?.data}');
debugPrint('🟥 Status Code: ${e.response?.statusCode}');
}
}
String message = 'Unknown error'; String message = 'Unknown error';
int? statusCode = e.response?.statusCode; int? statusCode = e.response?.statusCode;
@ -77,8 +130,11 @@ class BaseApiService {
message = e.message ?? 'Something went wrong'; message = e.message ?? 'Something went wrong';
} }
return ApiResponse.error(message, statusCode: statusCode); return ApiResponse.error(message, errorCode: statusCode);
} catch (e) { } catch (e) {
if (kDebugMode) {
debugPrint('🟥 Unexpected Error: $e');
}
return ApiResponse.error('Unexpected error: $e'); return ApiResponse.error('Unexpected error: $e');
} }
} }

View File

@ -0,0 +1,26 @@
//
// 使 sendValidationCodeAuth channelcodeType
import 'package:starwork_flutter/common/constant/validation_code_type.dart';
class SendValidationCodeRequest {
final String countryCode;
final String account;
final String channel; //1 2
final ValidationCodeType codeType;
SendValidationCodeRequest({
required this.countryCode,
required this.account,
required this.channel,
required this.codeType,
});
Map<String, dynamic> toJson() {
return {
'countryCode': countryCode,
'account': account,
'channel': channel,
'codeType': codeType.value,
};
}
}

View File

@ -0,0 +1,18 @@
// models/string_response.dart
class StringResponse {
final String value;
StringResponse(this.value);
factory StringResponse.fromJson(dynamic data) {
if (data is String) {
return StringResponse(data);
} else if (data is Map && data['message'] != null) {
return StringResponse(data['message'].toString());
} else if (data is Map && data['data'] != null) {
return StringResponse(data['data'].toString());
} else {
return StringResponse(data.toString());
}
}
}

View File

@ -0,0 +1,19 @@
class ValidationCodeLoginRequest {
final int platId; // 1web 2app 3 4pc
final String phone; //
final String verificationCode; //
ValidationCodeLoginRequest({
required this.platId,
required this.phone,
required this.verificationCode,
});
Map<String, dynamic> toJson() {
return {
'platId': platId,
'phone': phone,
'verificationCode': verificationCode,
};
}
}

View File

@ -0,0 +1,25 @@
import 'package:starwork_flutter/api/api_path.dart';
import 'package:starwork_flutter/api/api_response.dart';
import 'package:starwork_flutter/api/base_api_service.dart';
import 'package:starwork_flutter/api/model/common/request/send_validation_code_request.dart';
import 'package:starwork_flutter/api/model/string_response.dart';
import 'package:starwork_flutter/api/model/user/user_model.dart';
import 'package:starwork_flutter/common/constant/http_constant.dart';
class CommonApiService {
final BaseApiService _api;
CommonApiService(this._api); //
//
Future<ApiResponse<void>> sendValidationCode({
required SendValidationCodeRequest request,
}) {
return _api.makeRequest(
//
path: ApiPath.sendValidationCode,
method: HttpConstant.post,
data: request.toJson(),
fromJson: (data) {},
);
}
}

View File

@ -4,7 +4,9 @@ import 'package:starwork_flutter/api/base_api_service.dart';
import 'package:starwork_flutter/api/model/user/user_model.dart'; import 'package:starwork_flutter/api/model/user/user_model.dart';
class UserApiService { class UserApiService {
final BaseApiService _api = BaseApiService(); final BaseApiService _api;
UserApiService(this._api); //
Future<ApiResponse<UserModel>> login(Map<String, dynamic> userData) { Future<ApiResponse<UserModel>> login(Map<String, dynamic> userData) {
return _api.makeRequest( return _api.makeRequest(

View File

@ -19,12 +19,11 @@ class App extends StatelessWidget {
builder: (_, child) { builder: (_, child) {
return GetMaterialApp( return GetMaterialApp(
theme: ThemeData( theme: ThemeData(
textSelectionTheme: TextSelectionThemeData( textSelectionTheme: TextSelectionThemeData(
cursorColor: Colors.blue.shade200, cursorColor: Colors.blue.shade200,
selectionColor: Colors.blue.shade100, selectionColor: Colors.blue.shade100,
selectionHandleColor: Colors.blue.shade300, selectionHandleColor: Colors.blue.shade300,
) )),
),
// //
localizationsDelegates: const [ localizationsDelegates: const [
GlobalMaterialLocalizations.delegate, // Material组件本地化字符串 GlobalMaterialLocalizations.delegate, // Material组件本地化字符串
@ -42,7 +41,7 @@ class App extends StatelessWidget {
fallbackLocale: const Locale('zh', 'CN'), fallbackLocale: const Locale('zh', 'CN'),
// fallback 使 // fallback 使
getPages: AppPages.pages, getPages: AppPages.pages,
initialRoute: AppRoutes.main, initialRoute: AppRoutes.login,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
); );
}, },

View File

@ -5,20 +5,31 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:starwork_flutter/api/base_api_service.dart'; import 'package:starwork_flutter/api/base_api_service.dart';
import 'package:starwork_flutter/api/service/common_api_service.dart';
import 'package:starwork_flutter/api/service/user_api_service.dart'; import 'package:starwork_flutter/api/service/user_api_service.dart';
import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart'; import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart';
import 'package:starwork_flutter/i18n/app_i18n.dart'; import 'package:starwork_flutter/i18n/app_i18n.dart';
class AppInitialization { class AppInitialization {
static Future<void> initializeApp() async { static Future<void> initializeApp() async {
// ,使 try {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// setSystemStatusBar();
setSystemStatusBar(); await SharedPreferencesUtils.init();
// SharedPreferences
await SharedPreferencesUtils.init(); // 便
// api服务 print('✅ SharedPreferences initialized');
Get.put(BaseApiService(),permanent: true);
Get.lazyPut(() => BaseApiService());
Get.lazyPut(() => CommonApiService(Get.find<BaseApiService>()));
print('✅ API services registered');
} catch (e, stack) {
print('❌ Initialization failed: $e');
print(stack);
// Sentry
rethrow;
}
} }
static void setSystemStatusBar() { static void setSystemStatusBar() {

View File

@ -0,0 +1,8 @@
class HttpConstant {
static const String post="POST";
static const String get="GET";
static const String put="PUT";
static const String delete="DELETE";
static const String patch="PATCH";
}

View File

@ -0,0 +1,34 @@
class LoginType {
static const phoneCodeLogin = LoginType('1', '手机验证码');
static const emailCodeLogin = LoginType('2', '邮箱验证码');
static const emailPasswordLogin = LoginType('3', '邮箱验证码');
static const phonePassword = LoginType('4', '手机号密码');
final String value;
final String label;
const LoginType(this.value, this.label);
//
static LoginType? fromValue(String? value) {
return {
'1': phoneCodeLogin,
'2': phonePassword,
}[value];
}
// toString() value
@override
String toString() => value;
// ==
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LoginType &&
runtimeType == other.runtimeType &&
value == other.value;
@override
int get hashCode => value.hashCode;
}

View File

@ -0,0 +1,33 @@
// Description:
class ValidationCodeChannel {
static const sms = ValidationCodeChannel('1', '短信');
static const email = ValidationCodeChannel('2', '邮箱');
final String value;
final String label;
const ValidationCodeChannel(this.value, this.label);
//
static ValidationCodeChannel? fromValue(String? value) {
return {
'1': sms,
'2': email,
}[value];
}
// toString() value
@override
String toString() => value;
// ==
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ValidationCodeChannel &&
runtimeType == other.runtimeType &&
value == other.value;
@override
int get hashCode => value.hashCode;
}

View File

@ -0,0 +1,46 @@
class ValidationCodeType {
static const login = ValidationCodeType('1', '登录');
static const reset = ValidationCodeType('2', '重置密码');
static const bindPhone = ValidationCodeType('3', '绑定手机号');
static const unbindPhone = ValidationCodeType('4', '解绑手机号');
static const deleteAccount = ValidationCodeType('5', '删除账号');
static const bindEmail = ValidationCodeType('6', '绑定邮箱');
static const unbindEmail = ValidationCodeType('7', '解绑邮箱');
static const deleteLock = ValidationCodeType('8', '删除门锁');
static const updatePassword = ValidationCodeType('9', '修改密码');
final String value;
final String label;
const ValidationCodeType(this.value, this.label);
//
static ValidationCodeType? fromValue(String? value) {
return {
'1': login,
'2': reset,
'3': bindPhone,
'4': unbindPhone,
'5': deleteAccount,
'6': bindEmail,
'7': unbindEmail,
'8': deleteLock,
'9': updatePassword,
}[value];
}
// toString() value
@override
String toString() => value;
// ==
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ValidationCodeType &&
runtimeType == other.runtimeType &&
value == other.value;
@override
int get hashCode => value.hashCode;
}

View File

@ -1,5 +0,0 @@
enum LoginType {
phoneCode,
phonePassword;
}

View File

@ -32,7 +32,7 @@ class F {
static String get apiHost { static String get apiHost {
switch (appFlavor) { switch (appFlavor) {
case Flavor.skyDev: case Flavor.skyDev:
return 'https://loacl.work.star-lock.cn/api'; return 'http://192.168.1.136/api';
case Flavor.skyPre: case Flavor.skyPre:
return 'https://loacl.work.star-lock.cn/api'; return 'https://loacl.work.star-lock.cn/api';
case Flavor.skyRelease: case Flavor.skyRelease:

View File

@ -4,12 +4,19 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:starwork_flutter/api/model/common/request/send_validation_code_request.dart';
import 'package:starwork_flutter/api/service/common_api_service.dart';
import 'package:starwork_flutter/base/base_controller.dart'; import 'package:starwork_flutter/base/base_controller.dart';
import 'package:starwork_flutter/common/enums/login_type.dart'; import 'package:starwork_flutter/common/constant/login_type.dart';
import 'package:starwork_flutter/common/constant/validation_code_channel.dart';
import 'package:starwork_flutter/common/constant/validation_code_type.dart';
import 'package:starwork_flutter/routes/app_pages.dart'; import 'package:starwork_flutter/routes/app_pages.dart';
import 'package:starwork_flutter/routes/app_routes.dart'; import 'package:starwork_flutter/routes/app_routes.dart';
class LoginController extends BaseController { class LoginController extends BaseController {
final commonApi = Get.find<CommonApiService>();
int phoneNumberSize = 11; int phoneNumberSize = 11;
TextEditingController phoneController = TextEditingController(); TextEditingController phoneController = TextEditingController();
TextEditingController passwordController = TextEditingController(); TextEditingController passwordController = TextEditingController();
@ -17,7 +24,7 @@ class LoginController extends BaseController {
final isFormValid = false.obs; final isFormValid = false.obs;
final isPasswordVisible = true.obs; final isPasswordVisible = true.obs;
final isPrivacyAgreementValid = 0.obs; final isPrivacyAgreementValid = 0.obs;
final loginType = LoginType.phoneCode.obs; final loginType = LoginType.phoneCodeLogin.obs;
@override @override
void onInit() { void onInit() {
@ -41,28 +48,34 @@ class LoginController extends BaseController {
} }
// //
void requestPhoneCode() { void requestPhoneCode() async {
if (isPrivacyAgreementValid.value != 1) { if (isPrivacyAgreementValid.value != 1) {
_showCustomDialog( _showCustomDialog(
title: '欢迎使用星勤'.tr, title: '欢迎使用星勤'.tr,
content: '1', content: '1',
onConfirm: () { onConfirm: () async {
isPrivacyAgreementValid.value = 1; isPrivacyAgreementValid.value = 1;
Get.toNamed(
AppRoutes.inputVerificationCode,
parameters: {
'phone': phoneController.text.trim(),
},
);
}, },
); );
} else { }
_handleRequestPhoneCode();
}
void _handleRequestPhoneCode() async {
var sendValidationCodeResult = await commonApi.sendValidationCode(
request: SendValidationCodeRequest(
countryCode: '+86',
account: phoneController.text.trim(),
channel: ValidationCodeChannel.sms.value,
codeType: ValidationCodeType.login,
),
);
if (sendValidationCodeResult.isSuccess) {
Get.toNamed( Get.toNamed(
AppRoutes.inputVerificationCode, AppRoutes.inputVerificationCode,
parameters: { parameters: {
'phone': phoneController.text.trim(), 'phone': phoneController.text.trim(),
}, },
); );
} }

View File

@ -3,7 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:starwork_flutter/common/enums/login_type.dart'; import 'package:starwork_flutter/common/constant/login_type.dart';
import 'package:starwork_flutter/routes/app_routes.dart'; import 'package:starwork_flutter/routes/app_routes.dart';
import 'login_controller.dart'; import 'login_controller.dart';
@ -173,7 +174,7 @@ class LoginView extends GetView<LoginController> {
), ),
), ),
child: Text( child: Text(
controller.loginType.value == LoginType.phoneCode controller.loginType.value == LoginType.phoneCodeLogin
? '获取验证码'.tr ? '获取验证码'.tr
: '登录'.tr, : '登录'.tr,
style: TextStyle( style: TextStyle(
@ -188,7 +189,7 @@ class LoginView extends GetView<LoginController> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Visibility( Visibility(
visible: LoginType.phoneCode == controller.loginType.value, visible: LoginType.phoneCodeLogin == controller.loginType.value,
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
controller.loginType.value = LoginType.phonePassword; controller.loginType.value = LoginType.phonePassword;
@ -208,7 +209,7 @@ class LoginView extends GetView<LoginController> {
visible: LoginType.phonePassword == controller.loginType.value, visible: LoginType.phonePassword == controller.loginType.value,
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
controller.loginType.value = LoginType.phoneCode; controller.loginType.value = LoginType.phoneCodeLogin;
}, },
child: Text( child: Text(
'验证码登录'.tr, '验证码登录'.tr,
@ -236,7 +237,8 @@ class LoginView extends GetView<LoginController> {
visible: LoginType.phonePassword == controller.loginType.value, visible: LoginType.phonePassword == controller.loginType.value,
child: TextButton( child: TextButton(
style: ButtonStyle( style: ButtonStyle(
overlayColor: MaterialStateProperty.all(Colors.transparent), // overlayColor:
MaterialStateProperty.all(Colors.transparent), //
side: MaterialStateProperty.all(null), // side: MaterialStateProperty.all(null), //
), ),
onPressed: () { onPressed: () {
@ -266,7 +268,7 @@ class LoginView extends GetView<LoginController> {
} }
_handleLoginButtonText() { _handleLoginButtonText() {
if (controller.loginType.value == LoginType.phoneCode) { if (controller.loginType.value == LoginType.phoneCodeLogin) {
return; return;
} else if (controller.loginType.value == LoginType.phonePassword) { } else if (controller.loginType.value == LoginType.phonePassword) {
return '验证码登录'.tr + ' | '.tr + '忘记密码'.tr; return '验证码登录'.tr + ' | '.tr + '忘记密码'.tr;