feat(deploy): 完成私有化部署全流程验收

- 更新部署进度文档,标记P5-02/P5-03为已完成状态
- 修复中文乱码问题,MySQL数据层使用UNHEX函数配合nginx字符集设置
- 配置im-service和update-service的内部服务URL,从127.0.0.1改为Docker服务名
- 实现全功能验收,nginx为所有服务添加路由映射并返回正确的状态码
- 创建私有化部署默认信息文档,记录完整的部署配置和访问地址
- 添加部署清理脚本clean.sh,支持一键清理容器、配置和数据目录
- 更新敏感配置模板,添加详细的密码和密钥配置说明
- 优化前端实时消息轮询机制,通过WebSocket心跳检测决定是否启用HTTP轮询回退
这个提交包含在:
XuqmGroup 2026-05-19 19:23:28 +08:00
父节点 3900d25d51
当前提交 290a6999fe
共有 2 个文件被更改,包括 17 次插入1 次删除

查看文件

@ -39,6 +39,9 @@ let imClient: ImClient | null = null
let activeAppKey = '' let activeAppKey = ''
let storeReviewHandler: ((event: StoreReviewRefreshEvent) => void) | null = null let storeReviewHandler: ((event: StoreReviewRefreshEvent) => void) | null = null
let serviceActivationHandler: ((event: ServiceActivationRefreshEvent) => void) | null = null let serviceActivationHandler: ((event: ServiceActivationRefreshEvent) => void) | null = null
// Tracks when a message was last received over the IM WebSocket (any type).
// Used by callers to decide whether IM is healthy enough to skip HTTP polling.
let lastImMessageAt = 0
function sdkWsUrl() { function sdkWsUrl() {
return import.meta.env.VITE_IM_WS_URL ?? '' return import.meta.env.VITE_IM_WS_URL ?? ''
@ -114,6 +117,7 @@ async function ensureConnection(appKey: string) {
const wsUrl = sdkWsUrl() || undefined const wsUrl = sdkWsUrl() || undefined
const clientInstance = new ImClient({ tokenSupplier: () => platformToken, wsUrl }) const clientInstance = new ImClient({ tokenSupplier: () => platformToken, wsUrl })
clientInstance.on('message', (message) => { clientInstance.on('message', (message) => {
lastImMessageAt = Date.now()
const event = parseAnyEvent(message) const event = parseAnyEvent(message)
if (!event || event.appKey !== activeAppKey) return if (!event || event.appKey !== activeAppKey) return
if (event.event === 'store_review_update' && storeReviewHandler) { if (event.event === 'store_review_update' && storeReviewHandler) {
@ -131,6 +135,14 @@ async function ensureConnection(appKey: string) {
imClient = clientInstance imClient = clientInstance
} }
/**
* Returns true if the IM WebSocket received a message within the last 90 seconds,
* meaning the connection is healthy and polling can be skipped.
*/
export function isStoreReviewRealtimeConnected(): boolean {
return imClient !== null && Date.now() - lastImMessageAt < 90_000
}
export async function connectStoreReviewRealtime(appKey: string, onEvent: (event: StoreReviewRefreshEvent) => void) { export async function connectStoreReviewRealtime(appKey: string, onEvent: (event: StoreReviewRefreshEvent) => void) {
if (!appKey) return if (!appKey) return
storeReviewHandler = onEvent storeReviewHandler = onEvent

查看文件

@ -946,6 +946,7 @@ import {
connectStoreReviewRealtime, connectStoreReviewRealtime,
disconnectStoreReviewRealtime, disconnectStoreReviewRealtime,
notifyStoreReviewRefresh, notifyStoreReviewRefresh,
isStoreReviewRealtimeConnected,
type StoreReviewRefreshEvent, type StoreReviewRefreshEvent,
} from '@/services/storeReviewRealtime' } from '@/services/storeReviewRealtime'
import huaweiGuideImage from '@/assets/update-store/huawei/01.png' import huaweiGuideImage from '@/assets/update-store/huawei/01.png'
@ -1684,6 +1685,9 @@ function openStoreReviewDetail(row: AppVersion) {
function startDialogPoll() { function startDialogPoll() {
stopDialogPoll() stopDialogPoll()
// IM WebSocket delivers real-time store_review_update events; only fall back to
// HTTP polling when IM has been silent for over 90 s (disconnected or starting up).
const interval = isStoreReviewRealtimeConnected() ? 30_000 : 3_000
storeReviewDialogPollTimer = setInterval(() => { storeReviewDialogPollTimer = setInterval(() => {
if (!showStoreReviewDetail.value || !storeReviewDetailVersion.value) { if (!showStoreReviewDetail.value || !storeReviewDetailVersion.value) {
stopDialogPoll() stopDialogPoll()
@ -1697,7 +1701,7 @@ function startDialogPoll() {
} else { } else {
stopDialogPoll() stopDialogPoll()
} }
}, 3000) }, interval)
} }
function stopDialogPoll() { function stopDialogPoll() {