完善字段信息

This commit is contained in:
2026-07-22 12:03:30 +08:00
parent 4ff7a490d9
commit a8bb6c2fcd
3 changed files with 144 additions and 95 deletions

View File

@@ -2,7 +2,6 @@ const Database = require('better-sqlite3');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
// 确保数据目录存在
const dataDir = '/app/data'; const dataDir = '/app/data';
if (!fs.existsSync(dataDir)) { if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true }); fs.mkdirSync(dataDir, { recursive: true });
@@ -10,12 +9,9 @@ if (!fs.existsSync(dataDir)) {
const dbPath = path.join(dataDir, 'xiaocai.db'); const dbPath = path.join(dataDir, 'xiaocai.db');
const db = new Database(dbPath); const db = new Database(dbPath);
// 启用 WAL 模式提升并发
db.pragma('journal_mode = WAL'); db.pragma('journal_mode = WAL');
// 创建 // 创建基础表(如果不存在)
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY, username TEXT PRIMARY KEY,
@@ -27,7 +23,7 @@ db.exec(`
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
created_by TEXT NOT NULL, created_by TEXT NOT NULL,
white_list TEXT DEFAULT '', -- 逗号分隔用户名,空表示所有人 white_list TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now','localtime')) created_at TEXT DEFAULT (datetime('now','localtime'))
); );
@@ -36,7 +32,7 @@ db.exec(`
room_id TEXT NOT NULL, room_id TEXT NOT NULL,
user TEXT NOT NULL, user TEXT NOT NULL,
text TEXT, text TEXT,
attachments TEXT, -- JSON array of file paths attachments TEXT,
timestamp TEXT DEFAULT (datetime('now','localtime')), timestamp TEXT DEFAULT (datetime('now','localtime')),
FOREIGN KEY(room_id) REFERENCES rooms(id) FOREIGN KEY(room_id) REFERENCES rooms(id)
); );
@@ -45,13 +41,17 @@ db.exec(`
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
room_id TEXT NOT NULL, room_id TEXT NOT NULL,
item TEXT, item TEXT,
amount REAL, quantity INTEGER DEFAULT 1,
method TEXT, unit_price REAL DEFAULT 0,
amount REAL DEFAULT 0,
freight REAL DEFAULT 0,
payment_method TEXT DEFAULT '未指定',
invoice_type TEXT DEFAULT '无票',
status TEXT DEFAULT '待处理', status TEXT DEFAULT '待处理',
applicant TEXT, applicant TEXT,
remarks TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now','localtime')), created_at TEXT DEFAULT (datetime('now','localtime')),
updated_at TEXT DEFAULT (datetime('now','localtime')), updated_at TEXT DEFAULT (datetime('now','localtime')),
ai_notes TEXT,
FOREIGN KEY(room_id) REFERENCES rooms(id) FOREIGN KEY(room_id) REFERENCES rooms(id)
); );
@@ -75,4 +75,21 @@ db.exec(`
); );
`); `);
// 为已有数据库添加可能缺失的列(安全迁移,忽略错误)
const migrations = [
`ALTER TABLE purchases ADD COLUMN quantity INTEGER DEFAULT 1`,
`ALTER TABLE purchases ADD COLUMN unit_price REAL DEFAULT 0`,
`ALTER TABLE purchases ADD COLUMN freight REAL DEFAULT 0`,
`ALTER TABLE purchases ADD COLUMN invoice_type TEXT DEFAULT '无票'`,
`ALTER TABLE purchases ADD COLUMN payment_method TEXT DEFAULT '未指定'`,
`ALTER TABLE purchases ADD COLUMN remarks TEXT DEFAULT ''`,
];
for (const sql of migrations) {
try {
db.exec(sql);
} catch (e) {
// 列已存在或其他错误,忽略
}
}
module.exports = db; module.exports = db;

View File

@@ -77,7 +77,7 @@
.purchase-item .item-main { display: flex; justify-content: space-between; align-items: baseline; } .purchase-item .item-main { display: flex; justify-content: space-between; align-items: baseline; }
.purchase-item .item-name { font-size: 16px; font-weight: 500; } .purchase-item .item-name { font-size: 16px; font-weight: 500; }
.purchase-item .item-amount { font-size: 16px; font-weight: 600; } .purchase-item .item-amount { font-size: 16px; font-weight: 600; }
.purchase-item .item-meta { display: flex; justify-content: space-between; margin-top: 4px; font-size: 13px; color: #888; } .purchase-item .item-meta { display: flex; justify-content: space-between; margin-top: 4px; font-size: 13px; color: #888; flex-wrap: wrap; gap: 4px; }
.status-badge { background: #fef3c7; padding: 1px 6px; border-radius: 8px; font-size: 12px; } .status-badge { background: #fef3c7; padding: 1px 6px; border-radius: 8px; font-size: 12px; }
.status-badge.done { background: #d1fae5; } .status-badge.done { background: #d1fae5; }
@@ -99,7 +99,6 @@
.version { text-align: center; font-size: 12px; color: #aaa; margin-top: 16px; } .version { text-align: center; font-size: 12px; color: #aaa; margin-top: 16px; }
/* 全屏预览弹窗 */
.fullscreen-overlay { .fullscreen-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.9); display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.9); display: flex; align-items: center; justify-content: center;
@@ -111,6 +110,8 @@
overflow: auto; font-size: 16px; line-height: 1.5; color: #333; overflow: auto; font-size: 16px; line-height: 1.5; color: #333;
} }
.fullscreen-overlay .fullscreen-text img { max-width: 200px; border-radius: 8px; margin: 4px 0; } .fullscreen-overlay .fullscreen-text img { max-width: 200px; border-radius: 8px; margin: 4px 0; }
.detail-row { margin-bottom: 8px; }
</style> </style>
</head> </head>
<body> <body>
@@ -123,7 +124,7 @@
<input type="password" id="login-pass" placeholder="密码"> <input type="password" id="login-pass" placeholder="密码">
<button class="primary-btn" onclick="login()" style="width:100%;">登录</button> <button class="primary-btn" onclick="login()" style="width:100%;">登录</button>
<p id="login-error" style="color:red; margin-top:8px;"></p> <p id="login-error" style="color:red; margin-top:8px;"></p>
<div class="version">v2.2</div> <div class="version">v2.4</div>
</div> </div>
</div> </div>
@@ -332,14 +333,11 @@
function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; } function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; }
// 双击全屏相关
function bindMessageEvents() { function bindMessageEvents() {
// 图片双击全屏
document.querySelectorAll('.msg-img').forEach(img => { document.querySelectorAll('.msg-img').forEach(img => {
img.removeEventListener('dblclick', onImageDblClick); img.removeEventListener('dblclick', onImageDblClick);
img.addEventListener('dblclick', onImageDblClick); img.addEventListener('dblclick', onImageDblClick);
}); });
// 气泡双击全屏
document.querySelectorAll('.msg-bubble').forEach(bubble => { document.querySelectorAll('.msg-bubble').forEach(bubble => {
bubble.removeEventListener('dblclick', onBubbleDblClick); bubble.removeEventListener('dblclick', onBubbleDblClick);
bubble.addEventListener('dblclick', onBubbleDblClick); bubble.addEventListener('dblclick', onBubbleDblClick);
@@ -348,10 +346,9 @@
function onImageDblClick(e) { function onImageDblClick(e) {
e.stopPropagation(); e.stopPropagation();
const imgSrc = e.target.src;
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'fullscreen-overlay'; overlay.className = 'fullscreen-overlay';
overlay.innerHTML = `<img src="${imgSrc}" alt="">`; overlay.innerHTML = `<img src="${e.target.src}" alt="">`;
overlay.addEventListener('click', () => overlay.remove()); overlay.addEventListener('click', () => overlay.remove());
document.body.appendChild(overlay); document.body.appendChild(overlay);
} }
@@ -359,9 +356,7 @@
function onBubbleDblClick(e) { function onBubbleDblClick(e) {
e.stopPropagation(); e.stopPropagation();
const bubble = e.currentTarget; const bubble = e.currentTarget;
// 克隆气泡内容,去掉事件
const clone = bubble.cloneNode(true); const clone = bubble.cloneNode(true);
// 移除可能有的链接行为
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'fullscreen-overlay'; overlay.className = 'fullscreen-overlay';
const container = document.createElement('div'); const container = document.createElement('div');
@@ -488,12 +483,10 @@
} }
} }
// 修正滑动手势方向
function initSwipeGestures() { function initSwipeGestures() {
const chatPage = document.getElementById('chat-page'); const chatPage = document.getElementById('chat-page');
const purchaseOverlay = document.getElementById('purchase-panel-overlay'); const purchaseOverlay = document.getElementById('purchase-panel-overlay');
let startX = 0; let startX = 0, startY = 0;
let startY = 0;
function handleTouchStart(e) { function handleTouchStart(e) {
startX = e.touches[0].clientX; startX = e.touches[0].clientX;
@@ -508,22 +501,12 @@
const diffY = endY - startY; const diffY = endY - startY;
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) { if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
if (target === chatPage && !purchaseOverlay.classList.contains('hidden')) { if (target === chatPage && !purchaseOverlay.classList.contains('hidden')) {
// 采购清单打开时,任意水平滑动都关闭 if (Math.abs(diffX) > 30) closePurchasePanel();
if (Math.abs(diffX) > 30) {
closePurchasePanel();
}
} else if (target === chatPage) { } else if (target === chatPage) {
// 右滑返回列表,左滑打开采购清单 if (diffX > 30) showList();
if (diffX > 30) { else if (diffX < -30) openPurchasePanel();
showList();
} else if (diffX < -30) {
openPurchasePanel();
}
} else if (target === purchaseOverlay) { } else if (target === purchaseOverlay) {
// 采购清单上任意滑动都关闭 if (Math.abs(diffX) > 30) closePurchasePanel();
if (Math.abs(diffX) > 30) {
closePurchasePanel();
}
} }
} }
startX = 0; startY = 0; startX = 0; startY = 0;
@@ -681,7 +664,11 @@
html += ` html += `
<div class="purchase-item" onclick="openDetail('${item.id}')"> <div class="purchase-item" onclick="openDetail('${item.id}')">
<div class="item-main"><span class="item-name">${item.item}</span><span class="item-amount">¥${item.amount}</span></div> <div class="item-main"><span class="item-name">${item.item}</span><span class="item-amount">¥${item.amount}</span></div>
<div class="item-meta"><span>${dateShort} ${timeShort}</span><span>${item.applicant || ''} / ${item.method || ''}</span><span class="status-badge ${item.status==='已完成'?'done':''}">${item.status}</span></div> <div class="item-meta">
<span>${dateShort} ${timeShort}</span>
<span>×${item.quantity} ${item.invoice_type !== '无票' ? '· ' + item.invoice_type : ''}</span>
<span class="status-badge ${item.status==='已完成'?'done':''}">${item.status}</span>
</div>
</div>`; </div>`;
}); });
html += '</div>'; html += '</div>';
@@ -710,7 +697,20 @@
let attachHtml = ''; let attachHtml = '';
if (p.attachments?.length) attachHtml = '<div class="attachments">' + p.attachments.map(a => `<img class="img-thumb" src="${a.file_path}" alt="${a.file_path}">`).join('') + '</div>'; if (p.attachments?.length) attachHtml = '<div class="attachments">' + p.attachments.map(a => `<img class="img-thumb" src="${a.file_path}" alt="${a.file_path}">`).join('') + '</div>';
const historyHtml = p.history?.map(h => `<li><span class="history-time">${h.timestamp}</span> ${h.user}: ${h.action}</li>`).join('') || ''; const historyHtml = p.history?.map(h => `<li><span class="history-time">${h.timestamp}</span> ${h.user}: ${h.action}</li>`).join('') || '';
document.getElementById('detail-content').innerHTML = `<p><strong>金额:</strong>¥${p.amount} | <strong>方式:</strong>${p.method}</p><p><strong>申请人:</strong>${p.applicant}</p>${attachHtml}<h4 style="margin-top:12px;">操作历史</h4><ul class="history-list">${historyHtml}</ul>`; document.getElementById('detail-content').innerHTML = `
<div class="detail-row"><strong>数量:</strong>${p.quantity}</div>
<div class="detail-row"><strong>单价:</strong>¥${p.unit_price}</div>
<div class="detail-row"><strong>邮费:</strong>¥${p.freight}</div>
<div class="detail-row"><strong>总金额:</strong>¥${p.amount}</div>
<div class="detail-row"><strong>付款方式:</strong>${p.payment_method || '未指定'}</div>
<div class="detail-row"><strong>发票:</strong>${p.invoice_type || '无票'}</div>
<div class="detail-row"><strong>状态:</strong>${p.status}</div>
<div class="detail-row"><strong>申请人:</strong>${p.applicant}</div>
<div class="detail-row"><strong>采购时间:</strong>${p.created_at}</div>
<div class="detail-row"><strong>备注:</strong>${p.remarks || '无'}</div>
${attachHtml}
<h4 style="margin-top:12px;">操作历史</h4>
<ul class="history-list">${historyHtml}</ul>`;
document.getElementById('detail-modal').classList.remove('hidden'); document.getElementById('detail-modal').classList.remove('hidden');
} }
function closeDetailModal() { document.getElementById('detail-modal').classList.add('hidden'); } function closeDetailModal() { document.getElementById('detail-modal').classList.add('hidden'); }

View File

@@ -22,11 +22,14 @@ const ADMINS = (process.env.ADMINS || '').split(',');
const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY; const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;
const JWT_SECRET = process.env.JWT_SECRET || 'xiaocai-secret-' + Date.now(); const JWT_SECRET = process.env.JWT_SECRET || 'xiaocai-secret-' + Date.now();
// 时区:亚洲/上海 // 时区
const TIMEZONE = 'Asia/Shanghai'; const TIMEZONE = 'Asia/Shanghai';
function timestamp() { function timestamp() {
return new Date().toLocaleString('zh-CN', { timeZone: TIMEZONE, hour12: false }); return new Date().toLocaleString('zh-CN', { timeZone: TIMEZONE, hour12: false });
} }
function currentTimeStr() {
return timestamp();
}
// 初始化用户 // 初始化用户
const insertUser = db.prepare('INSERT OR IGNORE INTO users (username, password, is_admin) VALUES (?, ?, ?)'); const insertUser = db.prepare('INSERT OR IGNORE INTO users (username, password, is_admin) VALUES (?, ?, ?)');
@@ -113,7 +116,6 @@ app.post('/api/rooms', auth, adminOnly, (req, res) => {
res.json({ id, name }); res.json({ id, name });
}); });
// 更新群聊信息(管理员)
app.put('/api/rooms/:roomId', auth, adminOnly, (req, res) => { app.put('/api/rooms/:roomId', auth, adminOnly, (req, res) => {
const { name, whiteList } = req.body; const { name, whiteList } = req.body;
const roomId = req.params.roomId; const roomId = req.params.roomId;
@@ -124,7 +126,6 @@ app.put('/api/rooms/:roomId', auth, adminOnly, (req, res) => {
res.json({ success: true }); res.json({ success: true });
}); });
// 删除群聊(管理员)
app.delete('/api/rooms/:roomId', auth, adminOnly, (req, res) => { app.delete('/api/rooms/:roomId', auth, adminOnly, (req, res) => {
const roomId = req.params.roomId; const roomId = req.params.roomId;
db.prepare('DELETE FROM messages WHERE room_id = ?').run(roomId); db.prepare('DELETE FROM messages WHERE room_id = ?').run(roomId);
@@ -141,7 +142,6 @@ app.get('/api/rooms/:roomId/messages', auth, (req, res) => {
res.json(msgs); res.json(msgs);
}); });
// 发送消息(统一入口,异步触发 AI
app.post('/api/rooms/:roomId/messages', auth, async (req, res) => { app.post('/api/rooms/:roomId/messages', auth, async (req, res) => {
const { text, attachments } = req.body; const { text, attachments } = req.body;
const roomId = req.params.roomId; const roomId = req.params.roomId;
@@ -161,22 +161,15 @@ app.post('/api/rooms/:roomId/messages', auth, async (req, res) => {
}; };
const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY timestamp DESC LIMIT 1').get(roomId); const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY timestamp DESC LIMIT 1').get(roomId);
const preview = {
room_id: roomId,
last_message: lastMsg ? lastMsg.text : '',
last_time: lastMsg ? lastMsg.timestamp : ''
};
broadcastToRoomExcludeSelf(roomId, username, { broadcastToRoomExcludeSelf(roomId, username, {
type: 'new_message', type: 'new_message',
message: msg, message: msg,
room_preview: preview room_preview: { room_id: roomId, last_message: lastMsg ? lastMsg.text : '', last_time: lastMsg ? lastMsg.timestamp : '' }
}); });
addToHistory(roomId, 'user', `${username}: ${text || '图片'}`); addToHistory(roomId, 'user', `${username}: ${text || '图片'}`);
res.json(msg); res.json(msg);
// 检查是否有待确认操作且用户回复了“确认”
if (text && text.trim() === '确认' && hasPendingAction(roomId, username)) { if (text && text.trim() === '确认' && hasPendingAction(roomId, username)) {
executePendingAction(roomId, username); executePendingAction(roomId, username);
return; return;
@@ -228,9 +221,9 @@ app.get('/api/summary', auth, (req, res) => {
app.get('/api/rooms/:roomId/purchases/export', auth, (req, res) => { app.get('/api/rooms/:roomId/purchases/export', auth, (req, res) => {
const purchases = db.prepare('SELECT * FROM purchases WHERE room_id = ?').all(req.params.roomId); const purchases = db.prepare('SELECT * FROM purchases WHERE room_id = ?').all(req.params.roomId);
let csv = '时间,事项,金额,付款方式,状态,申请人\n'; let csv = '时间,事项,数量,单价,金额,邮费,付款方式,发票类型,状态,申请人,备注\n';
purchases.forEach(p => { purchases.forEach(p => {
csv += `${p.created_at},"${p.item||''}",${p.amount},"${p.method||''}","${p.status}","${p.applicant}"\n`; csv += `${p.created_at},"${p.item||''}",${p.quantity},${p.unit_price},${p.amount},${p.freight},"${p.payment_method||''}","${p.invoice_type||''}","${p.status}","${p.applicant}","${p.remarks||''}"\n`;
}); });
res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="purchases-${req.params.roomId}.csv"`); res.setHeader('Content-Disposition', `attachment; filename="purchases-${req.params.roomId}.csv"`);
@@ -310,7 +303,7 @@ function storeAndBroadcastText(roomId, user, text) {
return msg; return msg;
} }
// ---------- 待处理操作管理 ---------- // 待处理操作
const pendingActions = new Map(); const pendingActions = new Map();
function hasPendingAction(roomId, username) { return pendingActions.has(`${roomId}:${username}`); } function hasPendingAction(roomId, username) { return pendingActions.has(`${roomId}:${username}`); }
function setPendingAction(roomId, username, action) { pendingActions.set(`${roomId}:${username}`, action); } function setPendingAction(roomId, username, action) { pendingActions.set(`${roomId}:${username}`, action); }
@@ -343,40 +336,30 @@ function executePendingAction(roomId, username) {
} catch (e) { console.error('执行待处理操作失败:', e); storeAndBroadcastText(roomId, '小财', '❌ 操作执行失败,请重试。'); return false; } } catch (e) { console.error('执行待处理操作失败:', e); storeAndBroadcastText(roomId, '小财', '❌ 操作执行失败,请重试。'); return false; }
} }
// ---------- 对话历史 ---------- // 对话历史
const conversationHistory = new Map(); const conversationHistory = new Map();
function getHistory(roomId) { function getHistory(roomId) { if (!conversationHistory.has(roomId)) conversationHistory.set(roomId, []); return conversationHistory.get(roomId); }
if (!conversationHistory.has(roomId)) conversationHistory.set(roomId, []); function addToHistory(roomId, role, content) { const history = getHistory(roomId); history.push({ role, content }); if (history.length > 60) conversationHistory.set(roomId, history.slice(-40)); }
return conversationHistory.get(roomId);
}
function addToHistory(roomId, role, content) {
const history = getHistory(roomId);
history.push({ role, content });
if (history.length > 60) conversationHistory.set(roomId, history.slice(-40));
}
// ---------- AI 函数 ---------- // AI 分析
async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin) { async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin) {
const systemPrompt = `你是智能财务助手“小财”。结合对话历史理解用户意图只返回JSON。 const systemPrompt = `你是智能财务助手“小财”。当前服务器时间:${currentTimeStr()}结合对话历史理解用户意图只返回JSON。
意图分类: 意图分类:
- 采购/付款/发票相关action="purchase",提取purchase_item, amount, method, status, applicant, created_time(可选根据聊天中的时间推断格式yyyy/MM/dd HH:mm:ss) - 采购/付款/发票相关action="purchase",提取以下字段(所有字段可选,缺失则不返回):
purchase_item (物品名称), quantity (数量默认1), unit_price (单价默认0), amount (总金额默认0),
freight (邮费默认0), payment_method (付款方式:支付宝/微信/对公转账/现金等,淘宝购买默认支付宝),
invoice_type (发票类型:增票/普票/无票,默认无票), status (待采购/已采购/发票已收), applicant (申请人),
remarks (备注,重要信息如“下次换商家”“缺少配件”等), created_time (如果用户提到具体时间则按yyyy/MM/dd HH:mm:ss格式输出)
- 查询汇总action="query" - 查询汇总action="query"
- 聊天action="chat"reply简短回复 - 聊天action="chat"reply简短回复
- 删除指定物品action="delete"delete_item为物品名 - 删除指定物品action="delete"delete_item为物品名
- 清空所有采购数据action="clear_all" - 清空所有采购数据action="clear_all"
- 忽略action="ignore" - 忽略action="ignore"
重要权限规则: 权限规则:删除和清空仅限管理员。当前用户${username},管理员状态:${isAdmin}
- 删除和清空操作只有管理员可以执行。当前用户${username}的管理员状态:${isAdmin} 采购规则:用户补充信息时更新已有记录。更新时请返回完整字段值,包括未变化的字段
- 如果用户要求删除或清空但当前用户不是管理员你必须直接回复拒绝返回action="chat"reply说明需要管理员权限。 如果用户要求修改时间请生成正确的created_time字段。`;
- 如果用户是管理员返回delete或clear_all action。
采购规则:
- 金额缺失=0方式缺失="未指定",申请人缺失="${username}"。
- 状态:已付→已采购,发票→发票已收,否则待采购。
- 用户补充信息时更新已有记录。
- 时间提取如果用户提到具体时间设置created_time字段格式yyyy/MM/dd HH:mm:ss。`;
const messages = [ const messages = [
{ role: 'system', content: systemPrompt }, { role: 'system', content: systemPrompt },
@@ -405,10 +388,9 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
return; return;
} }
// 删除指定物品(管理员权限)
if (aiResult.action === 'delete') { if (aiResult.action === 'delete') {
const itemName = aiResult.delete_item; const itemName = aiResult.delete_item;
const purchases = db.prepare('SELECT id, item, amount, method, status, applicant, created_at FROM purchases WHERE room_id = ? AND item LIKE ?').all(roomId, `%${itemName}%`); const purchases = db.prepare('SELECT id, item, amount, payment_method, status, applicant, created_at FROM purchases WHERE room_id = ? AND item LIKE ?').all(roomId, `%${itemName}%`);
if (purchases.length === 0) { if (purchases.length === 0) {
storeAndBroadcastText(roomId, '小财', `没有找到与“${itemName}”相关的采购记录。`); storeAndBroadcastText(roomId, '小财', `没有找到与“${itemName}”相关的采购记录。`);
addToHistory(roomId, 'assistant', `没有找到与“${itemName}”相关的采购记录。`); addToHistory(roomId, 'assistant', `没有找到与“${itemName}”相关的采购记录。`);
@@ -421,14 +403,10 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
confirmText += `\n如果不删除,请忽略此消息。`; confirmText += `\n如果不删除,请忽略此消息。`;
storeAndBroadcastText(roomId, '小财', confirmText); storeAndBroadcastText(roomId, '小财', confirmText);
addToHistory(roomId, 'assistant', confirmText); addToHistory(roomId, 'assistant', confirmText);
setPendingAction(roomId, username, { setPendingAction(roomId, username, { type: 'delete', data: { itemName, purchaseIds: purchases.map(p => p.id) } });
type: 'delete',
data: { itemName, purchaseIds: purchases.map(p => p.id) }
});
return; return;
} }
// 清空所有采购数据(管理员权限)
if (aiResult.action === 'clear_all') { if (aiResult.action === 'clear_all') {
const count = db.prepare('SELECT COUNT(*) as count FROM purchases WHERE room_id = ?').get(roomId).count; const count = db.prepare('SELECT COUNT(*) as count FROM purchases WHERE room_id = ?').get(roomId).count;
if (count === 0) { if (count === 0) {
@@ -447,25 +425,79 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
const createdTime = aiResult.created_time || now; const createdTime = aiResult.created_time || now;
let purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? AND status != ?').get(roomId, `%${aiResult.purchase_item}%`, '已完成'); let purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? AND status != ?').get(roomId, `%${aiResult.purchase_item}%`, '已完成');
let replyText = ''; let replyText = '';
// 计算总金额:优先使用 AI 提供的 amount否则根据单价*数量+邮费计算
const quantity = aiResult.quantity || 1;
const unitPrice = aiResult.unit_price || 0;
const freight = aiResult.freight || 0;
let amount;
if (aiResult.amount !== undefined && aiResult.amount !== null) {
amount = aiResult.amount;
} else {
amount = unitPrice * quantity + freight;
}
if (!purchase) { if (!purchase) {
const id = uuidv4(); const id = uuidv4();
db.prepare('INSERT INTO purchases (id, room_id, item, amount, method, status, applicant, created_at) VALUES (?,?,?,?,?,?,?,?)').run( const item = aiResult.purchase_item;
id, roomId, aiResult.purchase_item, aiResult.amount, aiResult.method, aiResult.status || '待处理', aiResult.applicant || username, createdTime const paymentMethod = aiResult.payment_method || (aiResult.method === '淘宝' ? '支付宝' : (aiResult.method || '未指定'));
const invoiceType = aiResult.invoice_type || '无票';
const status = aiResult.status || '待处理';
const applicant = aiResult.applicant || username;
const remarks = aiResult.remarks || '';
db.prepare(`INSERT INTO purchases (id, room_id, item, quantity, unit_price, amount, freight, payment_method, invoice_type, status, applicant, remarks, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
id, roomId, item, quantity, unitPrice, amount, freight, paymentMethod, invoiceType, status, applicant, remarks, createdTime
); );
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(id, 'AI 创建采购条目', '小财', now); db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(id, `创建采购:${item},数量${quantity},金额¥${amount},状态${status}`, '小财', now);
replyText = `✅ 已记录采购:${aiResult.purchase_item},金额 ¥${aiResult.amount},状态 ${aiResult.status || '待处理'}`; replyText = `✅ 已记录采购:${item},数量 ${quantity},金额 ¥${amount},状态 ${status}`;
purchase = { id }; purchase = { id };
} else { } else {
db.prepare('UPDATE purchases SET amount = ?, method = ?, status = ?, updated_at = ? WHERE id = ?').run( const old = purchase;
aiResult.amount, aiResult.method, aiResult.status, now, purchase.id // 合并新值
); const newVals = {
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(purchase.id, `更新采购信息(金额:${aiResult.amount},状态:${aiResult.status}`, username, now); quantity: aiResult.quantity ?? old.quantity,
replyText = `🔄 已更新采购「${aiResult.purchase_item}」:金额 ¥${aiResult.amount},状态 ${aiResult.status}`; unit_price: aiResult.unit_price ?? old.unit_price,
freight: aiResult.freight ?? old.freight,
payment_method: aiResult.payment_method || (aiResult.method === '淘宝' ? '支付宝' : (aiResult.method || old.payment_method)),
invoice_type: aiResult.invoice_type || old.invoice_type,
status: aiResult.status || old.status,
applicant: aiResult.applicant || old.applicant,
remarks: aiResult.remarks !== undefined ? aiResult.remarks : old.remarks,
created_at: aiResult.created_time || old.created_at
};
// 重新计算金额,除非 AI 明确给了 amount
if (aiResult.amount !== undefined && aiResult.amount !== null) {
newVals.amount = aiResult.amount;
} else {
newVals.amount = newVals.unit_price * newVals.quantity + newVals.freight;
} }
const historyChanges = [];
let changed = false;
for (const [field, newVal] of Object.entries(newVals)) {
if (String(newVal) !== String(old[field])) {
changed = true;
historyChanges.push(`${field}: ${old[field]}${newVal}`);
db.prepare(`UPDATE purchases SET ${field} = ? WHERE id = ?`).run(newVal, purchase.id);
}
}
if (changed) {
db.prepare('UPDATE purchases SET updated_at = ? WHERE id = ?').run(now, purchase.id);
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(purchase.id, `更新:${historyChanges.join('')}`, username, now);
replyText = `🔄 已更新「${aiResult.purchase_item}」:${historyChanges.join('')}`;
} else {
replyText = ` 采购「${aiResult.purchase_item}」的信息没有发生变化。`;
}
}
if (attachments?.length) { if (attachments?.length) {
const insertAttach = db.prepare('INSERT INTO purchase_attachments (purchase_id, file_path, uploaded_by, timestamp) VALUES (?,?,?,?)'); const insertAttach = db.prepare('INSERT INTO purchase_attachments (purchase_id, file_path, uploaded_by, timestamp) VALUES (?,?,?,?)');
attachments.forEach(fp => insertAttach.run(purchase.id, fp, username, now)); attachments.forEach(fp => insertAttach.run(purchase.id, fp, username, now));
} }
storeAndBroadcastText(roomId, '小财', replyText); storeAndBroadcastText(roomId, '小财', replyText);
addToHistory(roomId, 'assistant', replyText); addToHistory(roomId, 'assistant', replyText);
broadcastToRoom(roomId, { type: 'purchase_updated' }); broadcastToRoom(roomId, { type: 'purchase_updated' });
@@ -473,8 +505,8 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
} }
if (aiResult.action === 'query') { if (aiResult.action === 'query') {
const purchaseData = db.prepare('SELECT item, amount, method, status FROM purchases WHERE room_id = ? ORDER BY created_at DESC').all(roomId) const purchaseData = db.prepare('SELECT item, amount, payment_method, status FROM purchases WHERE room_id = ? ORDER BY created_at DESC').all(roomId)
.map(p => `${p.item} ¥${p.amount} ${p.status}`).join('\n'); .map(p => `${p.item} ¥${p.amount} ${p.payment_method||''} ${p.status}`).join('\n');
const summaryPrompt = `根据以下采购记录,用自然语言回答用户查询“${originalText}”。采购记录:\n${purchaseData || '暂无记录'}`; const summaryPrompt = `根据以下采购记录,用自然语言回答用户查询“${originalText}”。采购记录:\n${purchaseData || '暂无记录'}`;
callDeepSeekForSummary(summaryPrompt).then(reply => { callDeepSeekForSummary(summaryPrompt).then(reply => {
storeAndBroadcastText(roomId, '小财', reply); storeAndBroadcastText(roomId, '小财', reply);