待测试

This commit is contained in:
rain
2026-07-27 17:17:31 +08:00
parent 1eae3bbef4
commit 04287e6777
35 changed files with 4238 additions and 22750 deletions
+184 -46
View File
@@ -479,12 +479,19 @@ const normalizeProfileAvatar = (value) => {
return Number.isSafeInteger(numericValue) ? numericValue : null
}
const normalizeProfileUserId = (value) => {
if (value === undefined || value === null || value === '') return ''
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('个人资料字段 userId 无效', 'PROFILE_RESPONSE_INVALID')
}
const normalizeAppProfile = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw createRequestError('个人资料响应格式无效', 'PROFILE_RESPONSE_INVALID')
}
return {
userId: normalizeProfileAvatar(payload.userId),
userId: normalizeProfileUserId(payload.userId),
tenantId: normalizeProfileResponseText(payload.tenantId, 'tenantId'),
userNo: normalizeProfileResponseText(payload.userNo, 'userNo'),
phone: normalizeProfileResponseText(payload.phone, 'phone'),
@@ -524,8 +531,8 @@ const normalizeProfileUpdatePayload = (payload) => {
if (Object.prototype.hasOwnProperty.call(payload, 'birthday')) {
const value = normalizeOptionalAuthText(payload.birthday, 'birthday')
if (value) {
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})?$/.test(value)) {
throw new TypeError('birthday 必须是 ISO 日期时间')
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
throw new TypeError('birthday 必须是 yyyy-MM-dd 日期')
}
normalized.birthday = value
}
@@ -633,6 +640,13 @@ const normalizeUploadOssId = (value, field) => {
return id
}
const normalizeOssIdString = (value, field) => {
if (typeof value !== 'string' || !/^[1-9]\d*$/.test(value)) {
throw new TypeError(`${field} 必须是正整数 OSS ID 字符串`)
}
return value
}
const normalizeResumableInitResult = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw createRequestError('文件初始化响应格式无效', 'FILE_RESPONSE_INVALID')
@@ -838,13 +852,7 @@ const normalizeGenealogyCreatePayload = (payload) => {
if (value) normalized[field] = value
}
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
const numericOssId = typeof payload.coverOssId === 'number'
? payload.coverOssId
: Number(payload.coverOssId)
if (!Number.isSafeInteger(numericOssId) || numericOssId <= 0) {
throw new TypeError('创建家谱字段 coverOssId 必须是正整数')
}
normalized.coverOssId = numericOssId
normalized.coverOssId = normalizeOssIdString(payload.coverOssId, '创建家谱字段 coverOssId')
}
return normalized
}
@@ -887,13 +895,7 @@ const normalizeGenealogyUpdatePayload = (payload) => {
if (value) normalized[field] = value
}
if (Object.prototype.hasOwnProperty.call(payload, 'coverOssId') && payload.coverOssId !== null && payload.coverOssId !== '') {
const numericOssId = typeof payload.coverOssId === 'number'
? payload.coverOssId
: Number(payload.coverOssId)
if (!Number.isSafeInteger(numericOssId) || numericOssId <= 0) {
throw new TypeError('更新家谱字段 coverOssId 必须是正整数')
}
normalized.coverOssId = numericOssId
normalized.coverOssId = normalizeOssIdString(payload.coverOssId, '更新家谱字段 coverOssId')
}
if (!Object.keys(normalized).length) throw new TypeError('请至少填写一项需要更新的家谱信息')
return normalized
@@ -1082,14 +1084,28 @@ const lineageDatePart = (value, label) => {
const normalized = normalizeLineageText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}/.test(normalized)) throw lineageTreeError(`世系树${label}无效`)
return normalized.slice(0, 10)
if (normalized.length === 10) return normalized
const instant = new Date(normalized)
if (Number.isNaN(instant.getTime())) throw lineageTreeError(`世系树${label}无效`)
const parts = Object.fromEntries(
new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).formatToParts(instant)
.filter((part) => part.type !== 'literal')
.map((part) => [part.type, part.value])
)
return `${parts.year}-${parts.month}-${parts.day}`
}
const normalizeLineageTree = (value) => {
if (!Array.isArray(value)) throw lineageTreeError('世系树响应不是列表')
const seen = new Set()
const normalized = []
const appendNode = (node, parentId, relationOverride = '') => {
const normalizedById = new Map()
const appendNode = (node, parentId, relationOverride = '', spouseOf = '') => {
if (!node || typeof node !== 'object' || Array.isArray(node)) {
throw lineageTreeError('世系树包含无效节点')
}
@@ -1101,16 +1117,19 @@ const normalizeLineageTree = (value) => {
throw lineageTreeError('世系树人物世代无效')
}
const generationName = normalizeLineageText(node.generationName, '字辈')
const relationName = normalizeLineageText(node.relationName, '人物关系')
const birthDate = lineageDatePart(node.birthDate, '出生日期')
const deathDate = lineageDatePart(node.deathDate, '逝世日期')
normalized.push({
const normalizedNode = {
id,
parentId,
...(spouseOf ? { spouseOf } : {}),
name: normalizeLineageText(node.name, '人物姓名', { required: true }),
relation:
relationOverride ||
normalizeLineageText(node.relationName, '人物关系') ||
(parentId ? '后代' : '始祖'),
(parentId
? (relationName && relationName !== '配偶' ? relationName : '后代')
: relationName || '始祖'),
generation: node.generation,
branch: generationName
? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`)
@@ -1121,19 +1140,38 @@ const normalizeLineageTree = (value) => {
: '生卒待补',
sex: normalizeLineageText(node.sex, '性别'),
personStatus: normalizeLineageText(node.personStatus, '人物状态')
})
}
normalized.push(normalizedNode)
normalizedById.set(id, normalizedNode)
return id
}
const walk = (node, parentId = null, depth = 0) => {
if (depth > 64) throw lineageTreeError('世系树深度超出客户端上限')
const id = appendNode(node, parentId)
if (!node || typeof node !== 'object' || Array.isArray(node)) {
throw lineageTreeError('世系树包含无效节点')
}
const spouses = node.spouses ?? []
const children = node.children ?? []
if (!Array.isArray(spouses) || !Array.isArray(children)) {
throw lineageTreeError('世系树亲属集合无效')
}
spouses.forEach((spouse) => appendNode(spouse, parentId, '配偶'))
children.forEach((child) => walk(child, id, depth + 1))
const id = normalizeLineagePersonId(node.personId)
const knownSpouseId = spouses
.map((spouse) => normalizeLineagePersonId(spouse?.personId))
.find((spouseId) => normalizedById.has(spouseId)) || ''
if (seen.has(id)) {
if (knownSpouseId) return id
throw lineageTreeError('世系树包含重复人物标识')
}
const knownSpouse = knownSpouseId ? normalizedById.get(knownSpouseId) : null
const nodeParentId = knownSpouse ? knownSpouse.parentId : parentId
const appendedId = appendNode(node, nodeParentId, knownSpouse ? '配偶' : '', knownSpouseId)
spouses.forEach((spouse) => {
const spouseId = normalizeLineagePersonId(spouse?.personId)
if (seen.has(spouseId)) return
appendNode(spouse, nodeParentId, '配偶', appendedId)
})
children.forEach((child) => walk(child, knownSpouseId || appendedId, depth + 1))
}
value.forEach((root) => walk(root))
return normalized
@@ -1148,6 +1186,11 @@ const normalizeLineagePersonIdentity = (value, label) => {
throw lineagePersonError(`成员详情${label}无效`)
}
const normalizeOptionalLineagePersonIdentity = (value, label) => {
if (value === undefined || value === null || value === '') return ''
return normalizeLineagePersonIdentity(value, label)
}
const normalizeLineagePersonText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw lineagePersonError(`成员详情缺少${label}`)
@@ -1162,7 +1205,7 @@ const normalizeLineagePersonText = (value, label, { required = false } = {}) =>
const normalizeLineagePersonDate = (value, label) => {
const normalized = normalizeLineagePersonText(value, label)
if (!normalized) return ''
if (!/^\d{4}-\d{2}-\d{2}(?:T.*)?$/.test(normalized)) {
if (!/^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/.test(normalized)) {
throw lineagePersonError(`成员详情${label}无效`)
}
const datePart = normalized.slice(0, 10)
@@ -1193,8 +1236,17 @@ const normalizeLineagePersonDetail = (
throw lineagePersonError('成员详情世代无效')
}
const name = normalizeLineagePersonText(value.name, '姓名', { required: true })
const appUserId = normalizeOptionalLineagePersonIdentity(value.appUserId, '绑定用户标识')
const bindingMode = value.bindingMode === undefined || value.bindingMode === null || value.bindingMode === ''
? (appUserId ? 'SPECIFIED' : 'NONE')
: normalizeLineagePersonText(value.bindingMode, '身份认领方式')
if (!['NONE', 'SELF', 'SPECIFIED'].includes(bindingMode)) {
throw lineagePersonError('成员详情身份认领方式无效')
}
const personNo = normalizeLineagePersonText(value.personNo, '人物编号')
const generationName = normalizeLineagePersonText(value.generationName, '字辈')
const aliasName = normalizeLineagePersonText(value.aliasName, '别名或曾用名')
const avatarOssId = normalizeOptionalLineagePersonIdentity(value.avatarOssId, '头像文件标识')
const birthDate = normalizeLineagePersonDate(value.birthDate, '出生日期')
const deathDate = normalizeLineagePersonDate(value.deathDate, '逝世日期')
const birthLunar = normalizeLineagePersonText(value.birthLunar, '出生农历')
@@ -1206,6 +1258,10 @@ const normalizeLineagePersonDetail = (
const personStatus = normalizeLineagePersonText(value.personStatus, '人物状态')
const biography = normalizeLineagePersonText(value.biography, '生平')
const remark = normalizeLineagePersonText(value.remark, '备注')
const relationName = normalizeLineagePersonText(value.relationName, '关系显示名称')
const sortOrder = value.sortOrder === undefined || value.sortOrder === null
? null
: normalizeOptionalSafeInteger(value.sortOrder, '人物排序值')
const relatives = []
for (const relation of [
{ id: value.fatherId, name: value.fatherName, label: '父亲' },
@@ -1227,9 +1283,13 @@ const normalizeLineagePersonDetail = (
genealogyId,
genealogyName: normalizeLineagePersonText(value.genealogyName, '家谱名称') || '当前家谱',
name,
appUserId,
bindingMode,
personNo,
aliasName,
generation: value.generation,
generationName,
avatarOssId,
relation: value.generation === 1 ? '始祖' : '家谱成员',
branch: generationName
? (generationName.endsWith('字辈') ? generationName : `${generationName}字辈`)
@@ -1247,6 +1307,8 @@ const normalizeLineagePersonDetail = (
personStatus,
biography,
remark,
relationName,
sortOrder,
status: ['DECEASED', 'DEAD'].includes(personStatus.toUpperCase()) ? 'deceased' : 'normal',
relatives
}
@@ -1376,8 +1438,8 @@ const normalizeFamilyFeedRows = (value, expectedGenealogyId) => {
if (genealogyId && genealogyId !== expectedGenealogyId) {
throw createRequestError('家族动态归属与请求不匹配', 'FEED_RESPONSE_INVALID')
}
const id = normalizeOptionalNumericId(item.feedId ?? item.id, '动态标识', 'FEED_RESPONSE_INVALID')
const content = normalizeOptionalGenealogyText(item.feedContent ?? item.content, 'feedContent')
const id = normalizeOptionalNumericId(item.feedId, '动态标识', 'FEED_RESPONSE_INVALID')
const content = normalizeOptionalGenealogyText(item.feedContent, 'feedContent')
if (!id || !content) {
throw createRequestError('家族动态响应缺少标识或内容', 'FEED_RESPONSE_INVALID')
}
@@ -1385,14 +1447,14 @@ const normalizeFamilyFeedRows = (value, expectedGenealogyId) => {
id,
genealogyId: expectedGenealogyId,
content,
publisher: normalizeOptionalGenealogyText(item.publisherNickName ?? item.author, 'publisherNickName') || '家族成员',
type: normalizeOptionalGenealogyText(item.feedType ?? item.tag, 'feedType') || '文字动态',
time: normalizeOptionalGenealogyText(item.createTime ?? item.time, 'createTime'),
publisher: normalizeOptionalGenealogyText(item.publisherNickName, 'publisherNickName') || '家族成员',
type: normalizeOptionalGenealogyText(item.feedType, 'feedType') || '文字动态',
time: normalizeOptionalGenealogyText(item.createTime, 'createTime'),
mediaOssIds: normalizeOptionalGenealogyText(item.mediaOssIds, 'mediaOssIds'),
likeCount: normalizeOptionalNonnegativeInteger(item.likeCount, '动态点赞数', 'FEED_RESPONSE_INVALID'),
commentCount: normalizeOptionalNonnegativeInteger(item.commentCount, '动态评论数', 'FEED_RESPONSE_INVALID'),
likedByMe: typeof item.likedByMe === 'boolean' ? item.likedByMe : null,
pinned: item.pinned === true
pinned: normalizeOptionalGenealogyText(item.pinned, 'pinned')
}
})
if (new Set(rows.map((item) => item.id)).size !== rows.length) {
@@ -1475,12 +1537,15 @@ const normalizeArticleCreatePayload = (payload) => {
const value = normalizeOptionalAuthText(payload[field], field)
if (value) data[field] = value
}
for (const field of ['categoryId', 'coverOssId', 'sortOrder']) {
for (const field of ['categoryId', 'sortOrder']) {
if (payload[field] === undefined || payload[field] === null || payload[field] === '') continue
const value = typeof payload[field] === 'number' ? payload[field] : Number(payload[field])
if (!Number.isSafeInteger(value)) throw new TypeError(`${field} 必须是安全整数`)
data[field] = value
}
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
data.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId')
}
return data
}
@@ -1495,7 +1560,10 @@ const normalizeAlbumCreatePayload = (payload) => {
const value = normalizeOptionalAuthText(payload[field], field)
if (value) data[field] = value
}
for (const field of ['coverOssId', 'sortOrder']) {
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
data.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId')
}
for (const field of ['sortOrder']) {
if (payload[field] === undefined || payload[field] === null || payload[field] === '') continue
const value = typeof payload[field] === 'number' ? payload[field] : Number(payload[field])
if (!Number.isSafeInteger(value)) throw new TypeError(`${field} 必须是安全整数`)
@@ -1507,8 +1575,7 @@ const normalizeAlbumCreatePayload = (payload) => {
const normalizeAlbumPhotoCreatePayload = (payload) => {
const allowedFields = new Set(['ossId', 'photoTitle', 'photoDesc', 'photographer', 'shootTime', 'sortOrder', 'status'])
assertPlainPayload(payload, allowedFields, '相册照片请求')
const ossId = normalizeOptionalSafeInteger(payload.ossId, 'ossId')
if (!ossId || ossId < 1) throw new TypeError('相册照片需要真实上传回执中的 ossId')
const ossId = normalizeOssIdString(payload.ossId, '相册照片 ossId')
const data = { ossId }
for (const field of ['photoTitle', 'photoDesc', 'photographer', 'shootTime', 'status']) {
const value = normalizeOptionalAuthText(payload[field], field)
@@ -1530,9 +1597,12 @@ const normalizeCeremonyPayload = (payload) => {
const value = normalizeOptionalAuthText(payload[field], field)
if (value) data[field] = value
}
for (const field of ['coverOssId', 'sortOrder']) {
const value = normalizeOptionalSafeInteger(payload[field], field)
if (value !== undefined) data[field] = value
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
data.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId')
}
const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder')
if (sortOrder !== undefined) {
data.sortOrder = sortOrder
}
return data
}
@@ -1657,6 +1727,44 @@ const normalizeResourcePathId = (value, label) => {
throw new TypeError(`${label}必须是有效的正整数标识`)
}
const normalizeNotificationText = (value, field) => {
if (value === undefined || value === null) return ''
if (typeof value !== 'string') {
throw createRequestError(`通知详情字段 ${field} 无效`, 'NOTIFICATION_DETAIL_RESPONSE_INVALID')
}
return value
}
const normalizeNotificationDetail = (value, expectedNotificationId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('通知详情响应格式无效', 'NOTIFICATION_DETAIL_RESPONSE_INVALID')
}
const notificationId = normalizeOptionalNumericId(value.notificationId, '通知标识', 'NOTIFICATION_DETAIL_RESPONSE_INVALID')
if (!notificationId || notificationId !== expectedNotificationId) {
throw createRequestError('通知详情响应标识不匹配', 'NOTIFICATION_DETAIL_RESPONSE_INVALID')
}
return {
notificationId,
genealogyId: normalizeOptionalNumericId(value.genealogyId, '通知家谱标识', 'NOTIFICATION_DETAIL_RESPONSE_INVALID'),
genealogyNo: normalizeNotificationText(value.genealogyNo, 'genealogyNo'),
genealogyName: normalizeNotificationText(value.genealogyName, 'genealogyName'),
senderUserId: normalizeOptionalNumericId(value.senderUserId, '通知发送人标识', 'NOTIFICATION_DETAIL_RESPONSE_INVALID'),
senderNickName: normalizeNotificationText(value.senderNickName, 'senderNickName'),
senderPhone: normalizeNotificationText(value.senderPhone, 'senderPhone'),
noticeType: normalizeNotificationText(value.noticeType, 'noticeType'),
noticeTitle: normalizeNotificationText(value.noticeTitle, 'noticeTitle'),
noticeContent: normalizeNotificationText(value.noticeContent, 'noticeContent'),
bizType: normalizeNotificationText(value.bizType, 'bizType'),
bizId: normalizeOptionalNumericId(value.bizId, '通知关联业务标识', 'NOTIFICATION_DETAIL_RESPONSE_INVALID'),
bizSummary: normalizeNotificationText(value.bizSummary, 'bizSummary'),
publishTime: normalizeNotificationText(value.publishTime, 'publishTime'),
readStatus: normalizeNotificationText(value.readStatus, 'readStatus'),
readTime: normalizeNotificationText(value.readTime, 'readTime'),
status: normalizeNotificationText(value.status, 'status'),
remark: normalizeNotificationText(value.remark, 'remark')
}
}
const requireRemoteResource = (label, operation) => {
if (hasRemoteConfig()) return
throw createRequestError(`${label}${operation}需要真实服务,当前本地预览不会伪造结果`, operation === '读取' ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED')
@@ -1947,6 +2055,7 @@ const normalizeLineageWritePayload = (payload) => {
throw new TypeError('人物写入请求必须是普通对象')
}
const allowedFields = new Set([
'bindingMode',
'appUserId',
'personNo',
'name',
@@ -1975,16 +2084,29 @@ const normalizeLineageWritePayload = (payload) => {
}
const name = normalizeLineagePersonText(payload.name, '姓名', { required: true })
if (name.length > 20) throw new TypeError('人物姓名长度超出当前页面合同')
const data = { name }
const bindingMode = normalizeLineagePersonText(payload.bindingMode, '身份认领方式')
if (!['NONE', 'SELF', 'SPECIFIED'].includes(bindingMode)) {
throw new TypeError('人物身份认领方式必须是 NONE、SELF 或 SPECIFIED')
}
const data = { bindingMode, name }
if (bindingMode === 'SPECIFIED') {
if (payload.appUserId === undefined || payload.appUserId === null || payload.appUserId === '') {
throw new TypeError('指定业务用户绑定必须提供业务用户标识')
}
data.appUserId = normalizeLineagePersonIdentity(payload.appUserId, '绑定用户标识')
} else if (payload.appUserId !== undefined && payload.appUserId !== null && payload.appUserId !== '') {
throw new TypeError(`${bindingMode} 身份认领不能提供业务用户标识`)
}
for (const [field, label] of [
['appUserId', '绑定用户标识'],
['fatherId', '父亲标识'],
['motherId', '母亲标识'],
['avatarOssId', '头像文件标识']
['motherId', '母亲标识']
]) {
if (payload[field] === undefined || payload[field] === null || payload[field] === '') continue
data[field] = normalizeLineagePersonIdentity(payload[field], label)
}
if (payload.avatarOssId !== undefined && payload.avatarOssId !== null && payload.avatarOssId !== '') {
data.avatarOssId = normalizeOssIdString(payload.avatarOssId, '头像文件标识')
}
if (payload.generation !== undefined) {
if (!Number.isSafeInteger(payload.generation) || payload.generation < 1) {
throw new TypeError('人物世代必须是正安全整数')
@@ -2708,13 +2830,14 @@ export const appApi = {
if (!hasRemoteConfig()) {
throw createRequestError('修改动态需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
}
return requestStrict({
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}`,
method: 'PUT',
data: normalizeFeedCreatePayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeFamilyFeed(result, normalizedGenealogyId, normalizedFeedId)
},
async deleteFeed(genealogyId, feedId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
@@ -2750,13 +2873,15 @@ export const appApi = {
async createFeed(genealogyId, payload, requestOptions = {}) {
if (hasRemoteConfig()) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
const result = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds`,
method: 'POST',
data: normalizeFeedCreatePayload(payload)
}, {
requestController: requestOptions.requestController ?? null
})
const [feed] = normalizeFamilyFeedRows([result], normalizedGenealogyId)
return feed
}
const error = new Error('动态发布接口尚未接入,当前内容不会保存')
error.code = 'WRITE_UNAVAILABLE'
@@ -3313,6 +3438,19 @@ export const appApi = {
if (!hasRemoteConfig()) return listNotificationFixtures()
return readRemoteList('/genealogy/app/notifications', '消息通知', requestOptions)
},
async getNotificationDetail(notificationId, requestOptions = {}) {
const normalizedNotificationId = normalizeResourcePathId(notificationId, '通知标识')
if (!hasRemoteConfig()) {
throw createRequestError('通知详情需要真实服务读取,当前本地预览不伪造通知正文', 'REMOTE_READ_REQUIRED')
}
const result = await requestStrict({
url: `/genealogy/app/notifications/${normalizedNotificationId}`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeNotificationDetail(result, normalizedNotificationId)
},
async getUnreadNotificationCount(requestOptions = {}) {
requireRemoteResource('未读通知数量', '读取')
const value = await requestStrict({