fix: 修复商品图片管理和缓存一致性问题
- 重构 ImageUpload 组件,使用 rawUrl 跟踪原始路径 - 新增 normalizeStorageImageUrl 避免存储绝对 URL - 商品增删改后同步清除缓存和旧图片文件 - 修复秒杀活动列表商品图片为空的问题
This commit is contained in:
@@ -102,7 +102,10 @@ export const adminApi = {
|
|||||||
return request.post<ApiResponse<any>>('/api/admin/products', data).then((res) => ({ ...res, data: normalizeAdminProduct(res.data) }))
|
return request.post<ApiResponse<any>>('/api/admin/products', data).then((res) => ({ ...res, data: normalizeAdminProduct(res.data) }))
|
||||||
},
|
},
|
||||||
updateProduct(id: number, data: Record<string, unknown>): Promise<ApiResponse<AdminProductRow>> {
|
updateProduct(id: number, data: Record<string, unknown>): Promise<ApiResponse<AdminProductRow>> {
|
||||||
return request.put<ApiResponse<any>>(`/api/admin/products/${id}`, data).then((res) => ({ ...res, data: normalizeAdminProduct(res.data) }))
|
return request.put<ApiResponse<any>>(`/api/admin/products/${id}`, data).then((res) => ({
|
||||||
|
...res,
|
||||||
|
data: res.data ? normalizeAdminProduct(res.data) : undefined,
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
deleteProduct(id: number): Promise<ApiResponse> { return request.delete(`/api/admin/products/${id}`) },
|
deleteProduct(id: number): Promise<ApiResponse> { return request.delete(`/api/admin/products/${id}`) },
|
||||||
getReviewStats(): Promise<ApiResponse<AdminReviewStats>> { return request.get('/api/admin/reviews/stats') },
|
getReviewStats(): Promise<ApiResponse<AdminReviewStats>> { return request.get('/api/admin/reviews/stats') },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
:class="{ 'hide-upload': fileList.length >= limit }"
|
:class="{ 'hide-upload': fileList.length >= limit }"
|
||||||
:action="uploadUrl"
|
:action="uploadUrl"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
|
:with-credentials="true"
|
||||||
:file-list="fileList"
|
:file-list="fileList"
|
||||||
:limit="limit"
|
:limit="limit"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
@@ -67,7 +68,7 @@ import { ref, computed, watch } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { resolveImageUrl } from '@/utils/image'
|
import { resolveImageUrl } from '@/utils/image'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import type { UploadFile, UploadFiles, UploadRawFile } from 'element-plus'
|
import type { UploadFile, UploadRawFile } from 'element-plus'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: string | string[]
|
modelValue?: string | string[]
|
||||||
@@ -92,6 +93,10 @@ const emit = defineEmits<{
|
|||||||
change: [value: string | string[]]
|
change: [value: string | string[]]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
interface UploadFileWithRawUrl extends UploadFile {
|
||||||
|
rawUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 上传相关
|
// 上传相关
|
||||||
@@ -100,32 +105,42 @@ const headers = computed(() => ({
|
|||||||
Authorization: `Bearer ${userStore.token}`
|
Authorization: `Bearer ${userStore.token}`
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const fileList = ref<UploadFile[]>([])
|
const fileList = ref<UploadFileWithRawUrl[]>([])
|
||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
const previewUrl = ref('')
|
const previewUrl = ref('')
|
||||||
const previewName = ref('')
|
const previewName = ref('')
|
||||||
const previewSize = ref(0)
|
const previewSize = ref(0)
|
||||||
|
|
||||||
|
const buildFileItem = (rawUrl: string, index: number): UploadFileWithRawUrl => ({
|
||||||
|
name: props.multiple ? `image-${index}` : 'image',
|
||||||
|
url: resolveImageUrl(rawUrl),
|
||||||
|
status: 'success',
|
||||||
|
uid: `${Date.now()}-${index}`,
|
||||||
|
rawUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
const getRawUrl = (file: UploadFile) => {
|
||||||
|
const currentFile = file as UploadFileWithRawUrl
|
||||||
|
return currentFile.rawUrl?.trim() || file.url?.trim() || ''
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化文件列表
|
// 初始化文件列表
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (val) {
|
const nextFiles = Array.isArray(val)
|
||||||
if (Array.isArray(val)) {
|
? val
|
||||||
fileList.value = val.map((url, index) => ({
|
.map((url) => String(url || '').trim())
|
||||||
name: `image-${index}`,
|
.filter(Boolean)
|
||||||
url: resolveImageUrl(url),
|
.map((url, index) => buildFileItem(url, index))
|
||||||
status: 'success',
|
: val && String(val).trim()
|
||||||
uid: Date.now() + index
|
? [buildFileItem(String(val).trim(), 0)]
|
||||||
} as UploadFile))
|
: []
|
||||||
} else {
|
|
||||||
fileList.value = [{
|
const currentRawUrls = fileList.value.map((file) => getRawUrl(file))
|
||||||
name: 'image',
|
const nextRawUrls = nextFiles.map((file) => getRawUrl(file))
|
||||||
url: resolveImageUrl(val),
|
if (JSON.stringify(currentRawUrls) !== JSON.stringify(nextRawUrls)) {
|
||||||
status: 'success',
|
fileList.value = nextFiles
|
||||||
uid: Date.now()
|
|
||||||
} as UploadFile]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -185,9 +200,33 @@ const beforeUpload = (rawFile: UploadRawFile) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 上传成功
|
// 上传成功
|
||||||
const handleSuccess = (response: any, file: UploadFile, files: UploadFiles) => {
|
const handleSuccess = (response: any, file: UploadFile) => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
file.url = resolveImageUrl(response.data?.url || response.imageUrl || response.data?.imageUrl)
|
const rawImageUrl = String(response.imageUrl || response.data?.imageUrl || response.data?.url || '').trim()
|
||||||
|
if (!rawImageUrl) {
|
||||||
|
handleRemove(file)
|
||||||
|
ElMessage.error('上传成功但未返回图片地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadedFile = file as UploadFileWithRawUrl
|
||||||
|
uploadedFile.status = 'success'
|
||||||
|
uploadedFile.url = resolveImageUrl(rawImageUrl)
|
||||||
|
uploadedFile.rawUrl = rawImageUrl
|
||||||
|
|
||||||
|
if (props.limit === 1 && !props.multiple) {
|
||||||
|
fileList.value = [uploadedFile]
|
||||||
|
} else {
|
||||||
|
const nextFiles = [...fileList.value]
|
||||||
|
const index = nextFiles.findIndex((item) => item.uid === uploadedFile.uid)
|
||||||
|
if (index > -1) {
|
||||||
|
nextFiles[index] = uploadedFile
|
||||||
|
} else {
|
||||||
|
nextFiles.push(uploadedFile)
|
||||||
|
}
|
||||||
|
fileList.value = nextFiles
|
||||||
|
}
|
||||||
|
|
||||||
updateValue()
|
updateValue()
|
||||||
ElMessage.success('上传成功')
|
ElMessage.success('上传成功')
|
||||||
} else {
|
} else {
|
||||||
@@ -197,13 +236,13 @@ const handleSuccess = (response: any, file: UploadFile, files: UploadFiles) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 上传失败
|
// 上传失败
|
||||||
const handleError = (error: Error, file: UploadFile) => {
|
const handleError = (error: Error) => {
|
||||||
ElMessage.error('上传失败,请重试')
|
ElMessage.error('上传失败,请重试')
|
||||||
console.error('Upload error:', error)
|
console.error('Upload error:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 超出数量限制
|
// 超出数量限制
|
||||||
const handleExceed = (files: File[]) => {
|
const handleExceed = () => {
|
||||||
ElMessage.warning(`最多只能上传 ${props.limit} 张图片`)
|
ElMessage.warning(`最多只能上传 ${props.limit} 张图片`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,18 +264,16 @@ const handleDownload = (file: UploadFile) => {
|
|||||||
|
|
||||||
// 删除图片
|
// 删除图片
|
||||||
const handleRemove = (file: UploadFile) => {
|
const handleRemove = (file: UploadFile) => {
|
||||||
const index = fileList.value.findIndex(f => f.uid === file.uid)
|
fileList.value = fileList.value.filter(f => f.uid !== file.uid)
|
||||||
if (index > -1) {
|
updateValue()
|
||||||
fileList.value.splice(index, 1)
|
|
||||||
updateValue()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新值
|
// 更新值
|
||||||
const updateValue = () => {
|
const updateValue = () => {
|
||||||
const urls = fileList.value
|
const urls = fileList.value
|
||||||
.filter(f => f.status === 'success' && f.url)
|
.filter(f => f.status === 'success')
|
||||||
.map(f => f.url!)
|
.map(f => getRawUrl(f))
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
const value = props.multiple ? urls : urls[0] || ''
|
const value = props.multiple ? urls : urls[0] || ''
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
|
|||||||
@@ -28,6 +28,30 @@ export const resolveImageUrl = (value?: string | null) => {
|
|||||||
return imageUrl.startsWith('/') ? `${baseUrl}${imageUrl}` : `${baseUrl}/${imageUrl}`
|
return imageUrl.startsWith('/') ? `${baseUrl}${imageUrl}` : `${baseUrl}/${imageUrl}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const normalizeStorageImageUrl = (value?: string | null) => {
|
||||||
|
if (!value || !String(value).trim()) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = String(value).trim()
|
||||||
|
if (ABSOLUTE_URL_PATTERN.test(imageUrl)) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(imageUrl.startsWith('//') ? `http:${imageUrl}` : imageUrl)
|
||||||
|
if (
|
||||||
|
parsed.pathname.startsWith('/uploads/') ||
|
||||||
|
parsed.pathname.startsWith('/images/') ||
|
||||||
|
parsed.pathname.startsWith('/static/')
|
||||||
|
) {
|
||||||
|
return parsed.pathname
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return imageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageUrl
|
||||||
|
}
|
||||||
|
|
||||||
export const applyFallbackImage = (event: Event) => {
|
export const applyFallbackImage = (event: Event) => {
|
||||||
const target = event.target as HTMLImageElement | null
|
const target = event.target as HTMLImageElement | null
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
AdminRecentOrderRow,
|
AdminRecentOrderRow,
|
||||||
AdminUserRow,
|
AdminUserRow,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
import { DEFAULT_PRODUCT_IMAGE, resolveImageUrl } from '@/utils/image'
|
import { DEFAULT_PRODUCT_IMAGE, normalizeStorageImageUrl, resolveImageUrl } from '@/utils/image'
|
||||||
|
|
||||||
const toNumber = (value: unknown, fallback = 0) => {
|
const toNumber = (value: unknown, fallback = 0) => {
|
||||||
const result = Number(value)
|
const result = Number(value)
|
||||||
@@ -280,7 +280,7 @@ export const normalizeAdminProduct = (product: Record<string, any>): AdminProduc
|
|||||||
price: toNumber(product.price),
|
price: toNumber(product.price),
|
||||||
stock: toNumber(product.stock),
|
stock: toNumber(product.stock),
|
||||||
status: toNumber(product.status, 1),
|
status: toNumber(product.status, 1),
|
||||||
imageUrl: resolveImageUrl(toString(product.imageUrl, '')),
|
imageUrl: normalizeStorageImageUrl(toString(product.imageUrl, '')),
|
||||||
createdAt: toIsoLikeString(product.createdAt),
|
createdAt: toIsoLikeString(product.createdAt),
|
||||||
updatedAt: product.updatedAt ? toIsoLikeString(product.updatedAt) : undefined,
|
updatedAt: product.updatedAt ? toIsoLikeString(product.updatedAt) : undefined,
|
||||||
totalSales: toNumber(product.totalSales),
|
totalSales: toNumber(product.totalSales),
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public class ApiController {
|
|||||||
item.put("id", flashSale.getId());
|
item.put("id", flashSale.getId());
|
||||||
item.put("productId", flashSale.getProductId());
|
item.put("productId", flashSale.getProductId());
|
||||||
item.put("productName", flashSale.getProductName());
|
item.put("productName", flashSale.getProductName());
|
||||||
item.put("productImage", "");
|
item.put("productImage", flashSale.getProductImageUrl());
|
||||||
item.put("originalPrice", flashSale.getOriginalPrice());
|
item.put("originalPrice", flashSale.getOriginalPrice());
|
||||||
item.put("flashPrice", flashSale.getFlashPrice());
|
item.put("flashPrice", flashSale.getFlashPrice());
|
||||||
item.put("flashStock", flashSale.getFlashStock());
|
item.put("flashStock", flashSale.getFlashStock());
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ public interface FlashSaleRepository extends JpaRepository<FlashSale, Long> {
|
|||||||
".flashStock > 0")
|
".flashStock > 0")
|
||||||
List<FlashSale> findActiveFlashSalesWithStock(@Param("now") LocalDateTime now);
|
List<FlashSale> findActiveFlashSalesWithStock(@Param("now") LocalDateTime now);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据商品ID查找所有秒杀活动
|
||||||
|
*/
|
||||||
|
List<FlashSale> findByProductId(Long productId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计指定时间范围内正在进行的秒杀活动数量
|
* 统计指定时间范围内正在进行的秒杀活动数量
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ public class AdminService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RequestMetricsService requestMetricsService;
|
private RequestMetricsService requestMetricsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FileUploadService fileUploadService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProductService productService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取仪表盘统计数据
|
* 获取仪表盘统计数据
|
||||||
*/
|
*/
|
||||||
@@ -638,6 +644,8 @@ public class AdminService {
|
|||||||
Optional<Product> productOpt = productRepository.findById(id);
|
Optional<Product> productOpt = productRepository.findById(id);
|
||||||
if (productOpt.isPresent()) {
|
if (productOpt.isPresent()) {
|
||||||
Product product = productOpt.get();
|
Product product = productOpt.get();
|
||||||
|
String oldImageUrl = product.getImageUrl();
|
||||||
|
boolean stockUpdated = false;
|
||||||
|
|
||||||
if (productData.containsKey("name")) {
|
if (productData.containsKey("name")) {
|
||||||
product.setName((String) productData.get("name"));
|
product.setName((String) productData.get("name"));
|
||||||
@@ -647,6 +655,7 @@ public class AdminService {
|
|||||||
}
|
}
|
||||||
if (productData.containsKey("stock")) {
|
if (productData.containsKey("stock")) {
|
||||||
product.setStock(Integer.parseInt(productData.get("stock").toString()));
|
product.setStock(Integer.parseInt(productData.get("stock").toString()));
|
||||||
|
stockUpdated = true;
|
||||||
}
|
}
|
||||||
if (productData.containsKey("category")) {
|
if (productData.containsKey("category")) {
|
||||||
product.setCategory((String) productData.get("category"));
|
product.setCategory((String) productData.get("category"));
|
||||||
@@ -663,6 +672,16 @@ public class AdminService {
|
|||||||
|
|
||||||
product.setUpdatedAt(LocalDateTime.now());
|
product.setUpdatedAt(LocalDateTime.now());
|
||||||
productRepository.save(product);
|
productRepository.save(product);
|
||||||
|
if (stockUpdated) {
|
||||||
|
productService.syncProductStockCache(id, product.getStock());
|
||||||
|
}
|
||||||
|
productService.invalidateProductCaches(id);
|
||||||
|
if (!Objects.equals(oldImageUrl, product.getImageUrl())) {
|
||||||
|
fileUploadService.deleteProductImage(oldImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除引用该商品的秒杀活动缓存,确保图片/名称/价格更新及时生效
|
||||||
|
invalidateFlashSaleCacheByProductId(id);
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("商品不存在");
|
throw new RuntimeException("商品不存在");
|
||||||
}
|
}
|
||||||
@@ -672,12 +691,37 @@ public class AdminService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除引用指定商品的所有秒杀活动缓存
|
||||||
|
*/
|
||||||
|
private void invalidateFlashSaleCacheByProductId(Long productId) {
|
||||||
|
try {
|
||||||
|
List<com.org.flashsalesystem.entity.FlashSale> flashSales =
|
||||||
|
flashSaleRepository.findByProductId(productId);
|
||||||
|
for (com.org.flashsalesystem.entity.FlashSale fs : flashSales) {
|
||||||
|
String cacheKey = "flashsale:" + fs.getId();
|
||||||
|
redisService.delete(cacheKey);
|
||||||
|
log.debug("清除秒杀活动缓存: {}", cacheKey);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("清除秒杀活动缓存失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除商品
|
* 删除商品
|
||||||
*/
|
*/
|
||||||
public void deleteProduct(Long id) {
|
public void deleteProduct(Long id) {
|
||||||
try {
|
try {
|
||||||
if (productRepository.existsById(id)) {
|
Optional<Product> productOpt = productRepository.findById(id);
|
||||||
|
if (productOpt.isPresent()) {
|
||||||
|
Product product = productOpt.get();
|
||||||
|
// 清理磁盘上的图片文件
|
||||||
|
fileUploadService.deleteProductImage(product.getImageUrl());
|
||||||
|
productService.invalidateProductCaches(id);
|
||||||
|
productService.removeProductStockCache(id);
|
||||||
|
// 清除关联的秒杀活动缓存
|
||||||
|
invalidateFlashSaleCacheByProductId(id);
|
||||||
productRepository.deleteById(id);
|
productRepository.deleteById(id);
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("商品不存在");
|
throw new RuntimeException("商品不存在");
|
||||||
@@ -705,6 +749,8 @@ public class AdminService {
|
|||||||
product.setUpdatedAt(LocalDateTime.now());
|
product.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
Product savedProduct = productRepository.save(product);
|
Product savedProduct = productRepository.save(product);
|
||||||
|
productService.syncProductStockCache(savedProduct.getId(), savedProduct.getStock());
|
||||||
|
productService.invalidateProductCaches(savedProduct.getId());
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("id", savedProduct.getId());
|
result.put("id", savedProduct.getId());
|
||||||
|
|||||||
@@ -129,15 +129,23 @@ public class FileUploadService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只处理本地上传的图片
|
String normalizedImageUrl = imageUrl;
|
||||||
if (!imageUrl.startsWith(urlPrefix)) {
|
if (!imageUrl.startsWith(urlPrefix)) {
|
||||||
|
int prefixIndex = imageUrl.indexOf(urlPrefix);
|
||||||
|
if (prefixIndex >= 0) {
|
||||||
|
normalizedImageUrl = imageUrl.substring(prefixIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理本地上传的图片
|
||||||
|
if (!normalizedImageUrl.startsWith(urlPrefix)) {
|
||||||
log.warn("非本地图片,跳过删除: {}", imageUrl);
|
log.warn("非本地图片,跳过删除: {}", imageUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 从URL中提取相对路径
|
// 从URL中提取相对路径
|
||||||
String relativePath = imageUrl.substring(urlPrefix.length());
|
String relativePath = normalizedImageUrl.substring(urlPrefix.length());
|
||||||
Path filePath = Paths.get(uploadPath + relativePath);
|
Path filePath = Paths.get(uploadPath + relativePath);
|
||||||
|
|
||||||
if (Files.exists(filePath)) {
|
if (Files.exists(filePath)) {
|
||||||
@@ -200,4 +208,4 @@ public class FileUploadService {
|
|||||||
return "application/octet-stream";
|
return "application/octet-stream";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,6 +253,27 @@ public class ProductService {
|
|||||||
return productDTO;
|
return productDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void invalidateProductCaches(Long productId) {
|
||||||
|
if (productId != null) {
|
||||||
|
redisService.delete(PRODUCT_CACHE_PREFIX + productId);
|
||||||
|
}
|
||||||
|
clearProductListCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncProductStockCache(Long productId, Integer stock) {
|
||||||
|
if (productId == null || stock == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisService.set(PRODUCT_STOCK_PREFIX + productId, stock);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeProductStockCache(Long productId) {
|
||||||
|
if (productId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redisService.delete(PRODUCT_STOCK_PREFIX + productId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新商品库存
|
* 更新商品库存
|
||||||
*/
|
*/
|
||||||
@@ -452,8 +473,10 @@ public class ProductService {
|
|||||||
* 清除商品列表缓存
|
* 清除商品列表缓存
|
||||||
*/
|
*/
|
||||||
private void clearProductListCache() {
|
private void clearProductListCache() {
|
||||||
// 使用通配符删除所有商品列表缓存
|
Set<String> listCacheKeys = redisService.keys(PRODUCT_LIST_CACHE_PREFIX + "*");
|
||||||
// 注意:这里简化处理,实际生产环境可能需要更精确的缓存管理
|
if (!listCacheKeys.isEmpty()) {
|
||||||
|
redisService.delete(listCacheKeys);
|
||||||
|
}
|
||||||
redisService.delete(HOT_PRODUCTS_CACHE);
|
redisService.delete(HOT_PRODUCTS_CACHE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -319,6 +319,14 @@ public class RedisService {
|
|||||||
return redisTemplate.delete(keys);
|
return redisTemplate.delete(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据模式查找键
|
||||||
|
*/
|
||||||
|
public Set<String> keys(String pattern) {
|
||||||
|
Set<String> keys = stringRedisTemplate.keys(pattern);
|
||||||
|
return keys == null ? Collections.emptySet() : keys;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查键是否存在
|
* 检查键是否存在
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user