## 重大修复 ### 安全性修复 - 修复 notification read-all SQL 运算符优先级 bug - 修复 link_url XSS 漏洞(验证 URL 协议) - 修复 LIKE 通配符注入(转义 % 和 _) - 修复 uploadImageIfNeeded 运算符优先级 bug - 401 时清理所有本地存储(token/nickname/avatar/role/group) ### 数据完整性修复 - 修复公告已读状态共享问题(新建 notification_reads 表) - 修复群组账单数据缺失(退出群组时保留 group_id) - 修复群组解散后邀请码仍可加入 - 修复分类迁移未校验目标类型 - 修复群组公告缺少 group_id 必填校验 ### 功能修复 - 修复 category PUT /sort 路由冲突 - 修复 GROUP BY 不完整问题 - 修复 budget API 类型不匹配 - 修复 categoryStore.migrateCategory 不刷新本地数据 - 修复 groupStore 并发请求问题 - 修复账单页覆盖 store 数据 - 修复群组预算查询返回 0 而非 null - 修复通知页面 onShow 不刷新列表 - 修复统计页面不必要的重复请求 ### 用户体验优化 - 添加通知详情查看功能(弹窗) - 添加通知图片服务器上传 - 添加 Markdown 富文本工具栏 - 添加管理页面客户端认证检查 - 添加管理员公告页面下拉刷新 - 添加数据导出进度反馈 - 添加账单删除后筛选金额更新 - Numpad 添加安全区域 padding ### 代码质量提升 - 提取 requireAdmin 为共享中间件 - filter-panel 使用设计 token - 修复 getCurrentMonth 时区不一致 - 备份功能使用分页查询避免内存问题 - 管理后台仪表盘添加缓存 - 邀请码碰撞重试后报错 ## 新增文件 - server/src/middleware/requireAdmin.ts — 共享管理员权限中间件 - client/src/pages/admin/notifications/index.vue — 公告管理页面 ## 数据库变更 - 新增 notification_reads 表(公告已读记录) - 群组解散时保留 groups 记录和 transactions.group_id
118 lines
3.1 KiB
TypeScript
118 lines
3.1 KiB
TypeScript
import { request } from '@/utils/request'
|
||
import { API_BASE } from '@/config'
|
||
|
||
/** 通知 */
|
||
export interface Notification {
|
||
id: number
|
||
type: 'system' | 'group' | 'personal'
|
||
title: string
|
||
content: string
|
||
is_read: number
|
||
created_at: string
|
||
group_id?: number | null
|
||
is_pinned?: number
|
||
is_urgent?: number
|
||
publish_at?: string | null
|
||
expire_at?: string | null
|
||
image_url?: string
|
||
link_url?: string
|
||
}
|
||
|
||
/** 通知列表响应 */
|
||
export interface NotificationList {
|
||
list: Notification[]
|
||
total: number
|
||
page: number
|
||
pageSize: number
|
||
}
|
||
|
||
/** 发布公告参数 */
|
||
export interface PublishParams {
|
||
title: string
|
||
content?: string
|
||
type?: 'system' | 'group' | 'personal'
|
||
group_id?: number
|
||
is_pinned?: boolean
|
||
is_urgent?: boolean
|
||
publish_at?: string
|
||
expire_at?: string
|
||
image_url?: string
|
||
link_url?: string
|
||
}
|
||
|
||
/** 获取通知列表 */
|
||
export function getNotifications(params?: { type?: string; page?: number; pageSize?: number }) {
|
||
return request<NotificationList>({ url: '/notifications', data: params })
|
||
}
|
||
|
||
/** 获取置顶/强提醒公告 */
|
||
export function getPinnedNotifications() {
|
||
return request<Notification[]>({ url: '/notifications/pinned' })
|
||
}
|
||
|
||
/** 获取未读数量 */
|
||
export function getUnreadCount() {
|
||
return request<{ count: number }>({ url: '/notifications/unread-count' })
|
||
}
|
||
|
||
/** 标记单条已读 */
|
||
export function markRead(id: number) {
|
||
return request({ url: `/notifications/${id}/read`, method: 'PUT' })
|
||
}
|
||
|
||
/** 全部标记已读 */
|
||
export function markAllRead() {
|
||
return request({ url: '/notifications/read-all', method: 'PUT' })
|
||
}
|
||
|
||
/** 上传通知图片(返回文件名) */
|
||
export function uploadNotificationImage(filePath: string): Promise<{ image_url: string }> {
|
||
return new Promise((resolve, reject) => {
|
||
const token = uni.getStorageSync('xc:token')
|
||
uni.uploadFile({
|
||
url: `${API_BASE}/notifications/upload-image`,
|
||
filePath,
|
||
name: 'file',
|
||
header: {
|
||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||
},
|
||
success: (res) => {
|
||
try {
|
||
const data = JSON.parse(res.data)
|
||
if (data.code === 0) {
|
||
resolve(data.data)
|
||
} else {
|
||
reject(data)
|
||
}
|
||
} catch {
|
||
reject({ message: '上传失败' })
|
||
}
|
||
},
|
||
fail: () => reject({ message: '网络错误' })
|
||
})
|
||
})
|
||
}
|
||
|
||
/** 获取通知图片完整 URL */
|
||
export function getNotificationImageUrl(filename: string): string {
|
||
if (!filename) return ''
|
||
// 如果已经是完整 URL 或 blob URL,直接返回
|
||
if (filename.startsWith('http') || filename.startsWith('blob:')) return filename
|
||
return `${API_BASE}/notifications/image/${filename}`
|
||
}
|
||
|
||
/** 发布公告 */
|
||
export function publishNotification(data: PublishParams) {
|
||
return request<{ id: number }>({ url: '/notifications', method: 'POST', data })
|
||
}
|
||
|
||
/** 编辑公告 */
|
||
export function updateNotification(id: number, data: Partial<PublishParams>) {
|
||
return request({ url: `/notifications/${id}`, method: 'PUT', data })
|
||
}
|
||
|
||
/** 删除公告 */
|
||
export function deleteNotification(id: number) {
|
||
return request({ url: `/notifications/${id}`, method: 'DELETE' })
|
||
}
|