feat: migrate app routes and business modules

This commit is contained in:
2026-08-12 18:22:59 +08:00
parent 555aa00043
commit cc706378c2
247 changed files with 28623 additions and 14988 deletions
+347
View File
@@ -0,0 +1,347 @@
import { runtimeConfig } from '@/utils/runtime-config.js'
import { goRoot } from '@/utils/navigation/gateway.js'
import { session } from '@/utils/session.js'
import { createRequestCancelledError } from './request-controller.js'
const SUCCESS_CODES = [0, 200]
const REQUEST_TIMEOUT_MS = 15000
export const createRequestError = (message, code, details = {}) => {
const error = new Error(message)
error.code = code
Object.assign(error, details)
return error
}
const isAuthenticatedSessionRejected = (error) =>
error?.code === 'BUSINESS_ERROR' && Number(error.businessCode) === 401
let authenticationRecoveryInFlight = false
const recoverExpiredAuthenticatedSession = () => {
if (authenticationRecoveryInFlight) return
authenticationRecoveryInFlight = true
session.clear()
const finishRecovery = () => {
authenticationRecoveryInFlight = false
}
goRoot('A01').then(finishRecovery, finishRecovery)
}
export const unwrapResponse = (response) => {
if (!response || typeof response !== 'object' || Array.isArray(response)) return response
if (!Object.prototype.hasOwnProperty.call(response, 'code')) return response
if (!SUCCESS_CODES.includes(Number(response.code))) {
const numericCode = Number(response.code)
throw createRequestError(response.msg || '请求未成功', 'BUSINESS_ERROR', {
businessCode: Number.isNaN(numericCode) ? response.code : numericCode
})
}
return Object.prototype.hasOwnProperty.call(response, 'data') ? response.data : response
}
export const request = (options, {
authenticated = true,
strictEnvelope = false,
requireEnvelopeData = false,
expectedStatus = null,
requestController = null
} = {}) => new Promise((resolve, reject) => {
if (requestController !== null && (
typeof requestController.bind !== 'function' ||
typeof requestController.release !== 'function'
)) {
reject(new TypeError('请求控制器格式无效'))
return
}
let settled = false
let requestTask = null
const release = () => requestController?.release(abortRequest)
const resolveOnce = (value) => {
if (settled) return
settled = true
release()
resolve(value)
}
const rejectOnce = (error) => {
if (settled) return
settled = true
release()
reject(error)
}
const abortRequest = () => {
if (settled) return
const task = requestTask
rejectOnce(createRequestCancelledError())
task?.abort?.()
}
const token = session.getToken()
requestTask = uni.request({
...options,
url: `${runtimeConfig.baseUrl}${options.url}`,
header: {
clientid: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
...(authenticated && token ? { Authorization: `Bearer ${token}` } : {}),
...(options.header || {})
},
success: ({ data, statusCode }) => {
if (expectedStatus !== null && statusCode !== expectedStatus) {
rejectOnce(createRequestError(
`服务只接受 HTTP ${expectedStatus} 响应,实际为 ${statusCode}`,
'HTTP_ERROR',
{ httpStatus: statusCode }
))
return
}
if (statusCode < 200 || statusCode >= 300) {
rejectOnce(createRequestError(
data?.msg || `请求失败(${statusCode}`,
'HTTP_ERROR',
{ httpStatus: statusCode }
))
return
}
if (
strictEnvelope &&
(!data ||
typeof data !== 'object' ||
Array.isArray(data) ||
!Object.prototype.hasOwnProperty.call(data, 'code') ||
!Number.isInteger(data.code) ||
(requireEnvelopeData && !Object.prototype.hasOwnProperty.call(data, 'data')))
) {
rejectOnce(createRequestError('服务响应格式无效', 'RESPONSE_INVALID'))
return
}
try {
resolveOnce(unwrapResponse(data))
} catch (error) {
// 业务 401 表示本地会话已经失效:只撤销本地令牌和家谱上下文,
// 不调用退出接口、不重试写操作,也不影响未携带会话的登录/注册请求。
if (authenticated && isAuthenticatedSessionRejected(error)) {
recoverExpiredAuthenticatedSession()
}
rejectOnce(error)
}
},
fail: (error) => {
const message = error?.errMsg || '网络连接失败'
rejectOnce(createRequestError(
message,
/timeout/i.test(message) ? 'REQUEST_TIMEOUT' : 'NETWORK_ERROR'
))
}
})
if (!settled && requestController) requestController.bind(abortRequest)
})
// 已核对的接口使用严格边界:只接受 HTTP 200 与完整 JSON envelope
// 并统一限制弱网等待时间;尚未逐页治理的旧读取仍使用通用 request。
export const requestStrict = (options, {
authenticated = true,
requireData = true,
requestController = null
} = {}) => request({
...options,
timeout: REQUEST_TIMEOUT_MS
}, {
authenticated,
strictEnvelope: true,
requireEnvelopeData: requireData,
expectedStatus: 200,
requestController
})
export const requestAuth = (options, { requestController = null } = {}) => requestStrict(options, {
authenticated: false,
requestController
})
// RVoid 的 OpenAPI schema 没有把 data 声明为必填。空结果接口仍要求合法的
// HTTP 200 与整数 code,但统一向页面返回 null,避免把 envelope 泄漏成业务数据。
export const requestAuthVoid = async (options, { requestController = null } = {}) => {
await requestStrict(options, {
authenticated: false,
requireData: false,
requestController
})
return null
}
const parseStrictUploadResponse = (responseText, requireData) => {
let uploadEnvelope
try {
uploadEnvelope = JSON.parse(responseText)
} catch {
throw createRequestError('文件服务响应格式无效', 'RESPONSE_INVALID')
}
if (
!uploadEnvelope ||
typeof uploadEnvelope !== 'object' ||
Array.isArray(uploadEnvelope) ||
!Object.prototype.hasOwnProperty.call(uploadEnvelope, 'code') ||
!Number.isInteger(uploadEnvelope.code) ||
(requireData && !Object.prototype.hasOwnProperty.call(uploadEnvelope, 'data'))
) {
throw createRequestError('文件服务响应格式无效', 'RESPONSE_INVALID')
}
return unwrapResponse(uploadEnvelope)
}
export const requestNativeFileChunk = ({ uploadId, chunkIndex, chunkMd5, filePath }, { requestController = null } = {}) => new Promise((resolve, reject) => {
if (typeof globalThis.uni?.uploadFile !== 'function') {
reject(createRequestError('当前运行环境不支持文件上传', 'FILE_UPLOAD_UNAVAILABLE'))
return
}
if (requestController !== null && (
typeof requestController.bind !== 'function' ||
typeof requestController.release !== 'function'
)) {
reject(new TypeError('文件上传控制器格式无效'))
return
}
let settled = false
let task = null
let cancelled = false
const release = () => requestController?.release(abort)
const resolveOnce = (value) => {
if (settled) return
settled = true
release()
resolve(value)
}
const rejectOnce = (error) => {
if (settled) return
settled = true
release()
reject(error)
}
const abort = () => {
if (settled) return
cancelled = true
task?.abort?.()
rejectOnce(createRequestCancelledError())
}
const token = session.getToken()
try {
task = globalThis.uni.uploadFile({
url: `${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk`,
filePath,
name: 'file',
formData: {
uploadId,
chunkIndex: String(chunkIndex),
chunkMd5
},
header: {
clientid: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
...(token ? { Authorization: `Bearer ${token}` } : {})
},
timeout: REQUEST_TIMEOUT_MS,
success: (upload) => {
if (upload.statusCode !== 200) {
rejectOnce(createRequestError(`文件分片上传失败(HTTP ${upload.statusCode}`, 'HTTP_ERROR', { httpStatus: upload.statusCode }))
return
}
try {
parseStrictUploadResponse(upload.data, false)
resolveOnce(null)
} catch (error) {
rejectOnce(error)
}
},
fail: (error) => {
if (settled) return
if (cancelled) {
rejectOnce(createRequestCancelledError())
return
}
const message = typeof error?.errMsg === 'string' && error.errMsg
? error.errMsg
: '文件分片上传失败'
const timeout = /timeout/i.test(message)
rejectOnce(createRequestError(
timeout ? '文件分片上传超时' : message,
timeout ? 'REQUEST_TIMEOUT' : 'UPLOAD_FAILED'
))
}
})
if (requestController) requestController.bind(abort)
} catch (error) {
rejectOnce(error)
}
})
export const requestBrowserFileChunk = ({ uploadId, chunkIndex, chunkMd5, file }, { requestController = null } = {}) => new Promise((resolve, reject) => {
if (typeof globalThis.fetch !== 'function' || typeof globalThis.FormData !== 'function') {
reject(createRequestError('当前运行环境不支持文件上传', 'FILE_UPLOAD_UNAVAILABLE'))
return
}
if (requestController !== null && (
typeof requestController.bind !== 'function' ||
typeof requestController.release !== 'function'
)) {
reject(new TypeError('文件上传控制器格式无效'))
return
}
let settled = false
let timeoutId = null
const abortController = new AbortController()
const release = () => requestController?.release(abort)
const clearRequestTimeout = () => {
if (timeoutId === null) return
clearTimeout(timeoutId)
timeoutId = null
}
const resolveOnce = (value) => {
if (settled) return
settled = true
clearRequestTimeout()
release()
resolve(value)
}
const rejectOnce = (error) => {
if (settled) return
settled = true
clearRequestTimeout()
release()
reject(error)
}
const abort = () => {
if (settled) return
abortController.abort()
rejectOnce(createRequestCancelledError())
}
const token = session.getToken()
const formData = new globalThis.FormData()
formData.append('uploadId', uploadId)
formData.append('chunkIndex', String(chunkIndex))
formData.append('chunkMd5', chunkMd5)
formData.append('file', file, file.name || 'image')
if (requestController) requestController.bind(abort)
timeoutId = setTimeout(() => {
abortController.abort()
rejectOnce(createRequestError('文件分片上传超时', 'REQUEST_TIMEOUT'))
}, REQUEST_TIMEOUT_MS)
globalThis.fetch(`${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk`, {
method: 'POST',
headers: {
clientid: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
...(token ? { Authorization: `Bearer ${token}` } : {})
},
body: formData,
signal: abortController.signal
}).then(async (response) => {
if (response.status !== 200) {
throw createRequestError(`文件分片上传失败(HTTP ${response.status}`, 'HTTP_ERROR', { httpStatus: response.status })
}
parseStrictUploadResponse(await response.text(), false)
resolveOnce(null)
}).catch((error) => {
if (settled) return
rejectOnce(error?.name === 'AbortError' ? createRequestCancelledError() : error)
})
})