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
+414
View File
@@ -0,0 +1,414 @@
import {
fromApiGenealogyAccess,
isGenealogyJoinMode,
isGenealogyVisibility
} from '@/utils/genealogy/access-policy.js'
import { normalizeBusinessFileAccess } from './business-file-contract.js'
import { createRequestError } from './request-client.js'
import { assertPlainPayload, normalizeOssIdString } from './request-normalizers.js'
import {
normalizeOptionalNonnegativeInteger,
normalizeResponseText
} from './response-normalizers.js'
const normalizeGenealogyResponseText = (value, label) => normalizeResponseText(value, label, {
code: 'GENEALOGY_RESPONSE_INVALID',
subject: '我的家谱响应'
})
const normalizeGenealogySettingsResponseText = (value, label) => normalizeResponseText(value, label, {
code: 'GENEALOGY_SETTINGS_RESPONSE_INVALID',
subject: '家谱设置响应'
})
export const GENEALOGY_LIFECYCLE_STATUS = Object.freeze({
NORMAL: 'normal',
ARCHIVED: 'archived',
DELETE_PENDING: 'delete_pending'
})
const genealogyLifecycleStatuses = new Set(
Object.values(GENEALOGY_LIFECYCLE_STATUS)
)
const normalizeGenealogyLifecycleStatus = (value, code) => {
const status = normalizeResponseText(value, 'lifecycleStatus', {
code,
subject: '家谱响应'
})
if (!genealogyLifecycleStatuses.has(status)) {
throw createRequestError('家谱生命周期状态无效', code)
}
return status
}
const normalizeGenealogyAccessValue = (value, field, isValid, code) => {
const normalized = normalizeResponseText(value, field, {
code,
subject: '家谱响应'
})
if (normalized && !isValid(normalized)) {
throw createRequestError(`家谱字段 ${field} 无效`, code)
}
return normalized
}
const normalizeGenealogyCapabilities = (value, code) => {
const capabilities = {}
for (const field of ['canManage', 'canEditContent', 'canArchive', 'canRestore']) {
if (typeof value[field] !== 'boolean') {
throw createRequestError(`家谱权限字段 ${field} 无效`, code)
}
capabilities[field] = value[field]
}
return capabilities
}
const normalizeMyGenealogyId = (value) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw createRequestError('我的家谱响应包含无效标识', 'GENEALOGY_RESPONSE_INVALID')
}
export const normalizeGenealogyPathId = (value) => {
const id = typeof value === 'number' && Number.isSafeInteger(value) ? String(value) : value
if (typeof id !== 'string' || !/^[1-9]\d*$/.test(id)) {
throw createRequestError('家谱标识无效', 'GENEALOGY_ID_INVALID')
}
return id
}
export const normalizeCreatedGenealogy = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('创建家谱响应无效', 'GENEALOGY_RESPONSE_INVALID')
}
const id = value.genealogyId
if (typeof id === 'string' && /^[1-9]\d*$/.test(id)) return { id }
if (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) return { id: String(id) }
throw createRequestError('创建家谱响应缺少有效标识', 'GENEALOGY_RESPONSE_INVALID')
}
export const normalizeGenealogySettings = (value, expectedGenealogyId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('家谱设置响应无效', 'GENEALOGY_SETTINGS_RESPONSE_INVALID')
}
const id = normalizeMyGenealogyId(value.genealogyId)
if (id !== expectedGenealogyId) {
throw createRequestError('家谱设置响应标识不匹配', 'GENEALOGY_SETTINGS_RESPONSE_INVALID')
}
const genealogyName = normalizeGenealogySettingsResponseText(value.genealogyName, 'genealogyName')
const surname = normalizeGenealogySettingsResponseText(value.surname, 'surname')
const regionCode = normalizeGenealogySettingsResponseText(value.regionCode, 'regionCode')
if (!genealogyName || !surname || !regionCode) {
throw createRequestError('家谱设置响应缺少名称、姓氏或地区', 'GENEALOGY_SETTINGS_RESPONSE_INVALID')
}
const visibility = normalizeGenealogyAccessValue(
value.visibility,
'visibility',
isGenealogyVisibility,
'GENEALOGY_SETTINGS_RESPONSE_INVALID'
)
const joinMode = normalizeGenealogyAccessValue(
value.joinMode,
'joinMode',
isGenealogyJoinMode,
'GENEALOGY_SETTINGS_RESPONSE_INVALID'
)
const capabilities = normalizeGenealogyCapabilities(
value,
'GENEALOGY_SETTINGS_RESPONSE_INVALID'
)
return {
id,
genealogyName,
surname,
ancestralHall: normalizeGenealogySettingsResponseText(value.ancestralHall, 'ancestralHall'),
originPlace: normalizeGenealogySettingsResponseText(value.originPlace, 'originPlace'),
regionCode,
regionName: normalizeGenealogySettingsResponseText(value.regionName, 'regionName'),
regionFullName: normalizeGenealogySettingsResponseText(value.regionFullName, 'regionFullName'),
addressDetail: normalizeGenealogySettingsResponseText(value.addressDetail, 'addressDetail'),
coverFile: normalizeBusinessFileAccess(
value.coverFile,
'家谱封面',
'GENEALOGY_SETTINGS_RESPONSE_INVALID'
),
intro: normalizeGenealogySettingsResponseText(value.intro, 'intro'),
accessPreset: fromApiGenealogyAccess({ visibility, joinMode }),
visibility,
joinMode,
memberCount: normalizeOptionalNonnegativeInteger(value.memberCount, '家谱成员数量', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'),
personCount: normalizeOptionalNonnegativeInteger(value.personCount, '家谱人物数量', 'GENEALOGY_SETTINGS_RESPONSE_INVALID'),
...capabilities,
lifecycleStatus: normalizeGenealogyLifecycleStatus(
value.lifecycleStatus,
'GENEALOGY_SETTINGS_RESPONSE_INVALID'
),
archivedAt: normalizeGenealogySettingsResponseText(value.archivedAt, 'archivedAt')
}
}
export const normalizeAppGenealogy = (item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('我的家谱响应包含无效条目', 'GENEALOGY_RESPONSE_INVALID')
}
const name = normalizeGenealogyResponseText(item.genealogyName, 'genealogyName')
if (!name) {
throw createRequestError('我的家谱响应缺少家谱名称', 'GENEALOGY_RESPONSE_INVALID')
}
const capabilities = normalizeGenealogyCapabilities(item, 'GENEALOGY_RESPONSE_INVALID')
if (!Number.isSafeInteger(item.memberCount) || item.memberCount < 0) {
throw createRequestError('我的家谱响应成员数量无效', 'GENEALOGY_RESPONSE_INVALID')
}
const personCount = item.personCount ?? null
if (personCount !== null && (!Number.isSafeInteger(personCount) || personCount < 0)) {
throw createRequestError('我的家谱响应世系人数无效', 'GENEALOGY_RESPONSE_INVALID')
}
const visibility = normalizeGenealogyAccessValue(
item.visibility,
'visibility',
isGenealogyVisibility,
'GENEALOGY_RESPONSE_INVALID'
)
const joinMode = normalizeGenealogyAccessValue(
item.joinMode,
'joinMode',
isGenealogyJoinMode,
'GENEALOGY_RESPONSE_INVALID'
)
return {
id: normalizeMyGenealogyId(item.genealogyId),
name,
surname: normalizeGenealogyResponseText(item.surname, 'surname'),
hall: normalizeGenealogyResponseText(item.ancestralHall, 'ancestralHall'),
location:
normalizeGenealogyResponseText(item.regionFullName, 'regionFullName') ||
normalizeGenealogyResponseText(item.regionName, 'regionName') ||
normalizeGenealogyResponseText(item.originPlace, 'originPlace') ||
normalizeGenealogyResponseText(item.addressDetail, 'addressDetail') ||
'地区待补',
memberCount: item.memberCount,
personCount,
accessPreset: fromApiGenealogyAccess({ visibility, joinMode }),
...capabilities,
lifecycleStatus: normalizeGenealogyLifecycleStatus(
item.lifecycleStatus,
'GENEALOGY_RESPONSE_INVALID'
),
archivedAt: normalizeGenealogyResponseText(item.archivedAt, 'archivedAt'),
intro: normalizeGenealogyResponseText(item.intro, 'intro'),
joinTime: normalizeGenealogyResponseText(item.joinTime, 'joinTime')
}
}
export const normalizeMyGenealogies = (value) => {
if (!Array.isArray(value)) {
throw createRequestError('我的家谱响应不是列表', 'GENEALOGY_RESPONSE_INVALID')
}
const normalized = value.map((item) => {
const genealogy = normalizeAppGenealogy(item)
return {
id: genealogy.id,
name: genealogy.name,
surname: genealogy.surname,
hall: genealogy.hall,
location: genealogy.location,
memberCount: genealogy.memberCount,
canManage: genealogy.canManage,
canEditContent: genealogy.canEditContent,
lifecycleStatus: genealogy.lifecycleStatus,
archivedAt: genealogy.archivedAt,
canArchive: genealogy.canArchive,
canRestore: genealogy.canRestore
}
})
if (new Set(normalized.map((genealogy) => genealogy.id)).size !== normalized.length) {
throw createRequestError('我的家谱响应包含重复标识', 'GENEALOGY_RESPONSE_INVALID')
}
return normalized
}
export const normalizeGenealogyOrderIds = (value) => {
if (!Array.isArray(value)) {
throw createRequestError('家谱排序必须是列表', 'GENEALOGY_ORDER_INVALID')
}
const ids = value.map((item) => normalizeMyGenealogyId(item))
if (new Set(ids).size !== ids.length) {
throw createRequestError('家谱排序不能包含重复家谱', 'GENEALOGY_ORDER_INVALID')
}
return ids
}
export const normalizePublicGenealogies = (value) => {
if (!Array.isArray(value)) {
throw createRequestError('公开家谱响应不是列表', 'PUBLIC_GENEALOGY_RESPONSE_INVALID')
}
const genealogies = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('公开家谱响应包含无效条目', 'PUBLIC_GENEALOGY_RESPONSE_INVALID')
}
const id = normalizeMyGenealogyId(item.genealogyId)
const name = normalizeGenealogyResponseText(item.genealogyName, 'genealogyName')
if (!name) {
throw createRequestError('公开家谱响应缺少家谱名称', 'PUBLIC_GENEALOGY_RESPONSE_INVALID')
}
const memberCount = normalizeOptionalNonnegativeInteger(
item.memberCount,
'公开家谱成员数量',
'PUBLIC_GENEALOGY_RESPONSE_INVALID'
)
if (item.canManage !== undefined && typeof item.canManage !== 'boolean') {
throw createRequestError('公开家谱管理权限无效', 'PUBLIC_GENEALOGY_RESPONSE_INVALID')
}
return {
id,
name,
surname: normalizeGenealogyResponseText(item.surname, 'surname'),
location:
normalizeGenealogyResponseText(item.regionFullName, 'regionFullName') ||
normalizeGenealogyResponseText(item.regionName, 'regionName') ||
normalizeGenealogyResponseText(item.originPlace, 'originPlace') ||
normalizeGenealogyResponseText(item.addressDetail, 'addressDetail'),
intro: normalizeGenealogyResponseText(item.intro, 'intro'),
memberCount,
canManage: item.canManage === true
}
})
if (new Set(genealogies.map((genealogy) => genealogy.id)).size !== genealogies.length) {
throw createRequestError('公开家谱响应包含重复标识', 'PUBLIC_GENEALOGY_RESPONSE_INVALID')
}
return genealogies
}
export const projectPreviewPublicGenealogies = (value) =>
value.map((item) => ({
id: String(item.id),
name: String(item.name || '未命名家谱'),
surname: String(item.surname || ''),
location: String(item.location || ''),
intro: String(item.publicDescription || ''),
memberCount: Number.isSafeInteger(item.memberCount) ? item.memberCount : 0,
canManage: false
}))
const normalizeGenealogyWriteText = (value, field, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw new TypeError(`创建家谱缺少 ${field}`)
return ''
}
if (typeof value !== 'string') throw new TypeError(`创建家谱字段 ${field} 必须是字符串`)
const normalized = value.trim()
if (required && !normalized) throw new TypeError(`创建家谱缺少 ${field}`)
return normalized
}
const GENEALOGY_WRITE_FIELDS = new Set([
'genealogyName',
'surname',
'regionCode',
'ancestralHall',
'originPlace',
'addressDetail',
'intro',
'coverOssId',
'visibility',
'joinMode'
])
const assertGenealogyWritePayload = (payload, operation) => {
assertPlainPayload(payload, GENEALOGY_WRITE_FIELDS, `${operation}家谱请求`)
}
export const normalizeGenealogyCreatePayload = (payload) => {
assertGenealogyWritePayload(payload, '创建')
const normalizedPayload = {
genealogyName: normalizeGenealogyWriteText(payload.genealogyName, '谱名', { required: true }),
surname: normalizeGenealogyWriteText(payload.surname, '姓氏', { required: true }),
regionCode: normalizeGenealogyWriteText(payload.regionCode, '地区代码', { required: true })
}
for (const field of ['ancestralHall', 'originPlace', 'addressDetail', 'intro']) {
const normalizedText = normalizeGenealogyWriteText(payload[field], field)
if (normalizedText) normalizedPayload[field] = normalizedText
}
const visibility = normalizeGenealogyWriteText(payload.visibility, 'visibility')
if (visibility) {
if (!isGenealogyVisibility(visibility)) throw new TypeError('创建家谱字段 visibility 无效')
normalizedPayload.visibility = visibility
}
const joinMode = normalizeGenealogyWriteText(payload.joinMode, 'joinMode')
if (joinMode) {
if (!isGenealogyJoinMode(joinMode)) throw new TypeError('创建家谱字段 joinMode 无效')
normalizedPayload.joinMode = joinMode
}
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, '创建家谱字段 coverOssId')
}
return normalizedPayload
}
export const normalizeGenealogyUpdatePayload = (payload) => {
assertGenealogyWritePayload(payload, '更新')
const normalizedPayload = {}
for (const field of [
'genealogyName',
'surname',
'regionCode',
'ancestralHall',
'originPlace',
'addressDetail',
'intro'
]) {
if (!Object.prototype.hasOwnProperty.call(payload, field)) continue
const normalizedText = normalizeGenealogyWriteText(payload[field], field)
if (normalizedText) normalizedPayload[field] = normalizedText
}
if (Object.prototype.hasOwnProperty.call(payload, 'visibility')) {
const visibility = normalizeGenealogyWriteText(payload.visibility, 'visibility')
if (!isGenealogyVisibility(visibility)) throw new TypeError('更新家谱字段 visibility 无效')
normalizedPayload.visibility = visibility
}
if (Object.prototype.hasOwnProperty.call(payload, 'joinMode')) {
const joinMode = normalizeGenealogyWriteText(payload.joinMode, 'joinMode')
if (!isGenealogyJoinMode(joinMode)) throw new TypeError('更新家谱字段 joinMode 无效')
normalizedPayload.joinMode = joinMode
}
if (Object.prototype.hasOwnProperty.call(payload, 'coverOssId') && payload.coverOssId !== null && payload.coverOssId !== '') {
normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, '更新家谱字段 coverOssId')
}
if (!Object.keys(normalizedPayload).length) throw new TypeError('请至少填写一项需要更新的家谱信息')
return normalizedPayload
}
export const normalizeGenealogyQuota = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('家谱配额响应无效', 'GENEALOGY_QUOTA_RESPONSE_INVALID')
}
const normalizeUsage = (field) => {
if (!Number.isSafeInteger(value[field]) || value[field] < 0) {
throw createRequestError(`家谱配额字段 ${field} 无效`, 'GENEALOGY_QUOTA_RESPONSE_INVALID')
}
return value[field]
}
const normalizeLimit = (field) => {
if (!Number.isSafeInteger(value[field]) || value[field] < -1) {
throw createRequestError(`家谱配额字段 ${field} 无效`, 'GENEALOGY_QUOTA_RESPONSE_INVALID')
}
return value[field]
}
const normalizePermission = (field) => {
if (typeof value[field] !== 'boolean') {
throw createRequestError(`家谱配额字段 ${field} 无效`, 'GENEALOGY_QUOTA_RESPONSE_INVALID')
}
return value[field]
}
return {
createUsed: normalizeUsage('createUsed'),
createLimit: normalizeLimit('createLimit'),
createRemaining: normalizeLimit('createRemaining'),
canCreate: normalizePermission('canCreate'),
joinUsed: normalizeUsage('joinUsed'),
joinLimit: normalizeLimit('joinLimit'),
joinRemaining: normalizeLimit('joinRemaining'),
canJoin: normalizePermission('canJoin')
}
}