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 {
static const String login = "/auth/login";
static const String sendValidationCode = "/v1/common/sendValidationCode";
}

View File

@ -1,51 +1,70 @@
class ApiResponse<T> {
final bool success;
final String? description;
final T? data;
final String? message;
final int? statusCode;
final String? errorMsg;
final int? errorCode;
//
const ApiResponse({
required this.success,
this.description,
this.data,
this.message,
this.statusCode,
this.errorMsg,
this.errorCode,
});
//
factory ApiResponse.success(T data,
{String message = 'Success', int? statusCode}) {
// JSON ApiResponse
factory ApiResponse.fromJson(
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>(
success: true,
data: data,
message: message,
statusCode: statusCode,
errorMsg: errorMsg ?? 'success',
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>(
success: false,
message: message,
statusCode: statusCode,
data: data, //
description: description,
errorMsg: errorMsg,
errorCode: errorCode,
data: data,
);
}
//
factory ApiResponse.loading() {
return ApiResponse<T>(
success: false,
message: 'Loading...',
);
bool get isSuccess {
return errorCode == 0;
}
@override
List<Object?> get props => [success, data, message, statusCode];
@override
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:starwork_flutter/api/api_response.dart';
import 'package:starwork_flutter/common/constant/http_constant.dart';
import 'package:starwork_flutter/flavors.dart';
import 'package:flutter/foundation.dart'; // debugPrint
class BaseApiService {
final dioAlias.Dio dio = dioAlias.Dio();
@ -10,37 +12,62 @@ class BaseApiService {
dio.options.baseUrl = F.apiHost;
dio.options.connectTimeout = 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>({
required String path,
String method = 'GET',
String method = HttpConstant.post,
dynamic data,
Map<String, dynamic>? queryParameters,
required T Function(dynamic) fromJson,
}) async {
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;
switch (method.toUpperCase()) {
case 'GET':
case HttpConstant.get:
response = await dio.get(path, queryParameters: queryParameters);
break;
case 'POST':
case HttpConstant.post:
response = await dio.post(path,
data: data, queryParameters: queryParameters);
break;
case 'PUT':
case HttpConstant.put:
response =
await dio.put(path, data: data, queryParameters: queryParameters);
break;
case 'DELETE':
case HttpConstant.delete:
response = await dio.delete(path,
data: data, queryParameters: queryParameters);
break;
case 'PATCH':
case HttpConstant.patch:
response = await dio.patch(path,
data: data, queryParameters: queryParameters);
break;
@ -48,20 +75,46 @@ class BaseApiService {
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) {
final parsedData = fromJson(response.data);
return ApiResponse.success(
parsedData,
statusCode: response.statusCode,
message: 'Success',
);
final jsonMap = response.data as Map<String, dynamic>;
// ApiResponse.fromJson
final apiResponse = ApiResponse<T>.fromJson(jsonMap, fromJson);
// errorCode
if (apiResponse.errorCode == 0) {
return apiResponse; // data
} else {
// data
return ApiResponse.error(
apiResponse.errorMsg ?? 'Request failed',
errorCode: apiResponse.errorCode,
data: apiResponse.data,
);
}
} else {
return ApiResponse.error(
response.statusMessage ?? 'Request failed',
statusCode: response.statusCode,
errorCode: response.statusCode,
);
}
} 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';
int? statusCode = e.response?.statusCode;
@ -77,8 +130,11 @@ class BaseApiService {
message = e.message ?? 'Something went wrong';
}
return ApiResponse.error(message, statusCode: statusCode);
return ApiResponse.error(message, errorCode: statusCode);
} catch (e) {
if (kDebugMode) {
debugPrint('🟥 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';
class UserApiService {
final BaseApiService _api = BaseApiService();
final BaseApiService _api;
UserApiService(this._api); //
Future<ApiResponse<UserModel>> login(Map<String, dynamic> userData) {
return _api.makeRequest(

View File

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

View File

@ -5,20 +5,31 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.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/common/utils/shared_preferences_utils.dart';
import 'package:starwork_flutter/i18n/app_i18n.dart';
class AppInitialization {
static Future<void> initializeApp() async {
// ,使
WidgetsFlutterBinding.ensureInitialized();
//
setSystemStatusBar();
// SharedPreferences
await SharedPreferencesUtils.init();
// api服务
Get.put(BaseApiService(),permanent: true);
try {
WidgetsFlutterBinding.ensureInitialized();
setSystemStatusBar();
await SharedPreferencesUtils.init();
// 便
print('✅ SharedPreferences initialized');
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() {

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 {
switch (appFlavor) {
case Flavor.skyDev:
return 'https://loacl.work.star-lock.cn/api';
return 'http://192.168.1.136/api';
case Flavor.skyPre:
return 'https://loacl.work.star-lock.cn/api';
case Flavor.skyRelease:

View File

@ -4,12 +4,19 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fluttertoast/fluttertoast.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/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_routes.dart';
class LoginController extends BaseController {
final commonApi = Get.find<CommonApiService>();
int phoneNumberSize = 11;
TextEditingController phoneController = TextEditingController();
TextEditingController passwordController = TextEditingController();
@ -17,7 +24,7 @@ class LoginController extends BaseController {
final isFormValid = false.obs;
final isPasswordVisible = true.obs;
final isPrivacyAgreementValid = 0.obs;
final loginType = LoginType.phoneCode.obs;
final loginType = LoginType.phoneCodeLogin.obs;
@override
void onInit() {
@ -41,28 +48,34 @@ class LoginController extends BaseController {
}
//
void requestPhoneCode() {
void requestPhoneCode() async {
if (isPrivacyAgreementValid.value != 1) {
_showCustomDialog(
title: '欢迎使用星勤'.tr,
content: '1',
onConfirm: () {
onConfirm: () async {
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(
AppRoutes.inputVerificationCode,
parameters: {
'phone': phoneController.text.trim(),
},
);
}

View File

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