docs(deploy): 添加部署文档和安全设计规范

- 新增 XuqmGroup 部署文档,包含部署方案、架构建议和部署步骤
- 添加安全设计规范,涵盖密码安全、AppSecret验证和服务端API认证
- 补充平台REST API规范,定义Server-to-Server调用接口和错误码
- 创建Java IM服务端SDK计划文档,规划Maven包发布和接口实现
这个提交包含在:
徐勤民 2026-05-08 18:31:59 +08:00
父节点 7909e4cf66
当前提交 69f92836b7
共有 9 个文件被更改,包括 24 次插入24 次删除

2
.nvmrc
查看文件

@ -1 +1 @@
22
22.22.2

查看文件

@ -18,7 +18,7 @@
| API 域名 | `https://dev.xuqinmin.com` |
| 登录服务 | `demo-service``/api/demo/auth/*` |
| IM WebSocket | `wss://im.dev.xuqinmin.com/ws/im` |
| 演示 AppId | `ak_demo_chat` |
| 演示 AppKey | `ak_demo_chat` |
| 演示用户 | `demo_alice`Alice、`demo_bob`Bob |
| 演示模块 | `chat-home` |
| 本地 App 版本码 | `1`(写死,便于触发更新) |
@ -95,7 +95,7 @@ npm run ios # iOS 模拟器
### Section 3 · 群聊演示
1. 点击 **"+ 创建演示群"** → 创建含 Alice + Bob 的群组并订阅群 topic
2. 点击 **"加载群列表"** 列出当前 appId 下所有群组
2. 点击 **"加载群列表"** 列出当前 appKey 下所有群组
3. 点击群组名称切换当前群,自动拉取群历史
4. 使用消息类型选择器在群内发送任意类型消息
5. 点击 **"全部类型演示"** 依次发送 12 种群消息
@ -128,7 +128,7 @@ cd XuqmGroup-RNChatDemo
发布内容:
- `appId=ak_demo_chat` Android App 版本 `1.0.1`versionCode=2
- `appKey=ak_demo_chat` Android App 版本 `1.0.1`versionCode=2
- `moduleId=chat-home` Android RN bundle 版本 `1.0.1`
---
@ -147,7 +147,7 @@ cd XuqmGroup-RNChatDemo
所有接口通过 Nginx 反代至 `https://dev.xuqinmin.com`
`demo-service` 负责 demo 账号体系的注册、登录、找回密码和资料更新;登录成功后会返回 `demoToken`、`imToken` 和 `profile`。其中 `profile.appId` 用于后续初始化 IM 和更新能力,不走租户服务登录链路。
`demo-service` 负责 demo 账号体系的注册、登录、找回密码和资料更新;登录成功后会返回 `demoToken`、`imToken` 和 `profile`。其中 `profile.appKey` 用于后续初始化 IM 和更新能力,不走租户服务登录链路。
---

查看文件

@ -15,7 +15,7 @@ BUNDLE_FILE="${ASSET_DIR}/chat-home.android.bundle"
echo "Uploading demo APK metadata to ${BASE_URL}"
APP_UPLOAD_JSON="$(curl -fsS -X POST "${BASE_URL}/api/v1/updates/app/upload" \
-F "appId=${APP_ID}" \
-F "appKey=${APP_ID}" \
-F "platform=ANDROID" \
-F "versionName=1.0.1" \
-F "versionCode=2" \
@ -36,7 +36,7 @@ curl -fsS -X POST "${BASE_URL}/api/v1/updates/app/${APP_VERSION_ID}/publish" >/d
echo "Uploading demo RN bundle to ${BASE_URL}"
RN_UPLOAD_JSON="$(curl -fsS -X POST "${BASE_URL}/api/v1/rn/upload" \
-F "appId=${APP_ID}" \
-F "appKey=${APP_ID}" \
-F "moduleId=${MODULE_ID}" \
-F "platform=ANDROID" \
-F "version=1.0.1" \

查看文件

@ -12,7 +12,7 @@ async function request<T>(
options: { method?: string; body?: unknown; params?: Record<string, string>; skipAuth?: boolean } = {},
): Promise<T> {
let url = BASE + path
const params = { appId: APP_ID, ...(options.params ?? {}) }
const params = { appKey: APP_ID, ...(options.params ?? {}) }
url += '?' + new URLSearchParams(params).toString()
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
@ -33,7 +33,7 @@ async function request<T>(
}
export interface UserProfile {
appId: string
appKey: string
userId: string
nickname: string
avatar: string
@ -52,7 +52,7 @@ export const demoApi = {
return request('/auth/register', {
method: 'POST',
skipAuth: true,
body: { appId: APP_ID, userId, password, nickname },
body: { appKey: APP_ID, userId, password, nickname },
})
},
@ -60,7 +60,7 @@ export const demoApi = {
return request('/auth/login', {
method: 'POST',
skipAuth: true,
body: { appId: APP_ID, userId, password },
body: { appKey: APP_ID, userId, password },
})
},
@ -73,7 +73,7 @@ export const demoApi = {
},
resetPassword(userId: string, newPassword: string): Promise<void> {
return request('/auth/reset-password', { method: 'POST', skipAuth: true, body: { appId: APP_ID, userId, newPassword } })
return request('/auth/reset-password', { method: 'POST', skipAuth: true, body: { appKey: APP_ID, userId, newPassword } })
},
changePassword(oldPassword: string, newPassword: string): Promise<void> {

查看文件

@ -13,7 +13,7 @@ interface Props {
currentUserId: string
peerUserId: string
peerNickname: string
appId: string
appKey: string
onLog: (msg: string) => void
groupMessages: ImMessage[]
onMergeMessage: (msg: ImMessage) => void

查看文件

@ -27,7 +27,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<AuthState>({ ready: false, userId: null, profile: null })
const initSDK = useCallback(async (profile: UserProfile, imToken: string) => {
const appKey = profile.appId || APP_ID
const appKey = profile.appKey || APP_ID
await XuqmSDK.initialize({ appKey })
await XuqmSDK.login({ userId: profile.userId, userSig: imToken })
setState({ ready: true, userId: profile.userId, profile })

查看文件

@ -19,7 +19,7 @@ interface ConvWithMeta extends ConversationData {
}
export default function ConversationListScreen() {
const appId = 'ak_demo_chat'
const appKey = 'ak_demo_chat'
const navigation = useNavigation<Nav>()
const [conversations, setConversations] = useState<ConvWithMeta[]>([])
const profileCache = useRef<Record<string, UserProfile>>({})
@ -30,10 +30,10 @@ export default function ConversationListScreen() {
try {
const results = await ImSDK.searchUsers(conv.targetId)
const match = results.find(u => u.userId === conv.targetId)
profile = match ? toDemoUserProfile(match) : { appId, userId: conv.targetId, nickname: conv.targetId, avatar: '', gender: 'UNKNOWN', status: '' }
profile = match ? toDemoUserProfile(match) : { appKey, userId: conv.targetId, nickname: conv.targetId, avatar: '', gender: 'UNKNOWN', status: '' }
profileCache.current[conv.targetId] = profile
} catch {
profile = { appId, userId: conv.targetId, nickname: conv.targetId, avatar: '', gender: 'UNKNOWN', status: '' }
profile = { appKey, userId: conv.targetId, nickname: conv.targetId, avatar: '', gender: 'UNKNOWN', status: '' }
}
}
return { ...conv, targetName: profile.nickname, targetAvatar: profile.avatar }

查看文件

@ -13,7 +13,7 @@ type Props = NativeStackScreenProps<RootStackParams, 'GroupMembers'>
export default function GroupMembersScreen({ route }: Props) {
const { groupId } = route.params
const appId = 'ak_demo_chat'
const appKey = 'ak_demo_chat'
const [members, setMembers] = useState<UserProfile[]>([])
const [loading, setLoading] = useState(true)
@ -30,7 +30,7 @@ export default function GroupMembersScreen({ route }: Props) {
ids.map(async id => {
const res = await ImSDK.searchUsers(id)
const match = res.find(u => u.userId === id)
return match ? toDemoUserProfile(match, appId) : { appId, userId: id, nickname: id, avatar: '', gender: 'UNKNOWN' as const, status: '' }
return match ? toDemoUserProfile(match, appKey) : { appKey, userId: id, nickname: id, avatar: '', gender: 'UNKNOWN' as const, status: '' }
})
)
setMembers(profiles)

查看文件

@ -1,7 +1,7 @@
import type { UserProfile } from '../api/demo'
export interface SearchUserProfileLike {
appId?: string
appKey?: string
userId: string
nickname?: string | null
avatar?: string | null
@ -9,9 +9,9 @@ export interface SearchUserProfileLike {
status?: string | null
}
export function toDemoUserProfile(user: SearchUserProfileLike, appId = 'ak_demo_chat'): UserProfile {
export function toDemoUserProfile(user: SearchUserProfileLike, appKey = 'ak_demo_chat'): UserProfile {
return {
appId: user.appId ?? appId,
appKey: user.appKey ?? appKey,
userId: user.userId,
nickname: user.nickname ?? user.userId,
avatar: user.avatar ?? '',
@ -20,6 +20,6 @@ export function toDemoUserProfile(user: SearchUserProfileLike, appId = 'ak_demo_
}
}
export function toDemoUserProfiles(users: SearchUserProfileLike[], appId = 'ak_demo_chat'): UserProfile[] {
return users.map(user => toDemoUserProfile(user, appId))
export function toDemoUserProfiles(users: SearchUserProfileLike[], appKey = 'ak_demo_chat'): UserProfile[] {
return users.map(user => toDemoUserProfile(user, appKey))
}