2026-10-01 20:26:41 +08:00
|
|
|
import { getSession, requestJson } from '@/services/api.uts'
|
|
|
|
|
|
|
|
|
|
export type UserMessage = { id: string, type: string, category: string, icon: string, title: string, content: string, time: string, isRead: boolean }
|
|
|
|
|
export type MessagePage = { items: UserMessage[], snapshotId: string, nextBeforeId: string, unreadCount: number }
|
|
|
|
|
export type MessageDetail = { item: UserMessage, targetUrl: string, targetLabel: string, targetUnavailable: boolean }
|
|
|
|
|
|
2026-10-05 20:11:24 +08:00
|
|
|
const validId = (value: string, allowZero: boolean): boolean => {
|
|
|
|
|
if (!(allowZero ? /^(?:0|[1-9][0-9]{0,9})$/.test(value) : /^[1-9][0-9]{0,9}$/.test(value))) return false
|
|
|
|
|
return !(value.length == 10 && value > '2147483647')
|
|
|
|
|
}
|
|
|
|
|
const requiredId = (value: string, message: string): string => {
|
|
|
|
|
if (!validId(value, false)) throw new Error(message)
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
const requiredText = (data: UTSJSONObject, key: string, message: string): string => {
|
|
|
|
|
const value = data.getString(key)
|
|
|
|
|
if (value == null || value.length == 0) throw new Error(message)
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
const parseSnapshotId = (value: string): string => {
|
|
|
|
|
if (!validId(value, true)) throw new Error('消息快照格式无效')
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
const optionalId = (value: any | null): string => {
|
|
|
|
|
if (value == null) return ''
|
|
|
|
|
if (typeof value != 'string') throw new Error('消息分页游标格式无效')
|
|
|
|
|
if (value.length == 0) return ''
|
|
|
|
|
return requiredId(value as string, '消息分页游标格式无效')
|
|
|
|
|
}
|
2026-10-01 20:26:41 +08:00
|
|
|
const count = (data: UTSJSONObject): number => {
|
|
|
|
|
const value = data.getNumber('unreadCount')
|
|
|
|
|
if (value == null || !Number.isInteger(value) || value < 0) throw new Error('未读数量格式无效')
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
function messageFrom(data: UTSJSONObject): UserMessage {
|
|
|
|
|
const id = data.getString('id') ?? ''; const type = data.getString('type') ?? ''
|
2026-10-05 20:11:24 +08:00
|
|
|
if (!validId(id, false) || !['system', 'service', 'activity'].includes(type)) throw new Error('消息格式无效')
|
|
|
|
|
const title = requiredText(data, 'title', '消息标题格式无效')
|
|
|
|
|
const content = requiredText(data, 'content', '消息内容格式无效')
|
|
|
|
|
const createdAt = requiredText(data, 'createdAt', '消息时间格式无效')
|
|
|
|
|
const isRead = data.getBoolean('isRead')
|
|
|
|
|
if (isRead == null) throw new Error('消息已读标记无效')
|
2026-10-01 20:26:41 +08:00
|
|
|
const categories: UTSJSONObject = { system: '系统通知', service: '服务通知', activity: '活动消息' }
|
|
|
|
|
const icons: UTSJSONObject = { system: 'system-bell', service: 'approval', activity: 'gift' }
|
2026-10-05 20:11:24 +08:00
|
|
|
const date = new Date(createdAt.replace(' ', 'T'))
|
2026-10-01 20:26:41 +08:00
|
|
|
const now = new Date(); const pad = (n: number): string => n < 10 ? '0' + n.toString() : n.toString()
|
|
|
|
|
let time = '未记录'
|
|
|
|
|
if (!isNaN(date.getTime())) time = date.getFullYear() == now.getFullYear() && date.getMonth() == now.getMonth() && date.getDate() == now.getDate()
|
|
|
|
|
? pad(date.getHours()) + ':' + pad(date.getMinutes()) : date.getFullYear().toString() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate())
|
2026-10-05 20:11:24 +08:00
|
|
|
return { id, type, category: categories.getString(type) ?? '', icon: icons.getString(type) ?? '', title,
|
|
|
|
|
content, time, isRead }
|
2026-10-01 20:26:41 +08:00
|
|
|
}
|
|
|
|
|
export async function fetchUnreadMessages(): Promise<number> {
|
|
|
|
|
if (getSession() == null) return 0
|
|
|
|
|
return count(await requestJson('/messages/unread-count', 'GET', {}))
|
|
|
|
|
}
|
|
|
|
|
export async function fetchMessages(type: string, beforeId: string = '', snapshotId: string = ''): Promise<MessagePage> {
|
2026-10-05 20:11:24 +08:00
|
|
|
if (!['all', 'system', 'service', 'activity'].includes(type)) throw new Error('消息分类无效')
|
|
|
|
|
if (beforeId.length > 0 && !validId(beforeId, false)) throw new Error('消息分页游标格式无效')
|
|
|
|
|
if (beforeId.length > 0 && snapshotId.length == 0) throw new Error('消息分页快照缺失')
|
|
|
|
|
if (snapshotId.length > 0 && !validId(snapshotId, true)) throw new Error('消息快照格式无效')
|
2026-10-01 20:26:41 +08:00
|
|
|
const params: UTSJSONObject = { type, limit: 20 }
|
|
|
|
|
if (beforeId.length > 0) { params['beforeId'] = beforeId; params['snapshotId'] = snapshotId }
|
|
|
|
|
const data = await requestJson('/messages', 'GET', params)
|
2026-10-05 20:11:24 +08:00
|
|
|
const rows = data['items']
|
|
|
|
|
if (rows == null || !Array.isArray(rows)) throw new Error('消息列表格式无效')
|
|
|
|
|
;(rows as any[]).forEach((item: any) => {
|
|
|
|
|
if (item == null || typeof item != 'object' || Array.isArray(item)) throw new Error('消息列表格式无效')
|
|
|
|
|
})
|
|
|
|
|
const returnedSnapshot = parseSnapshotId(data.getString('snapshotId') ?? '')
|
|
|
|
|
return { items: (rows as UTSJSONObject[]).map((item: UTSJSONObject): UserMessage => messageFrom(item)), snapshotId: returnedSnapshot,
|
|
|
|
|
nextBeforeId: optionalId(data['nextBeforeId']), unreadCount: count(data) }
|
2026-10-01 20:26:41 +08:00
|
|
|
}
|
|
|
|
|
export async function fetchMessage(id: string): Promise<MessageDetail> {
|
2026-10-05 20:11:24 +08:00
|
|
|
if (!validId(id, false)) throw new Error('消息编号无效')
|
2026-10-01 20:26:41 +08:00
|
|
|
const data = await requestJson('/messages/' + id, 'GET', {})
|
2026-10-05 20:11:24 +08:00
|
|
|
const item = data['item']
|
|
|
|
|
if (item == null || typeof item != 'object' || Array.isArray(item)) throw new Error('消息详情格式无效')
|
|
|
|
|
const targetValue = data['target']
|
|
|
|
|
if (targetValue != null && (typeof targetValue != 'object' || Array.isArray(targetValue))) throw new Error('消息关联格式无效')
|
|
|
|
|
const target = targetValue as UTSJSONObject | null
|
2026-10-01 20:26:41 +08:00
|
|
|
let targetUrl = ''; let targetLabel = ''
|
|
|
|
|
if (target != null) {
|
2026-10-05 20:11:24 +08:00
|
|
|
const kind = target.getString('kind') ?? ''
|
|
|
|
|
if (kind == 'feedback') {
|
|
|
|
|
const feedbackId = requiredId(target.getString('feedbackId') ?? '', '关联反馈格式无效')
|
|
|
|
|
targetUrl = '/packages/content/help/help?feedback=1&feedbackId=' + feedbackId; targetLabel = '查看回复'
|
|
|
|
|
} else {
|
|
|
|
|
const assessmentId = target.getString('assessmentId') ?? ''; const revisionId = target.getString('revisionId') ?? ''
|
|
|
|
|
const type = target.getString('type') ?? ''
|
|
|
|
|
if (!validId(assessmentId, false) || !validId(revisionId, false) || !['personal', 'enterprise'].includes(type)) throw new Error('关联评估格式无效')
|
|
|
|
|
// 只构造白名单路由,通知不能下发任意链接;历史失败也只打开原提交版本。
|
|
|
|
|
if (kind == 'report') { targetUrl = '/packages/finance/report/report?assessmentId=' + assessmentId + '&revisionId=' + revisionId; targetLabel = '查看报告' }
|
|
|
|
|
else if (kind == 'revision') { targetUrl = '/packages/finance/archive/archive?assessmentId=' + assessmentId + '&revisionId=' + revisionId + '&type=' + type; targetLabel = '查看评估' }
|
|
|
|
|
else throw new Error('关联评估格式无效')
|
|
|
|
|
}
|
2026-10-01 20:26:41 +08:00
|
|
|
}
|
2026-10-05 20:11:24 +08:00
|
|
|
const targetUnavailable = data.getBoolean('targetUnavailable')
|
|
|
|
|
if (targetUnavailable == null) throw new Error('消息关联状态无效')
|
|
|
|
|
return { item: messageFrom(item as UTSJSONObject), targetUrl, targetLabel, targetUnavailable }
|
2026-10-01 20:26:41 +08:00
|
|
|
}
|
|
|
|
|
export async function readMessage(id: string): Promise<number> { return count(await requestJson('/messages/' + id + '/read', 'POST', {})) }
|
2026-10-05 20:11:24 +08:00
|
|
|
export async function readAllMessages(snapshotId: string): Promise<number> {
|
|
|
|
|
if (!validId(snapshotId, true)) throw new Error('消息快照格式无效')
|
|
|
|
|
const data = await requestJson('/messages/read-all', 'POST', { snapshotId })
|
|
|
|
|
const updatedCount = data.getNumber('updatedCount')
|
|
|
|
|
if (updatedCount == null || !Number.isInteger(updatedCount) || updatedCount < 0) throw new Error('消息已读回执格式无效')
|
|
|
|
|
const returnedSnapshot = parseSnapshotId(data.getString('snapshotId') ?? '')
|
|
|
|
|
if (returnedSnapshot != snapshotId) throw new Error('消息已读回执快照不一致')
|
|
|
|
|
return count(data)
|
|
|
|
|
}
|