docs(deploy): 添加部署文档并更新SDK API设计规范

- 新增完整的XuqmGroup部署文档,包含服务器配置、Docker Compose部署策略
- 更新SDK API重设计规范至V2.0,统一各端SDK初始化和登录接口
- 添加安全设计规范文档,涵盖密码安全、AppSecret验证等内容
- 新增离线推送架构设计文档,定义厂商推送集成方案
- 重构SDK登录流程,统一使用userId + userSig鉴权模式
- 移除dbName等外部配置参数,实现零感知平台地址配置
- 完善部署架构图和配置示例文件
这个提交包含在:
XuqmGroup 2026-05-02 11:29:49 +08:00
父节点 edd2adc5aa
当前提交 6d1d2ec634
共有 6 个文件被更改,包括 12 次插入66 次删除

查看文件

@ -56,31 +56,11 @@ public class DemoAuthController {
private Map<String, Object> buildResponse(DemoAuthService.AuthResult result) {
return Map.of(
"demoToken", result.demoToken() != null ? result.demoToken() : "",
"demoTokenExpiresAt", result.demoTokenExpiresAt(),
"imToken", result.imToken() != null ? result.imToken() : "",
"imTokenExpiresAt", result.imTokenExpiresAt(),
"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")
public ApiResponse<Void> resetPassword(@RequestBody ResetPasswordRequest body) {
if (body.appId() == null || body.appId().isBlank()) {

查看文件

@ -51,8 +51,8 @@ public class DemoAuthService {
this.appSecretClient = appSecretClient;
}
public record AuthResult(String demoToken, long demoTokenExpiresAt, String imToken, long imTokenExpiresAt, UserProfile profile) {}
public record ImCredential(String token, long expiresAt) {}
public record AuthResult(String demoToken, String imToken, UserProfile profile) {}
public record ImCredential(String token) {}
public record UserProfile(String appId, String userId, String nickname, String avatar, String gender) {}
@ -77,9 +77,7 @@ public class DemoAuthService {
return new AuthResult(
demoToken,
tokenExpiresAt(),
imCredential.token(),
imCredential.expiresAt(),
toProfile(user)
);
}
@ -98,9 +96,7 @@ public class DemoAuthService {
return new AuthResult(
demoToken,
tokenExpiresAt(),
imCredential.token(),
imCredential.expiresAt(),
toProfile(user)
);
}
@ -117,20 +113,8 @@ public class DemoAuthService {
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.
* POST {imServiceUrl}/api/im/auth/login?appId={appId}&userId={userId}
* Response: {"code":200,"data":{"token":"..."}}
*/
private ImCredential callImServiceLogin(String appId, String userId) {
long timestamp = System.currentTimeMillis();
@ -163,18 +147,15 @@ public class DemoAuthService {
JsonNode data = body.path("data");
String token = data.path("token").asText(null);
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(
token,
data.path("expiresAt").asLong(tokenExpiresAt())
);
return new ImCredential(token);
}
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) {
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 更新 |
| 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 版本 |
| GET | `/api/v1/updates/app/list` | 是 | App 版本列表 |
| 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'
```
返回示例中的 `data` 会同时包含 `token``expiresAt`,用于客户端提前做静默续签。
### 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`
返回示例中的 `data` 只包含 `token`。如果需要更新登录态,请由业务服务端重新调用登录接口并覆盖旧会话。
### IM 会话与关系链

查看文件

@ -1576,7 +1576,7 @@ public final class XuqmImServerSdk {
}
}
public record LoginResponse(String token, long expiresAt) {}
public record LoginResponse(String token) {}
public record ConversationView(
String targetId,

查看文件

@ -34,9 +34,6 @@ public class AuthController {
}
accountService.validateSignature(appId, userId, timestamp, nonce, signature);
ImAccountService.LoginResult result = accountService.loginOrRegister(appId, userId);
return ResponseEntity.ok(ApiResponse.success(Map.of(
"token", result.token(),
"expiresAt", result.expiresAt()
)));
return ResponseEntity.ok(ApiResponse.success(Map.of("token", result.token())));
}
}

查看文件

@ -17,7 +17,7 @@ import java.util.UUID;
@Service
public class ImAccountService {
public record LoginResult(String token, long expiresAt) {}
public record LoginResult(String token) {}
private final ImAccountRepository accountRepository;
private final JwtUtil jwtUtil;
@ -68,10 +68,7 @@ public class ImAccountService {
throw new BusinessException(403, "账号已被封禁");
}
long expiresAt = jwtUtil.getExpirationMillis() > 0
? Instant.now().toEpochMilli() + jwtUtil.getExpirationMillis()
: Long.MAX_VALUE;
return new LoginResult(jwtUtil.generate(userId, Map.of("appId", appId, "role", "USER")), expiresAt);
return new LoginResult(jwtUtil.generate(userId, Map.of("appId", appId, "role", "USER")));
}
public ImAccountEntity getAccount(String appId, String userId) {