feat: 完成前端业务闭环与后端联调

This commit is contained in:
2026-08-24 23:37:56 +08:00
parent c59e36f933
commit 9ad572907b
175 changed files with 10711 additions and 1740 deletions
+20 -10
View File
@@ -1,15 +1,6 @@
import { resolveRuntimeMode } from '@/utils/runtime-config.js'
import { AUTH_VERIFICATION_OPERATION } from '@/utils/auth/verification.js'
import { session } from '@/utils/session.js'
export const requireRemoteAuth = () => {
if (resolveRuntimeMode() !== 'remote') {
const error = new Error('当前为本地预览模式,真实认证服务未启用')
error.code = 'AUTH_REMOTE_REQUIRED'
throw error
}
}
export const assertAuthVerificationOperation = (operationCode) => {
if (!Object.values(AUTH_VERIFICATION_OPERATION).includes(operationCode)) {
throw new TypeError('认证动作不属于当前认证合同')
@@ -34,9 +25,28 @@ export const assertPasswordHash = (passwordHash) => {
return passwordHash
}
export const normalizeOptionalReferralCode = (referralCode) => {
if (referralCode === undefined || referralCode === null || referralCode === '') return ''
if (typeof referralCode !== 'string') throw new TypeError('推荐码必须是字符串')
const normalized = referralCode.trim()
if (!/^[A-Za-z0-9_-]{4,64}$/.test(normalized)) {
throw new TypeError('推荐码格式无效')
}
return normalized
}
export const assertWechatAuthorizationCode = (authorizationCode) => {
if (typeof authorizationCode !== 'string' || !authorizationCode.trim()) {
throw new TypeError('微信授权未返回有效临时票据')
}
return authorizationCode.trim()
}
export const saveLogin = (loginResult) => {
const token = loginResult?.access_token
if (!token) throw new Error('登录响应未包含会话令牌')
if (typeof token !== 'string' || !token || token.trim() !== token) {
throw new Error('登录响应未包含有效的会话令牌')
}
session.saveToken(token)
return loginResult
}
+38 -14
View File
@@ -1,15 +1,17 @@
import { assertSmsCode } from '@/utils/auth/verification.js'
import { resolveRuntimeMode, runtimeConfig } from '@/utils/runtime-config.js'
import { runtimeConfig } from '@/utils/runtime-config.js'
import {
assertAuthVerificationOperation,
assertPasswordHash,
assertWechatAuthorizationCode,
normalizeOptionalReferralCode,
normalizeOptionalValidToken,
requireRemoteAuth,
saveLogin
} from './auth-contract.js'
import { normalizeOptionalText } from './request-normalizers.js'
import { normalizePhoneChangePayload } from './profile-contract.js'
import {
createRequestError,
requestAuth,
requestAuthVoid,
requestStrict
@@ -17,7 +19,6 @@ import {
export const authApi = {
async getCaptchaRequirement({ operationCode, subject }, requestOptions = {}) {
requireRemoteAuth()
return requestAuth({
url: `/genealogy/app/auth/verification/${encodeURIComponent(assertAuthVerificationOperation(operationCode))}/require`,
method: 'GET',
@@ -26,7 +27,6 @@ export const authApi = {
},
async sendSmsCode({ operationCode, phone, validToken }, requestOptions = {}) {
requireRemoteAuth()
const normalizedValidToken = normalizeOptionalValidToken(validToken)
return requestAuthVoid({
url: `/genealogy/app/auth/sms/${encodeURIComponent(assertAuthVerificationOperation(operationCode))}/code`,
@@ -41,7 +41,6 @@ export const authApi = {
},
async loginWithPassword({ phone, passwordHash, validToken }, requestOptions = {}) {
requireRemoteAuth()
const normalizedValidToken = normalizeOptionalValidToken(validToken)
const loginSession = await requestAuth({
url: '/genealogy/app/auth/login',
@@ -58,7 +57,6 @@ export const authApi = {
},
async loginWithSms({ phone, smsCode }, requestOptions = {}) {
requireRemoteAuth()
const loginSession = await requestAuth({
url: '/genealogy/app/auth/login/sms',
method: 'POST',
@@ -72,9 +70,39 @@ export const authApi = {
return saveLogin(loginSession)
},
async registerWithPassword({ phone, passwordHash, smsCode, nickName }, requestOptions = {}) {
requireRemoteAuth()
async loginWithWechat({ code }, requestOptions = {}) {
const loginResult = await requestAuth({
url: '/genealogy/app/auth/login/wechat',
method: 'POST',
data: { code: assertWechatAuthorizationCode(code) }
}, requestOptions)
if (loginResult?.status === 'ACCOUNT_BINDING_REQUIRED') {
throw createRequestError(
'该微信尚未绑定账号,请先用手机号登录,再到“账号与安全”绑定微信',
'ACCOUNT_BINDING_REQUIRED'
)
}
if (loginResult?.status !== 'AUTHENTICATED' || !loginResult.login) {
throw createRequestError('微信登录响应无效', 'AUTH_RESPONSE_INVALID')
}
return saveLogin(loginResult.login)
},
async bindWechat({ code }, requestOptions = {}) {
await requestStrict({
url: '/genealogy/app/auth/wechat/bind',
method: 'POST',
data: { code: assertWechatAuthorizationCode(code) }
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
},
async registerWithPassword({ phone, passwordHash, smsCode, nickName, referralCode }, requestOptions = {}) {
const normalizedNickName = normalizeOptionalText(nickName, '昵称')
const normalizedReferralCode = normalizeOptionalReferralCode(referralCode)
const loginSession = await requestAuth({
url: '/genealogy/app/auth/register',
method: 'POST',
@@ -84,14 +112,14 @@ export const authApi = {
phone,
password: assertPasswordHash(passwordHash),
smsCode: assertSmsCode(smsCode),
...(normalizedNickName ? { nickName: normalizedNickName } : {})
...(normalizedNickName ? { nickName: normalizedNickName } : {}),
...(normalizedReferralCode ? { referralCode: normalizedReferralCode } : {})
}
}, requestOptions)
return saveLogin(loginSession)
},
async resetPassword({ phone, passwordHash, smsCode }, requestOptions = {}) {
requireRemoteAuth()
return requestAuthVoid({
url: '/genealogy/app/auth/password/reset',
method: 'PUT',
@@ -106,7 +134,6 @@ export const authApi = {
},
async changePassword({ oldPasswordHash, newPasswordHash }, requestOptions = {}) {
requireRemoteAuth()
await requestStrict({
url: '/genealogy/app/auth/password',
method: 'PUT',
@@ -122,7 +149,6 @@ export const authApi = {
},
async deactivateAccount({ smsCode }, requestOptions = {}) {
requireRemoteAuth()
await requestStrict({
url: '/genealogy/app/auth/account/deactivate',
method: 'POST',
@@ -135,7 +161,6 @@ export const authApi = {
},
async logout(requestOptions = {}) {
if (resolveRuntimeMode() !== 'remote') return null
await requestStrict({
url: '/genealogy/app/auth/logout',
method: 'DELETE'
@@ -147,7 +172,6 @@ export const authApi = {
},
async changePhone(payload, requestOptions = {}) {
requireRemoteAuth()
await requestStrict({
url: '/genealogy/app/auth/phone',
method: 'PUT',
+30 -2
View File
@@ -6,10 +6,34 @@ import {
} from './response-normalizers.js'
const supportedDictionaryTypes = new Set([
'gen_parent_relationship_variant',
'gen_education_type',
'gen_death_expression',
'gen_spouse_relationship_variant',
'gen_ceremony_type',
'gen_growth_record_type'
'gen_person_document_type',
'gen_growth_record_type',
'gen_merit_type',
'gen_feedback_type',
'gen_zodiac'
])
const optionStates = new Set(['ACTIVE', 'DISABLED', 'UNKNOWN'])
export const normalizeBusinessOptionProjection = (value, label, state, subject, errorCode) => {
const optionValue = normalizeResponseText(value, `${subject}编码`)
const optionLabel = normalizeResponseText(label, `${subject}名称`)
const optionState = normalizeResponseText(state, `${subject}状态`)
if (optionValue && !optionStates.has(optionState)) {
throw createRequestError(`${subject}历史值状态无效`, errorCode)
}
return {
value: optionValue,
label: optionLabel || optionValue,
state: optionValue ? optionState : ''
}
}
export const normalizeBusinessDictionaryType = (dictType) => {
if (!supportedDictionaryTypes.has(dictType)) {
throw new TypeError('当前页面不支持该业务字典')
@@ -27,6 +51,9 @@ export const normalizeBusinessDictionaryOptions = (dictType, value) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('业务字典响应包含无效条目', 'BUSINESS_DICTIONARY_RESPONSE_INVALID')
}
if (item.enabled !== true) {
throw createRequestError('业务字典选择接口返回了不可选项', 'BUSINESS_DICTIONARY_RESPONSE_INVALID')
}
const optionValue = normalizeResponseText(item.value, 'value')
const label = normalizeResponseText(item.label, 'label')
if (!optionValue || !label || values.has(optionValue)) {
@@ -38,7 +65,8 @@ export const normalizeBusinessDictionaryOptions = (dictType, value) => {
value: optionValue,
label,
sort: normalizeOptionalNonnegativeInteger(item.sort, '业务字典排序值', 'BUSINESS_DICTIONARY_RESPONSE_INVALID'),
default: item.default === true
default: item.default === true,
enabled: true
}
})
}
+1 -8
View File
@@ -1,19 +1,12 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeBusinessDictionaryOptions,
normalizeBusinessDictionaryType
} from './business-dictionary-contract.js'
import { createRequestError, requestStrict } from './request-client.js'
import { requestStrict } from './request-client.js'
export const businessDictionaryApi = {
async getBusinessDictionaryOptions(dictType, requestOptions = {}) {
const normalizedType = normalizeBusinessDictionaryType(dictType)
if (!hasRemoteConfig()) {
throw createRequestError(
'业务字典读取需要真实服务,当前本地预览不会伪造结果',
'REMOTE_READ_REQUIRED'
)
}
const dictionaryOptions = await requestStrict({
url: `/genealogy/app/dictionaries/${encodeURIComponent(normalizedType)}`,
method: 'GET'
+3
View File
@@ -45,6 +45,9 @@ export const normalizeBusinessFileAccess = (value, label, code, { required = fal
}
const accessUrl = normalizeText('accessUrl')
if (required && !accessUrl) throw createRequestError(`${label}缺少授权访问地址`, code)
if (accessUrl && !/^https:\/\/[^\s]+$/.test(accessUrl)) {
throw createRequestError(`${label}授权访问地址必须使用 HTTPS`, code)
}
return {
fileId,
ossId,
+18 -43
View File
@@ -8,6 +8,7 @@ import {
} from './response-normalizers.js'
import {
assertPlainPayload,
normalizeOptionalCurrencyNumber,
normalizeOptionalNormalDisableStatus,
normalizeOptionalSafeInteger,
normalizeOptionalText,
@@ -19,6 +20,7 @@ import {
normalizeResourceCapabilities
} from './business-file-contract.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import { normalizeBusinessOptionProjection } from './business-dictionary-contract.js'
const normalizeAppCeremonyCoordinate = (value, field) => {
if (value === undefined || value === null || value === '') return null
@@ -29,18 +31,6 @@ const normalizeAppCeremonyCoordinate = (value, field) => {
return coordinate
}
const CEREMONY_TYPE_LABELS = Object.freeze({
ancestor: '祭祖',
memorial: '纪念',
funeral: '丧礼',
wedding: '婚礼',
birth: '出生宴',
birthday: '生日宴',
banquet: '宴席',
other: '其他'
})
const ceremonyTypes = new Set(Object.keys(CEREMONY_TYPE_LABELS))
export const CEREMONY_INVITATION_STATUS = Object.freeze({
PENDING: 'PENDING',
ACCEPTED: 'ACCEPTED',
@@ -79,12 +69,22 @@ export const normalizeAppCeremony = (value, expectedGenealogyId, expectedCeremon
if ((longitude === null) !== (latitude === null)) {
throw createRequestError('礼仪活动响应的经纬度必须同时存在或同时为空', 'CEREMONY_RESPONSE_INVALID')
}
const ceremonyType = normalizeBusinessOptionProjection(
value.ceremonyType,
value.ceremonyTypeLabel,
value.ceremonyTypeOptionState,
'礼仪活动类型',
'CEREMONY_RESPONSE_INVALID'
)
return {
id,
genealogyId,
title: normalizeResponseText(value.ceremonyTitle, 'ceremonyTitle') || '未命名活动',
type: normalizeResponseText(value.ceremonyType, 'ceremonyType'),
type: ceremonyType.value,
typeLabel: ceremonyType.label,
typeOptionState: ceremonyType.state,
time: normalizeResponseText(value.ceremonyTime, 'ceremonyTime'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
location,
locationAddress,
longitude,
@@ -98,21 +98,13 @@ export const normalizeAppCeremony = (value, expectedGenealogyId, expectedCeremon
}
}
export const normalizeAppCeremonyWithTypeLabel = (ceremony) => {
const typeLabel = CEREMONY_TYPE_LABELS[ceremony.type]
if (!ceremony.type || !typeLabel) {
throw createRequestError('礼仪活动响应包含未公开的活动类型', 'CEREMONY_RESPONSE_INVALID')
}
return { ...ceremony, typeLabel }
}
export const normalizeAppCeremonies = (value, expectedGenealogyId) => {
if (!Array.isArray(value)) throw createRequestError('礼仪活动响应不是列表', 'CEREMONY_RESPONSE_INVALID')
const ceremonies = value.map((item) => normalizeAppCeremony(item, expectedGenealogyId))
if (new Set(ceremonies.map((item) => item.id)).size !== ceremonies.length) {
throw createRequestError('礼仪活动响应包含重复标识', 'CEREMONY_RESPONSE_INVALID')
}
return ceremonies.map((item) => normalizeAppCeremonyWithTypeLabel(item))
return ceremonies
}
export const normalizeAppCeremonyGift = (value, expectedGenealogyId, expectedCeremonyId) => {
@@ -147,29 +139,12 @@ export const normalizeAppCeremonyGifts = (value, expectedGenealogyId, expectedCe
return gifts
}
export const projectPreviewCeremonies = (value, expectedGenealogyId) =>
value.map((item) => ({
id: String(item.ceremonyId),
genealogyId: expectedGenealogyId,
title: String(item.ceremonyTitle || '未命名活动'),
type: String(item.ceremonyType || ''),
typeLabel: CEREMONY_TYPE_LABELS[String(item.ceremonyType || '')] || '',
time: String(item.ceremonyTime || ''),
location: String(item.location || ''),
description: String(item.ceremonyDesc || ''),
coverFile: null,
giftCount: Number.isSafeInteger(item.giftCount) ? item.giftCount : 0,
canEdit: false,
canDelete: false
}))
export const normalizeCeremonyPayload = (payload) => {
const allowedFields = new Set(['ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'ceremonyTime', 'location', 'locationAddress', 'longitude', 'latitude', 'coverOssId', 'sortOrder', 'status'])
assertPlainPayload(payload, allowedFields, '礼仪活动请求')
const ceremonyType = normalizeOptionalText(payload.ceremonyType, 'ceremonyType')
const ceremonyTitle = normalizeOptionalText(payload.ceremonyTitle, 'ceremonyTitle')
if (!ceremonyType || !ceremonyTitle) throw new TypeError('请填写活动类型和活动标题')
if (!ceremonyTypes.has(ceremonyType)) throw new TypeError('请选择有效的活动类型')
const normalizedPayload = { ceremonyType, ceremonyTitle }
for (const field of ['ceremonyDesc', 'ceremonyTime', 'location', 'locationAddress']) {
const normalizedText = normalizeOptionalText(payload[field], field)
@@ -189,8 +164,9 @@ export const normalizeCeremonyPayload = (payload) => {
normalizedPayload.longitude = longitude
normalizedPayload.latitude = latitude
}
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId')
if (Object.prototype.hasOwnProperty.call(payload, 'coverOssId')) {
if (payload.coverOssId === null) normalizedPayload.coverOssId = null
else if (payload.coverOssId !== '') normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId')
}
const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder')
if (sortOrder !== undefined) {
@@ -205,8 +181,7 @@ export const normalizeCeremonyGiftPayload = (payload) => {
if (payload.giftAmount === undefined || payload.giftAmount === null || payload.giftAmount === '') {
throw new TypeError('请填写献礼金额')
}
const giftAmount = typeof payload.giftAmount === 'number' ? payload.giftAmount : Number(payload.giftAmount)
if (!Number.isFinite(giftAmount)) throw new TypeError('献礼金额必须是有限数字')
const giftAmount = normalizeOptionalCurrencyNumber(payload.giftAmount, '献礼金额')
const normalizedPayload = { giftAmount }
for (const field of ['giverName', 'giftMessage']) {
const normalizedText = normalizeOptionalText(payload[field], field)
+3 -29
View File
@@ -1,32 +1,22 @@
import { listPreviewCeremonies } from '@/data/preview/records.js'
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeAppCeremonies,
normalizeAppCeremony,
normalizeAppCeremonyGift,
normalizeAppCeremonyGifts,
normalizeAppCeremonyWithTypeLabel,
normalizeCeremonyGiftPayload,
normalizeCeremonyInvitationResponsePayload,
normalizeCeremonyInvitationRows,
normalizeCeremonyInviteeOptions,
normalizeCeremonyInviteePayload,
normalizeCeremonyPayload,
projectPreviewCeremonies
normalizeCeremonyPayload
} from './ceremony-contract.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
import { createRequestError, requestStrict } from './request-client.js'
const requireRemoteCeremony = (message, code) => {
if (hasRemoteConfig()) return
throw createRequestError(message, code)
}
import { requestStrict } from './request-client.js'
export const ceremonyApi = {
async createCeremony(genealogyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteCeremony('创建礼仪活动需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies`,
method: 'POST',
@@ -37,7 +27,6 @@ export const ceremonyApi = {
async updateCeremony(genealogyId, ceremonyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
requireRemoteCeremony('修改礼仪活动需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}`,
method: 'PUT',
@@ -48,7 +37,6 @@ export const ceremonyApi = {
async deleteCeremony(genealogyId, ceremonyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
requireRemoteCeremony('删除礼仪活动需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED')
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}`,
method: 'DELETE'
@@ -59,7 +47,6 @@ export const ceremonyApi = {
async createCeremonyGift(genealogyId, ceremonyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
requireRemoteCeremony('新增礼仪献礼需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
const gift = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/gifts`,
method: 'POST',
@@ -72,7 +59,6 @@ export const ceremonyApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
const normalizedGiftId = normalizeResourcePathId(giftId, '献礼标识')
requireRemoteCeremony('删除礼仪献礼需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED')
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/gifts/${normalizedGiftId}`,
method: 'DELETE'
@@ -82,9 +68,6 @@ export const ceremonyApi = {
async getCeremonies(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
return projectPreviewCeremonies(listPreviewCeremonies(normalizedGenealogyId), normalizedGenealogyId)
}
const ceremonies = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies`,
method: 'GET'
@@ -96,7 +79,6 @@ export const ceremonyApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
const invitees = normalizeCeremonyInviteePayload(payload)
requireRemoteCeremony('活动受邀人写入需要真实服务,当前本地预览不会伪造结果', 'REMOTE_WRITE_REQUIRED')
const invitations = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitees`,
method: 'PUT',
@@ -108,7 +90,6 @@ export const ceremonyApi = {
async getCeremonyInviteeOptions(genealogyId, ceremonyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
requireRemoteCeremony('活动受邀候选读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
const options = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitee-options`,
method: 'GET'
@@ -119,7 +100,6 @@ export const ceremonyApi = {
async getCeremonyInvitations(genealogyId, ceremonyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
requireRemoteCeremony('活动邀请名单读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
const invitations = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitations`,
method: 'GET'
@@ -131,7 +111,6 @@ export const ceremonyApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
const response = normalizeCeremonyInvitationResponsePayload(payload)
requireRemoteCeremony('活动邀请响应写入需要真实服务,当前本地预览不会伪造结果', 'REMOTE_WRITE_REQUIRED')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/invitations/me`,
method: 'PUT',
@@ -140,7 +119,6 @@ export const ceremonyApi = {
},
async getMyCeremonyInvitations(requestOptions = {}) {
requireRemoteCeremony('我的活动邀请读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
const invitations = await requestStrict({
url: '/genealogy/app/genealogies/ceremony-invitations/mine',
method: 'GET'
@@ -151,20 +129,16 @@ export const ceremonyApi = {
async getCeremonyDetail(genealogyId, ceremonyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
requireRemoteCeremony('礼仪详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
const ceremony = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}`,
method: 'GET'
}, { requestController: requestOptions.requestController ?? null })
return normalizeAppCeremonyWithTypeLabel(
normalizeAppCeremony(ceremony, normalizedGenealogyId, normalizedCeremonyId)
)
return normalizeAppCeremony(ceremony, normalizedGenealogyId, normalizedCeremonyId)
},
async getCeremonyGifts(genealogyId, ceremonyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedCeremonyId = normalizeResourcePathId(ceremonyId, '礼仪活动标识')
requireRemoteCeremony('礼仪献礼需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED')
const gifts = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/ceremonies/${normalizedCeremonyId}/gifts`,
method: 'GET'
@@ -0,0 +1,57 @@
import { createRequestError } from './request-client.js'
import { normalizeContentPasswordPayload } from './protected-content-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
export const CONTENT_PASSWORD_RESOURCE_TYPE = Object.freeze({
ARTICLE: 'ARTICLE',
GROWTH_RECORD: 'GROWTH_RECORD',
PERSON_DOCUMENT: 'PERSON_DOCUMENT'
})
const resourceTypes = new Set(Object.values(CONTENT_PASSWORD_RESOURCE_TYPE))
export const normalizeContentPasswordResource = (resourceType, resourceId) => {
if (!resourceTypes.has(resourceType)) throw new TypeError('内容密码找回资源类型无效')
return {
resourceType,
resourceId: normalizeResourcePathId(resourceId, '内容密码资源标识')
}
}
const recoveryError = (message) =>
createRequestError(message, 'CONTENT_PASSWORD_RECOVERY_RESPONSE_INVALID')
const normalizeRecoveryText = (value, field, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw recoveryError(`内容密码找回响应缺少${field}`)
return ''
}
if (typeof value !== 'string') throw recoveryError(`内容密码找回响应${field}无效`)
const normalized = value.trim()
if (required && !normalized) throw recoveryError(`内容密码找回响应缺少${field}`)
return normalized
}
export const normalizeContentPasswordRecoveryCapability = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.available !== 'boolean') {
throw recoveryError('内容密码找回能力响应无效')
}
const maskedPhone = normalizeRecoveryText(value.mobileMasked, 'mobileMasked')
if (value.available && !maskedPhone) throw recoveryError('已开放找回但缺少脱敏手机号')
return {
enabled: value.available,
maskedPhone,
disabledReason: normalizeRecoveryText(value.disabledReason, 'disabledReason'),
cooldownSeconds: 0
}
}
export const normalizeContentPasswordResetPayload = ({ smsCode, newPassword }) => {
if (typeof smsCode !== 'string' || !/^\d{4}$/.test(smsCode)) {
throw new TypeError('内容密码找回验证码必须是4位数字')
}
return {
smsCode,
newPassword: normalizeContentPasswordPayload(newPassword).password
}
}
@@ -0,0 +1,46 @@
import {
normalizeContentPasswordRecoveryCapability,
normalizeContentPasswordResetPayload,
normalizeContentPasswordResource
} from './content-password-recovery-contract.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import { requestStrict } from './request-client.js'
const recoveryPath = (genealogyId, resourceType, resourceId) => {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const resource = normalizeContentPasswordResource(resourceType, resourceId)
return `/genealogy/app/genealogies/${normalizedGenealogyId}/content-password-recovery/${resource.resourceType}/${resource.resourceId}`
}
export const contentPasswordRecoveryApi = {
async getCapability(genealogyId, resourceType, resourceId, requestOptions = {}) {
const capability = await requestStrict({
url: `${recoveryPath(genealogyId, resourceType, resourceId)}/capability`,
method: 'GET'
}, { requestController: requestOptions.requestController ?? null })
return normalizeContentPasswordRecoveryCapability(capability)
},
async sendCode(genealogyId, resourceType, resourceId, requestOptions = {}) {
await requestStrict({
url: `${recoveryPath(genealogyId, resourceType, resourceId)}/code`,
method: 'POST'
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return { cooldownSeconds: 60 }
},
async resetPassword(genealogyId, resourceType, resourceId, payload, requestOptions = {}) {
await requestStrict({
url: `${recoveryPath(genealogyId, resourceType, resourceId)}/reset`,
method: 'POST',
data: normalizeContentPasswordResetPayload(payload)
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
}
}
+1 -18
View File
@@ -7,23 +7,10 @@ import {
normalizeEarningWithdrawalPayload
} from './earning-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
createRequestError,
requestStrict
} from './request-client.js'
const requireRemoteEarning = (label, operation) => {
if (hasRemoteConfig()) return
throw createRequestError(
`${label}${operation}需要真实服务,当前本地预览不会伪造结果`,
operation === '读取' ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED'
)
}
import { requestStrict } from './request-client.js'
export const earningApi = {
async getEarningSummary(requestOptions = {}) {
requireRemoteEarning('收益汇总', '读取')
const earningSummary = await requestStrict({
url: '/genealogy/app/earnings/summary',
method: 'GET'
@@ -34,7 +21,6 @@ export const earningApi = {
},
async getEarningLedgerPage(query = {}, requestOptions = {}) {
requireRemoteEarning('收益明细', '读取')
const ledgerPage = await requestStrict({
url: '/genealogy/app/earnings/ledger',
method: 'GET',
@@ -47,7 +33,6 @@ export const earningApi = {
},
async getEarningWithdrawalPage(query = {}, requestOptions = {}) {
requireRemoteEarning('提现记录', '读取')
const withdrawalPage = await requestStrict({
url: '/genealogy/app/earnings/withdrawals',
method: 'GET',
@@ -60,7 +45,6 @@ export const earningApi = {
},
async requestEarningWithdrawal(payload, requestOptions = {}) {
requireRemoteEarning('提现申请', '写入')
const withdrawal = await requestStrict({
url: '/genealogy/app/earnings/withdrawals',
method: 'POST',
@@ -73,7 +57,6 @@ export const earningApi = {
async cancelEarningWithdrawal(withdrawalId, requestOptions = {}) {
const normalizedWithdrawalId = normalizeResourcePathId(withdrawalId, '提现记录标识')
requireRemoteEarning('取消提现', '写入')
const withdrawal = await requestStrict({
url: `/genealogy/app/earnings/withdrawals/${normalizedWithdrawalId}/cancel`,
method: 'POST'
+3 -38
View File
@@ -45,42 +45,6 @@ export const normalizeArticleCategoryOptions = (value) => {
return categories
}
export const normalizePreviewFamilyArticles = (value, expectedGenealogyId) => {
if (!Array.isArray(value)) {
throw createRequestError('谱文响应不是列表', 'ARTICLE_RESPONSE_INVALID')
}
const rows = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('谱文响应包含无效条目', 'ARTICLE_RESPONSE_INVALID')
}
const genealogyId = normalizeOptionalNumericId(item.genealogyId, '谱文家谱标识', 'ARTICLE_RESPONSE_INVALID')
if (genealogyId && genealogyId !== expectedGenealogyId) {
throw createRequestError('谱文归属与请求不匹配', 'ARTICLE_RESPONSE_INVALID')
}
const id = normalizeOptionalNumericId(item.id, '谱文标识', 'ARTICLE_RESPONSE_INVALID')
const title = normalizeResponseText(item.title, 'title')
if (!id || !title) {
throw createRequestError('谱文响应缺少标识或标题', 'ARTICLE_RESPONSE_INVALID')
}
return {
id,
genealogyId: expectedGenealogyId,
title,
summary: normalizeResponseText(item.summary, 'summary'),
content: '',
author: normalizeResponseText(item.author, 'author') || '家族成员',
time: normalizeResponseText(item.updatedAt, 'updatedAt'),
category: normalizeResponseText(item.category, 'category'),
coverOssId: normalizeOptionalNumericId(item.coverOssId, 'coverOssId', 'ARTICLE_RESPONSE_INVALID'),
viewCount: normalizeOptionalNonnegativeInteger(item.viewCount, '谱文阅读数', 'ARTICLE_RESPONSE_INVALID')
}
})
if (new Set(rows.map((item) => item.id)).size !== rows.length) {
throw createRequestError('谱文响应包含重复标识', 'ARTICLE_RESPONSE_INVALID')
}
return rows
}
export const normalizeAppArticle = (value, expectedGenealogyId, expectedArticleId = '') => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('谱文响应无效', 'ARTICLE_RESPONSE_INVALID')
@@ -148,8 +112,9 @@ export const normalizeArticleCreatePayload = (payload) => {
}
const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder')
if (sortOrder !== undefined) normalizedPayload.sortOrder = sortOrder
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId')
if (Object.prototype.hasOwnProperty.call(payload, 'coverOssId')) {
if (payload.coverOssId === null) normalizedPayload.coverOssId = null
else if (payload.coverOssId !== '') normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId')
}
return normalizedPayload
}
+2 -23
View File
@@ -1,11 +1,8 @@
import { listPreviewFamilyArticles } from '@/data/preview/family-content.js'
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeAppArticle,
normalizeAppArticles,
normalizeArticleCategoryOptions,
normalizeArticleCreatePayload,
normalizePreviewFamilyArticles
normalizeArticleCreatePayload
} from './family-article-contract.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
@@ -14,12 +11,7 @@ import {
normalizeContentAccessGrant,
normalizeContentPasswordPayload
} from './protected-content-contract.js'
import { createRequestError, requestStrict } from './request-client.js'
const requireRemoteArticle = (message, code) => {
if (hasRemoteConfig()) return
throw createRequestError(message, code)
}
import { requestStrict } from './request-client.js'
const requestArticleContentProtection = async ({
genealogyId,
@@ -28,7 +20,6 @@ const requestArticleContentProtection = async ({
data,
requestOptions
}) => {
requireRemoteArticle('谱文内容保护需要真实服务,当前本地预览不会伪造结果', 'REMOTE_WRITE_REQUIRED')
return requestStrict({
url: `/genealogy/app/genealogies/${genealogyId}/articles/${articleId}/content-protection`,
method,
@@ -41,7 +32,6 @@ const requestArticleContentProtection = async ({
export const familyArticleApi = {
async createArticle(genealogyId, payload, requestOptions = {}) {
requireRemoteArticle('谱文创建需要真实服务,当前本地预览不会伪造创建成功', 'REMOTE_WRITE_REQUIRED')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles`,
@@ -54,12 +44,6 @@ export const familyArticleApi = {
async getArticles(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
return normalizePreviewFamilyArticles(
listPreviewFamilyArticles(normalizedGenealogyId),
normalizedGenealogyId
)
}
const articles = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles`,
method: 'GET'
@@ -72,7 +56,6 @@ export const familyArticleApi = {
async getArticleDetail(genealogyId, articleId, accessToken = '', requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedArticleId = normalizeResourcePathId(articleId, '谱文标识')
requireRemoteArticle('谱文详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
const article = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles/${normalizedArticleId}`,
method: 'GET',
@@ -86,7 +69,6 @@ export const familyArticleApi = {
async updateArticle(genealogyId, articleId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedArticleId = normalizeResourcePathId(articleId, '谱文标识')
requireRemoteArticle('修改谱文需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
const article = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles/${normalizedArticleId}`,
method: 'PUT',
@@ -100,7 +82,6 @@ export const familyArticleApi = {
async deleteArticle(genealogyId, articleId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedArticleId = normalizeResourcePathId(articleId, '谱文标识')
requireRemoteArticle('删除谱文需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED')
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles/${normalizedArticleId}`,
method: 'DELETE'
@@ -129,7 +110,6 @@ export const familyArticleApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedArticleId = normalizeResourcePathId(articleId, '谱文标识')
const passwordPayload = normalizeContentPasswordPayload(password)
requireRemoteArticle('谱文内容解锁需要真实服务,当前本地预览不会伪造结果', 'REMOTE_WRITE_REQUIRED')
const accessGrant = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/articles/${normalizedArticleId}/content-unlock`,
method: 'POST',
@@ -154,7 +134,6 @@ export const familyArticleApi = {
async getArticleCategories(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteArticle('谱文分类读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
const categories = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/article-categories`,
method: 'GET'
-23
View File
@@ -1,5 +1,3 @@
import { listPreviewFamilyFeeds } from '@/data/preview/family-content.js'
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
FEED_COMMENT_LEVEL,
normalizeDirectFeedReplies,
@@ -15,11 +13,6 @@ import { normalizeGenealogyPathId } from './genealogy-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
import { createRequestError, requestStrict } from './request-client.js'
const requireRemoteFeed = (message, code) => {
if (hasRemoteConfig()) return
throw createRequestError(message, code)
}
const normalizePageQuery = (pageNum, pageSize, label) => {
if (!Number.isSafeInteger(pageNum) || pageNum < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1) {
throw new TypeError(`${label}分页参数无效`)
@@ -35,7 +28,6 @@ const assertPageResponse = (page, label) => {
export const familyFeedApi = {
async createFeedComment(genealogyId, feedId, payload, requestOptions = {}) {
requireRemoteFeed('动态评论需要真实服务,当前本地预览不会伪造提交成功', 'REMOTE_WRITE_REQUIRED')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedResourceId(feedId, '动态标识')
return requestStrict({
@@ -49,12 +41,6 @@ export const familyFeedApi = {
async getFeedRecommendations(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
return normalizeFamilyFeedRows(
listPreviewFamilyFeeds(normalizedGenealogyId),
normalizedGenealogyId
).map((feed) => ({ ...feed, recommendationReason: '家谱内最新动态' }))
}
const recommendations = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feed-recommendations`,
method: 'GET'
@@ -67,7 +53,6 @@ export const familyFeedApi = {
async getFeedDetail(genealogyId, feedId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedResourceId(feedId, '动态标识')
requireRemoteFeed('动态详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
const feed = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}`,
method: 'GET'
@@ -80,7 +65,6 @@ export const familyFeedApi = {
async updateFeed(genealogyId, feedId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedResourceId(feedId, '动态标识')
requireRemoteFeed('修改动态需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
const feed = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}`,
method: 'PUT',
@@ -94,7 +78,6 @@ export const familyFeedApi = {
async deleteFeed(genealogyId, feedId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedResourceId(feedId, '动态标识')
requireRemoteFeed('删除动态需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED')
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}`,
method: 'DELETE'
@@ -109,7 +92,6 @@ export const familyFeedApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeFeedResourceId(feedId, '动态标识')
if (typeof liked !== 'boolean') throw new TypeError('动态点赞状态必须是布尔值')
requireRemoteFeed('动态点赞需要真实服务,当前本地预览不会伪造操作成功', 'REMOTE_WRITE_REQUIRED')
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/likes`,
method: liked ? 'POST' : 'DELETE'
@@ -121,7 +103,6 @@ export const familyFeedApi = {
},
async createFeed(genealogyId, payload, requestOptions = {}) {
requireRemoteFeed('动态发布需要真实服务,当前本地预览不会伪造发布成功', 'REMOTE_WRITE_REQUIRED')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const createdFeed = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds`,
@@ -136,7 +117,6 @@ export const familyFeedApi = {
async getFeedPage(genealogyId, { pageNum = 1, pageSize = 20 } = {}, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const pageQuery = normalizePageQuery(pageNum, pageSize, '动态')
requireRemoteFeed('家族动态读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
const feedPage = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/page`,
method: 'GET',
@@ -156,7 +136,6 @@ export const familyFeedApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeResourcePathId(feedId, '动态标识')
const pageQuery = normalizePageQuery(pageNum, pageSize, '评论')
requireRemoteFeed('动态评论读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
const commentPage = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/comments/page`,
method: 'GET',
@@ -182,7 +161,6 @@ export const familyFeedApi = {
const normalizedFeedId = normalizeResourcePathId(feedId, '动态标识')
const normalizedCommentId = normalizeResourcePathId(commentId, '评论标识')
const pageQuery = normalizePageQuery(pageNum, pageSize, '回复')
requireRemoteFeed('评论回复读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
const replyPage = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/comments/${normalizedCommentId}/replies/page`,
method: 'GET',
@@ -207,7 +185,6 @@ export const familyFeedApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedFeedId = normalizeResourcePathId(feedId, '动态标识')
const normalizedCommentId = normalizeResourcePathId(commentId, '评论标识')
requireRemoteFeed('动态评论写入需要真实服务,当前本地预览不会伪造结果', 'REMOTE_WRITE_REQUIRED')
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/feeds/${normalizedFeedId}/comments/${normalizedCommentId}`,
method: 'DELETE'
+103 -16
View File
@@ -17,6 +17,96 @@ import {
normalizeResourceCapabilities
} from './business-file-contract.js'
export const PLATFORM_VIDEO_PLACEMENT = Object.freeze({
HOME_FEATURED: 'home_featured',
VIDEO_CENTER: 'video_center',
PROFILE_FEATURED: 'profile_featured'
})
const platformVideoPlacements = new Set(Object.values(PLATFORM_VIDEO_PLACEMENT))
export const normalizePlatformVideoPlacement = (value) => {
if (typeof value !== 'string' || !platformVideoPlacements.has(value.trim())) {
throw new TypeError('平台视频投放位无效')
}
return value.trim()
}
const normalizePlatformVideo = (value, expectedPlacement) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('平台视频响应无效', 'PLATFORM_VIDEO_RESPONSE_INVALID')
}
const id = normalizeOptionalNumericId(
value.platformVideoId,
'平台视频标识',
'PLATFORM_VIDEO_RESPONSE_INVALID'
)
const title = normalizeResponseText(value.videoTitle, 'videoTitle')
if (
!id ||
!title ||
value.placement !== expectedPlacement ||
(value.platform !== 'app' && value.platform !== 'all')
) {
throw createRequestError('平台视频响应缺少稳定字段', 'PLATFORM_VIDEO_RESPONSE_INVALID')
}
if (typeof value.likedByCurrentUser !== 'boolean') {
throw createRequestError('平台视频点赞状态无效', 'PLATFORM_VIDEO_RESPONSE_INVALID')
}
return {
id,
platform: value.platform,
placement: expectedPlacement,
title,
description: normalizeResponseText(value.videoDesc, 'videoDesc'),
startAt: normalizeResponseText(value.startAt, 'startAt'),
endAt: normalizeResponseText(value.endAt, 'endAt'),
coverFile: normalizeBusinessFileAccess(
value.coverFile,
'平台视频封面',
'PLATFORM_VIDEO_RESPONSE_INVALID'
),
videoFile: normalizeBusinessFileAccess(
value.videoFile,
'平台视频文件',
'PLATFORM_VIDEO_RESPONSE_INVALID',
{ required: true }
),
durationSeconds: normalizeOptionalNonnegativeInteger(
value.durationSeconds,
'平台视频时长',
'PLATFORM_VIDEO_RESPONSE_INVALID'
),
viewCount: normalizeOptionalNonnegativeInteger(
value.viewCount,
'平台视频播放量',
'PLATFORM_VIDEO_RESPONSE_INVALID'
),
likeCount: normalizeOptionalNonnegativeInteger(
value.likeCount,
'平台视频点赞量',
'PLATFORM_VIDEO_RESPONSE_INVALID'
),
commentCount: normalizeOptionalNonnegativeInteger(
value.commentCount,
'平台视频评论量',
'PLATFORM_VIDEO_RESPONSE_INVALID'
),
likedByCurrentUser: value.likedByCurrentUser
}
}
export const normalizePlatformVideos = (value, expectedPlacement) => {
if (!Array.isArray(value)) {
throw createRequestError('平台视频响应不是列表', 'PLATFORM_VIDEO_RESPONSE_INVALID')
}
const videos = value.map((item) => normalizePlatformVideo(item, expectedPlacement))
if (new Set(videos.map((item) => item.id)).size !== videos.length) {
throw createRequestError('平台视频响应包含重复标识', 'PLATFORM_VIDEO_RESPONSE_INVALID')
}
return videos
}
export const normalizeAppVideo = (value, expectedGenealogyId, expectedVideoId = '') => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('家族视频响应无效', 'VIDEO_RESPONSE_INVALID')
@@ -27,8 +117,12 @@ export const normalizeAppVideo = (value, expectedGenealogyId, expectedVideoId =
if (!id || genealogyId !== expectedGenealogyId || !title || (expectedVideoId && id !== expectedVideoId)) {
throw createRequestError('家族视频响应缺少稳定字段', 'VIDEO_RESPONSE_INVALID')
}
if (typeof value.canEdit !== 'boolean' || typeof value.canDelete !== 'boolean') {
throw createRequestError('家族视频响应缺少权限字段', 'VIDEO_RESPONSE_INVALID')
if (
typeof value.canEdit !== 'boolean' ||
typeof value.canDelete !== 'boolean' ||
typeof value.likedByCurrentUser !== 'boolean'
) {
throw createRequestError('家族视频响应缺少状态或权限字段', 'VIDEO_RESPONSE_INVALID')
}
return {
id,
@@ -40,6 +134,9 @@ export const normalizeAppVideo = (value, expectedGenealogyId, expectedVideoId =
durationSeconds: normalizeOptionalNonnegativeInteger(value.durationSeconds, '视频时长', 'VIDEO_RESPONSE_INVALID'),
publishTime: normalizeResponseText(value.publishTime, 'publishTime'),
viewCount: normalizeOptionalNonnegativeInteger(value.viewCount, '视频播放量', 'VIDEO_RESPONSE_INVALID'),
likeCount: normalizeOptionalNonnegativeInteger(value.likeCount, '视频点赞量', 'VIDEO_RESPONSE_INVALID'),
commentCount: normalizeOptionalNonnegativeInteger(value.commentCount, '视频评论量', 'VIDEO_RESPONSE_INVALID'),
likedByCurrentUser: value.likedByCurrentUser,
sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '视频排序值', 'VIDEO_RESPONSE_INVALID'),
status: normalizeNormalDisableResponseStatus(value.status, '视频状态', 'VIDEO_RESPONSE_INVALID'),
canEdit: value.canEdit,
@@ -72,6 +169,7 @@ export const normalizeAppAlbum = (value, expectedGenealogyId) => {
description: normalizeResponseText(value.albumDesc, 'albumDesc'),
coverFile: normalizeBusinessFileAccess(value.coverFile, '相册封面', 'ALBUM_RESPONSE_INVALID'),
photoCount: normalizeOptionalNonnegativeInteger(value.photoCount, '相册照片数量', 'ALBUM_RESPONSE_INVALID'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '相册排序值', 'ALBUM_RESPONSE_INVALID'),
status: normalizeNormalDisableResponseStatus(value.status, '相册状态', 'ALBUM_RESPONSE_INVALID'),
...normalizeResourceCapabilities(value, '相册', 'ALBUM_RESPONSE_INVALID')
@@ -87,18 +185,6 @@ export const normalizeAppAlbums = (value, expectedGenealogyId) => {
return albums
}
export const projectPreviewAlbums = (value, expectedGenealogyId) =>
value.map((item) => ({
id: String(item.id),
genealogyId: expectedGenealogyId,
name: String(item.name || '未命名相册'),
description: String(item.description || ''),
coverFile: item.cover ? { accessUrl: String(item.cover) } : null,
photoCount: Number.isSafeInteger(item.photoCount) ? item.photoCount : 0,
canEdit: false,
canDelete: false
}))
const normalizeAppAlbumPhoto = (value, expectedGenealogyId, expectedAlbumId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('相册照片响应无效', 'ALBUM_PHOTO_RESPONSE_INVALID')
@@ -178,8 +264,9 @@ export const normalizeVideoPayload = (payload) => {
const normalizedPayload = { videoTitle, videoOssId }
const videoDesc = normalizeOptionalText(payload.videoDesc, '视频说明')
if (videoDesc) normalizedPayload.videoDesc = videoDesc
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, '视频封面 OSS ID')
if (Object.prototype.hasOwnProperty.call(payload, 'coverOssId')) {
if (payload.coverOssId === null) normalizedPayload.coverOssId = null
else if (payload.coverOssId !== '') normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, '视频封面 OSS ID')
}
for (const field of ['durationSeconds', 'sortOrder']) {
const normalizedInteger = normalizeOptionalSafeInteger(payload[field], field)
+2 -27
View File
@@ -1,5 +1,3 @@
import { listPreviewFamilyAlbums } from '@/data/preview/family-content.js'
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeAlbumCreatePayload,
normalizeAlbumPhotoCreatePayload,
@@ -8,21 +6,14 @@ import {
normalizeAppAlbums,
normalizeAppVideo,
normalizeAppVideos,
normalizeVideoPayload,
projectPreviewAlbums
normalizeVideoPayload
} from './family-media-contract.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
import { createRequestError, requestStrict } from './request-client.js'
const requireRemoteMedia = (message, code) => {
if (hasRemoteConfig()) return
throw createRequestError(message, code)
}
import { requestStrict } from './request-client.js'
export const familyMediaApi = {
async createAlbum(genealogyId, payload, requestOptions = {}) {
requireRemoteMedia('相册创建需要真实服务,当前本地预览不会伪造创建成功', 'REMOTE_WRITE_REQUIRED')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums`,
@@ -36,7 +27,6 @@ export const familyMediaApi = {
async updateAlbum(genealogyId, albumId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedAlbumId = normalizeResourcePathId(albumId, '相册标识')
requireRemoteMedia('修改相册需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
const album = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}`,
method: 'PUT',
@@ -50,7 +40,6 @@ export const familyMediaApi = {
async deleteAlbum(genealogyId, albumId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedAlbumId = normalizeResourcePathId(albumId, '相册标识')
requireRemoteMedia('删除相册需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED')
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}`,
method: 'DELETE'
@@ -64,7 +53,6 @@ export const familyMediaApi = {
async createAlbumPhoto(genealogyId, albumId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedAlbumId = normalizeResourcePathId(albumId, '相册标识')
requireRemoteMedia('上传相册照片需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}/photos`,
method: 'POST',
@@ -78,7 +66,6 @@ export const familyMediaApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedAlbumId = normalizeResourcePathId(albumId, '相册标识')
const normalizedPhotoId = normalizeResourcePathId(photoId, '照片标识')
requireRemoteMedia('删除相册照片需要真实服务,当前本地预览不会伪造删除成功', 'REMOTE_WRITE_REQUIRED')
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}/photos/${normalizedPhotoId}`,
method: 'DELETE'
@@ -91,12 +78,6 @@ export const familyMediaApi = {
async getAlbums(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
return projectPreviewAlbums(
listPreviewFamilyAlbums(normalizedGenealogyId),
normalizedGenealogyId
)
}
const albums = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums`,
method: 'GET'
@@ -109,7 +90,6 @@ export const familyMediaApi = {
async getAlbumPhotos(genealogyId, albumId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedAlbumId = normalizeResourcePathId(albumId, '相册标识')
requireRemoteMedia('相册照片需要真实读取服务,当前本地预览不会伪造照片列表', 'REMOTE_READ_REQUIRED')
const photos = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/albums/${normalizedAlbumId}/photos`,
method: 'GET'
@@ -121,7 +101,6 @@ export const familyMediaApi = {
async getVideos(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteMedia('家族视频读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
const videos = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/videos`,
method: 'GET'
@@ -134,7 +113,6 @@ export const familyMediaApi = {
async getVideoDetail(genealogyId, videoId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedVideoId = normalizeResourcePathId(videoId, '视频标识')
requireRemoteMedia('家族视频详情读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
const video = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/videos/${normalizedVideoId}`,
method: 'GET'
@@ -146,7 +124,6 @@ export const familyMediaApi = {
async createVideo(genealogyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteMedia('家族视频写入需要真实服务,当前本地预览不会伪造结果', 'REMOTE_WRITE_REQUIRED')
const video = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/videos`,
method: 'POST',
@@ -160,7 +137,6 @@ export const familyMediaApi = {
async updateVideo(genealogyId, videoId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedVideoId = normalizeResourcePathId(videoId, '视频标识')
requireRemoteMedia('家族视频写入需要真实服务,当前本地预览不会伪造结果', 'REMOTE_WRITE_REQUIRED')
const video = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/videos/${normalizedVideoId}`,
method: 'PUT',
@@ -174,7 +150,6 @@ export const familyMediaApi = {
async deleteVideo(genealogyId, videoId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedVideoId = normalizeResourcePathId(videoId, '视频标识')
requireRemoteMedia('家族视频写入需要真实服务,当前本地预览不会伪造结果', 'REMOTE_WRITE_REQUIRED')
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/videos/${normalizedVideoId}`,
method: 'DELETE'
-12
View File
@@ -1,14 +1,5 @@
import { assertPlainPayload } from './request-normalizers.js'
export const FEEDBACK_TYPE_OPTIONS = Object.freeze([
Object.freeze({ value: 'bug', label: '功能问题' }),
Object.freeze({ value: 'advice', label: '使用建议' }),
Object.freeze({ value: 'complaint', label: '投诉反馈' }),
Object.freeze({ value: 'other', label: '其他' })
])
const feedbackTypes = new Set(FEEDBACK_TYPE_OPTIONS.map(({ value }) => value))
export const normalizeFeedbackPayload = (payload) => {
const allowedFields = new Set(['feedbackType', 'feedbackContent', 'contactInfo'])
assertPlainPayload(payload, allowedFields, '反馈请求')
@@ -23,9 +14,6 @@ export const normalizeFeedbackPayload = (payload) => {
}
const normalizedText = payload[optionalField].trim()
if (!normalizedText) continue
if (optionalField === 'feedbackType' && !feedbackTypes.has(normalizedText)) {
throw new TypeError('feedbackType 必须为 advice、bug、complaint 或 other')
}
normalizedPayload[optionalField] = normalizedText
}
return normalizedPayload
-7
View File
@@ -1,4 +1,3 @@
import { hasRemoteConfig, resolveRuntimeMode } from '@/utils/runtime-config.js'
import { normalizeFeedbackPayload } from './feedback-contract.js'
import {
createRequestError,
@@ -8,9 +7,6 @@ import {
export const feedbackApi = {
async submitFeedback(payload, requestOptions = {}) {
const feedback = normalizeFeedbackPayload(payload)
if (resolveRuntimeMode() !== 'remote') {
throw createRequestError('当前为本地预览模式,反馈未提交服务器', 'WRITE_UNAVAILABLE')
}
return requestStrict({
url: '/genealogy/app/feedback',
method: 'POST',
@@ -22,9 +18,6 @@ export const feedbackApi = {
},
async getFeedback(requestOptions = {}) {
if (!hasRemoteConfig()) {
throw createRequestError('我的反馈读取需要真实服务,当前本地预览不会伪造结果', 'REMOTE_READ_REQUIRED')
}
const feedbackHistory = await requestStrict({
url: '/genealogy/app/feedback',
method: 'GET'
-10
View File
@@ -1,4 +1,3 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeResumableCompletePayload,
normalizeResumableCompleteResult,
@@ -15,11 +14,6 @@ import {
requestStrict
} from './request-client.js'
const requireRemoteUpload = () => {
if (hasRemoteConfig()) return
throw createRequestError('文件上传需要真实服务,当前本地预览不伪造回执', 'REMOTE_WRITE_REQUIRED')
}
const normalizeChunkIndex = (value) => {
if (!Number.isInteger(value) || value < 0 || value > 2147483647) {
throw new TypeError('chunkIndex必须是非负 int32')
@@ -30,7 +24,6 @@ const normalizeChunkIndex = (value) => {
export const fileUploadApi = {
async initializeResumableUpload(payload, requestOptions = {}) {
const uploadRequest = normalizeResumableInitPayload(payload)
requireRemoteUpload()
const uploadSession = await requestStrict({
url: '/genealogy/app/files/resumable/init',
method: 'POST',
@@ -50,7 +43,6 @@ export const fileUploadApi = {
if (typeof payload.filePath !== 'string' || !payload.filePath.trim()) {
throw new TypeError('filePath必须是非空字符串')
}
requireRemoteUpload()
return requestNativeFileChunk({
uploadId: normalizeUploadId(payload.uploadId, 'uploadId'),
chunkIndex: normalizeChunkIndex(payload.chunkIndex),
@@ -68,7 +60,6 @@ export const fileUploadApi = {
if (!file || typeof file !== 'object') {
throw new TypeError('浏览器文件分片必须提供文件对象')
}
requireRemoteUpload()
return requestBrowserFileChunk({
uploadId: normalizeUploadId(payload.uploadId, 'uploadId'),
chunkIndex: normalizeChunkIndex(payload.chunkIndex),
@@ -79,7 +70,6 @@ export const fileUploadApi = {
async completeResumableUpload(payload, requestOptions = {}) {
const completionRequest = normalizeResumableCompletePayload(payload)
requireRemoteUpload()
const uploadedFile = await requestStrict({
url: '/genealogy/app/files/resumable/complete',
method: 'POST',
@@ -0,0 +1,218 @@
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
import { createRequestError, requestStrict } from './request-client.js'
import {
normalizePlatformVideoPlacement,
normalizePlatformVideos
} from './family-media-contract.js'
import {
VIDEO_COMMENT_LEVEL,
normalizeCreatedPlatformVideoComment,
normalizeCreatedVideoComment,
normalizePlatformVideoComments,
normalizeVideoCommentPayload,
normalizeVideoComments
} from './video-comment-contract.js'
import {
normalizeMemberPermissionAssignment,
normalizeMemberPermissionPayload,
normalizePermissionCatalog
} from './genealogy-permission-contract.js'
const assertPage = (value, label) => {
if (!value || typeof value !== 'object' || !Array.isArray(value.rows) || !Number.isSafeInteger(value.total)) {
throw createRequestError(`${label}响应无效`, 'PAGE_RESPONSE_INVALID')
}
return value
}
const assertCompleteness = (value) => {
const isCount = (count) => Number.isSafeInteger(count) && count >= 0
if (
!value ||
typeof value !== 'object' ||
!isCount(value.totalCount) ||
value.totalCount === 0 ||
!isCount(value.completedCount) ||
value.completedCount > value.totalCount ||
!Number.isSafeInteger(value.completionRate) ||
value.completionRate < 0 ||
value.completionRate > 100 ||
!Array.isArray(value.missingItems)
) {
throw createRequestError('资料完整度响应无效', 'COMPLETENESS_RESPONSE_INVALID')
}
return value
}
export const genealogyCapabilityApi = {
async getCompleteness(genealogyId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
const completeness = await requestStrict({ url: `/genealogy/app/genealogies/${id}/completeness`, method: 'GET' }, { requestController: requestOptions.requestController ?? null })
return assertCompleteness(completeness)
},
async getRecyclePage(genealogyId, query = {}, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
const page = await requestStrict({ url: `/genealogy/app/genealogies/${id}/recycle-bin/page`, method: 'GET', data: query }, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return assertPage(page, '回收站')
},
async restoreRecycleItem(genealogyId, resourceType, resourceId, requestOptions = {}) {
if (typeof resourceType !== 'string' || !resourceType.trim()) throw new TypeError('资源类型不能为空')
const id = normalizeGenealogyPathId(genealogyId)
const resource = normalizeResourcePathId(resourceId, '资源标识')
return requestStrict({ url: `/genealogy/app/genealogies/${id}/recycle-bin/${encodeURIComponent(resourceType.trim())}/${resource}/restore`, method: 'PUT' }, { requestController: requestOptions.requestController ?? null })
},
async getPermissionCatalog(genealogyId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
const catalog = await requestStrict({
url: `/genealogy/app/genealogies/${id}/permission-catalog`,
method: 'GET'
}, { requestController: requestOptions.requestController ?? null })
return normalizePermissionCatalog(catalog)
},
async getMemberPermissions(genealogyId, memberId, enabledCodes, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId); const member = normalizeResourcePathId(memberId, '成员标识')
const assignment = await requestStrict({ url: `/genealogy/app/genealogies/${id}/members/${member}/permissions`, method: 'GET' }, { requestController: requestOptions.requestController ?? null })
return normalizeMemberPermissionAssignment(assignment, enabledCodes, {
expectedGenealogyId: id,
expectedMemberId: member
})
},
async saveMemberPermissions(genealogyId, memberId, permissionCodes, reason = '', enabledCodes = [], requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId); const member = normalizeResourcePathId(memberId, '成员标识')
const assignment = await requestStrict({
url: `/genealogy/app/genealogies/${id}/members/${member}/permissions`,
method: 'PUT',
data: normalizeMemberPermissionPayload(permissionCodes, reason, enabledCodes)
}, { requestController: requestOptions.requestController ?? null })
return normalizeMemberPermissionAssignment(assignment, enabledCodes, {
expectedGenealogyId: id,
expectedMemberId: member
})
},
async setVideoLike(genealogyId, videoId, liked, requestOptions = {}) {
if (typeof liked !== 'boolean') throw new TypeError('点赞状态无效')
const id = normalizeGenealogyPathId(genealogyId); const video = normalizeResourcePathId(videoId, '视频标识')
return requestStrict({ url: `/genealogy/app/genealogies/${id}/videos/${video}/likes`, method: liked ? 'POST' : 'DELETE' }, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
},
async getVideoComments(genealogyId, videoId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId); const video = normalizeResourcePathId(videoId, '视频标识')
const page = assertPage(await requestStrict({
url: `/genealogy/app/genealogies/${id}/videos/${video}/comments/page`,
method: 'GET',
data: { pageNum: 1, pageSize: 100 }
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
}), '视频评论')
return normalizeVideoComments(page.rows, video, { expectedGenealogyId: id })
},
async getVideoCommentReplies(genealogyId, videoId, commentId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
const video = normalizeResourcePathId(videoId, '视频标识')
const comment = normalizeResourcePathId(commentId, '父评论标识')
const page = assertPage(await requestStrict({
url: `/genealogy/app/genealogies/${id}/videos/${video}/comments/${comment}/replies/page`,
method: 'GET',
data: { pageNum: 1, pageSize: 100 }
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
}), '视频评论回复')
return normalizeVideoComments(page.rows, video, {
expectedGenealogyId: id,
expectedLevel: VIDEO_COMMENT_LEVEL.REPLY,
expectedParentCommentId: comment
})
},
async createVideoComment(genealogyId, videoId, content, parentCommentId = null, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId); const video = normalizeResourcePathId(videoId, '视频标识')
const payload = normalizeVideoCommentPayload({
commentContent: content,
...(parentCommentId ? { parentCommentId } : {})
})
const comment = await requestStrict({
url: `/genealogy/app/genealogies/${id}/videos/${video}/comments`,
method: 'POST',
data: payload
}, { requestController: requestOptions.requestController ?? null })
return normalizeCreatedVideoComment(comment, video, {
expectedGenealogyId: id,
expectedLevel: parentCommentId ? VIDEO_COMMENT_LEVEL.REPLY : VIDEO_COMMENT_LEVEL.ROOT,
expectedParentCommentId: payload.parentCommentId || ''
})
},
async deleteVideoComment(genealogyId, videoId, commentId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId); const video = normalizeResourcePathId(videoId, '视频标识'); const comment = normalizeResourcePathId(commentId, '评论标识')
return requestStrict({ url: `/genealogy/app/genealogies/${id}/videos/${video}/comments/${comment}`, method: 'DELETE' }, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
},
async getPlatformVideos(placement, requestOptions = {}) {
const normalizedPlacement = normalizePlatformVideoPlacement(placement)
const videos = await requestStrict({
url: '/genealogy/app/platform-videos',
method: 'GET',
data: { placement: normalizedPlacement }
}, {
requestController: requestOptions.requestController ?? null
})
return normalizePlatformVideos(videos, normalizedPlacement)
},
async setPlatformVideoLike(videoId, liked, requestOptions = {}) {
if (typeof liked !== 'boolean') throw new TypeError('点赞状态无效')
return requestStrict({ url: `/genealogy/app/platform-videos/${normalizeResourcePathId(videoId, '平台视频标识')}/likes`, method: liked ? 'POST' : 'DELETE' }, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
},
async getPlatformVideoComments(videoId, requestOptions = {}) {
const video = normalizeResourcePathId(videoId, '平台视频标识')
const comments = await requestStrict({ url: `/genealogy/app/platform-videos/${video}/comments`, method: 'GET' }, {
requestController: requestOptions.requestController ?? null
})
return normalizePlatformVideoComments(comments, video)
},
async createPlatformVideoComment(videoId, content, requestOptions = {}) {
const video = normalizeResourcePathId(videoId, '平台视频标识')
const payload = normalizeVideoCommentPayload({ commentContent: content })
const comment = await requestStrict({ url: `/genealogy/app/platform-videos/${video}/comments`, method: 'POST', data: payload }, {
requestController: requestOptions.requestController ?? null
})
return normalizeCreatedPlatformVideoComment(comment, video)
},
async deletePlatformVideoComment(videoId, commentId, requestOptions = {}) {
const video = normalizeResourcePathId(videoId, '平台视频标识')
const comment = normalizeResourcePathId(commentId, '平台视频评论标识')
await requestStrict({
url: `/genealogy/app/platform-videos/${video}/comments/${comment}`,
method: 'DELETE'
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
}
}
+92 -27
View File
@@ -64,6 +64,55 @@ const normalizeGenealogyCapabilities = (value, code) => {
return capabilities
}
export const normalizeGenealogyPermanentDeletionCapability = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.canDeletePermanently !== 'boolean') {
throw createRequestError('家谱永久删除能力响应无效', 'GENEALOGY_DELETION_RESPONSE_INVALID')
}
if (!Array.isArray(value.disabledReasons) || value.disabledReasons.some((reason) => typeof reason !== 'string')) {
throw createRequestError('家谱永久删除阻断原因无效', 'GENEALOGY_DELETION_RESPONSE_INVALID')
}
const phoneMasked = normalizeResponseText(value.verifiedMobileMasked, 'verifiedMobileMasked', {
code: 'GENEALOGY_DELETION_RESPONSE_INVALID',
subject: '家谱永久删除能力响应'
})
if (value.canDeletePermanently && !phoneMasked) {
throw createRequestError('家谱永久删除能力缺少验证手机号', 'GENEALOGY_DELETION_RESPONSE_INVALID')
}
return {
canDeletePermanently: value.canDeletePermanently,
phoneMasked,
disabledReasons: value.disabledReasons.map((reason) => reason.trim()).filter(Boolean)
}
}
export const normalizeGenealogyPermanentDeletionPayload = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new TypeError('家谱永久删除请求无效')
}
if (typeof payload.confirmationName !== 'string' || !payload.confirmationName.trim() || payload.confirmationName.trim().length > 24) {
throw new TypeError('家谱永久删除确认名称无效')
}
if (typeof payload.smsCode !== 'string' || !/^\d{4}$/.test(payload.smsCode)) {
throw new TypeError('家谱永久删除验证码必须是4位数字')
}
return { genealogyName: payload.confirmationName.trim(), smsCode: payload.smsCode }
}
export const normalizeGenealogyDeletionTask = (value, expectedGenealogyId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('家谱删除任务响应无效', 'GENEALOGY_DELETION_RESPONSE_INVALID')
}
const taskId = normalizeResponseText(String(value.taskId ?? ''), 'taskId', {
code: 'GENEALOGY_DELETION_RESPONSE_INVALID',
subject: '家谱删除任务'
})
const genealogyId = String(value.genealogyId ?? '')
if (!/^[1-9]\d*$/.test(taskId) || genealogyId !== expectedGenealogyId) {
throw createRequestError('家谱删除任务缺少稳定归属字段', 'GENEALOGY_DELETION_RESPONSE_INVALID')
}
return { taskId, genealogyId, status: normalizeResponseText(value.status, 'status') }
}
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)
@@ -179,6 +228,8 @@ export const normalizeAppGenealogy = (item) => {
return {
id: normalizeMyGenealogyId(item.genealogyId),
name,
firstAncestorName: normalizeGenealogyResponseText(item.firstAncestorName, 'firstAncestorName'),
rootPersonId: normalizeOptionalNumericId(item.rootPersonId, '始迁祖人物标识', 'GENEALOGY_RESPONSE_INVALID'),
surname: normalizeGenealogyResponseText(item.surname, 'surname'),
hall: normalizeGenealogyResponseText(item.ancestralHall, 'ancestralHall'),
location:
@@ -239,6 +290,15 @@ export const normalizeGenealogyOrderIds = (value) => {
return ids
}
export const normalizePublicGenealogyQuery = (query = {}) => {
assertPlainPayload(query, new Set(['keyword']), '公开家谱查询')
if (query.keyword === undefined || query.keyword === null) return {}
if (typeof query.keyword !== 'string') throw new TypeError('公开家谱关键词必须是字符串')
const keyword = query.keyword.trim()
if (keyword.length > 50) throw new TypeError('公开家谱关键词不能超过50个字符')
return keyword ? { keyword } : {}
}
export const normalizePublicGenealogies = (value) => {
if (!Array.isArray(value)) {
throw createRequestError('公开家谱响应不是列表', 'PUBLIC_GENEALOGY_RESPONSE_INVALID')
@@ -257,10 +317,11 @@ export const normalizePublicGenealogies = (value) => {
'公开家谱成员数量',
'PUBLIC_GENEALOGY_RESPONSE_INVALID'
)
if (item.canManage !== undefined && typeof item.canManage !== 'boolean') {
throw createRequestError('公开家谱管理权限无效', 'PUBLIC_GENEALOGY_RESPONSE_INVALID')
}
return {
if (item.canManage !== undefined && typeof item.canManage !== 'boolean') {
throw createRequestError('公开家谱管理权限无效', 'PUBLIC_GENEALOGY_RESPONSE_INVALID')
}
const memberStatus = normalizeGenealogyResponseText(item.memberStatus, 'memberStatus')
return {
id,
name,
surname: normalizeGenealogyResponseText(item.surname, 'surname'),
@@ -269,9 +330,11 @@ export const normalizePublicGenealogies = (value) => {
normalizeGenealogyResponseText(item.regionName, 'regionName') ||
normalizeGenealogyResponseText(item.originPlace, 'originPlace') ||
normalizeGenealogyResponseText(item.addressDetail, 'addressDetail'),
intro: normalizeGenealogyResponseText(item.intro, 'intro'),
memberCount,
canManage: item.canManage === true
intro: normalizeGenealogyResponseText(item.intro, 'intro'),
memberCount,
canManage: item.canManage === true,
memberStatus,
hasMembership: item.canManage === true || Boolean(memberStatus)
}
})
if (new Set(genealogies.map((genealogy) => genealogy.id)).size !== genealogies.length) {
@@ -280,17 +343,6 @@ export const normalizePublicGenealogies = (value) => {
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}`)
@@ -315,16 +367,26 @@ const GENEALOGY_WRITE_FIELDS = new Set([
'joinMode'
])
const assertGenealogyWritePayload = (payload, operation) => {
assertPlainPayload(payload, GENEALOGY_WRITE_FIELDS, `${operation}家谱请求`)
}
export const normalizeGenealogyCreatePayload = (payload) => {
assertGenealogyWritePayload(payload, '创建')
assertPlainPayload(
payload,
new Set([...GENEALOGY_WRITE_FIELDS, 'firstAncestorName', 'requestId', 'ownerIsFirstAncestor']),
'创建家谱请求'
)
const requestId = normalizeGenealogyWriteText(payload.requestId, 'requestId', { required: true })
if (requestId.length > 64) throw new TypeError('创建家谱请求号不能超过64个字符')
const firstAncestorName = normalizeGenealogyWriteText(payload.firstAncestorName, '始迁祖姓名', { required: true })
if (firstAncestorName.length > 30) throw new TypeError('始迁祖姓名不能超过30个字符')
if (payload.ownerIsFirstAncestor !== undefined && typeof payload.ownerIsFirstAncestor !== 'boolean') {
throw new TypeError('谱主是否为始迁祖必须是布尔值')
}
const normalizedPayload = {
genealogyName: normalizeGenealogyWriteText(payload.genealogyName, '谱名', { required: true }),
surname: normalizeGenealogyWriteText(payload.surname, '姓氏', { required: true }),
regionCode: normalizeGenealogyWriteText(payload.regionCode, '地区代码', { required: true })
regionCode: normalizeGenealogyWriteText(payload.regionCode, '地区代码', { required: true }),
firstAncestorName,
requestId,
ownerIsFirstAncestor: payload.ownerIsFirstAncestor === true
}
for (const field of ['ancestralHall', 'originPlace', 'addressDetail', 'intro']) {
const normalizedText = normalizeGenealogyWriteText(payload[field], field)
@@ -347,7 +409,7 @@ export const normalizeGenealogyCreatePayload = (payload) => {
}
export const normalizeGenealogyUpdatePayload = (payload) => {
assertGenealogyWritePayload(payload, '更新')
assertPlainPayload(payload, GENEALOGY_WRITE_FIELDS, '更新家谱请求')
const normalizedPayload = {}
for (const field of [
'genealogyName',
@@ -372,8 +434,11 @@ export const normalizeGenealogyUpdatePayload = (payload) => {
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.prototype.hasOwnProperty.call(payload, 'coverOssId')) {
if (payload.coverOssId === null) normalizedPayload.coverOssId = null
else if (payload.coverOssId !== '') {
normalizedPayload.coverOssId = normalizeOssIdString(payload.coverOssId, '更新家谱字段 coverOssId')
}
}
if (!Object.keys(normalizedPayload).length) throw new TypeError('请至少填写一项需要更新的家谱信息')
return normalizedPayload
+12 -27
View File
@@ -1,4 +1,3 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import {
normalizeGenealogyMemberOptions,
@@ -6,18 +5,9 @@ import {
normalizeMemberUpdatePayload
} from './genealogy-member-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
import { createRequestError, requestStrict } from './request-client.js'
import { requestStrict } from './request-client.js'
const requireRemoteMembership = (label, operation) => {
if (hasRemoteConfig()) return
throw createRequestError(
`${label}${operation}需要真实服务,当前本地预览不会伪造结果`,
operation === '读取' ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED'
)
}
const writeMembership = (url, method, data, label, requestOptions) => {
requireRemoteMembership(label, '写入')
const writeMembership = (url, method, data, requestOptions) => {
return requestStrict({
url,
method,
@@ -27,8 +17,7 @@ const writeMembership = (url, method, data, label, requestOptions) => {
})
}
const deleteMembership = async (url, label, requestOptions) => {
requireRemoteMembership(label, '写入')
const deleteMembership = async (url, requestOptions) => {
await requestStrict({ url, method: 'DELETE' }, {
requireData: false,
requestController: requestOptions.requestController ?? null
@@ -39,7 +28,6 @@ const deleteMembership = async (url, label, requestOptions) => {
export const genealogyMemberApi = {
async getMembers(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteMembership('家谱成员', '读取')
const members = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/members`,
method: 'GET'
@@ -51,7 +39,6 @@ export const genealogyMemberApi = {
async getMemberOptions(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteMembership('成员选项', '读取')
const memberOptions = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/members/options`,
method: 'GET'
@@ -68,7 +55,6 @@ export const genealogyMemberApi = {
`/genealogy/app/genealogies/${normalizedGenealogyId}/members/${normalizedMemberId}`,
'PUT',
normalizeMemberUpdatePayload(payload),
'家谱成员',
requestOptions
)
return normalizeGenealogyMembers([member], normalizedGenealogyId)[0]
@@ -79,7 +65,6 @@ export const genealogyMemberApi = {
const normalizedMemberId = normalizeResourcePathId(memberId, '成员标识')
return deleteMembership(
`/genealogy/app/genealogies/${normalizedGenealogyId}/members/${normalizedMemberId}/lineage-person`,
'成员世系绑定',
requestOptions
)
},
@@ -89,7 +74,6 @@ export const genealogyMemberApi = {
const normalizedMemberId = normalizeResourcePathId(memberId, '成员标识')
return deleteMembership(
`/genealogy/app/genealogies/${normalizedGenealogyId}/members/${normalizedMemberId}`,
'家谱成员',
requestOptions
)
},
@@ -98,19 +82,20 @@ export const genealogyMemberApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return deleteMembership(
`/genealogy/app/genealogies/${normalizedGenealogyId}/members/me`,
'退出家谱',
requestOptions
)
},
async transferGenealogyOwner(genealogyId, targetMemberId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return writeMembership(
`/genealogy/app/genealogies/${normalizedGenealogyId}/members/owner-transfer`,
'PUT',
{ targetMemberId: normalizeResourcePathId(targetMemberId, '目标成员标识') },
'家谱所有者转让',
requestOptions
)
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/members/owner-transfer`,
method: 'PUT',
data: { targetMemberId: normalizeResourcePathId(targetMemberId, '目标成员标识') }
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
}
}
+16 -101
View File
@@ -1,12 +1,5 @@
import {
createPreviewMyJoinApplications,
createPreviewPendingJoinApplications,
previewCurrentUser
} from '@/data/preview/genealogy-membership.js'
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import {
JOIN_APPLICATION_STATUS,
normalizeGenealogyInvitation,
normalizeGenealogyInvitationRedeemPayload,
normalizeGenealogyInvitationToken,
@@ -18,23 +11,7 @@ import {
import { normalizeResourcePathId } from './request-normalizers.js'
import { createRequestError, requestStrict } from './request-client.js'
const requireRemoteMembership = (label, operation) => {
if (hasRemoteConfig()) return
throw createRequestError(
`${label}${operation}需要真实服务,当前本地预览不会伪造结果`,
operation === '读取' ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED'
)
}
const previewMyJoinApplications = createPreviewMyJoinApplications()
const previewPendingJoinApplications = createPreviewPendingJoinApplications()
let nextPreviewApplicationId = Date.now()
const snapshotPreviewJoinApplications = (applications) =>
applications.map((application) => ({ ...application }))
const createPreviewApplicationId = () => String(nextPreviewApplicationId++)
const readMembershipList = async (url, label, requestOptions) => {
requireRemoteMembership(label, '读取')
const rows = await requestStrict({ url, method: 'GET' }, {
requestController: requestOptions.requestController ?? null
})
@@ -44,8 +21,7 @@ const readMembershipList = async (url, label, requestOptions) => {
return rows
}
const writeMembership = (url, method, data, label, requestOptions) => {
requireRemoteMembership(label, '写入')
const writeMembership = (url, method, data, requestOptions) => {
return requestStrict({
url,
method,
@@ -58,7 +34,6 @@ const writeMembership = (url, method, data, label, requestOptions) => {
export const genealogyMembershipApi = {
async previewGenealogyInvitation(token, requestOptions = {}) {
const normalizedToken = normalizeGenealogyInvitationToken(token)
requireRemoteMembership('家谱邀请码', '读取')
const invitation = await requestStrict({
url: `/genealogy/app/genealogies/invitations/preview?token=${encodeURIComponent(normalizedToken)}`,
method: 'GET'
@@ -73,8 +48,7 @@ export const genealogyMembershipApi = {
const invitationResponse = await writeMembership(
`/genealogy/app/genealogies/${normalizedGenealogyId}/invitations`,
'POST',
undefined,
'家谱邀请码签发',
{},
requestOptions
)
const invitation = normalizeIssuedGenealogyInvitation(invitationResponse)
@@ -88,7 +62,6 @@ export const genealogyMembershipApi = {
},
async getMyGenealogyInvitations(requestOptions = {}) {
requireRemoteMembership('我的家谱邀请', '读取')
const invitations = await requestStrict({
url: '/genealogy/app/genealogies/invitations/mine',
method: 'GET'
@@ -100,7 +73,6 @@ export const genealogyMembershipApi = {
async revokeGenealogyInvitation(inviteId, requestOptions = {}) {
const normalizedInviteId = normalizeResourcePathId(inviteId, '家谱邀请标识')
requireRemoteMembership('家谱邀请码撤销', '写入')
await requestStrict({
url: `/genealogy/app/genealogies/invitations/${normalizedInviteId}`,
method: 'DELETE'
@@ -116,7 +88,6 @@ export const genealogyMembershipApi = {
'/genealogy/app/genealogies/invitations/redeem',
'POST',
normalizeGenealogyInvitationRedeemPayload(payload),
'家谱邀请码兑换',
requestOptions
)
return normalizeGenealogyInvitation(invitation, 'REDEEMED')
@@ -125,32 +96,16 @@ export const genealogyMembershipApi = {
async applyToJoin(genealogyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const applicationDraft = normalizeJoinApplicationPayload(payload)
if (hasRemoteConfig()) {
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies`,
method: 'POST',
data: applicationDraft
}, {
requestController: requestOptions.requestController ?? null
})
}
previewMyJoinApplications.unshift({
id: createPreviewApplicationId(),
genealogyId: normalizedGenealogyId,
name: applicationDraft.applicantName || previewCurrentUser.name,
phone: applicationDraft.phone || previewCurrentUser.phone,
relation: applicationDraft.relationDesc || '关系待补充',
reason: applicationDraft.applyReason || '',
appliedAt: '刚刚',
status: JOIN_APPLICATION_STATUS.PENDING
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies`,
method: 'POST',
data: applicationDraft
}, {
requestController: requestOptions.requestController ?? null
})
return null
},
async getMyJoinApplications(requestOptions = {}) {
if (!hasRemoteConfig()) {
return snapshotPreviewJoinApplications(previewMyJoinApplications)
}
return readMembershipList(
'/genealogy/app/genealogies/join-applies/mine',
'我的加入申请',
@@ -160,21 +115,6 @@ export const genealogyMembershipApi = {
async withdrawJoinApplication(applicationId, requestOptions = {}) {
const normalizedApplicationId = normalizeResourcePathId(applicationId, '加入申请标识')
if (!hasRemoteConfig()) {
const application = previewMyJoinApplications.find(
(candidate) =>
candidate.id === normalizedApplicationId &&
candidate.status === JOIN_APPLICATION_STATUS.PENDING
)
if (!application) {
throw createRequestError(
'待撤回申请不存在或已处理',
'PREVIEW_JOIN_APPLICATION_NOT_FOUND'
)
}
application.status = JOIN_APPLICATION_STATUS.CANCELLED
return null
}
await requestStrict({
url: `/genealogy/app/genealogies/join-applies/${normalizedApplicationId}`,
method: 'DELETE'
@@ -187,15 +127,6 @@ export const genealogyMembershipApi = {
async getPendingApplications(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
return snapshotPreviewJoinApplications(
previewPendingJoinApplications.filter(
(application) =>
application.genealogyId === normalizedGenealogyId &&
application.status === JOIN_APPLICATION_STATUS.PENDING
)
)
}
return readMembershipList(
`/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies/pending`,
'待审核申请',
@@ -207,30 +138,14 @@ export const genealogyMembershipApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedApplicationId = normalizeResourcePathId(applicationId, '加入申请标识')
const auditDecision = normalizeJoinAuditPayload(payload)
if (hasRemoteConfig()) {
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies/${normalizedApplicationId}/audit`,
method: 'PUT',
data: auditDecision
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
}
const application = previewPendingJoinApplications.find(
(candidate) =>
candidate.id === normalizedApplicationId &&
candidate.genealogyId === normalizedGenealogyId &&
candidate.status === JOIN_APPLICATION_STATUS.PENDING
)
if (!application) {
throw createRequestError(
'待审核申请不存在、已处理或不属于当前家谱',
'PREVIEW_JOIN_APPLICATION_NOT_FOUND'
)
}
Object.assign(application, auditDecision)
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/join-applies/${normalizedApplicationId}/audit`,
method: 'PUT',
data: auditDecision
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
}
}
@@ -0,0 +1,94 @@
import { createRequestError } from './request-client.js'
const permissionResponseError = (message) =>
createRequestError(message, 'GENEALOGY_PERMISSION_RESPONSE_INVALID')
const normalizePermissionCode = (value, subject = '权限编码') => {
if (typeof value !== 'string' || !/^[A-Z][A-Z0-9_]{2,63}$/.test(value.trim())) {
throw permissionResponseError(`${subject}无效`)
}
return value.trim()
}
const normalizePermissionIdentity = (value, subject) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
throw permissionResponseError(`${subject}无效`)
}
const normalizePermissionText = (value, field, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw permissionResponseError(`权限响应缺少${field}`)
return ''
}
if (typeof value !== 'string') throw permissionResponseError(`权限响应${field}无效`)
const normalized = value.trim()
if (required && !normalized) throw permissionResponseError(`权限响应缺少${field}`)
return normalized
}
export const normalizePermissionCatalog = (value) => {
if (!Array.isArray(value)) throw permissionResponseError('权限目录不是列表')
const options = value.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item) || typeof item.enabled !== 'boolean') {
throw permissionResponseError('权限目录包含无效条目')
}
return {
code: normalizePermissionCode(item.code),
label: normalizePermissionText(item.name, 'name', { required: true }),
groupCode: normalizePermissionText(item.groupCode, 'groupCode', { required: true }),
groupName: normalizePermissionText(item.groupName, 'groupName', { required: true }),
description: normalizePermissionText(item.description, 'description'),
sensitive: item.sensitive === true,
enabled: item.enabled,
disabledReason: normalizePermissionText(item.disabledReason, 'disabledReason')
}
})
if (new Set(options.map((item) => item.code)).size !== options.length) {
throw permissionResponseError('权限目录包含重复编码')
}
return options
}
export const normalizeMemberPermissionAssignment = (
value,
enabledCodes,
{ expectedGenealogyId = '', expectedMemberId = '' } = {}
) => {
if (!value || typeof value !== 'object' || Array.isArray(value) || !Array.isArray(value.permissionCodes)) {
throw permissionResponseError('成员权限响应无效')
}
const permissionCodes = value.permissionCodes.map((code) => normalizePermissionCode(code))
if (new Set(permissionCodes).size !== permissionCodes.length) {
throw permissionResponseError('成员权限响应包含重复编码')
}
const allowedCodes = new Set(enabledCodes)
if (permissionCodes.some((code) => !allowedCodes.has(code))) {
throw permissionResponseError('成员权限响应包含目录外或已停用权限')
}
const genealogyId = normalizePermissionIdentity(value.genealogyId, '权限响应家谱标识')
const memberId = normalizePermissionIdentity(value.memberId, '权限响应成员标识')
if (
(expectedGenealogyId && genealogyId !== expectedGenealogyId) ||
(expectedMemberId && memberId !== expectedMemberId)
) {
throw permissionResponseError('成员权限响应归属与请求不匹配')
}
return { genealogyId, memberId, permissionCodes }
}
export const normalizeMemberPermissionPayload = (permissionCodes, reason, enabledCodes) => {
if (!Array.isArray(permissionCodes)) throw new TypeError('权限项必须是数组')
const allowedCodes = new Set(enabledCodes)
const normalizedCodes = permissionCodes.map((code) => normalizePermissionCode(code, '提交权限编码'))
if (new Set(normalizedCodes).size !== normalizedCodes.length || normalizedCodes.some((code) => !allowedCodes.has(code))) {
throw new TypeError('提交权限包含重复、目录外或已停用权限')
}
if (typeof reason !== 'string') throw new TypeError('授权原因必须是字符串')
const normalizedReason = reason.trim()
if (normalizedReason.length > 200) throw new TypeError('授权原因不能超过200个字符')
return {
permissionCodes: normalizedCodes,
...(normalizedReason ? { reason: normalizedReason } : {})
}
}
+49 -33
View File
@@ -1,33 +1,25 @@
import { listPreviewPublicGenealogies } from '@/data/preview/genealogies.js'
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeAppGenealogy,
normalizeCreatedGenealogy,
normalizeGenealogyCreatePayload,
normalizeGenealogyOrderIds,
normalizeGenealogyPathId,
normalizeGenealogyDeletionTask,
normalizeGenealogyPermanentDeletionCapability,
normalizeGenealogyPermanentDeletionPayload,
normalizeGenealogyQuota,
normalizeGenealogySettings,
normalizeGenealogyUpdatePayload,
normalizeMyGenealogies,
normalizePublicGenealogies,
projectPreviewPublicGenealogies
normalizePublicGenealogyQuery,
normalizePublicGenealogies
} from './genealogy-contract.js'
import {
createRequestError,
requestStrict
} from './request-client.js'
const requireRemoteGenealogy = (label, operation) => {
if (hasRemoteConfig()) return
throw createRequestError(
`${label}${operation}需要真实服务,当前本地预览不会伪造结果`,
operation === '读取' ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED'
)
}
const writeGenealogyState = (genealogyId, action, label, requestOptions) => {
requireRemoteGenealogy(label, '写入')
const writeGenealogyState = (genealogyId, action, requestOptions) => {
return requestStrict({
url: `/genealogy/app/genealogies/${genealogyId}/${action}`,
method: 'PUT'
@@ -37,13 +29,11 @@ const writeGenealogyState = (genealogyId, action, label, requestOptions) => {
}
export const genealogyApi = {
async getPublicGenealogies(requestOptions = {}) {
if (!hasRemoteConfig()) {
return projectPreviewPublicGenealogies(listPreviewPublicGenealogies())
}
async getPublicGenealogies(query = {}, requestOptions = {}) {
const publicGenealogyRows = await requestStrict({
url: '/genealogy/app/genealogies/public',
method: 'GET'
method: 'GET',
data: normalizePublicGenealogyQuery(query)
}, {
requestController: requestOptions.requestController ?? null
})
@@ -51,7 +41,6 @@ export const genealogyApi = {
},
async getMyGenealogies(requestOptions = {}) {
requireRemoteGenealogy('我的家谱', '读取')
const genealogyRows = await requestStrict({
url: '/genealogy/app/genealogies/mine',
method: 'GET'
@@ -63,7 +52,6 @@ export const genealogyApi = {
async saveMyGenealogyOrder(genealogyIds, requestOptions = {}) {
const orderedIds = normalizeGenealogyOrderIds(genealogyIds)
requireRemoteGenealogy('保存家谱排序', '写入')
const confirmedGenealogies = normalizeMyGenealogies(await requestStrict({
url: '/genealogy/app/genealogies/mine/order',
method: 'PUT',
@@ -81,7 +69,6 @@ export const genealogyApi = {
},
async getGenealogyQuota(requestOptions = {}) {
requireRemoteGenealogy('家谱配额', '读取')
const quota = await requestStrict({
url: '/genealogy/app/genealogies/quota',
method: 'GET'
@@ -93,7 +80,6 @@ export const genealogyApi = {
async createGenealogy(payload, requestOptions = {}) {
const genealogyDraft = normalizeGenealogyCreatePayload(payload)
requireRemoteGenealogy('创建家谱', '写入')
const createdGenealogy = await requestStrict({
url: '/genealogy/app/genealogies',
method: 'POST',
@@ -106,7 +92,6 @@ export const genealogyApi = {
async getGenealogySettings(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteGenealogy('家谱设置', '读取')
const genealogySettings = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}`,
method: 'GET'
@@ -119,7 +104,6 @@ export const genealogyApi = {
async updateGenealogy(genealogyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const genealogyChanges = normalizeGenealogyUpdatePayload(payload)
requireRemoteGenealogy('更新家谱', '写入')
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}`,
method: 'PUT',
@@ -135,7 +119,6 @@ export const genealogyApi = {
const genealogy = await writeGenealogyState(
normalizedGenealogyId,
'archive',
'家谱归档',
requestOptions
)
return normalizeAppGenealogy(genealogy)
@@ -146,24 +129,57 @@ export const genealogyApi = {
const genealogy = await writeGenealogyState(
normalizedGenealogyId,
'restore',
'家谱恢复',
requestOptions
)
return normalizeAppGenealogy(genealogy)
},
async sendPermanentDeletionCode(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/permanent-deletion/code`,
method: 'POST'
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
},
async getPermanentDeletionCapability(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const capability = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/permanent-deletion/capability`,
method: 'GET'
}, { requestController: requestOptions.requestController ?? null })
return normalizeGenealogyPermanentDeletionCapability(capability)
},
async deleteGenealogyPermanently(genealogyId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const deletionTask = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/permanent-deletion`,
method: 'POST',
data: normalizeGenealogyPermanentDeletionPayload(payload)
}, { requestController: requestOptions.requestController ?? null })
return normalizeGenealogyDeletionTask(deletionTask, normalizedGenealogyId)
},
async getOverview(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteGenealogy('家谱概览', '读取')
const overview = normalizeAppGenealogy(await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/overview`,
const genealogyRows = await requestStrict({
url: '/genealogy/app/genealogies/mine',
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
}))
if (overview.id !== normalizedGenealogyId) {
throw createRequestError('家谱概览响应标识不匹配', 'GENEALOGY_RESPONSE_INVALID')
})
if (!Array.isArray(genealogyRows)) {
throw createRequestError('家谱概览响应不是列表', 'GENEALOGY_OVERVIEW_RESPONSE_INVALID')
}
const overview = genealogyRows
.map((item) => normalizeAppGenealogy(item))
.find((item) => item.id === normalizedGenealogyId)
if (!overview) throw createRequestError('当前家谱不在我的家谱中', 'GENEALOGY_OVERVIEW_RESPONSE_INVALID')
return overview
}
}
+4 -24
View File
@@ -1,23 +1,13 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeGenerationPoemBatchPayload,
normalizeGenerationPoemPreview,
normalizeGenerationPoemRows
} from './generation-poem-contract.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import { createRequestError, requestStrict } from './request-client.js'
const requireRemoteGenerationPoem = (message, code) => {
if (hasRemoteConfig()) return
throw createRequestError(message, code)
}
import { requestStrict } from './request-client.js'
export const generationPoemApi = {
async getGenerationPoems(genealogyId, requestOptions = {}) {
requireRemoteGenerationPoem(
'字辈列表需要真实读取服务,当前本地预览不会伪造字辈数据',
'REMOTE_READ_REQUIRED'
)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const generationPoems = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems`,
@@ -29,10 +19,6 @@ export const generationPoemApi = {
},
async getGenerationPoemManagement(genealogyId, requestOptions = {}) {
requireRemoteGenerationPoem(
'字辈维护列表需要真实读取服务,当前本地预览不会伪造管理权限',
'REMOTE_READ_REQUIRED'
)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const generationPoems = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/management`,
@@ -44,10 +30,6 @@ export const generationPoemApi = {
},
async previewGenerationPoemBatch(genealogyId, payload, requestOptions = {}) {
requireRemoteGenerationPoem(
'字辈批量预览需要真实服务,当前本地预览不会伪造差异结果',
'REMOTE_READ_REQUIRED'
)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const preview = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/batch/preview`,
@@ -60,17 +42,15 @@ export const generationPoemApi = {
},
async saveGenerationPoemBatch(genealogyId, payload, requestOptions = {}) {
requireRemoteGenerationPoem(
'字辈批量保存需要真实服务,当前本地预览不会伪造保存成功',
'REMOTE_WRITE_REQUIRED'
)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/generation-poems/batch/save`,
method: 'POST',
data: normalizeGenerationPoemBatchPayload(payload)
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
}
}
+46 -43
View File
@@ -9,6 +9,7 @@ import {
import {
assertPlainPayload,
normalizeOptionalNormalDisableStatus,
normalizeOptionalCurrencyNumber,
normalizeOptionalOssIdList,
normalizeOptionalSafeInteger,
normalizeOptionalText,
@@ -20,6 +21,7 @@ import {
normalizeContentProtectionCapabilities,
normalizeResourceCapabilities
} from './business-file-contract.js'
import { normalizeBusinessOptionProjection } from './business-dictionary-contract.js'
const freezeOptions = (options) =>
Object.freeze(options.map((option) => Object.freeze(option)))
@@ -49,19 +51,12 @@ export const LIFE_EVENT_DATE_PRECISION_OPTIONS = freezeOptions([
{ value: 'DAY', label: '填到日期' }
])
const GROWTH_RECORD_FALLBACK_LABELS = Object.freeze({
birth: '出生',
preschool: '学龄前',
school: '求学',
'first-step': '第一次成长',
marriage: '婚姻',
career: '事业',
other: '其他'
export const MEMO_TYPE = Object.freeze({
GENERAL: 'general',
BENEFACTOR: 'benefactor'
})
const MERIT_TYPE_LABELS = Object.freeze(
Object.fromEntries(MERIT_TYPE_OPTIONS.map(({ value, label }) => [value, label]))
)
const memoTypes = new Set(Object.values(MEMO_TYPE))
export const normalizeAppGrowthRecord = (value, expectedGenealogyId, expectedRecordId = '') => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -72,19 +67,27 @@ export const normalizeAppGrowthRecord = (value, expectedGenealogyId, expectedRec
if (!id || genealogyId !== expectedGenealogyId || (expectedRecordId && id !== expectedRecordId)) {
throw createRequestError('成长记录响应缺少稳定归属字段', 'GROWTH_RECORD_RESPONSE_INVALID')
}
const type = normalizeResponseText(value.recordType, 'recordType')
const recordType = normalizeBusinessOptionProjection(
value.recordType,
value.recordTypeLabel,
value.recordTypeOptionState,
'成长记录类型',
'GROWTH_RECORD_RESPONSE_INVALID'
)
return {
id,
genealogyId,
lineagePersonId: normalizeOptionalNumericId(value.lineagePersonId, '成长记录人物标识', 'GROWTH_RECORD_RESPONSE_INVALID'),
personName: normalizeResponseText(value.lineagePersonName, 'lineagePersonName') || '未关联人物',
type,
typeLabel: GROWTH_RECORD_FALLBACK_LABELS[type] || '',
type: recordType.value,
typeLabel: recordType.label,
typeOptionState: recordType.state,
title: normalizeResponseText(value.recordTitle, 'recordTitle') || '未命名记录',
content: normalizeResponseText(value.recordContent, 'recordContent'),
recordDate: normalizeResponseText(value.recordDate, 'recordDate'),
remindTime: normalizeResponseText(value.remindTime, 'remindTime'),
date: normalizeResponseText(value.recordDate, 'recordDate') || normalizeResponseText(value.remindTime, 'remindTime'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
mediaFiles: normalizeBusinessFileAccessRows(value.mediaFiles, '成长记录媒体', 'GROWTH_RECORD_RESPONSE_INVALID'),
sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '成长记录排序值', 'GROWTH_RECORD_RESPONSE_INVALID'),
status: normalizeNormalDisableResponseStatus(value.status, '成长记录状态', 'GROWTH_RECORD_RESPONSE_INVALID'),
@@ -102,21 +105,6 @@ export const normalizeAppGrowthRecords = (value, expectedGenealogyId) => {
return records
}
export const projectPreviewGrowthRecords = (value, expectedGenealogyId) =>
value.map((item) => ({
id: String(item.recordId),
genealogyId: expectedGenealogyId,
lineagePersonId: item.lineagePersonId ? String(item.lineagePersonId) : null,
personName: String(item.lineagePersonName || '未关联人物'),
type: String(item.recordType || ''),
typeLabel: GROWTH_RECORD_FALLBACK_LABELS[String(item.recordType || '')] || '',
title: String(item.recordTitle || '未命名记录'),
content: String(item.recordContent || ''),
date: String(item.recordDate || item.remindTime || ''),
canEdit: false,
canDelete: false
}))
const lifeEventTypes = new Set(LIFE_EVENT_TYPE_OPTIONS.map(({ value }) => value))
const lifeEventDatePrecisions = new Set(
LIFE_EVENT_DATE_PRECISION_OPTIONS.map(({ value }) => value)
@@ -198,6 +186,7 @@ export const normalizeAppRelativeRecord = (value, expectedGenealogyId, expectedR
event: normalizeResponseText(value.eventName, 'eventName'),
time: normalizeResponseText(value.eventTime, 'eventTime'),
eventTime: normalizeResponseText(value.eventTime, 'eventTime'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
amount: normalizeOptionalCurrencyAmount(value.giftAmount, '亲友礼金金额', 'RELATIVE_RECORD_RESPONSE_INVALID'),
content: normalizeResponseText(value.recordContent, 'recordContent'),
mediaFiles: normalizeBusinessFileAccessRows(value.mediaFiles, '亲友往来媒体', 'RELATIVE_RECORD_RESPONSE_INVALID'),
@@ -225,13 +214,19 @@ export const normalizeAppMemo = (value, expectedGenealogyId, expectedMemoId = ''
if (!id || genealogyId !== expectedGenealogyId || (expectedMemoId && id !== expectedMemoId)) {
throw createRequestError('备忘录响应缺少稳定归属字段', 'MEMO_RESPONSE_INVALID')
}
const memoType = normalizeResponseText(value.memoType, 'memoType')
if (!memoTypes.has(memoType)) {
throw createRequestError('备忘录响应类型无效', 'MEMO_RESPONSE_INVALID')
}
return {
id,
genealogyId,
memoType,
title: normalizeResponseText(value.memoTitle, 'memoTitle') || '未命名备忘',
remindTime: normalizeResponseText(value.remindTime, 'remindTime'),
content: normalizeResponseText(value.memoContent, 'memoContent'),
completed: normalizeResponseText(value.completed, 'completed'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
mediaFiles: normalizeBusinessFileAccessRows(value.mediaFiles, '备忘媒体', 'MEMO_RESPONSE_INVALID'),
sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '备忘排序值', 'MEMO_RESPONSE_INVALID'),
status: normalizeNormalDisableResponseStatus(value.status, '备忘状态', 'MEMO_RESPONSE_INVALID'),
@@ -257,18 +252,26 @@ export const normalizeAppMeritRecord = (value, expectedGenealogyId, expectedMeri
if (!id || genealogyId !== expectedGenealogyId || (expectedMeritId && id !== expectedMeritId)) {
throw createRequestError('功德记录响应缺少稳定归属字段', 'MERIT_RECORD_RESPONSE_INVALID')
}
const type = normalizeResponseText(value.meritType, 'meritType')
const typeLabel = type ? (MERIT_TYPE_LABELS[type] || type) : ''
const meritType = normalizeBusinessOptionProjection(
value.meritType,
value.meritTypeLabel,
value.meritTypeOptionState,
'功德类型',
'MERIT_RECORD_RESPONSE_INVALID'
)
return {
id,
genealogyId,
donor: normalizeResponseText(value.donorName, 'donorName') || '未署名',
title: normalizeResponseText(value.meritTitle, 'meritTitle') || '未命名功德',
type,
typeLabel,
type: meritType.value,
typeLabel: meritType.label,
typeOptionState: meritType.state,
amount: normalizeOptionalCurrencyAmount(value.amount, '功德金额', 'MERIT_RECORD_RESPONSE_INVALID', { required: true }),
time: normalizeResponseText(value.meritTime, 'meritTime'),
createTime: normalizeResponseText(value.createTime, 'createTime'),
content: normalizeResponseText(value.meritContent, 'meritContent'),
mediaFiles: normalizeBusinessFileAccessRows(value.mediaFiles, '功德记录图片', 'MERIT_RECORD_RESPONSE_INVALID'),
sortOrder: normalizeOptionalNonnegativeInteger(value.sortOrder, '功德记录排序值', 'MERIT_RECORD_RESPONSE_INVALID'),
status: normalizeNormalDisableResponseStatus(value.status, '功德记录状态', 'MERIT_RECORD_RESPONSE_INVALID'),
...normalizeResourceCapabilities(value, '功德记录', 'MERIT_RECORD_RESPONSE_INVALID')
@@ -297,10 +300,7 @@ export const normalizeRelativeRecordCreatePayload = (payload) => {
const status = normalizeOptionalNormalDisableStatus(payload.status, '亲友往来状态')
if (status) normalizedPayload.status = status
if (payload.giftAmount !== undefined && payload.giftAmount !== '' && payload.giftAmount !== null) {
const giftAmount = typeof payload.giftAmount === 'number' ? payload.giftAmount : Number(payload.giftAmount)
if (!Number.isFinite(giftAmount)) {
throw new TypeError('亲友往来礼金金额必须是有限数字')
}
const giftAmount = normalizeOptionalCurrencyNumber(payload.giftAmount, '亲友往来礼金金额')
normalizedPayload.giftAmount = giftAmount
}
const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds)
@@ -326,7 +326,7 @@ export const normalizeGrowthRecordCreatePayload = (payload) => {
normalizedPayload.lineagePersonId = normalizeResourcePathId(payload.lineagePersonId, '关联人物标识')
}
const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds)
if (mediaOssIds) normalizedPayload.mediaOssIds = mediaOssIds
if (mediaOssIds || payload.mediaOssIds === '') normalizedPayload.mediaOssIds = mediaOssIds || ''
const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder')
if (sortOrder !== undefined) normalizedPayload.sortOrder = sortOrder
return normalizedPayload
@@ -357,11 +357,13 @@ export const normalizeLifeEventPayload = (payload) => {
}
export const normalizeMemoCreatePayload = (payload) => {
const allowedFields = new Set(['memoTitle', 'memoContent', 'remindTime', 'completed', 'mediaOssIds', 'sortOrder', 'status'])
const allowedFields = new Set(['memoType', 'memoTitle', 'memoContent', 'remindTime', 'completed', 'mediaOssIds', 'sortOrder', 'status'])
assertPlainPayload(payload, allowedFields, '备忘请求')
const memoTitle = typeof payload.memoTitle === 'string' ? payload.memoTitle.trim() : ''
if (!memoTitle) throw new TypeError('备忘标题不能为空')
const normalizedPayload = { memoTitle }
const memoType = normalizeOptionalText(payload.memoType, 'memoType') || MEMO_TYPE.GENERAL
if (!memoTypes.has(memoType)) throw new TypeError('备忘类型无效')
const normalizedPayload = { memoType, memoTitle }
for (const field of ['memoContent', 'remindTime', 'completed']) {
const normalizedText = normalizeOptionalText(payload[field], field)
if (normalizedText) normalizedPayload[field] = normalizedText
@@ -376,7 +378,7 @@ export const normalizeMemoCreatePayload = (payload) => {
}
export const normalizeMeritRecordCreatePayload = (payload) => {
const allowedFields = new Set(['donorName', 'meritTitle', 'meritType', 'meritContent', 'amount', 'meritTime', 'sortOrder', 'status'])
const allowedFields = new Set(['donorName', 'meritTitle', 'meritType', 'meritContent', 'amount', 'meritTime', 'mediaOssIds', 'sortOrder', 'status'])
assertPlainPayload(payload, allowedFields, '功德记录请求')
const donorName = typeof payload.donorName === 'string' ? payload.donorName.trim() : ''
const meritTitle = typeof payload.meritTitle === 'string' ? payload.meritTitle.trim() : ''
@@ -389,10 +391,11 @@ export const normalizeMeritRecordCreatePayload = (payload) => {
const status = normalizeOptionalNormalDisableStatus(payload.status, '功德记录状态')
if (status) normalizedPayload.status = status
if (payload.amount !== undefined && payload.amount !== '' && payload.amount !== null) {
const amount = typeof payload.amount === 'number' ? payload.amount : Number(payload.amount)
if (!Number.isFinite(amount)) throw new TypeError('功德金额必须是有限数字')
const amount = normalizeOptionalCurrencyNumber(payload.amount, '功德金额')
normalizedPayload.amount = amount
}
const mediaOssIds = normalizeOptionalOssIdList(payload.mediaOssIds)
if (mediaOssIds || payload.mediaOssIds === '') normalizedPayload.mediaOssIds = mediaOssIds || ''
const sortOrder = normalizeOptionalSafeInteger(payload.sortOrder, 'sortOrder')
if (sortOrder !== undefined) normalizedPayload.sortOrder = sortOrder
return normalizedPayload
+14 -52
View File
@@ -1,5 +1,3 @@
import { listPreviewGrowthRecords } from '@/data/preview/records.js'
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import {
normalizeAppGrowthRecord,
@@ -16,8 +14,7 @@ import {
normalizeLifeEventPayload,
normalizeMemoCreatePayload,
normalizeMeritRecordCreatePayload,
normalizeRelativeRecordCreatePayload,
projectPreviewGrowthRecords
normalizeRelativeRecordCreatePayload
} from './life-record-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
import {
@@ -25,22 +22,15 @@ import {
normalizeContentAccessGrant,
normalizeContentPasswordPayload
} from './protected-content-contract.js'
import { createRequestError, requestStrict } from './request-client.js'
import { requestStrict } from './request-client.js'
const requireRemoteLifeRecord = (message, code) => {
if (hasRemoteConfig()) return
throw createRequestError(message, code)
}
const writeLifeRecord = (url, method, data, label, requestOptions) => {
requireRemoteLifeRecord(`${label}需要真实服务,当前本地预览不会伪造操作成功`, 'REMOTE_WRITE_REQUIRED')
const writeLifeRecord = (url, method, data, requestOptions) => {
return requestStrict({ url, method, data }, {
requestController: requestOptions.requestController ?? null
})
}
const deleteLifeRecord = async (url, label, requestOptions) => {
requireRemoteLifeRecord(`${label}需要真实服务,当前本地预览不会伪造删除成功`, 'REMOTE_WRITE_REQUIRED')
const deleteLifeRecord = async (url, requestOptions) => {
await requestStrict({ url, method: 'DELETE' }, {
requireData: false,
requestController: requestOptions.requestController ?? null
@@ -50,7 +40,6 @@ const deleteLifeRecord = async (url, label, requestOptions) => {
export const lifeRecordApi = {
async createRelativeRecord(genealogyId, payload, requestOptions = {}) {
requireRemoteLifeRecord('亲友往来创建需要真实服务,当前本地预览不会伪造创建成功', 'REMOTE_WRITE_REQUIRED')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records`,
@@ -62,7 +51,6 @@ export const lifeRecordApi = {
async updateRelativeRecord(genealogyId, relativeId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedRelativeId = normalizeResourcePathId(relativeId, '亲友记录标识')
requireRemoteLifeRecord('修改亲友记录需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
const relativeRecord = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records/${normalizedRelativeId}`,
method: 'PUT',
@@ -76,14 +64,12 @@ export const lifeRecordApi = {
const normalizedRelativeId = normalizeResourcePathId(relativeId, '亲友记录标识')
return deleteLifeRecord(
`/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records/${normalizedRelativeId}`,
'删除亲友记录',
requestOptions
)
},
async getRelativeRecords(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteLifeRecord('亲友记录需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED')
const relativeRecords = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records`,
method: 'GET'
@@ -94,7 +80,6 @@ export const lifeRecordApi = {
async getRelativeRecordDetail(genealogyId, relativeId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedRelativeId = normalizeResourcePathId(relativeId, '亲友记录标识')
requireRemoteLifeRecord('亲友记录详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
const relativeRecord = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/relative-records/${normalizedRelativeId}`,
method: 'GET'
@@ -103,7 +88,6 @@ export const lifeRecordApi = {
},
async createGrowthRecord(genealogyId, payload, requestOptions = {}) {
requireRemoteLifeRecord('成长记录创建需要真实服务,当前本地预览不会伪造创建成功', 'REMOTE_WRITE_REQUIRED')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records`,
@@ -115,7 +99,6 @@ export const lifeRecordApi = {
async updateGrowthRecord(genealogyId, recordId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedRecordId = normalizeResourcePathId(recordId, '成长记录标识')
requireRemoteLifeRecord('修改成长记录需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
const growthRecord = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}`,
method: 'PUT',
@@ -129,19 +112,12 @@ export const lifeRecordApi = {
const normalizedRecordId = normalizeResourcePathId(recordId, '成长记录标识')
return deleteLifeRecord(
`/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}`,
'删除成长记录',
requestOptions
)
},
async getGrowthRecords(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
if (!hasRemoteConfig()) {
return projectPreviewGrowthRecords(
listPreviewGrowthRecords(normalizedGenealogyId),
normalizedGenealogyId
)
}
const growthRecords = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records`,
method: 'GET'
@@ -152,7 +128,6 @@ export const lifeRecordApi = {
async getGrowthRecordDetail(genealogyId, recordId, accessToken = '', requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedRecordId = normalizeResourcePathId(recordId, '成长记录标识')
requireRemoteLifeRecord('成长记录详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
const growthRecord = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}`,
method: 'GET',
@@ -165,13 +140,14 @@ export const lifeRecordApi = {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedRecordId = normalizeResourcePathId(recordId, '成长记录标识')
const passwordPayload = normalizeContentPasswordPayload(password)
await writeLifeRecord(
`/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}/content-protection`,
'PUT',
passwordPayload,
'成长记录内容密码设置',
requestOptions
)
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}/content-protection`,
method: 'PUT',
data: passwordPayload
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
},
@@ -183,7 +159,6 @@ export const lifeRecordApi = {
`/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}/content-unlock`,
'POST',
passwordPayload,
'成长记录内容解锁',
requestOptions
)
return normalizeContentAccessGrant(accessGrant)
@@ -194,7 +169,6 @@ export const lifeRecordApi = {
const normalizedRecordId = normalizeResourcePathId(recordId, '成长记录标识')
return deleteLifeRecord(
`/genealogy/app/genealogies/${normalizedGenealogyId}/growth-records/${normalizedRecordId}/content-protection`,
'成长记录内容密码停用',
requestOptions
)
},
@@ -202,7 +176,6 @@ export const lifeRecordApi = {
async createLifeEvent(genealogyId, lineagePersonId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedLineagePersonId = normalizeResourcePathId(lineagePersonId, '人物标识')
requireRemoteLifeRecord('人生大事创建需要真实服务,当前本地预览不会伪造创建成功', 'REMOTE_WRITE_REQUIRED')
const lifeEvent = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage-persons/${normalizedLineagePersonId}/life-events`,
method: 'POST',
@@ -220,7 +193,6 @@ export const lifeRecordApi = {
`/genealogy/app/genealogies/${normalizedGenealogyId}/lineage-persons/${normalizedLineagePersonId}/life-events/${normalizedEventId}`,
'PUT',
lifeEventChanges,
'人生大事更新',
requestOptions
)
return normalizeAppLifeEvent(lifeEvent, normalizedGenealogyId, normalizedLineagePersonId, normalizedEventId)
@@ -232,7 +204,6 @@ export const lifeRecordApi = {
const normalizedEventId = normalizeResourcePathId(eventId, '人生大事标识')
return deleteLifeRecord(
`/genealogy/app/genealogies/${normalizedGenealogyId}/lineage-persons/${normalizedLineagePersonId}/life-events/${normalizedEventId}`,
'人生大事删除',
requestOptions
)
},
@@ -240,7 +211,6 @@ export const lifeRecordApi = {
async getLifeEvents(genealogyId, lineagePersonId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedLineagePersonId = normalizeResourcePathId(lineagePersonId, '人物标识')
requireRemoteLifeRecord('人生大事需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED')
const lifeEvents = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage-persons/${normalizedLineagePersonId}/life-events`,
method: 'GET'
@@ -249,7 +219,6 @@ export const lifeRecordApi = {
},
async createMemo(genealogyId, payload, requestOptions = {}) {
requireRemoteLifeRecord('备忘创建需要真实服务,当前本地预览不会伪造创建成功', 'REMOTE_WRITE_REQUIRED')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/memos`,
@@ -261,7 +230,6 @@ export const lifeRecordApi = {
async updateMemo(genealogyId, memoId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedMemoId = normalizeResourcePathId(memoId, '备忘标识')
requireRemoteLifeRecord('修改备忘需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
const memo = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/memos/${normalizedMemoId}`,
method: 'PUT',
@@ -273,12 +241,11 @@ export const lifeRecordApi = {
async deleteMemo(genealogyId, memoId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedMemoId = normalizeResourcePathId(memoId, '备忘标识')
return deleteLifeRecord(`/genealogy/app/genealogies/${normalizedGenealogyId}/memos/${normalizedMemoId}`, '删除备忘', requestOptions)
return deleteLifeRecord(`/genealogy/app/genealogies/${normalizedGenealogyId}/memos/${normalizedMemoId}`, requestOptions)
},
async getMemos(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteLifeRecord('备忘列表需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED')
const memos = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/memos`,
method: 'GET'
@@ -289,7 +256,6 @@ export const lifeRecordApi = {
async getMemoDetail(genealogyId, memoId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedMemoId = normalizeResourcePathId(memoId, '备忘标识')
requireRemoteLifeRecord('备忘详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
const memo = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/memos/${normalizedMemoId}`,
method: 'GET'
@@ -298,7 +264,6 @@ export const lifeRecordApi = {
},
async createMeritRecord(genealogyId, payload, requestOptions = {}) {
requireRemoteLifeRecord('功德记录创建需要真实服务,当前本地预览不会伪造创建成功', 'REMOTE_WRITE_REQUIRED')
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records`,
@@ -310,7 +275,6 @@ export const lifeRecordApi = {
async updateMeritRecord(genealogyId, meritId, payload, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedMeritId = normalizeResourcePathId(meritId, '功德记录标识')
requireRemoteLifeRecord('修改功德记录需要真实服务,当前本地预览不会伪造保存成功', 'REMOTE_WRITE_REQUIRED')
const meritRecord = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records/${normalizedMeritId}`,
method: 'PUT',
@@ -322,12 +286,11 @@ export const lifeRecordApi = {
async deleteMeritRecord(genealogyId, meritId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedMeritId = normalizeResourcePathId(meritId, '功德记录标识')
return deleteLifeRecord(`/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records/${normalizedMeritId}`, '删除功德记录', requestOptions)
return deleteLifeRecord(`/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records/${normalizedMeritId}`, requestOptions)
},
async getMeritRecords(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteLifeRecord('功德记录需要真实读取服务,当前本地预览不会伪造列表', 'REMOTE_READ_REQUIRED')
const meritRecords = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records`,
method: 'GET'
@@ -338,7 +301,6 @@ export const lifeRecordApi = {
async getMeritRecordDetail(genealogyId, meritId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedMeritId = normalizeResourcePathId(meritId, '功德记录标识')
requireRemoteLifeRecord('功德记录详情需要真实读取服务,当前本地预览不会伪造详情', 'REMOTE_READ_REQUIRED')
const meritRecord = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/merit-records/${normalizedMeritId}`,
method: 'GET'
+83 -1
View File
@@ -1,6 +1,7 @@
import { normalizeOptionalSafeInteger } from './request-normalizers.js'
import { createRequestError } from './request-client.js'
import { LINEAGE_PERSON_OPTIONS } from './lineage-person-options.js'
import { normalizeBusinessOptionProjection } from './business-dictionary-contract.js'
const optionLabels = (options) => Object.freeze(
Object.fromEntries(options.map(({ value, label }) => [value, label]))
@@ -89,14 +90,47 @@ export const normalizeLineagePersonDetail = (value, expectedGenealogyId, expecte
}
const personNo = normalizeLineagePersonText(value.personNo, '人物编号')
const generationName = normalizeLineagePersonText(value.generationName, '字辈')
const aliasName = normalizeLineagePersonText(value.aliasName, '别名或曾用名')
const courtesyName = normalizeLineagePersonText(value.courtesyName, '表字')
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, '出生农历')
const deathLunar = normalizeLineagePersonText(value.deathLunar, '逝世农历')
const birthPlace = normalizeLineagePersonText(value.birthPlace, '出生地')
const zodiacOption = normalizeBusinessOptionProjection(
value.zodiacCode,
value.zodiacLabel,
value.zodiacOptionState,
'成员生肖',
'LINEAGE_PERSON_RESPONSE_INVALID'
)
const currentAddress = normalizeLineagePersonText(value.currentAddress, '现居地')
const mobile = normalizeLineagePersonText(value.mobile, '联系电话')
const email = normalizeLineagePersonText(value.email, '电子邮箱')
const educationOption = normalizeBusinessOptionProjection(
value.educationCode,
value.educationLabel,
value.educationOptionState,
'成员学历分类',
'LINEAGE_PERSON_RESPONSE_INVALID'
)
const occupation = normalizeLineagePersonText(value.occupation, '职业')
const deathAge = value.deathAge === undefined || value.deathAge === null
? null
: normalizeOptionalSafeInteger(value.deathAge, '人物享年')
if (deathAge !== null && (deathAge < 0 || deathAge > 200)) {
throw lineagePersonError('成员详情享年无效')
}
const deathPlace = normalizeLineagePersonText(value.deathPlace, '逝世地')
const deathExpressionOption = normalizeBusinessOptionProjection(
value.deathExpressionCode,
value.deathExpressionLabel,
value.deathExpressionOptionState,
'成员逝世表述',
'LINEAGE_PERSON_RESPONSE_INVALID'
)
const burialDate = normalizeLineagePersonDate(value.burialDate, '安葬日期')
const burialPlace = normalizeLineagePersonText(value.burialPlace, '安葬地')
const spouseNames = normalizeLineageSpouseNames(value.spouseNames)
const personStatus = normalizeLineagePersonText(value.personStatus, '人物状态')
@@ -107,6 +141,10 @@ export const normalizeLineagePersonDetail = (value, expectedGenealogyId, expecte
const deathLunarLabel = normalizeLineagePersonDictionaryLabel(deathLunar, '逝世农历', LINEAGE_BOOLEAN_LABELS)
const biography = normalizeLineagePersonText(value.biography, '生平')
const remark = normalizeLineagePersonText(value.remark, '备注')
const canManageSensitiveMedicalHistory = value.canManageSensitiveMedicalHistory === true
if (value.canManageSensitiveMedicalHistory !== undefined && typeof value.canManageSensitiveMedicalHistory !== 'boolean') {
throw lineagePersonError('成员详情敏感病史权限无效')
}
const relationName = normalizeLineagePersonText(value.relationName, '关系显示名称')
for (const field of ['canDisable', 'canCreateDocument', 'canManageDocuments']) {
if (value[field] !== undefined && typeof value[field] !== 'boolean') {
@@ -140,6 +178,7 @@ export const normalizeLineagePersonDetail = (value, expectedGenealogyId, expecte
appUserId,
bindingMode,
personNo,
courtesyName,
aliasName,
generation: value.generation,
generationName,
@@ -158,13 +197,29 @@ export const normalizeLineagePersonDetail = (value, expectedGenealogyId, expecte
deathLunarLabel,
years: birthDate || deathDate ? `${birthDate}${deathDate}` : '生卒待补',
birthplace: birthPlace,
zodiac: zodiacOption.label,
zodiacCode: zodiacOption.value,
zodiacOptionState: zodiacOption.state,
currentAddress,
mobile,
email,
education: educationOption.label,
educationCode: educationOption.value,
educationOptionState: educationOption.state,
occupation,
deathAge,
deathPlace,
deathType: deathExpressionOption.label,
deathExpressionCode: deathExpressionOption.value,
deathExpressionOptionState: deathExpressionOption.state,
burialDate,
burialPlace,
spouseNames,
personStatus,
personStatusLabel,
biography,
remark,
canManageSensitiveMedicalHistory,
relationName,
sortOrder,
status: personStatus === '1' ? 'deceased' : 'normal',
@@ -176,6 +231,33 @@ export const normalizeLineagePersonDetail = (value, expectedGenealogyId, expecte
}
}
export const normalizeLineageSensitiveProfile = (value, expectedPersonId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw lineagePersonError('成员敏感健康资料响应不是对象')
}
const personId = normalizeLineagePersonIdentity(value.personId, '敏感健康资料人物标识')
if (personId !== expectedPersonId) {
throw lineagePersonError('成员敏感健康资料响应标识与请求不匹配')
}
if (typeof value.present !== 'boolean' || typeof value.canManage !== 'boolean') {
throw lineagePersonError('成员敏感健康资料状态无效')
}
const hereditaryMedicalHistory = normalizeLineagePersonText(
value.hereditaryMedicalHistory,
'遗传病史'
)
if (!value.present && hereditaryMedicalHistory) {
throw lineagePersonError('成员敏感健康资料状态与正文不一致')
}
return {
personId,
hereditaryMedicalHistory,
present: value.present,
canManage: value.canManage,
updatedAt: normalizeLineagePersonText(value.updatedAt, '敏感健康资料更新时间')
}
}
export const normalizeLineagePersonPage = (value, expectedGenealogyId) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw lineagePersonError('成员分页响应不是对象')
+45 -44
View File
@@ -1,10 +1,10 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import { normalizeGenealogyPathId } from './genealogy-contract.js'
import {
normalizeLineagePersonDetail,
normalizeLineagePersonIdentity,
normalizeLineagePersonOptions,
normalizeLineagePersonPage
normalizeLineagePersonPage,
normalizeLineageSensitiveProfile
} from './lineage-person-contract.js'
import { normalizeLineageTree } from './lineage-tree-contract.js'
import {
@@ -12,19 +12,10 @@ import {
normalizeLineageWritePayload
} from './lineage-write-contract.js'
import { normalizeResourcePathId } from './request-normalizers.js'
import { createRequestError, requestStrict } from './request-client.js'
const requireRemoteLineage = (message, code) => {
if (hasRemoteConfig()) return
throw createRequestError(message, code)
}
import { requestStrict } from './request-client.js'
export const lineageApi = {
async getTree(genealogyId, requestOptions = {}) {
requireRemoteLineage(
'世系树需要真实读取服务,当前本地预览不会伪造人物节点',
'REMOTE_READ_REQUIRED'
)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const tree = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/tree`,
@@ -36,10 +27,6 @@ export const lineageApi = {
},
async getPerson(genealogyId, personId, requestOptions = {}) {
requireRemoteLineage(
'人物详情需要真实读取服务,当前本地预览不会伪造成员资料',
'REMOTE_READ_REQUIRED'
)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
const person = await requestStrict({
@@ -52,10 +39,6 @@ export const lineageApi = {
},
async getPersonPage(genealogyId, { pageNum = 1, pageSize = 10, keyword = '' } = {}, requestOptions = {}) {
requireRemoteLineage(
'成员目录需要真实读取服务,当前本地预览不会伪造目录数据',
'REMOTE_READ_REQUIRED'
)
if (!Number.isSafeInteger(pageNum) || pageNum < 1) throw new TypeError('成员目录页码无效')
if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new TypeError('成员目录页大小无效')
if (typeof keyword !== 'string') throw new TypeError('成员目录关键词无效')
@@ -73,10 +56,6 @@ export const lineageApi = {
},
async createPerson(genealogyId, payload, requestOptions = {}) {
requireRemoteLineage(
'人物创建接口在本地预览模式不可用,当前内容不会保存',
'WRITE_UNAVAILABLE'
)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
return requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons`,
@@ -88,10 +67,6 @@ export const lineageApi = {
},
async createRelatedPerson(genealogyId, personId, relationType, payload, requestOptions = {}) {
requireRemoteLineage(
'人物关系写入接口在本地预览模式不可用,当前内容不会保存',
'WRITE_UNAVAILABLE'
)
const relationPath = lineageRelationPath[relationType]
if (!relationPath) throw new TypeError('人物关系类型不属于当前合同')
@@ -107,10 +82,6 @@ export const lineageApi = {
},
async updatePerson(genealogyId, personId, payload, requestOptions = {}) {
requireRemoteLineage(
'人物编辑接口在本地预览模式不可用,当前内容不会保存',
'WRITE_UNAVAILABLE'
)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
return requestStrict({
@@ -122,13 +93,51 @@ export const lineageApi = {
})
},
async getSensitiveProfile(genealogyId, personId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
const profile = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}/sensitive-profile`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineageSensitiveProfile(profile, normalizedPersonId)
},
async saveSensitiveProfile(genealogyId, personId, hereditaryMedicalHistory, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
const normalizedHistory = typeof hereditaryMedicalHistory === 'string'
? hereditaryMedicalHistory.trim()
: ''
if (!normalizedHistory) throw new TypeError('遗传病史不能为空')
if (normalizedHistory.length > 500) throw new TypeError('遗传病史长度超出当前页面合同')
const profile = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}/sensitive-profile`,
method: 'PUT',
data: { hereditaryMedicalHistory: normalizedHistory }
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineageSensitiveProfile(profile, normalizedPersonId)
},
async clearSensitiveProfile(genealogyId, personId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
const profile = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}/sensitive-profile`,
method: 'DELETE'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeLineageSensitiveProfile(profile, normalizedPersonId)
},
async deletePerson(genealogyId, personId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeResourcePathId(personId, '人物标识')
requireRemoteLineage(
'世系人物写入需要真实服务,当前本地预览不会伪造结果',
'REMOTE_WRITE_REQUIRED'
)
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/${normalizedPersonId}`,
method: 'DELETE'
@@ -140,10 +149,6 @@ export const lineageApi = {
},
async updatePersonSortOrder(genealogyId, personId, sortOrder, requestOptions = {}) {
requireRemoteLineage(
'排行调整接口在本地预览模式不可用,当前内容不会保存',
'WRITE_UNAVAILABLE'
)
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
const normalizedPersonId = normalizeLineagePersonIdentity(personId, '人物标识')
const normalizedSortOrder = Number(sortOrder)
@@ -161,10 +166,6 @@ export const lineageApi = {
async getLineagePersonOptions(genealogyId, requestOptions = {}) {
const normalizedGenealogyId = normalizeGenealogyPathId(genealogyId)
requireRemoteLineage(
'世系人物选项需要真实读取服务,当前本地预览不会伪造候选项',
'REMOTE_READ_REQUIRED'
)
const options = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/lineage/persons/options`,
method: 'GET'
+39 -5
View File
@@ -23,6 +23,7 @@ export const normalizeLineageWritePayload = (payload) => {
'appUserId',
'personNo',
'name',
'courtesyName',
'aliasName',
'sex',
'generationName',
@@ -32,13 +33,23 @@ export const normalizeLineageWritePayload = (payload) => {
'birthDate',
'birthLunar',
'birthPlace',
'zodiacCode',
'currentAddress',
'mobile',
'email',
'educationCode',
'occupation',
'deathDate',
'deathLunar',
'deathAge',
'deathPlace',
'deathExpressionCode',
'burialDate',
'burialPlace',
'biography',
'remark',
'relationName',
'relationVariantCode',
'generation',
'personStatus',
'sortOrder'
@@ -79,15 +90,24 @@ export const normalizeLineageWritePayload = (payload) => {
['personNo', '人物编号', null],
['sex', '性别', null],
['generationName', '字辈', 12],
['aliasName', '别名或曾用名', null],
['courtesyName', '表字', 40],
['aliasName', '别号或曾用名', 40],
['birthLunar', '出生农历', null],
['birthPlace', '出生地', null],
['birthPlace', '出生地', 120],
['zodiacCode', '生肖编码', 60],
['currentAddress', '现居地', 120],
['mobile', '联系电话', 30],
['email', '电子邮箱', 254],
['educationCode', '学历分类编码', 60],
['occupation', '职业', 80],
['deathLunar', '逝世农历', null],
['deathPlace', '逝世地', null],
['burialPlace', '安葬地', null],
['deathPlace', '逝世地', 120],
['deathExpressionCode', '逝世表述编码', 60],
['burialPlace', '安葬地', 120],
['biography', '人物简介', 500],
['remark', '备注', null],
['relationName', '关系显示名称', null],
['relationVariantCode', '关系显示修饰编码', 60],
['personStatus', '人物状态', null]
]) {
if (payload[field] === undefined) continue
@@ -98,9 +118,13 @@ export const normalizeLineageWritePayload = (payload) => {
}
if (normalizedText) normalizedPayload[field] = normalizedText
}
if (normalizedPayload.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedPayload.email)) {
throw new TypeError('人物电子邮箱格式无效')
}
for (const [field, label] of [
['birthDate', '出生日期'],
['deathDate', '离世日期']
['deathDate', '离世日期'],
['burialDate', '安葬日期']
]) {
if (payload[field] === undefined) continue
const normalizedDate = normalizeLineagePersonDate(payload[field], label)
@@ -109,6 +133,16 @@ export const normalizeLineageWritePayload = (payload) => {
if (normalizedPayload.birthDate && normalizedPayload.deathDate && normalizedPayload.deathDate < normalizedPayload.birthDate) {
throw new TypeError('人物离世日期不能早于出生日期')
}
if (normalizedPayload.deathDate && normalizedPayload.burialDate && normalizedPayload.burialDate < normalizedPayload.deathDate) {
throw new TypeError('人物安葬日期不能早于离世日期')
}
if (payload.deathAge !== undefined && payload.deathAge !== null && payload.deathAge !== '') {
const deathAge = typeof payload.deathAge === 'number' ? payload.deathAge : Number(payload.deathAge)
if (!Number.isSafeInteger(deathAge) || deathAge < 0 || deathAge > 200) {
throw new TypeError('人物享年必须是 0 至 200 的整数')
}
normalizedPayload.deathAge = deathAge
}
if (payload.sortOrder !== undefined && payload.sortOrder !== null && payload.sortOrder !== '') {
const sortOrder = typeof payload.sortOrder === 'number' ? payload.sortOrder : Number(payload.sortOrder)
if (!Number.isSafeInteger(sortOrder)) throw new TypeError('人物排序值必须是安全整数')
-14
View File
@@ -1,4 +1,3 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeNotificationDetail,
normalizeNotifications
@@ -9,17 +8,8 @@ import {
requestStrict
} from './request-client.js'
const requireRemoteNotifications = (label, operation = '读取') => {
if (hasRemoteConfig()) return
throw createRequestError(
`${label}${operation}需要真实服务,当前本地预览不会伪造结果`,
operation === '读取' ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED'
)
}
export const notificationApi = {
async getNotifications(requestOptions = {}) {
requireRemoteNotifications('消息通知')
const notificationRows = await requestStrict({
url: '/genealogy/app/notifications',
method: 'GET'
@@ -31,7 +21,6 @@ export const notificationApi = {
async getNotificationDetail(notificationId, requestOptions = {}) {
const normalizedNotificationId = normalizeResourcePathId(notificationId, '通知标识')
requireRemoteNotifications('通知详情')
const notificationDetail = await requestStrict({
url: `/genealogy/app/notifications/${normalizedNotificationId}`,
method: 'GET'
@@ -42,7 +31,6 @@ export const notificationApi = {
},
async getUnreadNotificationCount(requestOptions = {}) {
requireRemoteNotifications('未读通知数量')
const unreadCount = await requestStrict({
url: '/genealogy/app/notifications/unread-count',
method: 'GET'
@@ -57,7 +45,6 @@ export const notificationApi = {
async markNotificationRead(notificationId, requestOptions = {}) {
const normalizedNotificationId = normalizeResourcePathId(notificationId, '通知标识')
requireRemoteNotifications('通知已读状态', '写入')
await requestStrict({
url: `/genealogy/app/notifications/${normalizedNotificationId}/read`,
method: 'POST'
@@ -69,7 +56,6 @@ export const notificationApi = {
},
async markAllNotificationsRead(requestOptions = {}) {
requireRemoteNotifications('全部通知已读状态', '写入')
await requestStrict({
url: '/genealogy/app/notifications/read-all',
method: 'POST'
+12 -9
View File
@@ -9,6 +9,7 @@ import {
normalizeOptionalText,
normalizeResourcePathId
} from './request-normalizers.js'
import { normalizeBusinessOptionProjection } from './business-dictionary-contract.js'
const normalizePersonDocumentText = (value, field) =>
normalizeOptionalResponseText(value, field, '重要证件响应', 'PERSON_DOCUMENT_RESPONSE_INVALID')
@@ -33,9 +34,6 @@ export const PERSON_DOCUMENT_RESOURCE_USAGE_LABELS = Object.freeze({
[PERSON_DOCUMENT_RESOURCE_USAGE.ATTACHMENT]: '附件'
})
const personDocumentTypes = new Set(
PERSON_DOCUMENT_TYPE_OPTIONS.map(({ value }) => value)
)
const personDocumentResourceUsages = new Set(
Object.values(PERSON_DOCUMENT_RESOURCE_USAGE)
)
@@ -64,10 +62,13 @@ export const normalizePersonDocument = (value, expectedGenealogyId) => {
if (genealogyId !== expectedGenealogyId) {
throw createRequestError('重要证件归属与请求不匹配', 'PERSON_DOCUMENT_RESPONSE_INVALID')
}
const documentType = normalizePersonDocumentText(value.documentType, 'documentType')
if (!personDocumentTypes.has(documentType)) {
throw createRequestError('重要证件类型无效', 'PERSON_DOCUMENT_RESPONSE_INVALID')
}
const documentType = normalizeBusinessOptionProjection(
value.documentType,
value.documentTypeLabel,
value.documentTypeOptionState,
'重要证件类型',
'PERSON_DOCUMENT_RESPONSE_INVALID'
)
const documentTitle = normalizePersonDocumentText(value.documentTitle, 'documentTitle')
if (!documentTitle) throw createRequestError('重要证件缺少标题', 'PERSON_DOCUMENT_RESPONSE_INVALID')
for (const field of ['contentProtected', 'contentUnlocked', 'canEdit', 'canDelete']) {
@@ -83,7 +84,9 @@ export const normalizePersonDocument = (value, expectedGenealogyId) => {
genealogyId,
lineagePersonId: normalizeResourcePathId(value.lineagePersonId, '世系人物标识'),
lineagePersonName: normalizePersonDocumentText(value.lineagePersonName, 'lineagePersonName'),
documentType,
documentType: documentType.value,
documentTypeLabel: documentType.label,
documentTypeOptionState: documentType.state,
documentTitle,
maskedIdentifier: normalizePersonDocumentText(value.maskedIdentifier, 'maskedIdentifier'),
description: normalizePersonDocumentText(value.description, 'description'),
@@ -110,7 +113,7 @@ export const normalizePersonDocuments = (value, expectedGenealogyId) => {
export const normalizePersonDocumentPayload = (payload) => {
assertPlainPayload(payload, new Set(['lineagePersonId', 'documentType', 'documentTitle', 'maskedIdentifier', 'description', 'sortOrder', 'status']), '重要证件请求')
const documentType = normalizeOptionalText(payload.documentType, 'documentType')
if (!personDocumentTypes.has(documentType)) throw new TypeError('请选择有效的证件类型')
if (!documentType) throw new TypeError('请选择有效的证件类型')
const documentTitle = normalizeOptionalText(payload.documentTitle, 'documentTitle')
if (!documentTitle) throw new TypeError('请填写证件标题')
if (documentTitle.length > 100) throw new TypeError('证件标题不能超过100个字符')
+11 -40
View File
@@ -1,4 +1,3 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeBusinessFileAccess
} from './business-file-contract.js'
@@ -16,35 +15,15 @@ import {
normalizeContentAccessGrant,
normalizeContentPasswordPayload
} from './protected-content-contract.js'
import { createRequestError, requestStrict } from './request-client.js'
import { requestStrict } from './request-client.js'
const requireRemotePersonDocument = (operation) => {
if (hasRemoteConfig()) return
throw createRequestError(
`重要证件${operation}需要真实服务,当前本地预览不会伪造结果`,
operation === '读取' ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED'
)
}
const writePersonDocument = (url, method, data, label, requestOptions) => {
if (!hasRemoteConfig()) {
throw createRequestError(
`${label}写入需要真实服务,当前本地预览不会伪造结果`,
'REMOTE_WRITE_REQUIRED'
)
}
const writePersonDocument = (url, method, data, requestOptions) => {
return requestStrict({ url, method, data }, {
requestController: requestOptions.requestController ?? null
})
}
const deletePersonDocumentEndpoint = async (url, label, requestOptions) => {
if (!hasRemoteConfig()) {
throw createRequestError(
`${label}写入需要真实服务,当前本地预览不会伪造结果`,
'REMOTE_WRITE_REQUIRED'
)
}
const deletePersonDocumentEndpoint = async (url, requestOptions) => {
await requestStrict({ url, method: 'DELETE' }, {
requireData: false,
requestController: requestOptions.requestController ?? null
@@ -65,7 +44,6 @@ export const personDocumentApi = {
if (query.lineagePersonId !== undefined && query.lineagePersonId !== null && query.lineagePersonId !== '') {
queryParams.lineagePersonId = normalizeResourcePathId(query.lineagePersonId, '世系人物标识')
}
requireRemotePersonDocument('读取')
const personDocuments = await requestStrict({
url: `/genealogy/app/genealogies/${normalizedGenealogyId}/person-documents`,
method: 'GET',
@@ -94,7 +72,6 @@ export const personDocumentApi = {
`/genealogy/app/genealogies/${normalizedGenealogyId}/person-documents`,
'POST',
normalizePersonDocumentPayload(payload),
'重要证件',
requestOptions
)
return normalizePersonDocument(personDocument, normalizedGenealogyId)
@@ -106,7 +83,6 @@ export const personDocumentApi = {
`/genealogy/app/genealogies/${normalizedIds.genealogyId}/person-documents/${normalizedIds.documentId}`,
'PUT',
normalizePersonDocumentPayload(payload),
'重要证件',
requestOptions
)
return normalizePersonDocument(personDocument, normalizedIds.genealogyId)
@@ -116,7 +92,6 @@ export const personDocumentApi = {
const normalizedIds = normalizeDocumentIds(genealogyId, documentId)
return deletePersonDocumentEndpoint(
`/genealogy/app/genealogies/${normalizedIds.genealogyId}/person-documents/${normalizedIds.documentId}`,
'重要证件',
requestOptions
)
},
@@ -127,7 +102,6 @@ export const personDocumentApi = {
`/genealogy/app/genealogies/${normalizedIds.genealogyId}/person-documents/${normalizedIds.documentId}/resources`,
'POST',
normalizePersonDocumentResourcePayload(payload),
'证件文件',
requestOptions
)
return normalizePersonDocumentResource(documentResource)
@@ -140,7 +114,6 @@ export const personDocumentApi = {
`/genealogy/app/genealogies/${normalizedIds.genealogyId}/person-documents/${normalizedIds.documentId}/resources/${normalizedResourceId}`,
'PUT',
normalizePersonDocumentResourcePayload(payload),
'证件文件',
requestOptions
)
return normalizePersonDocumentResource(documentResource)
@@ -151,7 +124,6 @@ export const personDocumentApi = {
const normalizedResourceId = normalizeResourcePathId(resourceId, '证件文件标识')
return deletePersonDocumentEndpoint(
`/genealogy/app/genealogies/${normalizedIds.genealogyId}/person-documents/${normalizedIds.documentId}/resources/${normalizedResourceId}`,
'证件文件',
requestOptions
)
},
@@ -176,13 +148,14 @@ export const personDocumentApi = {
async setPersonDocumentPassword(genealogyId, documentId, password, requestOptions = {}) {
const normalizedIds = normalizeDocumentIds(genealogyId, documentId)
await writePersonDocument(
`/genealogy/app/genealogies/${normalizedIds.genealogyId}/person-documents/${normalizedIds.documentId}/content-protection`,
'PUT',
normalizeContentPasswordPayload(password),
'证件内容密码',
requestOptions
)
await requestStrict({
url: `/genealogy/app/genealogies/${normalizedIds.genealogyId}/person-documents/${normalizedIds.documentId}/content-protection`,
method: 'PUT',
data: normalizeContentPasswordPayload(password)
}, {
requireData: false,
requestController: requestOptions.requestController ?? null
})
return null
},
@@ -192,7 +165,6 @@ export const personDocumentApi = {
`/genealogy/app/genealogies/${normalizedIds.genealogyId}/person-documents/${normalizedIds.documentId}/content-unlock`,
'POST',
normalizeContentPasswordPayload(password),
'证件内容解锁',
requestOptions
)
return normalizeContentAccessGrant(accessGrant)
@@ -202,7 +174,6 @@ export const personDocumentApi = {
const normalizedIds = normalizeDocumentIds(genealogyId, documentId)
return deletePersonDocumentEndpoint(
`/genealogy/app/genealogies/${normalizedIds.genealogyId}/person-documents/${normalizedIds.documentId}/content-protection`,
'证件内容密码',
requestOptions
)
}
+24 -4
View File
@@ -16,6 +16,15 @@ export const PROFILE_SEX_OPTIONS = Object.freeze([
const profileSexValues = new Set(PROFILE_SEX_OPTIONS.map(({ value }) => value))
const isCalendarDate = (value) => {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false
const [year, month, day] = value.split('-').map(Number)
const date = new Date(Date.UTC(year, month - 1, day))
return date.getUTCFullYear() === year &&
date.getUTCMonth() === month - 1 &&
date.getUTCDate() === day
}
const normalizeProfileResponseText = (value, field) => {
if (value === undefined || value === null) return ''
if (typeof value !== 'string') {
@@ -25,11 +34,18 @@ const normalizeProfileResponseText = (value, field) => {
}
export const normalizePhoneChangePayload = (payload) => {
assertPlainPayload(payload, new Set(['phone', 'smsCode']), '换绑手机号请求')
assertPlainPayload(payload, new Set(['phone', 'smsCode', 'currentPasswordHash']), '换绑手机号请求')
if (typeof payload.phone !== 'string' || !/^1\d{10}$/.test(payload.phone.trim())) {
throw new TypeError('新手机号格式无效')
}
return { phone: payload.phone.trim(), smsCode: assertSmsCode(payload.smsCode) }
if (typeof payload.currentPasswordHash !== 'string' || !/^[a-f0-9]{32}$/i.test(payload.currentPasswordHash)) {
throw new TypeError('当前密码摘要无效')
}
return {
phone: payload.phone.trim(),
smsCode: assertSmsCode(payload.smsCode),
currentPassword: payload.currentPasswordHash.toLowerCase()
}
}
const normalizeProfileUserId = (value) => {
@@ -43,6 +59,10 @@ export const normalizeAppProfile = (payload) => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw createRequestError('个人资料响应格式无效', 'PROFILE_RESPONSE_INVALID')
}
const birthday = normalizeProfileResponseText(payload.birthday, 'birthday')
if (birthday && !isCalendarDate(birthday.slice(0, 10))) {
throw createRequestError('个人资料字段 birthday 无效', 'PROFILE_RESPONSE_INVALID')
}
return {
userId: normalizeProfileUserId(payload.userId),
tenantId: normalizeProfileResponseText(payload.tenantId, 'tenantId'),
@@ -56,7 +76,7 @@ export const normalizeAppProfile = (payload) => {
'PROFILE_RESPONSE_INVALID'
),
sex: normalizeProfileResponseText(payload.sex, 'sex'),
birthday: normalizeProfileResponseText(payload.birthday, 'birthday'),
birthday,
email: normalizeProfileResponseText(payload.email, 'email'),
registerSource: normalizeProfileResponseText(payload.registerSource, 'registerSource'),
status: normalizeProfileResponseText(payload.status, 'status')
@@ -84,7 +104,7 @@ export const normalizeProfileUpdatePayload = (payload) => {
if (Object.prototype.hasOwnProperty.call(payload, 'birthday')) {
const birthday = normalizeOptionalText(payload.birthday, 'birthday')
if (birthday) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(birthday)) {
if (!isCalendarDate(birthday)) {
throw new TypeError('birthday 必须是 yyyy-MM-dd 日期')
}
normalizedPayload.birthday = birthday
+1 -26
View File
@@ -1,34 +1,12 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeAppProfile,
normalizeProfileUpdatePayload,
normalizeRecommendationPreference
} from './profile-contract.js'
import {
createRequestError,
requestStrict
} from './request-client.js'
const requireRemoteProfile = (operation) => {
if (hasRemoteConfig()) return
const isRead = operation === '读取'
throw createRequestError(
`个人资料${operation}需要真实服务,当前本地预览不伪造${isRead ? '资料' : '保存成功'}`,
isRead ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED'
)
}
const requireRemotePreference = (operation) => {
if (hasRemoteConfig()) return
throw createRequestError(
`个性化推荐偏好${operation}需要真实服务,当前本地预览不会伪造结果`,
operation === '读取' ? 'REMOTE_READ_REQUIRED' : 'REMOTE_WRITE_REQUIRED'
)
}
import { requestStrict } from './request-client.js'
export const profileApi = {
async getProfile(requestOptions = {}) {
requireRemoteProfile('读取')
const profile = await requestStrict({
url: '/genealogy/app/auth/profile',
method: 'GET'
@@ -40,7 +18,6 @@ export const profileApi = {
async updateProfile(payload, requestOptions = {}) {
const profileChanges = normalizeProfileUpdatePayload(payload)
requireRemoteProfile('更新')
const profile = await requestStrict({
url: '/genealogy/app/auth/profile',
method: 'PUT',
@@ -52,7 +29,6 @@ export const profileApi = {
},
async getRecommendationPreference(requestOptions = {}) {
requireRemotePreference('读取')
const preference = await requestStrict({
url: '/genealogy/app/recommendation-preference',
method: 'GET'
@@ -64,7 +40,6 @@ export const profileApi = {
async updateRecommendationPreference(enabled, requestOptions = {}) {
if (typeof enabled !== 'boolean') throw new TypeError('个性化推荐开关必须是布尔值')
requireRemotePreference('写入')
const preference = await requestStrict({
url: '/genealogy/app/recommendation-preference',
method: 'PUT',
+41
View File
@@ -0,0 +1,41 @@
import { createRequestError } from './request-client.js'
const referralResponseError = (message) =>
createRequestError(message, 'REFERRAL_RESPONSE_INVALID')
const normalizeReferralText = (value, field, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw referralResponseError(`推广关系响应缺少 ${field}`)
return ''
}
if (typeof value !== 'string') throw referralResponseError(`推广关系响应 ${field} 无效`)
const normalized = value.trim()
if (required && !normalized) throw referralResponseError(`推广关系响应缺少 ${field}`)
return normalized
}
export const normalizeReferralProfile = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw referralResponseError('推广关系响应无效')
}
const referralCode = normalizeReferralText(value.referralCode, 'referralCode', { required: true })
if (!/^[A-Za-z0-9_-]{4,64}$/.test(referralCode)) {
throw referralResponseError('推广关系响应 referralCode 格式无效')
}
if (!Number.isSafeInteger(value.referredUserCount) || value.referredUserCount < 0) {
throw referralResponseError('推广关系响应 referredUserCount 无效')
}
const shareUrl = normalizeReferralText(value.shareUrl, 'shareUrl', { required: true })
if (!/^https:\/\/[^\s]+$/i.test(shareUrl)) {
throw referralResponseError('推广关系响应 shareUrl 必须是 HTTPS 链接')
}
return {
enabled: true,
disabledReason: '',
referralCode,
referredUserCount: value.referredUserCount,
shareTitle: normalizeReferralText(value.shareTitle, 'shareTitle', { required: true }),
shareDescription: normalizeReferralText(value.shareText, 'shareText', { required: true }),
shareUrl
}
}
+14
View File
@@ -0,0 +1,14 @@
import { normalizeReferralProfile } from './referral-contract.js'
import { requestStrict } from './request-client.js'
export const referralApi = {
async getMyReferralProfile(requestOptions = {}) {
const profile = await requestStrict({
url: '/genealogy/app/referrals/me',
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeReferralProfile(profile)
}
}
-8
View File
@@ -1,4 +1,3 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeRegionCode,
normalizeRegionParentCode,
@@ -9,14 +8,8 @@ import {
requestStrict
} from './request-client.js'
const requireRemoteRegion = (label) => {
if (hasRemoteConfig()) return
throw createRequestError(`${label}读取需要真实服务,当前本地预览不会伪造结果`, 'REMOTE_READ_REQUIRED')
}
export const regionApi = {
async getRegionChildren(parentCode, requestOptions = {}) {
requireRemoteRegion('地区选择')
const regionChildren = await requestStrict({
url: '/genealogy/app/region/children',
method: 'GET',
@@ -28,7 +21,6 @@ export const regionApi = {
},
async getRegionPath(regionCode, requestOptions = {}) {
requireRemoteRegion('行政区划路径')
const regionPath = await requestStrict({
url: `/genealogy/app/region/path/${encodeURIComponent(normalizeRegionCode(regionCode))}`,
method: 'GET'
+42 -13
View File
@@ -1,10 +1,11 @@
import { runtimeConfig } from '@/utils/runtime-config.js'
import { goRoot } from '@/utils/navigation/gateway.js'
import { recoverToAuthRoot } 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
const FILE_UPLOAD_TIMEOUT_MS = 120000
export const createRequestError = (message, code, details = {}) => {
const error = new Error(message)
@@ -14,7 +15,8 @@ export const createRequestError = (message, code, details = {}) => {
}
const isAuthenticatedSessionRejected = (error) =>
error?.code === 'BUSINESS_ERROR' && Number(error.businessCode) === 401
(error?.code === 'BUSINESS_ERROR' && Number(error.businessCode) === 401) ||
(error?.code === 'HTTP_ERROR' && Number(error.httpStatus) === 401)
let authenticationRecoveryInFlight = false
@@ -25,7 +27,7 @@ const recoverExpiredAuthenticatedSession = () => {
const finishRecovery = () => {
authenticationRecoveryInFlight = false
}
goRoot('A01').then(finishRecovery, finishRecovery)
recoverToAuthRoot().then(finishRecovery, finishRecovery)
}
export const unwrapResponse = (response) => {
@@ -87,19 +89,27 @@ export const request = (options, {
},
success: ({ data, statusCode }) => {
if (expectedStatus !== null && statusCode !== expectedStatus) {
rejectOnce(createRequestError(
const error = createRequestError(
`服务只接受 HTTP ${expectedStatus} 响应,实际为 ${statusCode}`,
'HTTP_ERROR',
{ httpStatus: statusCode }
))
)
if (authenticated && isAuthenticatedSessionRejected(error)) {
recoverExpiredAuthenticatedSession()
}
rejectOnce(error)
return
}
if (statusCode < 200 || statusCode >= 300) {
rejectOnce(createRequestError(
const error = createRequestError(
data?.msg || `请求失败(${statusCode}`,
'HTTP_ERROR',
{ httpStatus: statusCode }
))
)
if (authenticated && isAuthenticatedSessionRejected(error)) {
recoverExpiredAuthenticatedSession()
}
rejectOnce(error)
return
}
if (
@@ -189,6 +199,25 @@ const parseStrictUploadResponse = (responseText, requireData) => {
return unwrapResponse(uploadEnvelope)
}
const rejectAuthenticatedUploadStatus = (statusCode) => {
const error = createRequestError(
`文件分片上传失败(HTTP ${statusCode}`,
'HTTP_ERROR',
{ httpStatus: statusCode }
)
if (isAuthenticatedSessionRejected(error)) recoverExpiredAuthenticatedSession()
return error
}
const unwrapAuthenticatedUploadResponse = (responseText) => {
try {
return parseStrictUploadResponse(responseText, false)
} catch (error) {
if (isAuthenticatedSessionRejected(error)) recoverExpiredAuthenticatedSession()
throw error
}
}
export const requestNativeFileChunk = ({ uploadId, chunkIndex, chunkMd5, filePath }, { requestController = null } = {}) => new Promise((resolve, reject) => {
if (typeof globalThis.uni?.uploadFile !== 'function') {
reject(createRequestError('当前运行环境不支持文件上传', 'FILE_UPLOAD_UNAVAILABLE'))
@@ -239,14 +268,14 @@ export const requestNativeFileChunk = ({ uploadId, chunkIndex, chunkMd5, filePat
tenantId: runtimeConfig.tenantId,
...(token ? { Authorization: `Bearer ${token}` } : {})
},
timeout: REQUEST_TIMEOUT_MS,
timeout: FILE_UPLOAD_TIMEOUT_MS,
success: (upload) => {
if (upload.statusCode !== 200) {
rejectOnce(createRequestError(`文件分片上传失败(HTTP ${upload.statusCode}`, 'HTTP_ERROR', { httpStatus: upload.statusCode }))
rejectOnce(rejectAuthenticatedUploadStatus(upload.statusCode))
return
}
try {
parseStrictUploadResponse(upload.data, false)
unwrapAuthenticatedUploadResponse(upload.data)
resolveOnce(null)
} catch (error) {
rejectOnce(error)
@@ -324,7 +353,7 @@ export const requestBrowserFileChunk = ({ uploadId, chunkIndex, chunkMd5, file }
timeoutId = setTimeout(() => {
abortController.abort()
rejectOnce(createRequestError('文件分片上传超时', 'REQUEST_TIMEOUT'))
}, REQUEST_TIMEOUT_MS)
}, FILE_UPLOAD_TIMEOUT_MS)
globalThis.fetch(`${runtimeConfig.baseUrl}/genealogy/app/files/resumable/chunk`, {
method: 'POST',
headers: {
@@ -336,9 +365,9 @@ export const requestBrowserFileChunk = ({ uploadId, chunkIndex, chunkMd5, file }
signal: abortController.signal
}).then(async (response) => {
if (response.status !== 200) {
throw createRequestError(`文件分片上传失败(HTTP ${response.status}`, 'HTTP_ERROR', { httpStatus: response.status })
throw rejectAuthenticatedUploadStatus(response.status)
}
parseStrictUploadResponse(await response.text(), false)
unwrapAuthenticatedUploadResponse(await response.text())
resolveOnce(null)
}).catch((error) => {
if (settled) return
+8 -2
View File
@@ -2,10 +2,16 @@ export const getRequestErrorMessage = (error, fallback = '操作未完成,请
if (error?.code === 'NETWORK_ERROR' || error?.code === 'REQUEST_TIMEOUT') {
return '网络不太稳定,请检查网络后重试。'
}
if (error?.code === 'BUSINESS_ERROR' && Number(error.businessCode) === 401) {
if (
(error?.code === 'BUSINESS_ERROR' && Number(error.businessCode) === 401) ||
(error?.code === 'HTTP_ERROR' && Number(error.httpStatus) === 401)
) {
return '登录状态已失效,请重新登录。'
}
if (error?.code === 'BUSINESS_ERROR' && Number(error.businessCode) === 403) {
if (
(error?.code === 'BUSINESS_ERROR' && Number(error.businessCode) === 403) ||
(error?.code === 'HTTP_ERROR' && Number(error.httpStatus) === 403)
) {
return '暂时没有权限进行这项操作。'
}
return fallback
+9
View File
@@ -39,6 +39,15 @@ export const normalizeOptionalSafeInteger = (value, field) => {
return numericValue
}
export const normalizeOptionalCurrencyNumber = (value, label) => {
if (value === undefined || value === null || value === '') return undefined
const amountText = String(value)
if (!/^(?:0|[1-9]\d{0,9})(?:\.\d{1,2})?$/.test(amountText)) {
throw new TypeError(`${label}必须在 0 至 9999999999.99 之间且最多保留两位小数`)
}
return Number(amountText)
}
export const normalizeResourcePathId = (value, label) => {
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return String(value)
+2 -13
View File
@@ -1,4 +1,3 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
normalizeAppPromotions,
normalizeComplianceDocument,
@@ -6,19 +5,10 @@ import {
normalizeHelpArticles,
normalizePromotionPlacement
} from './site-content-contract.js'
import {
createRequestError,
requestStrict
} from './request-client.js'
const requireRemoteSiteContent = (label) => {
if (hasRemoteConfig()) return
throw createRequestError(`${label}读取需要真实服务,当前本地预览不会伪造结果`, 'REMOTE_READ_REQUIRED')
}
import { requestStrict } from './request-client.js'
export const siteContentApi = {
async getHelpArticles(requestOptions = {}) {
requireRemoteSiteContent('帮助文章')
const helpArticles = await requestStrict({
url: '/genealogy/app/help-articles',
method: 'GET'
@@ -30,7 +20,6 @@ export const siteContentApi = {
async getPromotions(requestOptions = {}) {
const placement = normalizePromotionPlacement(requestOptions.placement ?? 'home_banner')
requireRemoteSiteContent('应用推广')
const promotions = await requestStrict({
url: '/genealogy/app/promotions',
method: 'GET',
@@ -42,12 +31,12 @@ export const siteContentApi = {
},
async getComplianceDocument(documentKey, requestOptions = {}) {
requireRemoteSiteContent('协议正文')
const normalizedKey = normalizeComplianceDocumentKey(documentKey)
const complianceDocument = await requestStrict({
url: `/genealogy/app/compliance/documents/${normalizedKey}`,
method: 'GET'
}, {
authenticated: false,
requestController: requestOptions.requestController ?? null
})
return normalizeComplianceDocument(complianceDocument, normalizedKey)
+189
View File
@@ -0,0 +1,189 @@
import { createRequestError } from './request-client.js'
import {
normalizeNormalDisableResponseStatus,
normalizeOptionalNonnegativeInteger
} from './response-normalizers.js'
import { assertPlainPayload } from './request-normalizers.js'
export const VIDEO_COMMENT_LEVEL = Object.freeze({
ROOT: 'root',
REPLY: 'reply'
})
const commentLevels = new Set(Object.values(VIDEO_COMMENT_LEVEL))
const normalizeCommentId = (value, label) => {
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(`视频评论${label}无效`, 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const normalizeCommentText = (value, label, { required = false } = {}) => {
if (value === undefined || value === null) {
if (required) throw createRequestError(`视频评论缺少${label}`, 'VIDEO_COMMENT_RESPONSE_INVALID')
return ''
}
if (typeof value !== 'string') {
throw createRequestError(`视频评论${label}无效`, 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const normalized = value.trim()
if (required && !normalized) {
throw createRequestError(`视频评论缺少${label}`, 'VIDEO_COMMENT_RESPONSE_INVALID')
}
return normalized
}
const normalizeParentCommentId = (value) => {
if (value === undefined || value === null || value === 0) return null
return normalizeCommentId(value, '父评论标识')
}
const normalizeVideoComment = (
item,
expectedVideoId,
{ expectedGenealogyId = '', expectedLevel = '', expectedParentCommentId = '' } = {}
) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('视频评论响应包含无效条目', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const videoId = normalizeCommentId(item.videoId, '视频标识')
if (videoId !== expectedVideoId) {
throw createRequestError('视频评论归属与请求视频不匹配', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
if (expectedGenealogyId) {
const genealogyId = normalizeCommentId(item.genealogyId, '家谱标识')
if (genealogyId !== expectedGenealogyId) {
throw createRequestError('视频评论归属与请求家谱不匹配', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
} else if (item.genealogyId !== undefined && item.genealogyId !== null) {
throw createRequestError('平台视频评论不能携带家谱归属', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const parentCommentId = normalizeParentCommentId(item.parentCommentId)
const declaredLevel = normalizeCommentText(item.commentLevel, '层级')
const level = declaredLevel || (parentCommentId ? VIDEO_COMMENT_LEVEL.REPLY : VIDEO_COMMENT_LEVEL.ROOT)
if (!commentLevels.has(level) || (expectedLevel && level !== expectedLevel)) {
throw createRequestError('视频评论层级无效', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
if (
(level === VIDEO_COMMENT_LEVEL.ROOT && parentCommentId !== null) ||
(level === VIDEO_COMMENT_LEVEL.REPLY && parentCommentId === null) ||
(expectedParentCommentId && parentCommentId !== expectedParentCommentId)
) {
throw createRequestError('视频评论父级与层级不匹配', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const deletedStatus = item.userDeleted
if (deletedStatus !== '0' && deletedStatus !== '1') {
throw createRequestError('视频评论删除状态无效', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
if (typeof item.canDelete !== 'boolean') {
throw createRequestError('视频评论删除权限无效', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const userDeleted = deletedStatus === '1'
return {
id: normalizeCommentId(item.commentId, '标识'),
author: normalizeCommentText(item.appUserNickName, '用户昵称') || '未署名成员',
content: userDeleted
? '该评论已删除'
: normalizeCommentText(item.commentContent, '内容', { required: true }),
time: normalizeCommentText(item.createTime, '创建时间', { required: true }),
parentCommentId,
parentAuthor: normalizeCommentText(item.parentAppUserNickName, '被回复用户昵称'),
level,
userDeleted,
canDelete: item.canDelete,
replyCount: normalizeOptionalNonnegativeInteger(
item.replyCount,
'视频评论回复数',
'VIDEO_COMMENT_RESPONSE_INVALID'
),
status: normalizeNormalDisableResponseStatus(
item.status,
'视频评论状态',
'VIDEO_COMMENT_RESPONSE_INVALID'
)
}
}
export const normalizeVideoComments = (
value,
expectedVideoId,
options = {}
) => {
if (!Array.isArray(value)) {
throw createRequestError('视频评论响应不是列表', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const comments = value.map((item) => normalizeVideoComment(item, expectedVideoId, options))
if (new Set(comments.map((comment) => comment.id)).size !== comments.length) {
throw createRequestError('视频评论响应包含重复标识', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
return comments
}
export const normalizeCreatedVideoComment = (
value,
expectedVideoId,
options = {}
) => normalizeVideoComment(value, expectedVideoId, options)
const normalizePlatformVideoComment = (item, expectedVideoId) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('平台视频评论响应包含无效条目', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const videoId = normalizeCommentId(item.platformVideoId, '平台视频标识')
if (videoId !== expectedVideoId || normalizeParentCommentId(item.parentCommentId) !== null) {
throw createRequestError('平台视频评论归属或层级无效', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
if (item.userDeleted !== '0' && item.userDeleted !== '1') {
throw createRequestError('平台视频评论删除状态无效', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
if (typeof item.canDelete !== 'boolean') {
throw createRequestError('平台视频评论删除权限无效', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const userDeleted = item.userDeleted === '1'
return {
id: normalizeCommentId(item.commentId, '标识'),
author: normalizeCommentText(item.appUserNickName, '用户昵称') || '未署名用户',
content: userDeleted
? '该评论已删除'
: normalizeCommentText(item.commentContent, '内容', { required: true }),
time: normalizeCommentText(item.createTime, '创建时间', { required: true }),
parentCommentId: null,
parentAuthor: '',
level: VIDEO_COMMENT_LEVEL.ROOT,
userDeleted,
canDelete: item.canDelete,
replyCount: 0,
status: '0'
}
}
export const normalizePlatformVideoComments = (value, expectedVideoId) => {
if (!Array.isArray(value)) {
throw createRequestError('平台视频评论响应不是列表', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
const comments = value.map((item) => normalizePlatformVideoComment(item, expectedVideoId))
if (new Set(comments.map((comment) => comment.id)).size !== comments.length) {
throw createRequestError('平台视频评论响应包含重复标识', 'VIDEO_COMMENT_RESPONSE_INVALID')
}
return comments
}
export const normalizeCreatedPlatformVideoComment = (value, expectedVideoId) =>
normalizePlatformVideoComment(value, expectedVideoId)
export const normalizeVideoCommentPayload = (payload) => {
assertPlainPayload(payload, new Set(['parentCommentId', 'commentContent']), '视频评论请求')
if (typeof payload.commentContent !== 'string' || !payload.commentContent.trim()) {
throw new TypeError('视频评论内容不能为空')
}
const commentContent = payload.commentContent.trim()
if (Array.from(commentContent).length > 1000) {
throw new TypeError('视频评论不能超过 1000 个字符')
}
const normalizedPayload = { commentContent }
if (payload.parentCommentId !== undefined && payload.parentCommentId !== null) {
normalizedPayload.parentCommentId = normalizeCommentId(payload.parentCommentId, '父评论标识')
}
return normalizedPayload
}
+127 -1
View File
@@ -41,6 +41,26 @@ const normalizeVipOrderStatus = (value) => {
}
}
export const VIP_PAYMENT_METHOD = Object.freeze({
WECHAT: 'WECHAT',
ALIPAY: 'ALIPAY',
BALANCE: 'BALANCE'
})
const vipPaymentMethods = new Set(Object.values(VIP_PAYMENT_METHOD))
const vipPaymentMethodLabels = Object.freeze({
[VIP_PAYMENT_METHOD.WECHAT]: '微信支付',
[VIP_PAYMENT_METHOD.ALIPAY]: '支付宝',
[VIP_PAYMENT_METHOD.BALANCE]: '余额支付'
})
export const assertVipPaymentMethod = (value) => {
if (!vipPaymentMethods.has(value)) {
throw new TypeError('VIP 支付方式必须是 WECHAT、ALIPAY 或 BALANCE')
}
return value
}
export const normalizeVipPackages = (value) => {
if (!Array.isArray(value)) throw createRequestError('VIP 套餐响应不是列表', 'VIP_RESPONSE_INVALID')
const packages = value.map((item) => {
@@ -76,8 +96,10 @@ export const normalizeVipOrders = (value) => {
return {
key: `vip-order-${id}`,
id,
orderNo: normalizeVipText(item.orderNo, 'orderNo'),
packageName: normalizeVipText(item.packageName, 'packageName', { required: true }),
amount: normalizeVipAmount(item.payAmount ?? item.orderAmount, 'payAmount', { required: true }),
paymentMethod: normalizeVipText(item.payType, 'payType'),
status,
statusLabel,
paidAt: normalizeVipText(item.payTime, 'payTime'),
@@ -94,8 +116,112 @@ export const normalizeVipCapability = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.enabled !== 'boolean') {
throw createRequestError('VIP 购买能力响应无效', 'VIP_RESPONSE_INVALID')
}
if (value.paymentMethods !== undefined && !Array.isArray(value.paymentMethods)) {
throw createRequestError('VIP 支付方式能力不是列表', 'VIP_RESPONSE_INVALID')
}
const paymentMethods = (value.paymentMethods || []).map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
throw createRequestError('VIP 支付方式能力包含无效条目', 'VIP_RESPONSE_INVALID')
}
const method = normalizeVipText(item.method, 'paymentMethods.method', { required: true })
if (!vipPaymentMethods.has(method) || typeof item.enabled !== 'boolean') {
throw createRequestError('VIP 支付方式能力字段无效', 'VIP_RESPONSE_INVALID')
}
return {
method,
label: vipPaymentMethodLabels[method],
enabled: item.enabled,
disabledReason: normalizeVipText(item.disabledReason, 'paymentMethods.disabledReason')
}
})
if (new Set(paymentMethods.map((item) => item.method)).size !== paymentMethods.length) {
throw createRequestError('VIP 支付方式能力包含重复方式', 'VIP_RESPONSE_INVALID')
}
if (value.enabled && !paymentMethods.some((item) => item.enabled)) {
throw createRequestError('VIP 已开放购买但没有可用支付方式', 'VIP_RESPONSE_INVALID')
}
return {
enabled: value.enabled,
disabledReason: normalizeVipText(value.disabledReason, 'disabledReason')
disabledReason: normalizeVipText(value.disabledReason, 'disabledReason'),
paymentMethods
}
}
const normalizeVipId = (value, field) => {
const id = value === undefined || value === null ? '' : String(value)
if (!/^[1-9]\d*$/.test(id)) {
throw createRequestError(`VIP 响应 ${field} 无效`, 'VIP_RESPONSE_INVALID')
}
return id
}
const vipPaymentStatuses = new Set([
'CREATED',
'PAYING',
'SUCCESS',
'CLOSED',
'REFUNDING',
'REFUNDED'
])
export const normalizeVipPaymentOrder = (value, expectedPaymentMethod) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('VIP 支付订单响应无效', 'VIP_RESPONSE_INVALID')
}
const paymentMethod = normalizeVipText(value.paymentMethod, 'paymentMethod', { required: true })
if (!vipPaymentMethods.has(paymentMethod) || paymentMethod !== expectedPaymentMethod) {
throw createRequestError('VIP 支付订单方式与请求不匹配', 'VIP_RESPONSE_INVALID')
}
const transactionId = normalizeVipId(value.transactionId, 'transactionId')
if (paymentMethod === VIP_PAYMENT_METHOD.WECHAT) {
if (value.tradeType !== 'APP') {
throw createRequestError('VIP 微信支付订单 tradeType 无效', 'VIP_RESPONSE_INVALID')
}
return {
paymentMethod,
transactionId,
nativePaymentRequired: true,
orderInfo: {
appid: normalizeVipText(value.appId, 'appId', { required: true }),
partnerid: normalizeVipText(value.partnerId, 'partnerId', { required: true }),
prepayid: normalizeVipText(value.prepayId, 'prepayId', { required: true }),
package: normalizeVipText(value.packageValue, 'packageValue', { required: true }),
noncestr: normalizeVipText(value.nonceStr, 'nonceStr', { required: true }),
timestamp: normalizeVipText(value.timestamp, 'timestamp', { required: true }),
sign: normalizeVipText(value.sign, 'sign', { required: true })
}
}
}
if (paymentMethod === VIP_PAYMENT_METHOD.ALIPAY) {
return {
paymentMethod,
transactionId,
nativePaymentRequired: true,
orderInfo: normalizeVipText(value.orderString, 'orderString', { required: true })
}
}
if (value.completed !== true) {
throw createRequestError('VIP 余额支付没有原子完成', 'VIP_RESPONSE_INVALID')
}
return {
paymentMethod,
transactionId: normalizeVipId(value.transactionId, 'transactionId'),
nativePaymentRequired: false,
orderInfo: null,
paymentStatus: 'SUCCESS'
}
}
export const normalizeVipPaymentStatus = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw createRequestError('VIP 支付状态响应无效', 'VIP_RESPONSE_INVALID')
}
const status = normalizeVipText(value.status, 'status', { required: true })
if (!vipPaymentStatuses.has(status)) {
throw createRequestError('VIP 支付状态超出约定范围', 'VIP_RESPONSE_INVALID')
}
return {
transactionId: normalizeVipId(value.transactionId, 'transactionId'),
status
}
}
+49 -8
View File
@@ -1,22 +1,26 @@
import { hasRemoteConfig } from '@/utils/runtime-config.js'
import {
assertVipPaymentMethod,
normalizeVipCapability,
normalizeVipOrders,
normalizeVipPackages
normalizeVipPackages,
normalizeVipPaymentOrder,
normalizeVipPaymentStatus
} from './vip-contract.js'
import {
createRequestError,
requestStrict
} from './request-client.js'
const requireRemoteVip = (label) => {
if (hasRemoteConfig()) return
throw createRequestError(`${label}读取需要真实服务,当前本地预览不会伪造结果`, 'REMOTE_READ_REQUIRED')
const assertVipId = (value, field) => {
const id = value === undefined || value === null ? '' : String(value)
if (!/^[1-9]\d*$/.test(id)) {
throw createRequestError(`${field}无效`, 'VIP_REQUEST_INVALID')
}
return id
}
export const vipApi = {
async getVipCapability(requestOptions = {}) {
requireRemoteVip('VIP 购买能力')
const capability = await requestStrict({
url: '/genealogy/app/vip/capability',
method: 'GET'
@@ -27,7 +31,6 @@ export const vipApi = {
},
async getVipPackages(requestOptions = {}) {
requireRemoteVip('VIP 套餐')
const packages = await requestStrict({
url: '/genealogy/app/vip/packages',
method: 'GET'
@@ -38,7 +41,6 @@ export const vipApi = {
},
async getVipOrders(requestOptions = {}) {
requireRemoteVip('VIP 订单')
const orders = await requestStrict({
url: '/genealogy/app/vip/orders',
method: 'GET'
@@ -46,5 +48,44 @@ export const vipApi = {
requestController: requestOptions.requestController ?? null
})
return normalizeVipOrders(orders)
},
async createVipOrder(packageId, genealogyId = '', paymentMethod, requestId, requestOptions = {}) {
if (typeof requestId !== 'string' || !requestId.trim() || requestId.length > 64) {
throw createRequestError('VIP 下单请求号无效', 'VIP_REQUEST_INVALID')
}
const order = await requestStrict({
url: '/genealogy/app/vip/orders',
method: 'POST',
data: {
packageId: assertVipId(packageId, 'VIP 套餐标识'),
...(genealogyId ? { genealogyId: assertVipId(genealogyId, '家谱标识') } : {}),
paymentMethod: assertVipPaymentMethod(paymentMethod),
requestId: requestId.trim()
}
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeVipPaymentOrder(order, paymentMethod)
},
async getVipPaymentStatus(transactionId, requestOptions = {}) {
const status = await requestStrict({
url: `/genealogy/app/vip/orders/${assertVipId(transactionId, '支付流水标识')}/payment`,
method: 'GET'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeVipPaymentStatus(status)
},
async closeVipPayment(transactionId, requestOptions = {}) {
const status = await requestStrict({
url: `/genealogy/app/vip/orders/${assertVipId(transactionId, '支付流水标识')}/close`,
method: 'POST'
}, {
requestController: requestOptions.requestController ?? null
})
return normalizeVipPaymentStatus(status)
}
}