Kaynağa Gözat

读取配置

yuhongqi 1 hafta önce
ebeveyn
işleme
2415a60a36

+ 8 - 6
fs-service-system/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -260,12 +260,14 @@ public class LiveServiceImpl implements ILiveService
 			long seconds = live.getStartTime().until(now, ChronoUnit.SECONDS);
 			liveVo.setNowDuration(seconds);
 		}
-        ThreadUtil.execute(()->{
-
-            redisUtil.delete(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, live.getLiveId()));
-            redisUtil.set(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, live.getLiveId()), liveVo,LiveKeysConstant.LIVE_HOME_PAGE_DETAIL_EXPIRE, TimeUnit.SECONDS);
-
-        });
+        // 同步写入,避免异步期间读到脏数据;用 JSON 字符串避免类型反序列化差异
+        try {
+            String detailKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, live.getLiveId());
+            redisUtil.delete(detailKey);
+            redisUtil.set(detailKey, JSON.toJSONString(liveVo), LiveKeysConstant.LIVE_HOME_PAGE_DETAIL_EXPIRE, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            log.warn("写入直播详情缓存失败 liveId={}", id, e);
+        }
 
         return liveVo;
     }

+ 15 - 0
fs-service-system/src/main/java/com/fs/store/service/impl/FsStoreOrderServiceImpl.java

@@ -1495,8 +1495,14 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService
             address = userAddressMapper.selectFsUserAddressById(param.getUserAddressId());
         }
         FsStoreProductPackage storeProductPackage=productPackageService.selectFsStoreProductPackageById(param.getPackageId());
+        if (storeProductPackage == null) {
+            return R.error("套餐不存在或已下架");
+        }
         String uuid = IdUtil.randomUUID();
         BigDecimal totalMoney=storeProductPackage.getPayMoney();
+        if (totalMoney == null) {
+            totalMoney = BigDecimal.ZERO;
+        }
         if(param.getCouponUserId()!=null){
             FsStoreCouponUser couponUser=couponUserService.selectFsStoreCouponUserById(param.getCouponUserId());
             if(couponUser!=null&&couponUser.getStatus()==0){
@@ -1600,6 +1606,9 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService
         String packageId = redisCache.getCacheObject("orderKey:" + param.getOrderKey());
         if(packageId!=null){
             FsStoreProductPackage storeProductPackage=productPackageService.selectFsStoreProductPackageById(param.getPackageId());
+            if (storeProductPackage == null) {
+                return R.error("套餐不存在或已下架");
+            }
             if(storeProductPackage.getStatus().equals(0)){
                 return R.error("此套餐已下架" );
             }
@@ -2995,7 +3004,13 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService
     @Override
     public R computedPackageOrder(long userId, FsStoreComputedPackageIdOrderParam param) {
         FsStoreProductPackage storeProductPackage=productPackageService.selectFsStoreProductPackageById(param.getPackageId());
+        if (storeProductPackage == null) {
+            return R.error("套餐不存在或已下架");
+        }
         BigDecimal totalMoney=storeProductPackage.getPayMoney();
+        if (totalMoney == null) {
+            totalMoney = BigDecimal.ZERO;
+        }
         if(param.getCouponUserId()!=null){
             FsStoreCouponUser couponUser=couponUserService.selectFsStoreCouponUserById(param.getCouponUserId());
             if(couponUser!=null&&couponUser.getStatus()==0){

+ 24 - 4
fs-service-system/src/main/java/com/fs/store/service/impl/FsUserServiceImpl.java

@@ -18,6 +18,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.TypeReference;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.IpUtil;
 import com.fs.live.domain.LiveOrder;
@@ -508,8 +509,13 @@ public class FsUserServiceImpl implements IFsUserService
                 // 解密
                 CompletableFuture.runAsync(() -> {
                     try {
+                        if (StringUtils.isBlank(param.getEncryptedData()) || StringUtils.isBlank(param.getIv())) {
+                            updateUserLastIp(finalUser, ip, session);
+                            return;
+                        }
                         WxMaPhoneNumberInfo phoneNoInfo = wxService.getUserService().getPhoneNoInfo(session.getSessionKey(), param.getEncryptedData(), param.getIv());
-                        if (phoneNoInfo.getPhoneNumber() != null && !phoneNoInfo.getPhoneNumber().equals(finalUser.getPhone())) {
+                        if (phoneNoInfo != null && phoneNoInfo.getPhoneNumber() != null
+                                && !phoneNoInfo.getPhoneNumber().equals(finalUser.getPhone())) {
                             finalUser.setPhone(phoneNoInfo.getPhoneNumber());
                         } else {
                             finalUser.setPhone(null);
@@ -522,6 +528,8 @@ public class FsUserServiceImpl implements IFsUserService
             }
             String token = jwtUtils.generateToken(user.getUserId());
             return R.ok("登录成功").put("token",token).put("user", user);
+        } catch (CustomException e) {
+            return R.error(e.getMessage());
         } catch (WxErrorException e) {
             return R.error("授权失败,"+e.getMessage());
         }
@@ -530,14 +538,26 @@ public class FsUserServiceImpl implements IFsUserService
     private FsUser saveUser(LoginMpWxParam param, WxMaService wxService, WxMaJscode2SessionResult session, String ip) {
         FsUser user;
         // 解密
-        WxMaPhoneNumberInfo phoneNoInfo = wxService.getUserService().getPhoneNoInfo(session.getSessionKey(), param.getEncryptedData(), param.getIv());
+        if (StringUtils.isBlank(param.getEncryptedData()) || StringUtils.isBlank(param.getIv())) {
+            throw new CustomException("请授权手机号后再登录");
+        }
+        WxMaPhoneNumberInfo phoneNoInfo;
+        try {
+            phoneNoInfo = wxService.getUserService().getPhoneNoInfo(session.getSessionKey(), param.getEncryptedData(), param.getIv());
+        } catch (Exception e) {
+            throw new CustomException("获取手机号失败,请重新授权");
+        }
+        if (phoneNoInfo == null || StringUtils.isBlank(phoneNoInfo.getPhoneNumber())) {
+            throw new CustomException("获取手机号失败,请重新授权");
+        }
         //写入
         user=new FsUser();
         user.setPhone(phoneNoInfo.getPhoneNumber());
         //先看手机是否是11位,再加密查询
         Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
-        if (PHONE_PATTERN.matcher(phoneNoInfo.getPhoneNumber().trim()).matches()){
-            user.setPhone(encryptPhone(phoneNoInfo.getPhoneNumber()));
+        String phone = phoneNoInfo.getPhoneNumber().trim();
+        if (PHONE_PATTERN.matcher(phone).matches()){
+            user.setPhone(encryptPhone(phone));
         }
         user.setNickname("微信用户");
         user.setStatus(1);

+ 2 - 1
fs-service-system/src/main/java/com/fs/store/strategy/ShippingTemplate.java

@@ -1,5 +1,6 @@
 package com.fs.store.strategy;
 
+import com.fs.common.exception.CustomException;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -64,7 +65,7 @@ public class ShippingTemplate {
         }
 
         if (targetRegion == null) {
-            throw new IllegalArgumentException("找不到匹配的配送区域: " + order.getCityId());
+            throw new CustomException("当前地址不在快递配送范围内,请更换收货地址");
         }
 
         // 计算基础运费

+ 3 - 0
fs-user-app/src/main/java/com/fs/app/controller/StoreProductPackageController.java

@@ -73,6 +73,9 @@ public class StoreProductPackageController extends  AppBaseController {
     @GetMapping("/getStoreProductPackageDetails")
     public R getStoreProductPackageDetails(@RequestParam("packageId") Long packageId){
         FsStoreProductPackage storeProductPackage=productPackageService.selectFsStoreProductPackageById(packageId);
+        if (storeProductPackage == null) {
+            return R.error("套餐不存在或已下架");
+        }
         List<StoreOrderProductDTO> productList=new ArrayList<>();
         JSONArray jsonArray= JSONUtil.parseArray(storeProductPackage.getProducts());
         List<StorePackageProductDTO> goodsList=JSONUtil.toList(jsonArray, StorePackageProductDTO.class);

+ 10 - 1
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java

@@ -301,7 +301,16 @@ public class LiveCompletionPointsController extends AppBaseController {
     @PostMapping("/update-watch-duration")
     @Transactional
     public R updateWatchDuration(@RequestParam Long liveId, @RequestParam Long watchDuration) {
-        Long userId = Long.parseLong(getUserId());
+        String userIdStr = getUserId();
+        if (userIdStr == null || userIdStr.trim().isEmpty()) {
+            return R.error(401, "请先登录");
+        }
+        Long userId;
+        try {
+            userId = Long.parseLong(userIdStr);
+        } catch (NumberFormatException e) {
+            return R.error(401, "登录状态异常,请重新登录");
+        }
 
         try {
             // 1. 获取直播间信息并验证(复用公共方法)

+ 52 - 8
fs-user-app/src/main/java/com/fs/app/exception/FSExceptionHandler.java

@@ -4,6 +4,7 @@ package com.fs.app.exception;
 
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
+import org.apache.catalina.connector.ClientAbortException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.dao.DuplicateKeyException;
@@ -12,10 +13,12 @@ import org.springframework.validation.BindException;
 import org.springframework.validation.FieldError;
 import org.springframework.validation.ObjectError;
 import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.servlet.NoHandlerFoundException;
 
+import java.io.IOException;
 import java.util.List;
 
 
@@ -50,27 +53,68 @@ public class FSExceptionHandler {
 		return R.error("数据库中已存在该记录");
 	}
 
+	/**
+	 * 客户端主动断开(切后台/弱网),不按系统错误打 ERROR
+	 */
+	@ExceptionHandler(ClientAbortException.class)
+	public void handleClientAbortException(ClientAbortException e) {
+		logger.warn("客户端中断连接: {}", e.getMessage());
+	}
+
+	@ExceptionHandler(MissingServletRequestParameterException.class)
+	public R handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
+		logger.warn("缺少请求参数: {}", e.getMessage());
+		return R.error("缺少必要参数: " + e.getParameterName());
+	}
+
+	@ExceptionHandler(NumberFormatException.class)
+	public R handleNumberFormatException(NumberFormatException e) {
+		logger.warn("参数格式错误: {}", e.getMessage());
+		return R.error("参数格式错误,请检查登录状态或入参");
+	}
+
+	@ExceptionHandler(IllegalArgumentException.class)
+	public R handleIllegalArgumentException(IllegalArgumentException e) {
+		logger.warn("非法参数: {}", e.getMessage());
+		return R.error(e.getMessage());
+	}
 
 	@ExceptionHandler(Exception.class)
 	public R handleException(Exception e){
+		if (isClientAbort(e)) {
+			logger.warn("客户端中断连接: {}", e.getMessage());
+			return null;
+		}
 		logger.error(e.getMessage(), e);
 		if (e instanceof BindException){
 			BindException ex = (BindException)e;
-			List<ObjectError> allErrors = ex.getAllErrors();//捕获的所有错误对象
+			List<ObjectError> allErrors = ex.getAllErrors();
 			ObjectError error = allErrors.get(0);
-			String defaultMessage = error.getDefaultMessage();//异常内容
+			String defaultMessage = error.getDefaultMessage();
 
 			return R.error(defaultMessage);
 		}
 
-/*		if(e instanceof IllegalArgumentException){
-			return R.error(e.getMessage());
-		} else {
-			return R.error();
-		}*/
-
 		return R.error(e.getMessage());
 	}
+
+	private boolean isClientAbort(Throwable e) {
+		Throwable cur = e;
+		while (cur != null) {
+			if (cur instanceof ClientAbortException) {
+				return true;
+			}
+			if (cur instanceof IOException) {
+				String msg = cur.getMessage();
+				if (msg != null && (msg.contains("远程主机强迫关闭") || msg.contains("Broken pipe")
+						|| msg.contains("Connection reset") || msg.contains("你的主机中的软件中止了一个已建立的连接"))) {
+					return true;
+				}
+			}
+			cur = cur.getCause();
+		}
+		return false;
+	}
 	@ExceptionHandler(BindException.class)
 	public R bindExceptionHandler(BindException e) {
 		FieldError error = e.getFieldError();

+ 38 - 15
fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

@@ -154,13 +154,7 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
 
     @Override
     public R liveDetail(Long id) {
-        Object o = redisUtil.get(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, id));
-        LiveVo liveVo;
-        if (ObjectUtil.isNotEmpty(o)) {
-            liveVo = JSON.parseObject(o.toString(), LiveVo.class);
-        } else {
-            liveVo = liveService.asyncToCacheLiveDetail(id);
-        }
+        LiveVo liveVo = parseLiveDetailCache(id);
         if (ObjectUtil.isEmpty(liveVo)) {
             return R.error("未找到直播");
         }
@@ -174,15 +168,9 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
 
     @Override
     public R liveDetailWithUserId(Long id, Long userId) {
-        Object o = redisUtil.get(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, id));
-        LiveVo liveVo;
-        if (ObjectUtil.isNotEmpty(o)) {
-            liveVo = JSON.parseObject(o.toString(), LiveVo.class);
-        } else {
-            liveVo = liveService.asyncToCacheLiveDetail(id);
-        }
+        LiveVo liveVo = parseLiveDetailCache(id);
         if (ObjectUtil.isEmpty(liveVo)) {
-            R.error("未找到直播");
+            return R.error("未找到直播");
         }
         if(liveVo.getIsShow() == 2) {
             return R.error("直播未开放");
@@ -251,6 +239,41 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
                 cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
     }
 
+    /**
+     * Redis 可能已是 LiveVo / JSONObject / JSON 字符串;禁止用 Lombok toString 再 parse
+     */
+    private LiveVo parseLiveDetailCache(Long id) {
+        String detailKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, id);
+        Object cached;
+        try {
+            cached = redisUtil.get(detailKey);
+        } catch (Exception e) {
+            log.warn("读取直播详情缓存失败,重建缓存 liveId={}, key={}, err={}", id, detailKey, e.getMessage());
+            redisUtil.delete(detailKey);
+            return liveService.asyncToCacheLiveDetail(id);
+        }
+        if (ObjectUtil.isEmpty(cached)) {
+            return liveService.asyncToCacheLiveDetail(id);
+        }
+        try {
+            if (cached instanceof LiveVo) {
+                return (LiveVo) cached;
+            }
+            if (cached instanceof String) {
+                String text = ((String) cached).trim();
+                if (!text.startsWith("{")) {
+                    throw new IllegalArgumentException("invalid live detail cache text");
+                }
+                return JSON.parseObject(text, LiveVo.class);
+            }
+            return JSON.parseObject(JSON.toJSONString(cached), LiveVo.class);
+        } catch (Exception e) {
+            log.warn("解析直播详情缓存失败,删除并重建 liveId={}, key={}, err={}", id, detailKey, e.getMessage());
+            redisUtil.delete(detailKey);
+            return liveService.asyncToCacheLiveDetail(id);
+        }
+    }
+
 
     @Override
     public R currentActivities(Long liveId, String userId) {

+ 17 - 1
fs-user-app/src/main/java/com/fs/core/config/FilterConfig.java

@@ -3,10 +3,12 @@ package com.fs.core.config;
 import com.fs.common.filter.RepeatableFilter;
 import com.fs.common.filter.XssFilter;
 import com.fs.common.utils.StringUtils;
+import com.fs.core.filter.ForbiddenHttpMethodFilter;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
 
 import javax.servlet.DispatcherType;
 import java.util.HashMap;
@@ -29,6 +31,20 @@ public class FilterConfig
     @Value("${xss.urlPatterns}")
     private String urlPatterns;
 
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean forbiddenHttpMethodFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
+        registration.setFilter(new ForbiddenHttpMethodFilter());
+        registration.addUrlPatterns("/*");
+        registration.setName("forbiddenHttpMethodFilter");
+        // 早于 Spring Security,记录非法方法并直接 400
+        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
+        return registration;
+    }
+
     @SuppressWarnings({ "rawtypes", "unchecked" })
     @Bean
     public FilterRegistrationBean xssFilterRegistration()
@@ -38,7 +54,7 @@ public class FilterConfig
         registration.setFilter(new XssFilter());
         registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
         registration.setName("xssFilter");
-        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1);
         Map<String, String> initParameters = new HashMap<String, String>();
         initParameters.put("excludes", excludes);
         initParameters.put("enabled", enabled);

+ 54 - 0
fs-user-app/src/main/java/com/fs/core/filter/ForbiddenHttpMethodFilter.java

@@ -0,0 +1,54 @@
+package com.fs.core.filter;
+
+import com.fs.common.utils.ip.IpUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 拦截 Spring Security StrictHttpFirewall 白名单外的 HTTP 方法(如 PROPFIND),
+ * 记录 method/URI/IP 后直接返回 400,避免刷 ERROR 堆栈。
+ */
+public class ForbiddenHttpMethodFilter extends OncePerRequestFilter {
+
+    private static final Logger log = LoggerFactory.getLogger(ForbiddenHttpMethodFilter.class);
+
+    /** 与 StrictHttpFirewall 默认允许方法保持一致 */
+    private static final Set<String> ALLOWED_METHODS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
+    )));
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+        String method = request.getMethod();
+        if (method != null && ALLOWED_METHODS.contains(method.toUpperCase())) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        String uri = request.getRequestURI();
+        String query = request.getQueryString();
+        String url = query == null ? uri : uri + "?" + query;
+        log.warn("非法 HTTP 方法请求已拒绝 method={}, url={}, host={}, ip={}, ua={}",
+                method,
+                url,
+                request.getHeader("Host"),
+                IpUtils.getIpAddr(request),
+                request.getHeader("User-Agent"));
+        // 不用 sendError,避免错误页二次 forward 再次触发 Security 防火墙
+        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+        response.setContentType("text/plain;charset=UTF-8");
+        response.getWriter().write("Unsupported HTTP method");
+    }
+}