优化PUT请求传参机制,实现最小化传参以减少网络传输和提高性能,在请求拦截器中自动计算差异只传递修改过的字段,同时新增通用的表单差异比较工具函数,

This commit is contained in:
WUSIJIAN
2026-01-13 18:06:53 +08:00
parent cdbda577cc
commit f8af956f06
6 changed files with 289 additions and 24 deletions

View File

@@ -137,8 +137,8 @@ export function getAssetSku(id: string) {
});
}
// 修改 SKU
export function updateAssetSku(data: CreateSkuParams & { id: string }) {
// 修改 SKU(支持部分更新,只传递修改过的字段)
export function updateAssetSku(data: Partial<CreateSkuParams> & { id: string }) {
return newService({
url: '/assets/asset/sku/updateAssetSku',
method: 'put',

152
src/utils/diffUtils.ts Normal file
View File

@@ -0,0 +1,152 @@
/**
* 差异比较工具函数
* 用于编辑时最小化传参,只传递修改过的字段
*/
/**
* 深度比较两个值是否相等
* @param val1 值1
* @param val2 值2
* @returns 是否相等
*/
export function isEqual(val1: any, val2: any): boolean {
// 处理 null 和 undefined
if (val1 === val2) return true;
if (val1 == null || val2 == null) return val1 == val2;
// 处理基本类型
if (typeof val1 !== 'object' || typeof val2 !== 'object') {
return val1 === val2;
}
// 处理数组
if (Array.isArray(val1) && Array.isArray(val2)) {
if (val1.length !== val2.length) return false;
return val1.every((item, index) => isEqual(item, val2[index]));
}
// 处理对象
if (Array.isArray(val1) !== Array.isArray(val2)) return false;
const keys1 = Object.keys(val1);
const keys2 = Object.keys(val2);
if (keys1.length !== keys2.length) return false;
return keys1.every((key) => isEqual(val1[key], val2[key]));
}
/**
* 比较两个对象,返回差异部分
* @param original 原始数据
* @param current 当前数据
* @param options 配置选项
* @returns 差异数据对象
*/
export function getChangedFields<T extends Record<string, any>>(
original: T,
current: T,
options?: {
/** 需要包含的字段(即使没有变化也会包含) */
alwaysInclude?: string[];
/** 需要排除的字段(即使有变化也不会包含) */
exclude?: string[];
/** 字段值转换器,用于提交前转换值 */
transformers?: Record<string, (value: any) => any>;
}
): Partial<T> {
const { alwaysInclude = [], exclude = [], transformers = {} } = options || {};
const changed: Partial<T> = {};
// 遍历当前数据的所有字段
const allKeys = new Set([...Object.keys(original), ...Object.keys(current)]);
allKeys.forEach((key) => {
// 排除指定字段
if (exclude.includes(key)) return;
const originalValue = original[key];
const currentValue = current[key];
// 检查是否需要始终包含
if (alwaysInclude.includes(key)) {
const value = transformers[key] ? transformers[key](currentValue) : currentValue;
(changed as any)[key] = value;
return;
}
// 比较值是否变化
if (!isEqual(originalValue, currentValue)) {
const value = transformers[key] ? transformers[key](currentValue) : currentValue;
(changed as any)[key] = value;
}
});
return changed;
}
/**
* 创建一个用于保存和比较表单数据的工具
* @returns 工具对象
*/
export function createFormDiff<T extends Record<string, any>>() {
let originalData: T | null = null;
return {
/**
* 保存原始数据
* @param data 原始数据
*/
saveOriginal(data: T) {
// 深拷贝保存原始数据
originalData = JSON.parse(JSON.stringify(data));
},
/**
* 获取原始数据
* @returns 原始数据
*/
getOriginal(): T | null {
return originalData;
},
/**
* 获取变化的字段
* @param current 当前数据
* @param options 配置选项
* @returns 差异数据对象
*/
getChanges(
current: T,
options?: {
alwaysInclude?: string[];
exclude?: string[];
transformers?: Record<string, (value: any) => any>;
}
): Partial<T> {
if (!originalData) {
// 如果没有原始数据,返回所有当前数据
return { ...current };
}
return getChangedFields(originalData, current, options);
},
/**
* 检查是否有变化
* @param current 当前数据
* @param exclude 排除的字段
* @returns 是否有变化
*/
hasChanges(current: T, exclude?: string[]): boolean {
if (!originalData) return true;
const changes = getChangedFields(originalData, current, { exclude });
return Object.keys(changes).length > 0;
},
/**
* 重置原始数据
*/
reset() {
originalData = null;
},
};
}

View File

@@ -2,6 +2,7 @@ import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from
import { ElMessage, ElMessageBox } from 'element-plus';
import { Session } from '/@/utils/storage';
import qs from 'qs';
import { getChangedFields } from '/@/utils/diffUtils';
// 标记是否正在处理 token 过期,避免重复弹窗
let isHandlingTokenExpired = false;
@@ -92,6 +93,33 @@ const requestInterceptor = (config: InternalAxiosRequestConfig) => {
// 可以在这里添加 token 有效性检查(如果需要)
config.headers!['Authorization'] = `Bearer ${token}`;
}
// PUT 请求最小化传参处理
// 如果请求数据中包含 _originalData则自动计算差异只传递修改过的字段
if (config.method?.toLowerCase() === 'put' && config.data && typeof config.data === 'object') {
const { _originalData, ...currentData } = config.data;
if (_originalData && typeof _originalData === 'object') {
// 获取 id 字段(必须保留)
const idField = currentData.id || currentData.Id || currentData.ID;
// 计算差异
const changedFields = getChangedFields(_originalData, currentData, {
exclude: ['_originalData', 'id', 'Id', 'ID'],
});
// 如果有变化,只传递 id + 变化的字段
if (Object.keys(changedFields).length > 0) {
config.data = { id: idField, ...changedFields };
} else {
// 没有变化,只传递 id
config.data = { id: idField };
}
console.log('[最小化传参] 原始字段数:', Object.keys(currentData).length, '-> 传递字段数:', Object.keys(config.data).length);
}
}
return config;
};

View File

@@ -445,6 +445,7 @@ import type { FormInstance, FormRules } from 'element-plus';
import { Plus, Delete } from '@element-plus/icons-vue';
import { getAsset, createAsset, updateAsset, uploadAssetImage } from '/@/api/assets/asset';
import { getCategoryTree, getCategory } from '/@/api/assets/category';
import { createFormDiff } from '/@/utils/diffUtils';
import Editor from '/@/components/editor/index.vue';
import type { UploadFile, UploadUserFile, UploadRequestOptions } from 'element-plus';
@@ -555,6 +556,8 @@ const dialogVisible = ref(false);
const dialogImageUrl = ref('');
// 图片拼接
const fileAddressPrefix = ref('');
// 使用通用工具函数保存原始数据,用于最小化传参
const assetFormDiff = createFormDiff<Record<string, any>>();
const formatImageUrl = (url?: string) => {
if (!url) return '';
@@ -1021,6 +1024,9 @@ const openDialog = (row?: any, edit?: boolean) => {
categoryAttrs.value = [];
});
}
// 保存原始数据用于最小化传参
assetFormDiff.saveOriginal(JSON.parse(JSON.stringify(ruleForm)));
})
.finally(() => {
formLoading.value = false;
@@ -1187,7 +1193,21 @@ const onSubmit = async () => {
if (valid) {
submitLoading.value = true;
try {
const requestBody = await buildRequestBody();
const fullRequestBody = await buildRequestBody();
let requestBody: any;
if (isEdit.value) {
// 编辑模式:通过 _originalData 让拦截器自动处理最小化传参
const originalData = assetFormDiff.getOriginal();
requestBody = {
...fullRequestBody,
_originalData: originalData,
};
} else {
// 新增模式:传递所有字段
requestBody = fullRequestBody;
}
const request = isEdit.value ? updateAsset(requestBody) : createAsset(requestBody);
await request;

View File

@@ -174,6 +174,7 @@ import { ref, reactive } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import { listAssetSkus, createAssetSku, updateAssetSku, deleteAssetSku, getAssetSku, getAsset, uploadAssetImage, getSpecsUnitOptions } from '/@/api/assets/asset';
import { createFormDiff } from '/@/utils/diffUtils';
import type { UploadRequestOptions, UploadUserFile } from 'element-plus';
interface SpecValueItem {
@@ -228,6 +229,9 @@ const skuForm = reactive({
const specValuesList = ref<SpecValueItem[]>([{ key: '', value: '' }]);
const specValuesMap = reactive<Record<string, string>>({});
// 使用通用工具函数保存原始数据,用于最小化传参
const skuFormDiff = createFormDiff<Record<string, any>>();
const specValuesMapDiff = createFormDiff<Record<string, string>>();
const skuRules: FormRules = {
skuName: [{ required: true, message: '请输入SKU名称', trigger: 'blur' }],
@@ -402,6 +406,19 @@ const onEditSku = async (row: any) => {
skuForm.specsUnit = data.specsUnit || '';
}
skuForm.specsCount = data.specsCount || 1;
// 使用工具函数保存原始数据用于最小化传参
skuFormDiff.saveOriginal({
skuName: skuForm.skuName,
price: skuForm.price,
stock: skuForm.stock,
unlimitedStock: skuForm.unlimitedStock,
status: skuForm.status,
sort: skuForm.sort,
imageUrl: skuForm.imageUrl,
description: skuForm.description,
specsUnit: skuForm.specsUnit,
specsCount: skuForm.specsCount,
});
// 图片预览回显
if (data.imageUrl) {
skuImagePreview.value = formatImageUrl(data.imageUrl);
@@ -437,6 +454,8 @@ const onEditSku = async (row: any) => {
} else {
specValuesList.value = [{ key: '', value: '' }];
}
// 保存原始规格属性数据
specValuesMapDiff.saveOriginal({ ...specValuesMap });
} catch (error) {
skuFormVisible.value = false;
} finally {
@@ -599,7 +618,7 @@ const onSubmitSku = async () => {
});
// 构建 specsUnit 对象格式
let specsUnitObj = undefined;
let specsUnitObj: { key: string; value: string } | undefined = undefined;
if (skuForm.specsUnit) {
const unitOption = specsUnitOptions.value.find((opt) => opt.key === skuForm.specsUnit);
specsUnitObj = {
@@ -608,6 +627,42 @@ const onSubmitSku = async () => {
};
}
try {
if (isEditSku.value) {
// 编辑模式:使用工具函数获取修改过的字段
const currentFormData = {
skuName: skuForm.skuName,
price: skuForm.price,
stock: skuForm.stock,
unlimitedStock: skuForm.unlimitedStock,
status: skuForm.status,
sort: skuForm.sort,
imageUrl: skuForm.imageUrl,
description: skuForm.description,
specsUnit: skuForm.specsUnit,
specsCount: skuForm.specsCount,
};
const changedFields = skuFormDiff.getChanges(currentFormData, {
alwaysInclude: ['id'],
transformers: {
price: (val) => Math.round(val * 100),
specsUnit: () => specsUnitObj,
imageUrl: (val) => val || undefined,
},
});
// 添加 id
const changedData: Record<string, any> = { id: editSkuId.value, ...changedFields };
// 比较规格属性
if (specValuesMapDiff.hasChanges(specValuesMap) && specValues.length > 0) {
changedData.specValues = specValues;
}
await updateAssetSku(changedData as any);
} else {
// 新增模式:传递所有字段
const data = {
assetId: assetId.value,
assetName: assetName.value,
@@ -623,11 +678,6 @@ const onSubmitSku = async () => {
specsUnit: specsUnitObj,
specsCount: skuForm.specsCount || undefined,
};
try {
if (isEditSku.value) {
await updateAssetSku({ ...data, id: editSkuId.value });
} else {
await createAssetSku(data);
}
ElMessage.success(isEditSku.value ? '编辑成功' : '添加成功');

View File

@@ -126,6 +126,7 @@ import { ElMessage } from 'element-plus';
import { Plus, Delete } from '@element-plus/icons-vue';
import { getCategoryTree, getCategory, addCategory, updateCategory, getCategoryAttrTypeOptions } from '/@/api/assets/category';
import { getDicts } from '/@/api/system/dict/data';
import { createFormDiff } from '/@/utils/diffUtils';
interface CategoryRow {
id: string;
@@ -181,6 +182,8 @@ const attrTypeOptions = ref<{ key: string; value: string }[]>([]);
const dictTypeOptions = ref<DictInfo[]>([]);
const dictValueOptions = ref<any[]>([]);
const dictLoading = ref(false);
// 使用通用工具函数保存原始数据,用于最小化传参
const categoryFormDiff = createFormDiff<Record<string, any>>();
const ruleForm = reactive<RuleForm>({
id: '',
@@ -362,7 +365,12 @@ const openDialog = (row?: CategoryRow | string, edit?: boolean) => {
}
}
});
// 保存原始数据用于最小化传参
categoryFormDiff.saveOriginal(JSON.parse(JSON.stringify(ruleForm)));
});
} else {
// 保存原始数据用于最小化传参
categoryFormDiff.saveOriginal(JSON.parse(JSON.stringify(ruleForm)));
}
});
} else if (row && typeof row === 'string') {
@@ -432,10 +440,16 @@ const onSubmit = () => {
options: [],
};
});
const submitData = { ...ruleForm, attrs: processedAttrs };
if (isEdit.value) {
// 修改
// 编辑模式:通过 _originalData 让拦截器自动处理最小化传参
const originalData = categoryFormDiff.getOriginal();
const submitData = {
...ruleForm,
attrs: processedAttrs,
_originalData: originalData,
};
updateCategory(submitData)
.then(() => {
ElMessage.success('修改成功');
@@ -446,7 +460,8 @@ const onSubmit = () => {
submitLoading.value = false;
});
} else {
// 新增
// 新增模式:传递所有字段
const submitData = { ...ruleForm, attrs: processedAttrs };
addCategory(submitData)
.then(() => {
ElMessage.success('添加成功');