feat: 完成前端业务闭环与后端联调
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 } : {})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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('成员分页响应不是对象')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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('人物排序值必须是安全整数')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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个字符')
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user