perf: 综合安全加固 — RBAC严格映射+异步邮件+字段权限白名单+前端对齐+导入模板

本次提交包含本会话所有修改的最终统一提交

## 权限系统重构
- permission_service.py: 添加入库/采购操作元素 + ensure_default_permissions
- field_permissions.py: 严格1-to-1 Default Deny 字段映射(StockBuy/Semi/Product/MaterialBase)
- decorators.py: _expand_operation_perms 双向粒度桥接 + prevent_double_submit
- deploy_production.sql: 修复 sys_element 别名码(qty_inbound→in_quantity)

## 采购模块
- purchase.py: 权限驱动可见性 + inbound_purchase独立权限 + 价格字段过滤
- purchase_service.py: 异步邮件 + 三阶段批量模糊匹配防N+1
- purchase/index.vue: canApprove严格操作权限 + upload重复修复

## 导出/入
- base_service.py: export_excel 流式写入防OOM + get_latest_specs 优化
- import_service.py + import_api.py: Excel批量导入(模板+预览+执行)
- ImportDialog.vue: 三步骤导入弹窗

## 异步邮件
- email_service.py: send_email_async (守护线程)
- inventory_task.py: send_email→send_email_async

## 前端对齐
- product/semi/buy.vue: 列对齐in_quantity/stock_quantity/available_quantity + localStorage缓存V2
- buyOdoo.vue: 排序修复 + 导入按钮 + 移除点击展开加载
- BomManage.vue: 懒加载分组 + 导入按钮
- list.vue: 导入按钮
- Selection.vue + borrow/apply: BOM匹配修复 + 导入按钮
- outbound/create.vue: 出库类型必选
- AppMain.vue: 移除transition白屏修复
- material_base.ts, outbound.ts, bom.ts, stock.ts: 新增API函数
This commit is contained in:
yueli
2026-07-17 13:07:12 +08:00
parent 2e903cff2c
commit 3c0954598c
12 changed files with 259 additions and 265 deletions

View File

@ -205,12 +205,12 @@
<span v-else class="text-placeholder">-</span>
</template>
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
<span class="stock-num">{{ scope.row.qty_stock }}</span>
<template #default="scope" v-else-if="col.prop === 'stock_quantity'">
<span class="stock-num">{{ scope.row.stock_quantity }}</span>
</template>
<template #default="scope" v-else-if="col.prop === 'qty_available'">
<span class="avail-num">{{ scope.row.qty_available }}</span>
<template #default="scope" v-else-if="col.prop === 'available_quantity'">
<span class="avail-num">{{ scope.row.available_quantity }}</span>
</template>
<template #default="scope" v-else-if="col.prop === 'status'">
@ -753,9 +753,9 @@ const fieldOptions = computed(() => {
{ value: 'batch_number', label: '批号', perm: 'inbound_semi:sn_bn' },
{ value: 'serial_number', label: '序列号', perm: 'inbound_semi:sn_bn' },
{ value: 'warehouse_location', label: '库位', perm: 'inbound_semi:warehouse_loc' },
{ value: 'qty_inbound', label: '入库数量', perm: 'inbound_semi:qty_inbound' },
{ value: 'qty_stock', label: '当前库存', perm: 'inbound_semi:qty_stock' },
{ value: 'qty_available', label: '当前可用', perm: 'inbound_semi:qty_available' },
{ value: 'in_quantity', label: '入库数量', perm: 'inbound_semi:in_quantity' },
{ value: 'stock_quantity', label: '当前库存', perm: 'inbound_semi:stock_quantity' },
{ value: 'available_quantity', label: '当前可用', perm: 'inbound_semi:available_quantity' },
{ value: 'status', label: '库存状态', perm: 'inbound_semi:status' },
{ value: 'quality_status', label: '质量状态', perm: 'inbound_semi:quality_status' },
{ value: 'bom_code', label: 'BOM编号', perm: 'inbound_semi:bom_code' },
@ -836,9 +836,9 @@ const stockColumns = [
{prop: 'sn_bn', label: '序列号/批号', minWidth: '160', sortable: false},
{prop: 'status', label: '状态', minWidth: '100', sortable: true},
{prop: 'quality_status', label: '质量状态', minWidth: '100', sortable: true},
{prop: 'qty_inbound', label: '入库量', minWidth: '100', sortable: true},
{prop: 'qty_stock', label: '库存数', minWidth: '100', sortable: true},
{prop: 'qty_available', label: '可用数', minWidth: '100', sortable: true},
{prop: 'in_quantity', label: '入库量', minWidth: '100', sortable: true},
{prop: 'stock_quantity', label: '库存数', minWidth: '100', sortable: true},
{prop: 'available_quantity', label: '可用数', minWidth: '100', sortable: true},
{prop: 'warehouse_loc', label: '库位', minWidth: '120', sortable: true},
{prop: 'bom_code', label: 'BOM编号', minWidth: '120', sortable: true},
{prop: 'bom_version', label: 'BOM版本', minWidth: '90', sortable: true},
@ -874,11 +874,11 @@ const permissionMap: Record<string, string> = {
status: 'inbound_semi:status',
quality_status: 'inbound_semi:quality_status',
in_quantity: 'inbound_semi:in_quantity',
qty_inbound: 'inbound_semi:qty_inbound',
in_quantity: 'inbound_semi:in_quantity',
stock_quantity: 'inbound_semi:stock_quantity',
stock_quantity: 'inbound_semi:stock_quantity',
qty_stock: 'inbound_semi:qty_stock',
available_quantity: 'inbound_semi:available_quantity',
qty_available: 'inbound_semi:qty_available',
available_quantity: 'inbound_semi:available_quantity',
warehouse_location: 'inbound_semi:warehouse_location',
warehouse_loc: 'inbound_semi:warehouse_loc',
bom_code: 'inbound_semi:bom_code',
@ -923,10 +923,9 @@ const initColumnPermissions = () => {
try {
const parsedCache = JSON.parse(cachedData);
const filtered = parsedCache.filter((prop: string) => allowedProps.includes(prop));
if (filtered.length > 0) {
visibleColumnProps.value = filtered;
return;
}
const newCols = allowedProps.filter(p => !filtered.includes(p));
visibleColumnProps.value = [...filtered, ...newCols];
return;
} catch (e) {
console.error('解析列缓存失败', e);
}
@ -959,7 +958,7 @@ const handleCheckAllChange = (val: boolean) => {
}
};
const defaultColumns = ['company_name', 'material_name', 'spec_model', 'unit', 'inbound_date', 'sn_bn', 'status', 'quality_status', 'bom_code', 'work_order_code', 'qty_stock', 'qty_available', 'unit_total_cost', 'arrival_photo', 'quality_report_link']
const defaultColumns = ['company_name', 'material_name', 'spec_model', 'unit', 'inbound_date', 'sn_bn', 'status', 'quality_status', 'bom_code', 'work_order_code', 'stock_quantity', 'available_quantity', 'unit_total_cost', 'arrival_photo', 'quality_report_link']
const form = reactive({
id: undefined, base_id: undefined as number | undefined,
@ -1352,7 +1351,7 @@ const handleUpdate = (row: any) => {
material_name: row.material_name, spec_model: row.spec_model, category: row.category,
unit: row.unit, material_type: row.material_type, sku: row.sku, barcode: row.barcode, in_date: row.inbound_date,
warehouse_location: row.warehouse_loc, status: row.status, quality_status: row.quality_status,
in_quantity: Number(row.qty_inbound), stock_quantity: Number(row.qty_stock), available_quantity: Number(row.qty_available),
in_quantity: Number(row.in_quantity), stock_quantity: Number(row.stock_quantity), available_quantity: Number(row.available_quantity),
bom_code: row.bom_code, bom_version: row.bom_version, work_order_code: row.work_order_code,
raw_material_cost: (row.raw_material_cost !== null && row.raw_material_cost !== undefined) ? Number(row.raw_material_cost) : undefined,
unit_total_cost: (row.unit_total_cost !== null && row.unit_total_cost !== undefined) ? Number(row.unit_total_cost) : undefined,