feat(personal_collect,motion_reminder): 新增回收站与云端TTS语音功能
- 新增小程序个人收藏回收站页面,支持查看已删除收藏、恢复与彻底删除操作 - 修复个人收藏文件上传时的中文名乱码问题,新增原文件名透传参数 - 接入阿里云百炼TTS服务,实现运动提醒模块的云端语音合成与本地缓存,支持系统TTS与云端MP3双降级播放 - 新增语音引擎探测与异常预警UI,覆盖web端与移动端 - 优化web端订单盒页面布局,修复卡片操作按钮换行问题 - 新增调试探针脚本、ESLint配置与相关lint脚本 - 完善后端接口与路由,更新项目依赖与配置文件
This commit is contained in:
@@ -244,6 +244,45 @@ const bindRoutes = app => {
|
||||
}
|
||||
})
|
||||
|
||||
app.get(`${API_PREFIX}/trash`, (req, res) => {
|
||||
try {
|
||||
return res.json({ ok: true, data: store.listTrash() })
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
app.put(`${API_PREFIX}/trash/restore`, (req, res) => {
|
||||
try {
|
||||
const id = String((req.body && req.body.id) || req.query.id || '').trim()
|
||||
if (!id) return res.status(400).json({ ok: false, error: 'missing id' })
|
||||
const ok = store.restoreItem(id)
|
||||
if (!ok) return res.status(404).json({ ok: false, error: '回收站中不存在该收藏' })
|
||||
return res.json({ ok: true })
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
app.delete(`${API_PREFIX}/trash`, (req, res) => {
|
||||
try {
|
||||
const id = String((req.body && req.body.id) || req.query.id || '').trim()
|
||||
if (!id) return res.status(400).json({ ok: false, error: 'missing id' })
|
||||
const r = store.purgeItem(id)
|
||||
if (!r || !r.ok) return res.status(404).json({ ok: false, error: r && r.error || 'not found' })
|
||||
return res.json({
|
||||
ok: true,
|
||||
data: {
|
||||
filesRemoved: r.filesRemoved,
|
||||
bytesRemoved: r.bytesRemoved,
|
||||
attachments: r.attachments,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
}
|
||||
})
|
||||
|
||||
app.post(`${API_PREFIX}/upload`, upload.single('file'), (req, res) => {
|
||||
try {
|
||||
const f = req.file
|
||||
@@ -253,7 +292,10 @@ const bindRoutes = app => {
|
||||
const relPath = path.join(relDir, f.filename).replace(/\\/g, '/')
|
||||
const urlPath = '/' + relPath
|
||||
const mimeType = String(f.mimetype || '').trim()
|
||||
const originalName = fixFileNameEncoding(f.originalname)
|
||||
// 优先使用小程序端透传的原始文件名(UTF-8 完好);
|
||||
// 兜底才解析 multipart 里的 originalname(桌面微信传输中文名可能已被损坏)
|
||||
const originalNameRaw = String(req.body && req.body.originalName || '').trim() || f.originalname
|
||||
const originalName = fixFileNameEncoding(originalNameRaw)
|
||||
const isImage = isImageByMime(mimeType) || isImageByName(originalName)
|
||||
const saved = store.insertAttachment({
|
||||
id: createId(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const Database = require('better-sqlite3')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const iconv = require('iconv-lite')
|
||||
|
||||
function scoreChinese(str) {
|
||||
@@ -278,6 +279,59 @@ const deleteItem = id => {
|
||||
getDb().prepare(`UPDATE favorites SET deleted = 1, updated_at = ? WHERE id = ? AND deleted = 0`).run(nowIso(), id)
|
||||
}
|
||||
|
||||
// 回收站:列出软删除的收藏(含附件),按删除时间倒序
|
||||
const listTrash = () => {
|
||||
const rows = getDb().prepare(`
|
||||
SELECT * FROM favorites
|
||||
WHERE deleted = 1
|
||||
ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC
|
||||
`).all()
|
||||
return rows.map(row => {
|
||||
const item = mapFavorite(row)
|
||||
item.attachments = getAttachmentsByFavoriteId(row.id)
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// 回收站:恢复软删除的收藏
|
||||
const restoreItem = id => {
|
||||
const info = getDb().prepare(`UPDATE favorites SET deleted = 0, updated_at = ? WHERE id = ? AND deleted = 1`).run(nowIso(), id)
|
||||
return info.changes > 0
|
||||
}
|
||||
|
||||
// 回收站:彻底删除(物理删除记录 + 磁盘文件 + 清理空目录),不可恢复
|
||||
const purgeItem = id => {
|
||||
const row = getDb().prepare(`SELECT * FROM favorites WHERE id = ? AND deleted = 1 LIMIT 1`).get(id)
|
||||
if (!row) return { ok: false, error: '回收站中不存在该收藏' }
|
||||
const attachments = getDb().prepare(`SELECT * FROM attachments WHERE favorite_id = ?`).all(id)
|
||||
let filesRemoved = 0
|
||||
let bytesRemoved = 0
|
||||
const dirsToTry = new Set()
|
||||
for (const a of attachments) {
|
||||
const rel = String(a.file_path || '')
|
||||
if (rel) {
|
||||
const abs = path.join(process.cwd(), rel)
|
||||
try {
|
||||
if (fs.statSync(abs).isFile()) fs.unlinkSync(abs)
|
||||
filesRemoved++
|
||||
bytesRemoved += Number(a.file_size || 0)
|
||||
} catch {}
|
||||
try { dirsToTry.add(path.dirname(abs)) } catch {}
|
||||
}
|
||||
}
|
||||
const tx = getDb().transaction(() => {
|
||||
getDb().prepare(`DELETE FROM attachments WHERE favorite_id = ?`).run(id)
|
||||
getDb().prepare(`DELETE FROM favorites WHERE id = ? AND deleted = 1`).run(id)
|
||||
})
|
||||
tx()
|
||||
for (const dir of dirsToTry) {
|
||||
try {
|
||||
if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir)
|
||||
} catch {}
|
||||
}
|
||||
return { ok: true, filesRemoved, bytesRemoved, attachments: attachments.length }
|
||||
}
|
||||
|
||||
const insertAttachment = ({ id, favoriteId, fileName, storedName, filePath, fileSize, mimeType, isImage }) => {
|
||||
const createdAt = nowIso()
|
||||
const payload = {
|
||||
@@ -324,6 +378,9 @@ module.exports = {
|
||||
insertItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
listTrash,
|
||||
restoreItem,
|
||||
purgeItem,
|
||||
insertAttachment,
|
||||
getAttachmentById,
|
||||
getAttachmentsByFavoriteId,
|
||||
|
||||
Reference in New Issue
Block a user