Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
092039bc1a | ||
|
|
7bda2f280a | ||
|
|
9001688c9e |
@@ -0,0 +1,64 @@
|
||||
# B-261 Android「我的信息」404
|
||||
|
||||
分支:本卡施工分支(基于联网修复复测包 `1895830` / `agent/agent/a4ad7eed`)
|
||||
对照用户安装包:Gitea `v3.8.3-network-fix-retest`(`changlian-android-3.8.3-b225.apk`)
|
||||
本卡只改客户端用户资料读取,不改现网服务、不改账号数据、不覆盖旧 Release。
|
||||
|
||||
**结论:404 是客户端把官方 chat 接口打到了账号服务。责任在客户端。已改为走 OpenIM SDK,不再请求 `/user/find/full`。**
|
||||
|
||||
## 直接原因
|
||||
|
||||
用户截图:`GET http://192.168.200.11:10010/user/find/full` → 404。
|
||||
|
||||
| 核对项 | 结果 |
|
||||
|---|---|
|
||||
| 账号服务真实路由 | 只有 `/api/login`、`/api/directory`、`/api/rtc_token`、管理接口;没有 `/user/find/full` |
|
||||
| 现网探测 | `GET/POST http://192.168.200.11:10010/user/find/full` 均 404;`GET /api/health` 200,员工数 3 |
|
||||
| 当前发布包调用方 | 官方页「我的信息」走 `Apis.queryMyFullInfo()` → `HttpUtil.post(Urls.getUsersFullInfo)` |
|
||||
| URL 来源 | B-225 把 `Config.appAuthUrl` 从已关闭的 :10008 改到 :10010,登录能通,但这条 chat 路径仍打到账号服务 |
|
||||
| 代码方法 | 源码是 POST;GET/POST 对不存在的路径都是 404,和截图一致 |
|
||||
|
||||
旧 `mobile/` 和 PC 早已用 OpenIM SDK 取资料,不打这条路径。新 `mobile-next/` 漏改。
|
||||
|
||||
责任边界:客户端请求了账号服务没有的接口。不是工号密码错误,也不是服务器宕机。不需要改现网。
|
||||
|
||||
## 最小修复
|
||||
|
||||
`Apis.getUserFullInfo` / `queryMyFullInfo` / `searchUserFullInfo` / `updateUserInfo` 改为 OpenIM SDK(与 PC `pc-client/src/api/login.ts` 同一策略)。
|
||||
|
||||
- 能同步:工号、昵称、头像、勿扰
|
||||
- 不再有值:手机号、邮箱、性别、生日等原 chat 业务字段
|
||||
- 未改:`account-service/`、docker、旧 `mobile/`、PC
|
||||
|
||||
## 回退
|
||||
|
||||
从新到旧:`git revert` 本验收记录提交,再 `git revert` 代码提交。用户侧可继续用 `v3.8.3-network-fix-retest` 或旧包 `v1.0.6-internal-test`。
|
||||
|
||||
## 构建
|
||||
|
||||
- Flutter 3.24.5 / Dart 3.5.4 / Temurin 17.0.20
|
||||
- `flutter build apk --release` 成功
|
||||
- 上游告警同 B-225:Flutter Gradle apply-script 弃用;部分依赖 Kotlin metadata 1.8.0 vs 工程预期 1.6.0。release 通过。
|
||||
|
||||
| 产物 | 版本 | 包名 | 大小 | SHA256 |
|
||||
|---|---|---|---|---|
|
||||
| Android release APK | 3.8.3+180 | `io.openim.flutter.demo` | 53,286,915 B | `42D74E85AEC52A3889ED79CE7345C3BA5F69B4B3FA994032C2D38DF6A9CF5987` |
|
||||
|
||||
路径:`mobile-next/build/app/outputs/flutter-apk/app-release.apk`(APK 内嵌时间戳,哈希不可位级复现)
|
||||
|
||||
## 已测 / 未测
|
||||
|
||||
已测:
|
||||
|
||||
- 现网 `:10010/user/find/full` GET/POST 404,`/api/health` 200
|
||||
- `UserFullInfoMapper` 单测 3 项通过
|
||||
- 改动文件 `flutter analyze` 无新增 error(剩余 8 条均为 `apis.dart` 原有 warning/info)
|
||||
- Android release 构建通过,校验值见上表
|
||||
- `Apis` 不再引用 `Urls.getUsersFullInfo` / `updateUserInfo` / `searchUserFullInfo`
|
||||
|
||||
未测(不得写成已通过):
|
||||
|
||||
- 公司内网真机登录后打开「我的信息」
|
||||
- 登录、联系人、发消息完整回归
|
||||
|
||||
残余:官方 `searchFriendInfo`、改密、验证码仍指向已下线 chat 路径;工号登录主路径和「我的信息」不再走这些接口。
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:flutter_datetime_picker_plus/flutter_datetime_picker_plus.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim/pages/login/login_logic.dart';
|
||||
@@ -26,8 +25,6 @@ class MyInfoLogic extends GetxController {
|
||||
attr: EditAttr.mobile,
|
||||
);
|
||||
|
||||
void editEmail() => AppNavigator.startEditMyInfo(attr: EditAttr.email, maxLength: 30);
|
||||
|
||||
void openPhotoSheet() {
|
||||
IMViews.openPhotoSheet(
|
||||
onData: (path, url) async {
|
||||
@@ -43,62 +40,6 @@ class MyInfoLogic extends GetxController {
|
||||
quality: 15);
|
||||
}
|
||||
|
||||
void openDatePicker() {
|
||||
var appLocale = Get.locale;
|
||||
var isZh = appLocale!.languageCode.toLowerCase().contains("zh");
|
||||
DatePicker.showDatePicker(
|
||||
Get.context!,
|
||||
locale: isZh ? LocaleType.zh : LocaleType.en,
|
||||
maxTime: DateTime.now(),
|
||||
currentTime: DateTime.fromMillisecondsSinceEpoch(imLogic.userInfo.value.birth ?? 0),
|
||||
theme: DatePickerTheme(
|
||||
cancelStyle: Styles.ts_0C1C33_17sp,
|
||||
doneStyle: Styles.ts_0089FF_17sp,
|
||||
itemStyle: Styles.ts_0C1C33_17sp,
|
||||
),
|
||||
onConfirm: (dateTime) {
|
||||
_updateBirthday(dateTime.millisecondsSinceEpoch ~/ 1000);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void selectGender() {
|
||||
Get.bottomSheet(
|
||||
BottomSheetView(
|
||||
items: [
|
||||
SheetItem(
|
||||
label: StrRes.man,
|
||||
onTap: () => _updateGender(1),
|
||||
),
|
||||
SheetItem(
|
||||
label: StrRes.woman,
|
||||
onTap: () => _updateGender(2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateGender(int gender) {
|
||||
LoadingView.singleton.wrap(
|
||||
asyncFunction: () => Apis.updateUserInfo(userID: OpenIM.iMManager.userID, gender: gender)
|
||||
.then((value) => imLogic.userInfo.update((val) {
|
||||
val?.gender = gender;
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateBirthday(int birthday) {
|
||||
LoadingView.singleton.wrap(
|
||||
asyncFunction: () => Apis.updateUserInfo(
|
||||
userID: OpenIM.iMManager.userID,
|
||||
birth: birthday * 1000,
|
||||
).then((value) => imLogic.userInfo.update((val) {
|
||||
val?.birth = birthday * 1000;
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
_queryMyFullIno();
|
||||
|
||||
@@ -42,16 +42,18 @@ class MyInfoPage extends StatelessWidget {
|
||||
),
|
||||
_buildItemView(
|
||||
label: StrRes.gender,
|
||||
value: imLogic.userInfo.value.isMale ? StrRes.man : StrRes.woman,
|
||||
onTap: logic.selectGender,
|
||||
value: _genderText(imLogic.userInfo.value.gender),
|
||||
showRightArrow: false,
|
||||
),
|
||||
_buildItemView(
|
||||
label: StrRes.birthDay,
|
||||
value: DateUtil.formatDateMs(
|
||||
imLogic.userInfo.value.birth ?? 0,
|
||||
value: imLogic.userInfo.value.birth == null
|
||||
? ''
|
||||
: DateUtil.formatDateMs(
|
||||
imLogic.userInfo.value.birth!,
|
||||
format: IMUtils.getTimeFormat1(),
|
||||
),
|
||||
onTap: logic.openDatePicker,
|
||||
showRightArrow: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -68,7 +70,7 @@ class MyInfoPage extends StatelessWidget {
|
||||
_buildItemView(
|
||||
label: StrRes.email,
|
||||
value: imLogic.userInfo.value.email,
|
||||
onTap: logic.editEmail,
|
||||
showRightArrow: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -93,6 +95,12 @@ class MyInfoPage extends StatelessWidget {
|
||||
child: Column(children: children),
|
||||
);
|
||||
|
||||
String _genderText(int? gender) {
|
||||
if (gender == 1) return StrRes.man;
|
||||
if (gender == 2) return StrRes.woman;
|
||||
return '';
|
||||
}
|
||||
|
||||
Widget _buildItemView({
|
||||
required String label,
|
||||
String? value,
|
||||
|
||||
@@ -33,6 +33,7 @@ export 'src/utils/sp_util.dart';
|
||||
export 'src/utils/utils.dart';
|
||||
export 'src/utils/voice_record.dart';
|
||||
export 'src/utils/api_service.dart';
|
||||
export 'src/utils/user_full_info_mapper.dart';
|
||||
export 'src/widgets/avatar_view.dart';
|
||||
export 'src/widgets/azlist_view.dart';
|
||||
export 'src/widgets/bottom_bar.dart';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -11,14 +10,19 @@ import 'package:sprintf/sprintf.dart';
|
||||
import 'utils/api_service.dart';
|
||||
|
||||
class Apis {
|
||||
static Options get imTokenOptions => Options(headers: {'token': DataSp.imToken});
|
||||
static Options get imTokenOptions =>
|
||||
Options(headers: {'token': DataSp.imToken});
|
||||
|
||||
static Options get chatTokenOptions => Options(headers: {'token': DataSp.chatToken});
|
||||
static Options get chatTokenOptions =>
|
||||
Options(headers: {'token': DataSp.chatToken});
|
||||
|
||||
static StreamController kickoffController = StreamController<int>.broadcast();
|
||||
|
||||
static void _kickoff(int? errCode) {
|
||||
if (errCode == 1501 || errCode == 1503 || errCode == 1504 || errCode == 1505) {
|
||||
if (errCode == 1501 ||
|
||||
errCode == 1503 ||
|
||||
errCode == 1504 ||
|
||||
errCode == 1505) {
|
||||
kickoffController.sink.add(errCode);
|
||||
}
|
||||
}
|
||||
@@ -197,43 +201,16 @@ class Apis {
|
||||
int? allowBeep,
|
||||
int? allowVibration,
|
||||
}) async {
|
||||
try {
|
||||
Map<String, dynamic> param = {'userID': userID};
|
||||
void put(String key, dynamic value) {
|
||||
if (null != value) {
|
||||
param[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
put('account', account);
|
||||
put('phoneNumber', phoneNumber);
|
||||
put('areaCode', areaCode);
|
||||
put('email', email);
|
||||
put('nickname', nickname);
|
||||
put('faceURL', faceURL);
|
||||
put('gender', gender);
|
||||
put('gender', gender);
|
||||
put('level', level);
|
||||
put('birth', birth);
|
||||
put('allowAddFriend', allowAddFriend);
|
||||
put('allowBeep', allowBeep);
|
||||
put('allowVibration', allowVibration);
|
||||
|
||||
return HttpUtil.post(
|
||||
Urls.updateUserInfo,
|
||||
data: {
|
||||
...param,
|
||||
'platform': IMUtils.getPlatform(),
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
// 原 chat `/user/update` 已随 :10008 下线。OpenIM 只同步昵称/头像;
|
||||
// 邮箱、性别、生日等业务字段由调用方更新本地状态,不打账号服务。
|
||||
if (userID.isEmpty) return {};
|
||||
if (nickname != null || faceURL != null) {
|
||||
await OpenIM.iMManager.userManager.setSelfInfo(
|
||||
nickname: nickname,
|
||||
faceURL: faceURL,
|
||||
);
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static Future<List<FriendInfo>> searchFriendInfo(
|
||||
@@ -251,7 +228,9 @@ class Apis {
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
if (data['users'] is List) {
|
||||
return (data['users'] as List).map((e) => FriendInfo.fromJson(e)).toList();
|
||||
return (data['users'] as List)
|
||||
.map((e) => FriendInfo.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e, s) {
|
||||
@@ -269,26 +248,16 @@ class Apis {
|
||||
int showNumber = 10,
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
if (userIDList.isEmpty) return [];
|
||||
try {
|
||||
final data = await HttpUtil.post(
|
||||
Urls.getUsersFullInfo,
|
||||
data: {
|
||||
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
|
||||
'userIDs': userIDList,
|
||||
'platform': IMUtils.getPlatform(),
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
final users = await OpenIM.iMManager.userManager.getUsersInfo(
|
||||
userIDList: userIDList,
|
||||
);
|
||||
if (data['users'] is List) {
|
||||
return (data['users'] as List).map((e) => UserFullInfo.fromJson(e)).toList();
|
||||
}
|
||||
return null;
|
||||
return users
|
||||
.map((e) => UserFullInfoMapper.fromImJson(e.toJson()))
|
||||
.toList();
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
Logger.print('getUserFullInfo e:$e s:$s');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -298,34 +267,20 @@ class Apis {
|
||||
int pageNumber = 1,
|
||||
int showNumber = 10,
|
||||
}) async {
|
||||
try {
|
||||
final data = await HttpUtil.post(
|
||||
Urls.searchUserFullInfo,
|
||||
data: {
|
||||
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
|
||||
'keyword': content,
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
if (data['users'] is List) {
|
||||
return (data['users'] as List).map((e) => UserFullInfo.fromJson(e)).toList();
|
||||
}
|
||||
return null;
|
||||
} catch (e, s) {
|
||||
final t = e as (int, String?);
|
||||
final errCode = t.$1;
|
||||
final errMsg = t.$2;
|
||||
_kickoff(errCode);
|
||||
Logger.print('e:$errCode s:$errMsg');
|
||||
return [];
|
||||
}
|
||||
final keyword = content.trim();
|
||||
if (keyword.isEmpty) return [];
|
||||
if (pageNumber > 1) return [];
|
||||
return getUserFullInfo(userIDList: [keyword], showNumber: showNumber);
|
||||
}
|
||||
|
||||
static Future<UserFullInfo?> queryMyFullInfo() async {
|
||||
final list = await Apis.getUserFullInfo(
|
||||
userIDList: [OpenIM.iMManager.userID],
|
||||
);
|
||||
return list?.firstOrNull;
|
||||
try {
|
||||
final user = await OpenIM.iMManager.userManager.getSelfUserInfo();
|
||||
return UserFullInfoMapper.fromImJson(user.toJson());
|
||||
} catch (e, s) {
|
||||
Logger.print('queryMyFullInfo e:$e s:$s');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> requestVerificationCode({
|
||||
@@ -353,7 +308,8 @@ class Apis {
|
||||
});
|
||||
}
|
||||
|
||||
static Future<SignalingCertificate> getTokenForRTC(String roomID, String userID) async {
|
||||
static Future<SignalingCertificate> getTokenForRTC(
|
||||
String roomID, String userID) async {
|
||||
return HttpUtil.post(
|
||||
Urls.getTokenForRTC,
|
||||
data: {
|
||||
@@ -410,7 +366,10 @@ class Apis {
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getClientConfig() async {
|
||||
return {'discoverPageURL': Config.discoverPageURL, 'allowSendMsgNotFriend': Config.allowSendMsgNotFriend};
|
||||
return {
|
||||
'discoverPageURL': Config.discoverPageURL,
|
||||
'allowSendMsgNotFriend': Config.allowSendMsgNotFriend
|
||||
};
|
||||
}
|
||||
|
||||
static void _catchError(Object e, StackTrace s, {bool forceBack = true}) {
|
||||
|
||||
@@ -5,6 +5,8 @@ class Urls {
|
||||
"${Config.imApiUrl}/manager/get_users_online_status";
|
||||
static String get queryAllUsers =>
|
||||
"${Config.imApiUrl}/manager/get_all_users_uid";
|
||||
// 以下 chat 路径仅作历史对照。账号服务 :10010 没有这些路由;
|
||||
// 用户资料读写已改走 OpenIM SDK,见 Apis.getUserFullInfo / updateUserInfo。
|
||||
static String get updateUserInfo => "${Config.appAuthUrl}/user/update";
|
||||
static String get searchFriendInfo => "${Config.appAuthUrl}/friend/search";
|
||||
static String get getUsersFullInfo => "${Config.appAuthUrl}/user/find/full";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import '../models/user_full_info.dart';
|
||||
|
||||
/// 把 OpenIM 公开资料收成原 chat 服务 `UserFullInfo` 形状。
|
||||
///
|
||||
/// 官方 `open-im-chat`(:10008 `/user/find/full`)已下线,账号服务 :10010
|
||||
/// 没有这条路由。SDK 只保证 userID / nickname / faceURL / ex / 勿扰。
|
||||
class UserFullInfoMapper {
|
||||
UserFullInfoMapper._();
|
||||
|
||||
static UserFullInfo fromImJson(Map<String, dynamic> json) {
|
||||
return UserFullInfo.fromJson({
|
||||
'userID': json['userID'],
|
||||
'nickname': json['nickname'],
|
||||
'faceURL': json['faceURL'],
|
||||
'ex': json['ex'],
|
||||
'remark': json['remark'],
|
||||
'globalRecvMsgOpt': json['globalRecvMsgOpt'],
|
||||
});
|
||||
}
|
||||
|
||||
static List<UserFullInfo> fromImList(Iterable<Map<String, dynamic>> items) {
|
||||
return items.map(fromImJson).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
|
||||
void main() {
|
||||
test('把 OpenIM 公开资料映射成我的信息可用字段', () {
|
||||
final info = UserFullInfoMapper.fromImJson({
|
||||
'userID': '10001',
|
||||
'nickname': '张三',
|
||||
'faceURL': 'http://example/a.png',
|
||||
'ex': '',
|
||||
'globalRecvMsgOpt': 0,
|
||||
});
|
||||
expect(info.userID, '10001');
|
||||
expect(info.nickname, '张三');
|
||||
expect(info.faceURL, 'http://example/a.png');
|
||||
expect(info.globalRecvMsgOpt, 0);
|
||||
});
|
||||
|
||||
test('不把 chat 业务字段当成仍可从账号服务拿到', () {
|
||||
final info = UserFullInfoMapper.fromImJson({
|
||||
'userID': '10001',
|
||||
'nickname': '张三',
|
||||
'phoneNumber': '13800000000',
|
||||
'email': 'a@b.c',
|
||||
'gender': 1,
|
||||
});
|
||||
expect(info.userID, '10001');
|
||||
expect(info.nickname, '张三');
|
||||
expect(info.phoneNumber, isNull);
|
||||
expect(info.email, isNull);
|
||||
expect(info.gender, isNull);
|
||||
});
|
||||
|
||||
test('空列表保持为空', () {
|
||||
expect(UserFullInfoMapper.fromImList(const []), isEmpty);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user