feat: add admin web updates and verified username changes
Add isolated update jobs with database backups, progress logs and application rollback. Remove default account hints and prevent default admin recreation after renaming. Enforce customer and order search scopes, secure session cookies, fix quotation rounding and amount wording, and update backend dependencies. Add API and deployment regression coverage.
This commit is contained in:
+39
-9
@@ -766,8 +766,11 @@ app.put('/api/products/:sku/packaging', auth.requirePermission('product.edit'),
|
||||
});
|
||||
|
||||
// 订单路由
|
||||
app.get('/api/orders', (req, res) => {
|
||||
app.get('/api/orders', auth.requirePermission('order.view'), (req, res) => {
|
||||
try {
|
||||
const canViewAllOrders = auth.permissionsOfRole(req.user.role).includes('order.edit');
|
||||
const orderFilter = canViewAllOrders ? '' : 'WHERE o.owner_id IS NULL OR o.owner_id = ?';
|
||||
const orderParams = canViewAllOrders ? [] : [req.user.id];
|
||||
/* 带上杂费合计:订单列表上只看 total_amount 看不出赚没赚 ——
|
||||
快递费吊卡费加起来几百块,小单子能把毛利吃掉一截。
|
||||
逐行取两位再 SUM,跟支出明细页的合计口径一致。 */
|
||||
@@ -782,8 +785,9 @@ app.get('/api/orders', (req, res) => {
|
||||
(SELECT COUNT(*) FROM order_expenses e WHERE e.order_id = o.id) AS expense_count
|
||||
FROM orders o
|
||||
LEFT JOIN customers c ON o.customer_id = c.id
|
||||
${orderFilter}
|
||||
ORDER BY o.order_date DESC
|
||||
`).all();
|
||||
`).all(...orderParams);
|
||||
res.json(orders);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -925,6 +929,7 @@ app.get('/api/orders/:id/expenses', auth.requirePermission('order.view'), (req,
|
||||
try {
|
||||
const order = getOrder(req.params.id);
|
||||
if (!order) return res.status(404).json({ error: '订单不存在' });
|
||||
if (!canTouchOrderExpense(req.user, order)) return res.status(403).json({ error: '这张订单不在你名下' });
|
||||
const expenses = db.prepare(`
|
||||
SELECT e.*, s.name AS supplier_name, u.display_name AS created_by_name
|
||||
FROM order_expenses e
|
||||
@@ -1226,7 +1231,7 @@ app.post('/api/assistant/command', async (req, res) => {
|
||||
|
||||
|
||||
|
||||
// 全局搜索:仅索引业务表的非敏感字段,查询参数使用绑定变量。
|
||||
// 全局搜索:查询参数使用绑定变量;客户及其关联记录必须遵守客户归属范围。
|
||||
const SEARCH_SOURCES = [
|
||||
{ table: 'suppliers', type: 'supplier', typeLabel: '供应商', title: ['name', 'short_name'], detail: ['contact_person', 'phone', 'email', 'address', 'website', 'payment_terms', 'main_categories', 'notes'], path: '/suppliers' },
|
||||
{ table: 'product_groups', type: 'product', typeLabel: '产品分组', title: ['name', 'group_key'], detail: ['brand', 'category', 'notes'], path: '/products' },
|
||||
@@ -1258,9 +1263,32 @@ app.get('/api/search', (req, res) => {
|
||||
const columns = existingColumns(source.table);
|
||||
const searchable = [...source.title, ...source.detail].filter(column => columns.has(column));
|
||||
if (!searchable.length) continue;
|
||||
const where = searchable.map(column => `CAST(${column} AS TEXT) LIKE ?`).join(' OR ');
|
||||
const selected = ['rowid AS _search_id', ...searchable].join(', ');
|
||||
const rows = db.prepare(`SELECT ${selected} FROM ${source.table} WHERE ${where} LIMIT 8`).all(...searchable.map(() => pattern));
|
||||
const where = searchable.map(column => `CAST(s.${column} AS TEXT) LIKE ?`).join(' OR ');
|
||||
const selected = ['s.rowid AS _search_id', ...searchable.map(column => `s.${column}`)].join(', ');
|
||||
const conditions = [`(${where})`];
|
||||
const params = searchable.map(() => pattern);
|
||||
let from = `${source.table} s`;
|
||||
if (['customers', 'contacts', 'activities', 'deals', 'todos'].includes(source.table)) {
|
||||
const link = source.table === 'customers' ? 's.id' : 's.customer_id';
|
||||
from += ` LEFT JOIN customers c ON c.id = ${link}`;
|
||||
const scope = customerAccess.scopeFilter(req.user, 'all');
|
||||
if (scope.conditions.length) {
|
||||
const owned = scope.conditions.join(' AND ');
|
||||
// 没有关联客户的工作台记录仍可检索;关联客户的记录不能绕过归属。
|
||||
conditions.push(['activities', 'todos'].includes(source.table)
|
||||
? `(s.customer_id IS NULL OR (${owned}))`
|
||||
: `(${owned})`);
|
||||
}
|
||||
params.push(...scope.params);
|
||||
}
|
||||
if (['orders', 'order_items'].includes(source.table)
|
||||
&& !auth.permissionsOfRole(req.user.role).includes('order.edit')) {
|
||||
const orderLink = source.table === 'orders' ? 's.id' : 's.order_id';
|
||||
from += ` JOIN orders o ON o.id = ${orderLink}`;
|
||||
conditions.push('(o.owner_id IS NULL OR o.owner_id = ?)');
|
||||
params.push(req.user.id);
|
||||
}
|
||||
const rows = db.prepare(`SELECT ${selected} FROM ${from} WHERE ${conditions.join(' AND ')} LIMIT 8`).all(...params);
|
||||
rows.forEach(row => {
|
||||
const title = source.title.map(column => row[column]).filter(Boolean).join(' · ') || source.typeLabel;
|
||||
const detail = source.detail.map(column => row[column]).filter(value => value !== undefined && value !== null && value !== '').join(' · ');
|
||||
@@ -1694,18 +1722,20 @@ app.post('/api/quotes', auth.requirePermission('quote.edit'), (req, res) => {
|
||||
|
||||
const provided = pick('unit_price');
|
||||
// 前端为了即时预览会自己算一次并传上来;没传就按同一口径在后端补算,兜住
|
||||
const unitPrice = provided !== null ? provided : priceFromCost(purchasePrice, exchangeRate, markupRate);
|
||||
if (unitPrice === null || unitPrice < 0) {
|
||||
const rawUnitPrice = provided !== null ? provided : priceFromCost(purchasePrice, exchangeRate, markupRate);
|
||||
if (rawUnitPrice === null || rawUnitPrice < 0) {
|
||||
throw badRequest(`${label}的单价无法确定:请填单价,或同时填齐采购价 / 汇率 / 加价率`);
|
||||
}
|
||||
|
||||
// 行金额必须由单据上实际存储、显示的四位单价计算。
|
||||
const unitPrice = roundTo(rawUnitPrice, 4);
|
||||
return {
|
||||
sku,
|
||||
productName: String(raw?.product_name || sku).trim(),
|
||||
productNameEn: raw?.product_name_en ? String(raw.product_name_en).trim() : null,
|
||||
spec: raw?.spec ? String(raw.spec).trim() : null,
|
||||
qty,
|
||||
unitPrice: roundTo(unitPrice, 4),
|
||||
unitPrice,
|
||||
totalPrice: roundTo(unitPrice * qty, 2),
|
||||
currency,
|
||||
purchasePrice, exchangeRate, markupRate,
|
||||
|
||||
Reference in New Issue
Block a user