商品库存累加减公共方法修正

This commit is contained in:
Jack 2025-06-13 10:36:38 +08:00
parent c665492023
commit bce489fe55
5 changed files with 407 additions and 205 deletions

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2025. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
* Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan.
* Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna.
* Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus.
* Vestibulum commodo. Ut rhoncus gravida arcu.
*/
package com.suisung.mall.common.service;
/**
* 通用服务接口
*/
public interface CommonService {
/**
* 尝试获取分布式锁
*
* @param lockKey 锁的key
* @param expireSeconds 锁过期时间
* @return 锁标识解锁时需用加锁失败返回null
*/
String tryDistributedLock(String lockKey, long expireSeconds);
/**
* 释放分布式锁
*
* @param lockKey 锁的key
* @param lockValue 加锁时返回的value确保只有持有锁的线程能解锁
* @return 是否释放成功
*/
boolean releaseLock(String lockKey, String lockValue);
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (c) 2025. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
* Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan.
* Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna.
* Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus.
* Vestibulum commodo. Ut rhoncus gravida arcu.
*/
package com.suisung.mall.common.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class CommonServiceImpl {
@Lazy
@Resource
private RedisTemplate redisTemplate;
/**
* 尝试获取分布式锁
* <p>
* 使用实例
* String lockKey = "order:123";
* long expireSeconds = 10;
* String lockValue = commonServiceImpl.tryDistributedLock(lockKey, expireSeconds);
* if (lockValue != null) {
* try {
* // 执行业务逻辑
* } finally {
* commonServiceImpl.releaseLock(lockKey, lockValue);
* }
* } else {
* // 获取锁失败做相应处理
* }
*
* @param lockKey 锁的key
* @param expireSeconds 锁过期时间
* @return 锁标识解锁时需用加锁失败返回null
*/
public String tryDistributedLock(String lockKey, long expireSeconds) {
String lockValue = UUID.randomUUID().toString();
try {
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success) ? lockValue : null;
} catch (Exception e) {
// 记录异常日志实际项目可用Logger
e.printStackTrace();
return null;
}
}
/**
* 释放分布式锁
*
* @param lockKey 锁的key
* @param lockValue 加锁时返回的value确保只有持有锁的线程能解锁
* @return 是否释放成功
*/
public boolean releaseLock(String lockKey, String lockValue) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
try {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = (Long) redisTemplate.execute(script, Collections.singletonList(lockKey), lockValue);
return result != null && result > 0;
} catch (Exception e) {
log.error("释放分布式锁异常key={}, error={}", lockKey, e.getMessage(), e);
return false;
}
}
}

View File

@ -28,6 +28,16 @@ redis:
separator: ":"
expire: 3600
redisson:
address: redis://@redis.host@:@redis.port@
database: @redis.database@ # Redis 库索引
password: @redis.password@ # Redis 密码
connectionPoolSize: 64 # 连接池大小
connectionMinimumIdleSize: 10 # 最小空闲连接数
idleConnectionTimeout: 10000 # 空闲连接超时时间(毫秒)
connectTimeout: 10000 # 连接超时时间(毫秒)
timeout: 3000 # 命令等待超时时间(毫秒)
baidu:
map:
app_id: 116444176

View File

@ -10,14 +10,12 @@ package com.suisung.mall.shop.sync.service;
import cn.hutool.json.JSONArray;
import com.suisung.mall.common.api.CommonResult;
import com.suisung.mall.common.modules.sync.StoreDbConfig;
import com.suisung.mall.common.pojo.req.SyncThirdMemberReq;
import com.suisung.mall.common.pojo.res.ThirdApiRes;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@ -41,6 +39,7 @@ public interface SyncThirdDataService {
/**
* 批量保存商品记录
*
* @param goodsListJSON
* @return
*/
@ -48,6 +47,7 @@ public interface SyncThirdDataService {
/**
* 批量保存会员记录
*
* @param memberList
* @return
*/
@ -55,6 +55,7 @@ public interface SyncThirdDataService {
/**
* 手动触发同步
*
* @param storeId
* @param syncType
* @return
@ -62,7 +63,6 @@ public interface SyncThirdDataService {
CommonResult syncManual(String storeId, Integer syncType);
/**
*
* @param appKey
* @param sign
* @param multipartFile
@ -72,7 +72,6 @@ public interface SyncThirdDataService {
/**
*
* @param appKey
* @param sign
* @param folders
@ -83,6 +82,7 @@ public interface SyncThirdDataService {
/**
* 下载客户端更新包
*
* @param primaryKey
* @return
*/
@ -90,6 +90,7 @@ public interface SyncThirdDataService {
/**
* 获取客户端数据库配置
*
* @param appKey
* @param sign
* @return
@ -98,6 +99,7 @@ public interface SyncThirdDataService {
/**
* 同步商品数据库存到客户端
*
* @param appKey
* @param sign
* @return
@ -105,14 +107,29 @@ public interface SyncThirdDataService {
ThirdApiRes getStoreDataRelease(String appKey, String sign);
/**
* 存储扣减商品到redis
* 保存一个或多个商品刚刚变化的库存到 redis hash 缓存
*
* @param storeData
*/
void saveStoreRealeas(Map storeData);
// void saveStoreRelease(Map<String, Integer> storeData);
/**
* Redis 中获取商品有变动的库存数据
*
* @return
*/
Map<String, Integer> getProductStockFromRedis();
/**
* 下单或支付后批量累加减商品库存使用 Redis Hash 的原子自增操作保证并发安全
*
* @param stockDeltaMap key 为商品唯一keyvalue 为库存增降量 例如 {"1234567890123": 100, "1234567890124": 50} 库存数为正负整数单位可能是个数或重量
* 数量为正数时库存数增加数量为负数时库存数减少
*/
void incrProductStockToRedis(Map<String, Integer> stockDeltaMap);
/**
*
* @param appKey
* @param sign
* @param folders

View File

@ -9,27 +9,22 @@
package com.suisung.mall.shop.sync.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.qcloud.cos.model.COSObjectSummary;
import com.suisung.mall.common.api.CommonResult;
import com.suisung.mall.common.enums.DicEnum;
import com.suisung.mall.common.modules.base.ShopBaseProductBrand;
import com.suisung.mall.common.modules.base.ShopBaseProductCategory;
import com.suisung.mall.common.modules.sixun.SxSyncGoods;
import com.suisung.mall.common.modules.sixun.SxSyncVip;
import com.suisung.mall.common.modules.sync.StoreDbConfig;
@ -38,9 +33,7 @@ import com.suisung.mall.common.modules.sync.SyncConfig;
import com.suisung.mall.common.modules.sync.SyncFileLog;
import com.suisung.mall.common.pojo.req.SyncThirdMemberReq;
import com.suisung.mall.common.pojo.res.ThirdApiRes;
import com.suisung.mall.common.utils.I18nUtil;
import com.suisung.mall.common.utils.StringUtils;
import com.suisung.mall.core.web.service.RedisService;
import com.suisung.mall.shop.base.service.ShopBaseProductCategoryService;
@ -60,15 +53,15 @@ import com.suisung.mall.shop.sixun.utils.FileUtils;
import com.suisung.mall.shop.sync.Utils.ThreadFileUtils;
import com.suisung.mall.shop.sync.keymanage.RedisKey;
import com.suisung.mall.shop.sync.service.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -76,6 +69,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -85,17 +79,19 @@ import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
@Service
public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements SyncThirdDataService {
private static Logger logger = LoggerFactory.getLogger(SyncThirdDataServiceImpl.class);
private static final Logger logger = LoggerFactory.getLogger(SyncThirdDataServiceImpl.class);
private final int limitCnt = 300;
private final AtomicLong threadNum = new AtomicLong(0);
@Value("${client.path}")
public String clientPath;
@Autowired
@ -104,15 +100,12 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
private SyncConfigService syncConfigService;
@Autowired
private SxSyncCategoryService sxSyncCategoryService;
@Autowired
private SxSyncGoodsService sxSyncGoodsService;
@Autowired
private SxSyncVipService sxSyncVipService;
@Autowired
private ShopNumberSeqService shopNumberSeqService;
private final AtomicLong threadNum=new AtomicLong(0);
@Autowired
private SyncFileLogService syncFileLogService;
@Autowired
@ -121,6 +114,11 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
@Autowired
private RedisService redisService;
@Lazy
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StoreDbConfigService storeDbConfigService;
@ -135,6 +133,7 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
@Value("#{accountBaseConfigService.getConfig('tengxun_default_dir')}")
private String TENGXUN_DEFA;
/**
* 批量保存商品的分类
*
@ -382,6 +381,7 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
/**
* 同步商品分类
*
* @return
*/
public CommonResult syncProductClazz(DataBaseInfo dataBaseInfo, String storeId) {
@ -414,6 +414,7 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
/**
* 文件上传
*
* @param appKey
* @param sign
* @param page 分页
@ -446,7 +447,7 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
String filePath = FileUtils.createFolderAndFileUsingFile(folder, filName);
Path path = Paths.get(filePath);
Files.write(path, bytes);
logger.info("path-{},parent-{},filename-{},root-{}",path.toString(),path.getParent(),path.getFileName().toString(),path.getRoot());
logger.info("path-{},parent-{},filename-{},root-{}", path, path.getParent(), path.getFileName().toString(), path.getRoot());
// String filaPath=path.toString();
// if(filePath.contains(":")){
// filePath=filePath.substring(filePath.indexOf(":")+1);
@ -466,6 +467,7 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
/**
* 多线程处理文件
*
* @param appKey
* @param sign
* @param syncType
@ -679,6 +681,7 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
if (StrUtil.isBlank(appKey) || StrUtil.isBlank(sign)) {
return new ThirdApiRes().fail(1003, I18nUtil._("缺少必要参数!"));
}
// 验签appid必要参数判断
SyncApp syncAppO = syncAppService.getOne(new LambdaQueryWrapper<SyncApp>()
.select(SyncApp::getApp_key, SyncApp::getApp_secret, SyncApp::getStore_id)
@ -687,28 +690,78 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
if (syncAppO == null) {
return new ThirdApiRes().fail(1001, I18nUtil._("签名有误!"));
}
Object obRst= redisService.get(RedisKey.STOREDATARELEASE);//商品库存扣减
Map storeDataResultMap=new HashMap();
if(obRst!=null){
Map resultMap=(Map)obRst;
Set<Map.Entry> sme= resultMap.entrySet();
storeDataResultMap= sme.stream().filter(m->!(m.getValue().equals((double)0))).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
// Object obRst = redisService.get(RedisKey.STOREDATARELEASE);//商品库存扣减
// Map storeDataResultMap = new HashMap();
// if (obRst != null) {
// Map resultMap = (Map) obRst;
// Set<Map.Entry> sme = resultMap.entrySet();
// storeDataResultMap = sme.stream().filter(m -> !(m.getValue().equals((double) 0))).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// }
Map<String, Integer> storeDataResultMap = getProductStockFromRedis();
return new ThirdApiRes().success("success", storeDataResultMap);
}
// @Override
// public void saveStoreRelease(Map storeData) {
// // RMK: 这样写存在严重的线程安全问题可能会导致数据丢失或覆盖
// // 改成 redis 原子级别的 hash 类别存储
// if (CollectionUtil.isEmpty(storeData)) {
// return;
// }
// Object obRst = redisService.get(RedisKey.STOREDATARELEASE);
// if (obRst != null) {
// Map map = (Map) obRst;
// map.putAll(storeData);
// redisService.set(RedisKey.STOREDATARELEASE, map);
// } else {
// redisService.set(RedisKey.STOREDATARELEASE, storeData);
// }
// }
@Override
public void saveStoreRealeas(Map storeData) {
if(CollectionUtil.isEmpty(storeData)){
public Map<String, Integer> getProductStockFromRedis() {
try {
// Redis 获取 hash 结构的所有键值对
Map<Object, Object> redisHash = redisTemplate.opsForHash().entries(RedisKey.STOREDATARELEASE);
if (redisHash == null || redisHash.isEmpty()) {
return Collections.emptyMap();
}
// 转换为 Map<String, Integer>
return redisHash.entrySet().stream()
.collect(Collectors.toMap(
entry -> String.valueOf(entry.getKey()),
entry -> Convert.toInt(entry.getValue(), 0) // 转换失败时默认为 0
));
} catch (Exception e) {
logger.error("从 Redis 获取商品库存失败: {}", e.getMessage(), e);
return Collections.emptyMap();
}
}
@Override
public void incrProductStockToRedis(Map<String, Integer> stockDeltaMap) {
// 校验参数避免空指针
if (CollectionUtil.isEmpty(stockDeltaMap)) {
return;
}
Object obRst= redisService.get(RedisKey.STOREDATARELEASE);
if(obRst!=null){
Map map=(Map)obRst;
map.putAll(storeData);
redisService.set(RedisKey.STOREDATARELEASE,map);
}else {
redisService.set(RedisKey.STOREDATARELEASE,storeData);
for (Map.Entry<String, Integer> entry : stockDeltaMap.entrySet()) {
String productKey = entry.getKey();
Integer delta = entry.getValue();
if (StrUtil.isBlank(productKey) || delta == null) {
continue;
}
try {
// 使用 Redis HINCRBY 保证原子性和高性能
redisTemplate.opsForHash().increment(RedisKey.STOREDATARELEASE, productKey, delta);
} catch (Exception e) {
logger.error("库存累加失败productKey={}, delta={}, error={}", productKey, delta, e.getMessage(), e);
}
}
}
@ -717,6 +770,7 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
* 压缩商家数据并上传cos
* 保存商店数据
* "E:\\data\\uploaded\\goods\\2025\\6\\6\\1\\2"
*
* @param path
*/
public void upLoadZipToOss(String path) {
@ -744,6 +798,7 @@ public class SyncThirdDataServiceImpl extends SyncBaseThirdSxAbstract implements
* 压缩商家数据并上传cos
* 保存商店数据
* "E:\\data\\uploaded\\goods\\2025\\6\\6\\1\\2"
*
* @param path
*/
public void dowloadAndUnZip(String path) {