feat(client): 实现用户数据隔离与设备绑定优化- 添加用户会话ID构建逻辑,确保数据按用户隔离- 优化设备绑定流程,支持设备状态更新和绑定时间同步- 实现用户缓存清理功能,仅清除当前用户的数据- 增强客户端账号删除逻辑,级联删除相关数据

- 调整设备在线查询逻辑,确保只返回活跃绑定的设备
- 优化试用期逻辑,精确计算过期时间和类型- 添加账号管理弹窗和相关状态注入
-修复跟卖精灵按钮加载状态显示问题
- 增强文件上传区域UI,显示选中文件名
- 调整分页组件样式,优化界面展示效果- 优化反馈日志存储路径逻辑,默认使用用户目录
- 移除冗余代码和无用导入,提升代码整洁度
This commit is contained in:
2025-10-24 13:43:46 +08:00
parent e2a438c84e
commit 3a76aaa3c0
50 changed files with 860 additions and 590 deletions

View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.tashow.erp</groupId>
<artifactId>erp_client_sb</artifactId>
<version>2.4.9</version>
<version>2.5.2</version>
<name>erp_client_sb</name>
<description>erp客户端</description>
<properties>
@@ -95,11 +95,6 @@
<artifactId>selenium-java</artifactId>
<version>4.23.0</version>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.9.2</version>
</dependency>
<!-- JWT parsing for local RS256 verification -->
<dependency>

View File

@@ -4,11 +4,13 @@ import com.tashow.erp.repository.AmazonProductRepository;
import com.tashow.erp.service.AmazonScrapingService;
import com.tashow.erp.utils.ExcelParseUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.JwtUtil;
import com.tashow.erp.utils.LoggerUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
@@ -22,18 +24,25 @@ public class AmazonController {
private AmazonProductRepository amazonProductRepository;
@PostMapping("/products/batch")
public JsonData batchGetProducts(@RequestBody Map<String, Object> request) {
public JsonData batchGetProducts(@RequestBody Map<String, Object> request, HttpServletRequest httpRequest) {
@SuppressWarnings("unchecked")
List<String> asinList = (List<String>) request.get("asinList");
String batchId = (String) request.get("batchId");
String region = (String) request.getOrDefault("region", "JP");
List<AmazonProductEntity> products = amazonScrapingService.batchGetProductInfo(asinList, batchId, region);
// 从 token 中获取 username
String username = JwtUtil.getUsernameFromRequest(httpRequest);
// 构建带用户隔离的 sessionId
String userSessionId = JwtUtil.buildUserSessionId(username, batchId);
List<AmazonProductEntity> products = amazonScrapingService.batchGetProductInfo(asinList, userSessionId, region);
return JsonData.buildSuccess(Map.of("products", products, "total", products.size()));
}
@GetMapping("/products/latest")
public JsonData getLatestProducts() {
List<AmazonProductEntity> products = amazonProductRepository.findLatestProducts();
public JsonData getLatestProducts(HttpServletRequest request) {
String username = JwtUtil.getUsernameFromRequest(request);
List<AmazonProductEntity> products = amazonProductRepository.findLatestProducts(username);
return JsonData.buildSuccess(Map.of("products", products, "total", products.size()));
}

View File

@@ -2,10 +2,12 @@ package com.tashow.erp.controller;
import com.tashow.erp.repository.BanmaOrderRepository;
import com.tashow.erp.service.BanmaOrderService;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.JwtUtil;
import com.tashow.erp.utils.LoggerUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -27,9 +29,15 @@ public class BanmaOrderController {
@RequestParam(defaultValue = "1", name = "page") int page,
@RequestParam(defaultValue = "10", name = "pageSize") int pageSize,
@RequestParam("batchId") String batchId,
@RequestParam(required = false, name = "shopIds") String shopIds) {
@RequestParam(required = false, name = "shopIds") String shopIds,
HttpServletRequest request) {
// 从 token 中获取 username
String username = JwtUtil.getUsernameFromRequest(request);
// 构建带用户隔离的 sessionId
String userSessionId = JwtUtil.buildUserSessionId(username, batchId);
List<String> shopIdList = shopIds != null ? java.util.Arrays.asList(shopIds.split(",")) : null;
Map<String, Object> result = banmaOrderService.getOrdersByPage(accountId, startDate, endDate, page, pageSize, batchId, shopIdList);
Map<String, Object> result = banmaOrderService.getOrdersByPage(accountId, startDate, endDate, page, pageSize, userSessionId, shopIdList);
return result.containsKey("success") && !(Boolean)result.get("success")
? JsonData.buildError((String)result.get("message"))
: JsonData.buildSuccess(result);
@@ -41,9 +49,10 @@ public class BanmaOrderController {
}
@GetMapping("/orders/latest")
public JsonData getLatestOrders() {
public JsonData getLatestOrders(HttpServletRequest request) {
String username = JwtUtil.getUsernameFromRequest(request);
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
List<Map<String, Object>> orders = banmaOrderRepository.findLatestOrders()
List<Map<String, Object>> orders = banmaOrderRepository.findLatestOrders(username)
.parallelStream()
.map(entity -> {
try {

View File

@@ -1,5 +1,4 @@
package com.tashow.erp.controller;
import com.tashow.erp.model.RakutenProduct;
import com.tashow.erp.model.SearchResult;
import com.tashow.erp.service.Alibaba1688Service;
@@ -8,14 +7,14 @@ import com.tashow.erp.service.RakutenScrapingService;
import com.tashow.erp.utils.DataReportUtil;
import com.tashow.erp.utils.ExcelParseUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/rakuten")
@Slf4j
@@ -30,8 +29,13 @@ public class RakutenController {
private DataReportUtil dataReportUtil;
@PostMapping(value = "/products")
public JsonData getProducts(@RequestParam("file") MultipartFile file, @RequestParam(value = "batchId", required = false) String batchId) {
public JsonData getProducts(@RequestParam("file") MultipartFile file, @RequestParam(value = "batchId", required = false) String batchId, HttpServletRequest request) {
try {
// 从 token 中获取 username
String username = JwtUtil.getUsernameFromRequest(request);
// 构建带用户隔离的 sessionId
String userSessionId = JwtUtil.buildUserSessionId(username, batchId);
List<String> shopNames = ExcelParseUtil.parseFirstColumn(file);
if (CollectionUtils.isEmpty(shopNames)) {
return JsonData.buildError("Excel文件中未解析到店铺名");
@@ -39,10 +43,10 @@ public class RakutenController {
List<RakutenProduct> allProducts = new ArrayList<>();
List<String> skippedShops = new ArrayList<>();
for (String currentShopName : shopNames) {
if (rakutenCacheService.hasRecentData(currentShopName)) {
if (rakutenCacheService.hasRecentData(currentShopName, username)) {
// 从缓存获取
List<RakutenProduct> cached = rakutenCacheService.getProductsByShopName(currentShopName).stream().filter(p -> currentShopName.equals(p.getOriginalShopName())).toList();
rakutenCacheService.updateSpecificProductsSessionId(cached, batchId);
List<RakutenProduct> cached = rakutenCacheService.getProductsByShopName(currentShopName, username).stream().filter(p -> currentShopName.equals(p.getOriginalShopName())).toList();
rakutenCacheService.updateSpecificProductsSessionId(cached, userSessionId);
allProducts.addAll(cached);
skippedShops.add(currentShopName);
log.info("使用缓存数据,店铺: {},数量: {}", currentShopName, cached.size());
@@ -57,7 +61,7 @@ public class RakutenController {
}
List<RakutenProduct> newProducts = allProducts.stream().filter(p -> !skippedShops.contains(p.getOriginalShopName())).toList();
if (!newProducts.isEmpty()) {
rakutenCacheService.saveProductsWithSessionId(newProducts, batchId);
rakutenCacheService.saveProductsWithSessionId(newProducts, userSessionId);
}
int cachedCount = allProducts.size() - newProducts.size();
if (cachedCount > 0) {
@@ -77,12 +81,17 @@ public class RakutenController {
}
@PostMapping("/search1688")
public JsonData search1688(@RequestBody Map<String, Object> params) {
public JsonData search1688(@RequestBody Map<String, Object> params, HttpServletRequest request) {
String imageUrl = (String) params.get("imageUrl");
String sessionId = (String) params.get("sessionId");
try {
// 从 token 中获取 username
String username = JwtUtil.getUsernameFromRequest(request);
// 构建带用户隔离的 sessionId
String userSessionId = JwtUtil.buildUserSessionId(username, sessionId);
SearchResult result = alibaba1688Service.get1688Detail(imageUrl);
rakutenScrapingService.update1688DataByImageUrl(result, sessionId, imageUrl);
rakutenScrapingService.update1688DataByImageUrl(result, userSessionId, imageUrl);
return JsonData.buildSuccess(result);
} catch (Exception e) {
log.error("1688识图搜索失败", e);
@@ -91,9 +100,10 @@ public class RakutenController {
}
@GetMapping("/products/latest")
public JsonData getLatestProducts() {
public JsonData getLatestProducts(HttpServletRequest request) {
try {
List<Map<String, Object>> products = rakutenScrapingService.getLatestProductsForDisplay();
String username = JwtUtil.getUsernameFromRequest(request);
List<Map<String, Object>> products = rakutenScrapingService.getLatestProductsForDisplay(username);
return JsonData.buildSuccess(Map.of("products", products, "total", products.size()));
} catch (Exception e) {
log.error("获取最新商品数据失败", e);

View File

@@ -1,11 +1,11 @@
package com.tashow.erp.controller;
import com.tashow.erp.entity.AuthTokenEntity;
import com.tashow.erp.repository.AuthTokenRepository;
import com.tashow.erp.service.CacheService;
import com.tashow.erp.service.impl.GenmaiServiceImpl;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -13,7 +13,6 @@ import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@RestController
@RequestMapping("/api/system")
public class SystemController {
@@ -68,7 +67,6 @@ public class SystemController {
public JsonData getLocalIp() throws Exception {
return JsonData.buildSuccess(java.net.InetAddress.getLocalHost().getHostAddress());
}
@GetMapping("/computer-name")
public JsonData getComputerName() {
return JsonData.buildSuccess(System.getenv("COMPUTERNAME"));
@@ -124,7 +122,6 @@ public class SystemController {
responseHeaders.setContentType(MediaType.IMAGE_JPEG);
}
responseHeaders.setCacheControl("max-age=3600");
return new ResponseEntity<>(response.getBody(), responseHeaders, HttpStatus.OK);
} catch (Exception e) {
logger.error("代理图片失败: {}", imageUrl, e);
@@ -133,8 +130,9 @@ public class SystemController {
}
@PostMapping("/cache/clear")
public JsonData clearCache() {
cacheService.clearCache();
public JsonData clearCache(HttpServletRequest request) {
String username = com.tashow.erp.utils.JwtUtil.getUsernameFromRequest(request);
cacheService.clearCache(username);
return JsonData.buildSuccess("缓存清理成功");
}
}

View File

@@ -62,6 +62,12 @@ public interface AmazonProductRepository extends JpaRepository<AmazonProductEnti
*/
@Query(value = "SELECT * FROM amazon_products WHERE session_id = (SELECT session_id FROM amazon_products GROUP BY session_id ORDER BY session_id DESC LIMIT 1) ORDER BY updated_at ", nativeQuery = true)
List<AmazonProductEntity> findLatestProducts();
/**
* 获取指定用户最新会话的产品数据(按用户隔离)
*/
@Query(value = "SELECT * FROM amazon_products WHERE session_id = (SELECT session_id FROM amazon_products WHERE session_id LIKE :username || '#%' GROUP BY session_id ORDER BY session_id DESC LIMIT 1) ORDER BY updated_at ", nativeQuery = true)
List<AmazonProductEntity> findLatestProducts(@Param("username") String username);
/**
* 删除指定ASIN在指定时间后的数据用于清理12小时内重复

View File

@@ -66,6 +66,12 @@ public interface BanmaOrderRepository extends JpaRepository<BanmaOrderEntity, Lo
*/
@Query(value = "SELECT * FROM banma_orders WHERE session_id = (SELECT session_id FROM banma_orders ORDER BY created_at DESC LIMIT 1) ORDER BY updated_at ASC, id ASC", nativeQuery = true)
List<BanmaOrderEntity> findLatestOrders();
/**
* 获取指定用户最新会话的订单数据(按用户隔离)
*/
@Query(value = "SELECT * FROM banma_orders WHERE session_id = (SELECT session_id FROM banma_orders WHERE session_id LIKE :username || '#%' ORDER BY created_at DESC LIMIT 1) ORDER BY updated_at ASC, id ASC", nativeQuery = true)
List<BanmaOrderEntity> findLatestOrders(@Param("username") String username);
/**
* 删除指定追踪号在指定时间后的数据用于清理12小时内重复

View File

@@ -52,6 +52,16 @@ public interface RakutenProductRepository extends JpaRepository<RakutenProductEn
* 检查指定店铺在指定时间后是否有数据
*/
boolean existsByOriginalShopNameAndCreatedAtAfter(String originalShopName, LocalDateTime sinceTime);
/**
* 检查指定店铺在指定时间后是否有数据(按用户隔离)
*/
boolean existsByOriginalShopNameAndSessionIdStartingWithAndCreatedAtAfter(String originalShopName, String sessionIdPrefix, LocalDateTime sinceTime);
/**
* 根据店铺名和 sessionId 前缀获取产品(按用户隔离)
*/
List<RakutenProductEntity> findByOriginalShopNameAndSessionIdStartingWithOrderByCreatedAtAscIdAsc(String originalShopName, String sessionIdPrefix);
/**
* 获取最新的会话ID
@@ -64,6 +74,12 @@ public interface RakutenProductRepository extends JpaRepository<RakutenProductEn
*/
@Query(value = "SELECT * FROM rakuten_products WHERE session_id = (SELECT session_id FROM rakuten_products ORDER BY created_at DESC LIMIT 1) ORDER BY created_at ASC, id ASC", nativeQuery = true)
List<RakutenProductEntity> findLatestProducts();
/**
* 获取指定用户最新会话的产品数据(按用户隔离)
*/
@Query(value = "SELECT * FROM rakuten_products WHERE session_id = (SELECT session_id FROM rakuten_products WHERE session_id LIKE :username || '#%' ORDER BY created_at DESC LIMIT 1) ORDER BY created_at ASC, id ASC", nativeQuery = true)
List<RakutenProductEntity> findLatestProducts(@Param("username") String username);
/**
* 删除指定商品URL在指定时间后的数据用于清理12小时内重复

View File

@@ -1,6 +1,9 @@
package com.tashow.erp.service;
import com.tashow.erp.entity.AuthTokenEntity;
import com.tashow.erp.entity.RakutenProductEntity;
import com.tashow.erp.entity.AmazonProductEntity;
import com.tashow.erp.entity.BanmaOrderEntity;
import com.tashow.erp.repository.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -8,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Service
@@ -45,14 +49,39 @@ public class CacheService {
}
@Transactional
public void clearCache() {
rakutenProductRepository.deleteAll();
amazonProductRepository.deleteAll();
alibaba1688ProductRepository.deleteAll();
zebraOrderRepository.deleteAll();
banmaOrderRepository.deleteAll();
cacheDataRepository.deleteAll();
updateStatusRepository.deleteAll();
public void clearCache(String username) {
if (username == null || username.isEmpty()) {
logger.warn("尝试清理缓存但未提供用户名,操作已跳过");
return;
}
logger.info("开始清理用户缓存: {}", username);
// 清理当前用户的 Rakuten 产品数据
List<RakutenProductEntity> rakutenProducts = rakutenProductRepository.findAll();
List<RakutenProductEntity> userRakutenProducts = rakutenProducts.stream()
.filter(p -> p.getSessionId() != null && p.getSessionId().startsWith(username + "#"))
.toList();
rakutenProductRepository.deleteAll(userRakutenProducts);
logger.info("已清理 {} 条 Rakuten 产品数据", userRakutenProducts.size());
// 清理当前用户的 Amazon 产品数据
List<AmazonProductEntity> amazonProducts = amazonProductRepository.findAll();
List<AmazonProductEntity> userAmazonProducts = amazonProducts.stream()
.filter(p -> p.getSessionId() != null && p.getSessionId().startsWith(username + "#"))
.toList();
amazonProductRepository.deleteAll(userAmazonProducts);
logger.info("已清理 {} 条 Amazon 产品数据", userAmazonProducts.size());
// 清理当前用户的 Banma 订单数据
List<BanmaOrderEntity> banmaOrders = banmaOrderRepository.findAll();
List<BanmaOrderEntity> userBanmaOrders = banmaOrders.stream()
.filter(o -> o.getSessionId() != null && o.getSessionId().startsWith(username + "#"))
.toList();
banmaOrderRepository.deleteAll(userBanmaOrders);
logger.info("已清理 {} 条 Banma 订单数据", userBanmaOrders.size());
logger.info("用户 {} 的缓存清理完成", username);
}
}

View File

@@ -18,12 +18,12 @@ public interface RakutenCacheService {
/**
* 检查店铺是否有最近的数据12小时内
*/
boolean hasRecentData(String shopName);
boolean hasRecentData(String shopName, String username);
/**
* 根据店铺名获取已有数据
*/
List<RakutenProduct> getProductsByShopName(String shopName);
List<RakutenProduct> getProductsByShopName(String shopName, String username);
/**
* 更新指定店铺的产品sessionId

View File

@@ -32,9 +32,9 @@ public interface RakutenScrapingService {
void update1688DataByImageUrl(SearchResult searchResult, String sessionId, String imageUrl);
/**
* 获取最新产品数据并转换为前端格式
* 获取最新产品数据并转换为前端格式(按用户隔离)
*/
List<Map<String, Object>> getLatestProductsForDisplay();
List<Map<String, Object>> getLatestProductsForDisplay(String username);
}

View File

@@ -243,7 +243,6 @@ public class Alibaba1688ServiceImpl implements Alibaba1688Service {
return 0.0;
}
}
/**
* 上传图片并获取图片ID
* @return

View File

@@ -2,7 +2,6 @@ package com.tashow.erp.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qiniu.util.UrlUtils;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
@@ -30,7 +29,6 @@ public class GenmaiServiceImpl {
Runtime.getRuntime().exec("taskkill /f /im chrome.exe");
Thread.sleep(1000); // 等待进程关闭
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
String username = System.getProperty("user.name", "user");
String safeUsername;

View File

@@ -80,23 +80,30 @@ public class RakutenCacheServiceImpl implements RakutenCacheService {
}
/**
* 检查店铺是否有1小时内的缓存数据
* 检查店铺是否有1小时内的缓存数据(按用户隔离)
*/
@Override
public boolean hasRecentData(String shopName) {
boolean hasRecent = repository.existsByOriginalShopNameAndCreatedAtAfter(shopName, LocalDateTime.now().minusHours(1));
public boolean hasRecentData(String shopName, String username) {
if (username == null || username.isEmpty()) {
return false;
}
boolean hasRecent = repository.existsByOriginalShopNameAndSessionIdStartingWithAndCreatedAtAfter(
shopName, username + "#", LocalDateTime.now().minusHours(1));
if (hasRecent) {
log.info("店铺 {} 存在1小时内缓存数据将使用缓存", shopName);
log.info("店铺 {} 存在1小时内缓存数据(用户: {},将使用缓存", shopName, username);
}
return hasRecent;
}
/**
* 根据店铺名获取所有产品数据
* 根据店铺名获取所有产品数据(按用户隔离)
*/
@Override
public List<RakutenProduct> getProductsByShopName(String shopName) {
return repository.findByOriginalShopNameOrderByCreatedAtAscIdAsc(shopName).stream()
public List<RakutenProduct> getProductsByShopName(String shopName, String username) {
if (username == null || username.isEmpty()) {
return List.of();
}
return repository.findByOriginalShopNameAndSessionIdStartingWithOrderByCreatedAtAscIdAsc(shopName, username + "#").stream()
.map(entity -> {
RakutenProduct product = new RakutenProduct();
BeanUtils.copyProperties(entity, product);

View File

@@ -121,8 +121,9 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService {
public void update1688DataByImageUrl(SearchResult searchResult, String sessionId, String imageUrl) {
List<RakutenProductEntity> matchingProducts = rakutenProductRepository.findBySessionIdOrderByCreatedAtAscIdAsc(sessionId).stream().filter(product -> imageUrl.equals(product.getImgUrl())).peek(product -> {
product.setMapRecognitionLink(searchResult.getMapRecognitionLink());
product.setImage1688Url(searchResult.getMapRecognitionLink());
product.setDetailUrl1688(searchResult.getMapRecognitionLink());
product.setFreight(searchResult.getFreight());
product.setSkuPriceJson(searchResult.getSkuPrice().toString());
product.setMedian(searchResult.getMedian());
product.setWeight(searchResult.getWeight());
product.setSkuPriceJson(JSON.toJSONString(searchResult.getSkuPrice()));
@@ -133,8 +134,11 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService {
}
@Override
public List<Map<String, Object>> getLatestProductsForDisplay() {
return rakutenProductRepository.findLatestProducts().stream().map(entity -> {
public List<Map<String, Object>> getLatestProductsForDisplay(String username) {
if (username == null || username.isEmpty()) {
return List.of();
}
return rakutenProductRepository.findLatestProducts(username).stream().map(entity -> {
Map<String, Object> result = new HashMap<>();
result.put("originalShopName", entity.getOriginalShopName());
result.put("productUrl", entity.getProductUrl());
@@ -142,12 +146,14 @@ public class RakutenScrapingServiceImpl implements RakutenScrapingService {
result.put("productTitle", entity.getProductTitle());
result.put("price", entity.getPrice());
result.put("ranking", entity.getRanking());
result.put("price1688", entity.getPrice1688());
result.put("image1688Url", entity.getImage1688Url());
result.put("detailUrl1688", entity.getDetailUrl1688());
result.put("mapRecognitionLink", entity.getMapRecognitionLink());
result.put("freight", entity.getFreight());
result.put("median", entity.getMedian());
result.put("weight", entity.getWeight());
result.put("skuPriceJson", entity.getSkuPriceJson());
result.put("skuPrice", entity.getSkuPriceJson());
return result;
}).collect(Collectors.toList());

View File

@@ -0,0 +1,108 @@
package com.tashow.erp.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Base64;
/**
* JWT 工具类
* 用于从 HTTP 请求中解析 JWT token 并提取用户信息
*/
public class JwtUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 从 HTTP 请求中提取 username
* @param request HTTP 请求对象
* @return username如果解析失败返回空字符串
*/
public static String getUsernameFromRequest(HttpServletRequest request) {
try {
String token = extractToken(request);
if (token == null || token.isEmpty()) {
return "";
}
return extractUsernameFromToken(token);
} catch (Exception e) {
return "";
}
}
/**
* 从 token 字符串中提取 username
* @param token JWT token
* @return username
*/
public static String extractUsernameFromToken(String token) {
try {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return "";
}
String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]));
JsonNode payload = objectMapper.readTree(payloadJson);
// 尝试从 username 或 sub 字段获取用户名
if (payload.has("username")) {
return payload.get("username").asText();
} else if (payload.has("sub")) {
return payload.get("sub").asText();
}
return "";
} catch (Exception e) {
return "";
}
}
/**
* 从 HTTP 请求中提取 token
*/
private static String extractToken(HttpServletRequest request) {
// 从 Authorization header 获取
String auth = request.getHeader("Authorization");
if (auth != null && auth.startsWith("Bearer ")) {
return auth.substring(7).trim();
}
// 从 Cookie 获取
if (request.getCookies() != null) {
for (jakarta.servlet.http.Cookie cookie : request.getCookies()) {
if ("FX_TOKEN".equals(cookie.getName()) && cookie.getValue() != null) {
return cookie.getValue().trim();
}
}
}
return null;
}
/**
* 构建带用户隔离的 sessionId
* @param username 用户名
* @param originalSessionId 原始 sessionId
* @return 格式化的 sessionId: username#originalSessionId
*/
public static String buildUserSessionId(String username, String originalSessionId) {
if (username == null || username.isEmpty()) {
return originalSessionId;
}
return username + "#" + originalSessionId;
}
/**
* 从 sessionId 中提取原始 sessionId去除用户前缀
* @param userSessionId 带用户前缀的 sessionId
* @return 原始 sessionId
*/
public static String extractOriginalSessionId(String userSessionId) {
if (userSessionId == null || !userSessionId.contains("#")) {
return userSessionId;
}
int idx = userSessionId.indexOf("#");
return userSessionId.substring(idx + 1);
}
}

View File

@@ -1,24 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 固定日志路径到系统公共数据目录 -->
<property name="LOG_HOME" value="C:/ProgramData/erp-logs" />
<!-- 使用 Spring Boot 传递的日志路径 -->
<property name="LOG_HOME" value="${LOG_PATH:-logs}" />
<!--
日志上报说明:
日志文件按天滚动存储在 ${LOG_HOME} 目录下
格式spring-boot-yyyy-MM-dd.log
用户反馈系统集成说明:
- 客户端应用Electron可以读取本地日志文件
- 用户在"设置-反馈"页面提交反馈时,可选择附带某一天的日志文件
- 日志文件将随反馈内容一起上传到服务器
- 服务器存储路径C:/ProgramData/erp-logs/feedback/ (Windows) 或 /opt/erp/feedback-logs/ (Linux)
- 管理员可在后台管理界面查看反馈并下载相应日志文件
日志保留策略:
- 本地日志保留30天
- 总大小限制1GB
- 超过限制时自动删除最旧的日志文件
日志说明:
- 日志文件按天滚动存储在 ${LOG_HOME} 目录下
- 格式spring-boot-yyyy-MM-dd.log
- 客户端应用可读取本地日志文件用于反馈
- 日志保留30天总大小限制1GB
-->
<!-- 控制台输出 -->