docs(deploy): 添加部署文档并更新SDK API设计规范
- 新增完整的XuqmGroup部署文档,包含服务器配置、Docker Compose部署策略 - 更新SDK API重设计规范至V2.0,统一各端SDK初始化和登录接口 - 添加安全设计规范文档,涵盖密码安全、AppSecret验证等内容 - 新增离线推送架构设计文档,定义厂商推送集成方案 - 重构SDK登录流程,统一使用userId + userSig鉴权模式 - 移除dbName等外部配置参数,实现零感知平台地址配置 - 完善部署架构图和配置示例文件
这个提交包含在:
父节点
edd2adc5aa
当前提交
6d1d2ec634
@ -56,31 +56,11 @@ public class DemoAuthController {
|
|||||||
private Map<String, Object> buildResponse(DemoAuthService.AuthResult result) {
|
private Map<String, Object> buildResponse(DemoAuthService.AuthResult result) {
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"demoToken", result.demoToken() != null ? result.demoToken() : "",
|
"demoToken", result.demoToken() != null ? result.demoToken() : "",
|
||||||
"demoTokenExpiresAt", result.demoTokenExpiresAt(),
|
|
||||||
"imToken", result.imToken() != null ? result.imToken() : "",
|
"imToken", result.imToken() != null ? result.imToken() : "",
|
||||||
"imTokenExpiresAt", result.imTokenExpiresAt(),
|
|
||||||
"profile", result.profile()
|
"profile", result.profile()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/refresh-im")
|
|
||||||
public ApiResponse<Map<String, Object>> refreshIm(
|
|
||||||
@RequestParam(required = false) String appId,
|
|
||||||
Authentication auth) {
|
|
||||||
if (auth == null || auth.getPrincipal() == null) {
|
|
||||||
return ApiResponse.unauthorized("Unauthorized");
|
|
||||||
}
|
|
||||||
if (appId == null || appId.isBlank()) {
|
|
||||||
return ApiResponse.badRequest("appId is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
DemoAuthService.ImCredential result = authService.refreshImToken(appId, auth.getPrincipal().toString());
|
|
||||||
return ApiResponse.success(Map.of(
|
|
||||||
"imToken", result.token(),
|
|
||||||
"imTokenExpiresAt", result.expiresAt()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/reset-password")
|
@PostMapping("/reset-password")
|
||||||
public ApiResponse<Void> resetPassword(@RequestBody ResetPasswordRequest body) {
|
public ApiResponse<Void> resetPassword(@RequestBody ResetPasswordRequest body) {
|
||||||
if (body.appId() == null || body.appId().isBlank()) {
|
if (body.appId() == null || body.appId().isBlank()) {
|
||||||
|
|||||||
@ -51,8 +51,8 @@ public class DemoAuthService {
|
|||||||
this.appSecretClient = appSecretClient;
|
this.appSecretClient = appSecretClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public record AuthResult(String demoToken, long demoTokenExpiresAt, String imToken, long imTokenExpiresAt, UserProfile profile) {}
|
public record AuthResult(String demoToken, String imToken, UserProfile profile) {}
|
||||||
public record ImCredential(String token, long expiresAt) {}
|
public record ImCredential(String token) {}
|
||||||
|
|
||||||
public record UserProfile(String appId, String userId, String nickname, String avatar, String gender) {}
|
public record UserProfile(String appId, String userId, String nickname, String avatar, String gender) {}
|
||||||
|
|
||||||
@ -77,9 +77,7 @@ public class DemoAuthService {
|
|||||||
|
|
||||||
return new AuthResult(
|
return new AuthResult(
|
||||||
demoToken,
|
demoToken,
|
||||||
tokenExpiresAt(),
|
|
||||||
imCredential.token(),
|
imCredential.token(),
|
||||||
imCredential.expiresAt(),
|
|
||||||
toProfile(user)
|
toProfile(user)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -98,9 +96,7 @@ public class DemoAuthService {
|
|||||||
|
|
||||||
return new AuthResult(
|
return new AuthResult(
|
||||||
demoToken,
|
demoToken,
|
||||||
tokenExpiresAt(),
|
|
||||||
imCredential.token(),
|
imCredential.token(),
|
||||||
imCredential.expiresAt(),
|
|
||||||
toProfile(user)
|
toProfile(user)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -117,20 +113,8 @@ public class DemoAuthService {
|
|||||||
return jwtUtil.generate(userId, Map.of("appId", appId, "role", "USER"));
|
return jwtUtil.generate(userId, Map.of("appId", appId, "role", "USER"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private long tokenExpiresAt() {
|
|
||||||
return Instant.now().toEpochMilli() + jwtUtil.getExpirationMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImCredential refreshImToken(String appId, String userId) {
|
|
||||||
DemoUserEntity user = userRepository.findByAppIdAndUserId(appId, userId)
|
|
||||||
.orElseThrow(() -> new BusinessException(404, "User not found: " + userId));
|
|
||||||
return callImServiceLogin(appId, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls im-service to ensure the IM account exists and obtain an IM token.
|
* Calls im-service to ensure the IM account exists and obtain an IM token.
|
||||||
* POST {imServiceUrl}/api/im/auth/login?appId={appId}&userId={userId}
|
|
||||||
* Response: {"code":200,"data":{"token":"..."}}
|
|
||||||
*/
|
*/
|
||||||
private ImCredential callImServiceLogin(String appId, String userId) {
|
private ImCredential callImServiceLogin(String appId, String userId) {
|
||||||
long timestamp = System.currentTimeMillis();
|
long timestamp = System.currentTimeMillis();
|
||||||
@ -163,18 +147,15 @@ public class DemoAuthService {
|
|||||||
JsonNode data = body.path("data");
|
JsonNode data = body.path("data");
|
||||||
String token = data.path("token").asText(null);
|
String token = data.path("token").asText(null);
|
||||||
if (token == null || token.isBlank()) {
|
if (token == null || token.isBlank()) {
|
||||||
throw new BusinessException(502, "Failed to refresh IM token");
|
throw new BusinessException(502, "Failed to acquire IM token");
|
||||||
}
|
}
|
||||||
return new ImCredential(
|
return new ImCredential(token);
|
||||||
token,
|
|
||||||
data.path("expiresAt").asLong(tokenExpiresAt())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
log.warn("im-service login returned unexpected response for appId={} userId={}: {}", appId, userId, body);
|
log.warn("im-service login returned unexpected response for appId={} userId={}: {}", appId, userId, body);
|
||||||
throw new BusinessException(502, "Failed to refresh IM token");
|
throw new BusinessException(502, "Failed to acquire IM token");
|
||||||
} catch (RestClientException e) {
|
} catch (RestClientException e) {
|
||||||
log.error("Failed to call im-service login for appId={} userId={}: {}", appId, userId, e.getMessage());
|
log.error("Failed to call im-service login for appId={} userId={}: {}", appId, userId, e.getMessage());
|
||||||
throw new BusinessException(502, "Failed to refresh IM token");
|
throw new BusinessException(502, "Failed to acquire IM token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -143,7 +143,7 @@
|
|||||||
| 方法 | 路径 | 鉴权 | 说明 |
|
| 方法 | 路径 | 鉴权 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| GET | `/api/v1/updates/app/check` | 否 | 检查 App 更新 |
|
| GET | `/api/v1/updates/app/check` | 否 | 检查 App 更新 |
|
||||||
| POST | `/api/v1/updates/app/upload` | 是 | 上传 App 版本,支持即时发布 / 定时发布 / 市场提交配置;Android 支持 `apkUrl`(来自 file-service)或旧版直传 `apkFile`,iOS / Harmony 仅记录版本号与市场跳转信息,`marketUrl` 也可以为空,不要求本地安装包;可附带 `expectedPackageName` 作为当前应用包名守卫 |
|
| POST | `/api/v1/updates/app/upload` | 是 | 上传 App 版本,支持即时发布 / 定时发布 / 市场提交配置;Android 通过 `apkUrl`(来自 file-service)上传安装包,iOS / Harmony 仅记录版本号与市场跳转信息,`marketUrl` 也可以为空,不要求本地安装包;可附带 `expectedPackageName` 作为当前应用包名守卫 |
|
||||||
| POST | `/api/v1/updates/app/{id}/publish` | 是 | 发布 App 版本 |
|
| POST | `/api/v1/updates/app/{id}/publish` | 是 | 发布 App 版本 |
|
||||||
| GET | `/api/v1/updates/app/list` | 是 | App 版本列表 |
|
| GET | `/api/v1/updates/app/list` | 是 | App 版本列表 |
|
||||||
| GET | `/api/v1/updates/files/apk/{filename}` | 否 | 下载 APK |
|
| GET | `/api/v1/updates/files/apk/{filename}` | 否 | 下载 APK |
|
||||||
@ -240,16 +240,7 @@ curl 'https://dev.xuqinmin.com/api/v1/rn/update/check?appId=ak_demo_chat&platfor
|
|||||||
curl -X POST 'https://dev.xuqinmin.com/api/im/auth/login?appId=ak_demo_chat&userId=demo_alice'
|
curl -X POST 'https://dev.xuqinmin.com/api/im/auth/login?appId=ak_demo_chat&userId=demo_alice'
|
||||||
```
|
```
|
||||||
|
|
||||||
返回示例中的 `data` 会同时包含 `token` 和 `expiresAt`,用于客户端提前做静默续签。
|
返回示例中的 `data` 只包含 `token`。如果需要更新登录态,请由业务服务端重新调用登录接口并覆盖旧会话。
|
||||||
|
|
||||||
### Demo IM 刷新
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST 'https://dev.xuqinmin.com/api/demo/auth/refresh-im?appId=ak_demo_chat' \
|
|
||||||
-H 'Authorization: Bearer <demo_jwt>'
|
|
||||||
```
|
|
||||||
|
|
||||||
该接口会基于 demo 登录态重新签发 IM token,并返回新的 `expiresAt`。
|
|
||||||
|
|
||||||
### IM 会话与关系链
|
### IM 会话与关系链
|
||||||
|
|
||||||
|
|||||||
@ -1576,7 +1576,7 @@ public final class XuqmImServerSdk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record LoginResponse(String token, long expiresAt) {}
|
public record LoginResponse(String token) {}
|
||||||
|
|
||||||
public record ConversationView(
|
public record ConversationView(
|
||||||
String targetId,
|
String targetId,
|
||||||
|
|||||||
@ -34,9 +34,6 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
accountService.validateSignature(appId, userId, timestamp, nonce, signature);
|
accountService.validateSignature(appId, userId, timestamp, nonce, signature);
|
||||||
ImAccountService.LoginResult result = accountService.loginOrRegister(appId, userId);
|
ImAccountService.LoginResult result = accountService.loginOrRegister(appId, userId);
|
||||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
return ResponseEntity.ok(ApiResponse.success(Map.of("token", result.token())));
|
||||||
"token", result.token(),
|
|
||||||
"expiresAt", result.expiresAt()
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import java.util.UUID;
|
|||||||
@Service
|
@Service
|
||||||
public class ImAccountService {
|
public class ImAccountService {
|
||||||
|
|
||||||
public record LoginResult(String token, long expiresAt) {}
|
public record LoginResult(String token) {}
|
||||||
|
|
||||||
private final ImAccountRepository accountRepository;
|
private final ImAccountRepository accountRepository;
|
||||||
private final JwtUtil jwtUtil;
|
private final JwtUtil jwtUtil;
|
||||||
@ -68,10 +68,7 @@ public class ImAccountService {
|
|||||||
throw new BusinessException(403, "账号已被封禁");
|
throw new BusinessException(403, "账号已被封禁");
|
||||||
}
|
}
|
||||||
|
|
||||||
long expiresAt = jwtUtil.getExpirationMillis() > 0
|
return new LoginResult(jwtUtil.generate(userId, Map.of("appId", appId, "role", "USER")));
|
||||||
? Instant.now().toEpochMilli() + jwtUtil.getExpirationMillis()
|
|
||||||
: Long.MAX_VALUE;
|
|
||||||
return new LoginResult(jwtUtil.generate(userId, Map.of("appId", appId, "role", "USER")), expiresAt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImAccountEntity getAccount(String appId, String userId) {
|
public ImAccountEntity getAccount(String appId, String userId) {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户