feat: 集成 Apple Sign-In + iOS 上线准备

- 后端新增 Apple 登录端点 /api/auth/apple-login
- 新增 AppleTokenValidator 验证 IdentityToken
- User 实体添加 AppleUserId 字段,Phone 改为可空
- 前端添加 sign_in_with_apple 依赖和 Apple 登录按钮
- 统一 Bundle ID 为 com.datalumina.YYA,显示名称为小脉健康
- 配置 DEVELOPMENT_TEAM 和 Sign in with Apple Entitlements
- 补充 NSCamera/NSPhotoLibrary 权限描述
- 生产 API 地址改为 https://erpapi.datalumina.cn/xiaomai
- Flutter 升级至 3.44.6 / Dart 3.12.2

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sccsbc
2026-07-10 13:24:03 +08:00
parent d82e006cf4
commit fe4c81c54f
27 changed files with 1400 additions and 50 deletions

View File

@@ -3,10 +3,10 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'local_database.dart';
/// API 基础地址可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。
/// API 基础地址(生产模式)。本地开发可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。
const String baseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://10.4.221.78:5000',
defaultValue: 'https://erpapi.datalumina.cn/xiaomai',
);
class ApiException implements Exception {

View File

@@ -1,5 +1,8 @@
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import '../../core/app_colors.dart';
import '../../core/navigation_provider.dart';
import '../../providers/auth_provider.dart';
@@ -63,6 +66,70 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}
}
// Apple 登录
Future<void> _appleSignIn() async {
if (_loading) return;
setState(() {
_loading = true;
_error = null;
_successMsg = null;
});
try {
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.fullName,
],
webAuthenticationOptions: WebAuthenticationOptions(
clientId: 'com.datalumina.YYA.signin',
redirectUri: Uri.parse(
'https://erpapi.datalumina.cn/xiaomai/api/auth/apple-login',
),
),
);
final identityToken = credential.identityToken;
if (identityToken == null) {
if (mounted) {
setState(() {
_loading = false;
_error = 'Apple 登录未返回身份信息,请重试';
});
}
return;
}
final name = credential.givenName != null && credential.familyName != null
? '${credential.familyName}${credential.givenName}'
: null;
final err = await ref.read(authProvider.notifier).appleLogin(
identityToken,
credential.authorizationCode,
name,
);
if (mounted) {
setState(() => _loading = false);
if (err != null) {
setState(() => _error = err);
} else {
_goHome();
}
}
} catch (e) {
if (mounted) {
setState(() {
_loading = false;
if (e is SignInWithAppleAuthorizationException) {
_error = 'Apple 登录已取消';
} else {
_error = 'Apple 登录失败: $e';
}
});
}
}
}
Future<void> _submit() async {
if (!_agreed) {
final accepted = await _showAgreementDialog();
@@ -489,6 +556,15 @@ class _LoginPageState extends ConsumerState<LoginPage> {
label: _isLogin ? '登录' : '注册',
onTap: _loading ? null : _submit,
),
// Apple 登录按钮(仅 iOS/macOS
if (_isLogin && (Platform.isIOS || Platform.isMacOS)) ...[
const SizedBox(height: 20),
SignInWithAppleButton(
onPressed: _loading ? () {} : () { _appleSignIn(); },
style: SignInWithAppleButtonStyle.whiteOutlined,
height: 48,
),
],
const SizedBox(height: 16),
GestureDetector(
onTap: () => setState(() {

View File

@@ -207,6 +207,40 @@ class AuthNotifier extends Notifier<AuthState> {
}
}
/// Apple 登录
Future<String?> appleLogin(String identityToken, String? authorizationCode, String? name) async {
try {
final api = ref.read(apiClientProvider);
final response = await api.post(
'/api/auth/apple-login',
data: {
'identityToken': identityToken,
if (authorizationCode != null) 'authorizationCode': authorizationCode,
if (name != null && name.isNotEmpty) 'name': name,
},
);
final data = response.data['data'];
if (data == null) return response.data['message'] ?? 'Apple 登录失败';
await api.saveTokens(data['accessToken'], data['refreshToken']);
final user = data['user'];
state = AuthState(
isLoggedIn: true,
isLoading: false,
user: UserInfo(
id: user['id'] ?? '',
phone: user['phone'] ?? '',
role: user['role'] ?? 'User',
name: user['name'],
avatarUrl: user['avatarUrl'],
),
);
return null;
} catch (e) {
return 'Apple 登录失败: $e';
}
}
/// 登出
Future<void> logout() async {
final api = ref.read(apiClientProvider);