Ver Fonte

Merge branch 'fix_ai_bug' into pre

zoujiajian há 10 horas atrás
pai
commit
7534e30527

+ 2 - 2
netflix-common/src/main/java/com/cyksj/common/util/StringUtil.java

@@ -457,9 +457,9 @@ public final class StringUtil {
 		String code;
 		// 生成验证码
 		if (digit == 4) {
-			code = new Random().nextInt(8999) + 1000 + "";
+			code = new java.security.SecureRandom().nextInt(9000) + 1000 + "";
 		} else {
-			code = new Random().nextInt(899999) + 100000 + "";
+			code = new java.security.SecureRandom().nextInt(900000) + 100000 + "";
 		}
 		return code;
 	}

+ 9 - 0
netflix-service/src/main/java/com/cyksj/redis/RedisService.java

@@ -35,6 +35,15 @@ public class RedisService {
     @Resource
     private RedisTemplate<String, Object> redisTemplate;
 
+    /** Atomic fixed-window counter; Redis failure must fail closed. */
+    public long securityAttempt(String key) {
+        Long count = redisTemplate.execute(new DefaultRedisScript<Long>(
+                "local n=redis.call('incr',KEYS[1]); if n==1 then redis.call('expire',KEYS[1],900) end; return n",
+                Long.class), Collections.singletonList(key));
+        if (count == null) throw new IllegalStateException("Security counter unavailable");
+        return count;
+    }
+
     public static String env = "dev";
 
 

+ 37 - 14
netflix-service/src/main/java/com/cyksj/service/coupon/impl/CouponFontServiceImpl.java

@@ -65,12 +65,17 @@ public class CouponFontServiceImpl implements CouponFontService {
 	@Override
 	@Transactional(rollbackFor = Throwable.class)
 	public void collectCoupon(CouponCollectReq couponCollectReq, Long userId) {
-		collectCommonCheck(couponCollectReq.getCouponId(), userId);
-		CouponActive couponActive = couponActiveMapper.selectById(couponCollectReq.getCouponActiveId());
-		if (couponActive == null) {
+        if (couponCollectReq == null || couponCollectReq.getCouponId() == null || couponCollectReq.getCouponActiveId() == null) {
+            throw BusinessRuntimeException.getInstance("缺少优惠券领取参数");
+        }
+        // Inventory is protected by subRemainNum's compare-and-update predicate.
+        CouponActive couponActive = couponActiveMapper.selectById(couponCollectReq.getCouponActiveId());
+        if (couponActive == null || !java.util.Objects.equals(couponActive.getCouponId(), couponCollectReq.getCouponId())
+                || !Boolean.TRUE.equals(couponActive.getStatus()) || !Boolean.TRUE.equals(couponActive.getExpiryStatus())) {
 			throw BusinessRuntimeException.getInstance("该发放优惠券不存在");
 		}
-		if (couponActive.getRemainNum() <= 0) {
+        collectCommonCheck(couponActive.getCouponId(), userId);
+		if (couponActive.getRemainNum() == null || couponActive.getRemainNum() <= 0) {
 			throw BusinessRuntimeException.getInstance("该优惠券已领取完");
 		}
 
@@ -233,9 +238,13 @@ public class CouponFontServiceImpl implements CouponFontService {
 
 	public void collectCommonCheck(Long couponId, Long userId) {
 		Coupon coupon = couponMapper.selectById(couponId);
-		if (coupon == null || !coupon.getDeleted()) {
+		if (coupon == null || !Boolean.TRUE.equals(coupon.getDeleted())) {
 			throw BusinessRuntimeException.getInstance("该优惠券不存在");
 		}
+        if (Boolean.TRUE.equals(coupon.getIsValidTime()) &&
+                (coupon.getValidEndTime() == null || !coupon.getValidEndTime().after(new Date()))) {
+            throw BusinessRuntimeException.getInstance("该优惠券已失效");
+        }
 		//是否可领取
 		if (coupon.getScope() == Coupon.Scope.newUser) {
 			//注册后从没产生过虚拟订单的用户,产生过退款后也不是新用户
@@ -370,7 +379,7 @@ public class CouponFontServiceImpl implements CouponFontService {
 		try {
 			couponUserMapper.insert(couponUser);
 		} catch (DuplicateKeyException e) {
-			log.info("用户:{}重复领取优惠券{}插入数据错误", userId, couponId);
+            throw BusinessRuntimeException.getInstance("您已领取该优惠券,请勿重复领取");
 		}
 	}
 
@@ -390,14 +399,28 @@ public class CouponFontServiceImpl implements CouponFontService {
 	}
 
 	@Override
-	public CouponUserView receiveCoupon(long userId, Long couponId, CouponUser.Channel channel) {
-		try {
-			receiveCouponUser(couponId, userId, channel);
-		} catch (Exception e) {
-			return null;
-		}
-		return getCouponDetailById(userId, couponId);
-	}
+    @Transactional(rollbackFor = Throwable.class)
+    public CouponUserView receiveCoupon(long userId, Long couponId, CouponUser.Channel channel) {
+        if (couponId == null || couponId <= 0) throw BusinessRuntimeException.getInstance("优惠券不存在");
+        Coupon coupon = couponMapper.selectById(couponId);
+        if (coupon == null) throw BusinessRuntimeException.getInstance("优惠券不存在");
+        if (channel == CouponUser.Channel.manual) {
+            // Public callers may only claim coupons explicitly published to the center.
+            CouponActive active = couponActiveMapper.selectOne(Wrappers.lambdaQuery(CouponActive.class)
+                    .eq(CouponActive::getCouponId, couponId).eq(CouponActive::getStatus, true)
+                    .eq(CouponActive::getExpiryStatus, true).gt(CouponActive::getRemainNum, 0)
+                    .orderByAsc(CouponActive::getId).last("LIMIT 1"));
+            if (active == null) throw BusinessRuntimeException.getInstance("该优惠券未开放领取");
+            CouponCollectReq req = new CouponCollectReq();
+            req.setCouponId(couponId);
+            req.setCouponActiveId(active.getId());
+            collectCoupon(req, userId);
+        } else {
+            collectCommonCheck(couponId, userId);
+            receiveCouponUser(couponId, userId, channel);
+        }
+        return getCouponDetailById(userId, couponId);
+    }
 
 	@Override
 	public List<CouponView> getGoodsUserActivityCoupons(long userId) throws Exception {

+ 7 - 4
netflix-service/src/main/java/com/cyksj/service/exchange/impl/ExchangeCodeServiceImpl.java

@@ -164,7 +164,7 @@ public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, Exc
 	@Transactional(rollbackFor = Throwable.class)
 	@NoSubmit
 	public Result<String> getTicketByExchangeCode(ExchangeCodeTicket exchangeCodeTicket, Long userId) throws Exception {
-		if (StrUtil.isEmpty(exchangeCodeTicket.getCode())) {
+		if (exchangeCodeTicket == null || exchangeCodeTicket.getCode() == null || !exchangeCodeTicket.getCode().matches("[A-Za-z0-9_-]{1,256}")) {
 			throw BusinessRuntimeException.getInstance("兑换码有误,请使用正确的兑换码");
 		}
 		//是渠道兑换码
@@ -172,7 +172,10 @@ public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, Exc
 		UserDistributeExchangeCode userDistributeExchangeCode = userDistributeExchangeCodeMapper.selectOne(Wrappers.lambdaQuery(UserDistributeExchangeCode.class)
 				.eq(UserDistributeExchangeCode::getCode, code)
 				.last("limit 1"));
-		if (userDistributeExchangeCode != null) {
+		if (userDistributeExchangeCode != null && !code.equals(userDistributeExchangeCode.getCode())) {
+            throw BusinessRuntimeException.getInstance("兑换码有误,请使用正确的兑换码");
+        }
+        if (userDistributeExchangeCode != null) {
 			//是否是该渠道下的用户
 			Integer bindCount = userDistributeSharedMapper.selectCount(Wrappers.lambdaQuery(UserDistributeShared.class)
 					.eq(UserDistributeShared::getSharedId, userDistributeExchangeCode.getUserId())
@@ -193,7 +196,7 @@ public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, Exc
 				userDistributeExchangeCodeUseRecord.setUserId(userId);
 				userDistributeExchangeCodeUseRecord.setCode(code);
 				userDistributeExchangeCodeUseRecordMapper.insert(userDistributeExchangeCodeUseRecord);
-				log.info("用户userId:{}兑换渠道兑换码:{}", userId, code);
+				log.info("用户userId:{}兑换渠道兑换码记录id:{}", userId, userDistributeExchangeCode.getId());
 				GoodsDonSku sku = goodsDonSkuMapper.selectById(userDistributeExchangeCode.getSkuId());
 				GroupsRelation relation = getRelationNoOrder(sku, userId, 0l);
 				//重新设置时间
@@ -215,7 +218,7 @@ public class ExchangeCodeServiceImpl extends ServiceImpl<ExchangeCodeMapper, Exc
 			return GatewayResponse.SUCCESS.newBuilder().toResult();
 		}
 		ExchangeCode exchangeCode = this.getOne(Wrappers.lambdaQuery(ExchangeCode.class).eq(ExchangeCode::getCode, exchangeCodeTicket.getCode()).last("limit 1"));
-		if (exchangeCode == null) {
+		if (exchangeCode == null || !code.equals(exchangeCode.getCode())) {
 			throw BusinessRuntimeException.getInstance("兑换码有误,请使用正确的兑换码");
 		}
 		if (exchangeCode.getUseStatus()) {

+ 5 - 2
netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java

@@ -1678,9 +1678,12 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 
 	@Override
 	public OrderDetailView getDetail(Long orderId, Long userId) {
+        if (userId == null || userId <= 0 || orderId == null || orderId <= 0) {
+            throw BusinessRuntimeException.getInstance("订单不存在或无权查看");
+        }
 		OrderDon orderDon = this.getById(orderId);
-		if (orderDon == null) {
-			throw BusinessRuntimeException.getInstance("订单不存在");
+		if (orderDon == null || !userId.equals(orderDon.getUserId())) {
+			throw BusinessRuntimeException.getInstance("订单不存在或无权查看");
 		}
 		MapBuilder builder = MapUtils.builder()
 				.field(OrderDonView::getId, orderId)

+ 25 - 2
netflix-service/src/main/java/com/cyksj/service/user/impl/UserServiceImpl.java

@@ -516,6 +516,20 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
 		if (loginReq.getType() == null || loginReq.getType() < 1 || loginReq.getType() > 3) {
 			throw BusinessRuntimeException.getInstance("系统异常,请刷新页面重新操作");
 		}
+        if (loginReq.getType() == 1 || loginReq.getType() == 2) {
+            String identity = loginReq.getType() == 1 ? loginReq.getPhone() : loginReq.getEmail();
+            if (StrUtil.isBlank(identity) || identity.length() > 254
+                    || !identity.equals(identity.trim()) || loginReq.getCode() == null
+                    || !loginReq.getCode().matches("[0-9]{6}")) {
+                throw BusinessRuntimeException.getInstance("请输入正确的账号和六位验证码");
+            }
+            String attemptKey = "auth:verify:" + loginReq.getType() + ":"
+                    + cn.hutool.crypto.digest.DigestUtil.sha256Hex(identity.toLowerCase(java.util.Locale.ROOT));
+            // Count before comparison, including concurrent requests. Never reset on resend.
+            if (redisService.securityAttempt(attemptKey) > 10) {
+                throw BusinessRuntimeException.getInstance("验证码尝试次数过多,请15分钟后重试");
+            }
+        }
 		if (loginReq.getType() == 1) {
 			if (StrUtil.hasEmpty(loginReq.getPhone(), loginReq.getCode())) {
 				throw BusinessRuntimeException.getInstance("请输入对应信息");
@@ -527,7 +541,8 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
 			}
 			//3分钟失效
 			Date updateTime = phoneCode.getUpdateTime();
-			DateTime afterFiveMinutes = DateUtil.offsetMinute(updateTime, 3);
+			if (updateTime == null) throw BusinessRuntimeException.getInstance("请重新获取手机验证码");
+            DateTime afterFiveMinutes = DateUtil.offsetMinute(updateTime, 3);
 			if (now.isAfter(afterFiveMinutes)) {
 				throw BusinessRuntimeException.getInstance("手机验证码已失效,请重新获取");
 			}
@@ -536,6 +551,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
 				badIntentionOpService.isBadOpLoginPhone(request, loginReq.getPhone());
 				throw BusinessRuntimeException.getInstance("手机验证码错误,请重新输入");
 			}
+            int consumed = phoneCodeMapper.update(null, Wrappers.<PhoneCode>lambdaUpdate()
+                    .eq(PhoneCode::getId, phoneCode.getId())
+                    .eq(PhoneCode::getCode, phoneCode.getCode())
+                    .eq(PhoneCode::getUpdateTime, phoneCode.getUpdateTime())
+                    .set(PhoneCode::getCode, ""));
+            if (consumed != 1) throw BusinessRuntimeException.getInstance("验证码已使用,请重新获取");
 			wrapper.eq(User::getLoginPhone, loginReq.getPhone());
 		}
 		if (loginReq.getType() == 2) {
@@ -549,7 +570,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
 			if (!StrUtil.equals(loginReq.getCode(), code)) {
 				throw BusinessRuntimeException.getInstance("邮箱验证码错误,请重新输入");
 			}
-			redisService.del(RedisService.key.EMAIL_CODE_KEY.getNameFormat(loginReq.getEmail()));
+			if (!redisService.deleteIfValueEquals(RedisService.key.EMAIL_CODE_KEY.getNameFormat(loginReq.getEmail()), code)) {
+                throw BusinessRuntimeException.getInstance("验证码已使用,请重新获取");
+            }
 			wrapper.eq(User::getEmail, loginReq.getEmail());
 		}
 		if (loginReq.getType() == 3) {

+ 88 - 0
netflix-web/src/main/java/com/cyksj/security/ApiCacheProtectionFilter.java

@@ -0,0 +1,88 @@
+package com.cyksj.security;
+
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+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 javax.servlet.http.HttpServletResponseWrapper;
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+/** Sensitive API responses must never become shared CDN cache entries. */
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE)
+public class ApiCacheProtectionFilter extends OncePerRequestFilter {
+    private static final Pattern MATRIX_PATH = Pattern.compile(";|%(?:25)*3b", Pattern.CASE_INSENSITIVE);
+
+    private static final Pattern SENSITIVE_PATH = Pattern.compile(
+            "^/(?:manage|manager|auth|yhShop|applets/(?:user|order|register)|applet/coupon)(?:$|[/;.%])");
+    private static final Pattern STATIC_SUFFIX = Pattern.compile(
+            "\\.(?:css|js|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|map)$", Pattern.CASE_INSENSITIVE);
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String uri = request.getRequestURI();
+        String context = request.getContextPath();
+        if (uri != null && context != null && !context.isEmpty() && uri.startsWith(context)) {
+            uri = uri.substring(context.length());
+        }
+        String servletPath = request.getServletPath();
+        return !(uri != null && SENSITIVE_PATH.matcher(uri).find())
+                && !(servletPath != null && SENSITIVE_PATH.matcher(servletPath).find());
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+                                    FilterChain chain) throws ServletException, IOException {
+        HttpServletResponseWrapper protectedResponse = new HttpServletResponseWrapper(response) {
+            private String protectedValue(String name, String value) {
+                if ("Cache-Control".equalsIgnoreCase(name)) return "no-store, private, max-age=0";
+                if ("Pragma".equalsIgnoreCase(name)) return "no-cache";
+                if ("Expires".equalsIgnoreCase(name)) return "0";
+                return value;
+            }
+            @Override public void setHeader(String name, String value) {
+                super.setHeader(name, protectedValue(name, value));
+            }
+            @Override public void addHeader(String name, String value) {
+                if ("Cache-Control".equalsIgnoreCase(name) || "Pragma".equalsIgnoreCase(name)
+                        || "Expires".equalsIgnoreCase(name)) setHeader(name, value);
+                else super.addHeader(name, value);
+            }
+            @Override public void setDateHeader(String name, long date) {
+                if ("Expires".equalsIgnoreCase(name)) setHeader(name, "0");
+                else super.setDateHeader(name, date);
+            }
+            @Override public void addDateHeader(String name, long date) {
+                if ("Expires".equalsIgnoreCase(name)) setHeader(name, "0");
+                else super.addDateHeader(name, date);
+            }
+            @Override public void reset() {
+                super.reset();
+                noCache(this);
+            }
+        };
+        noCache(protectedResponse);
+        // Inspect the raw path, not query strings; never normalize an ambiguous path into an API call.
+        String uri = request.getRequestURI();
+        if (uri != null && (MATRIX_PATH.matcher(uri).find() || STATIC_SUFFIX.matcher(uri).find())) {
+            protectedResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            protectedResponse.setContentType("application/json;charset=UTF-8");
+            protectedResponse.getWriter().write("{\"code\":100002,\"msg\":\"Invalid request path\",\"flag\":false}");
+            return;
+        }
+        chain.doFilter(request, protectedResponse);
+    }
+
+    private static void noCache(HttpServletResponse response) {
+        response.setHeader("Cache-Control", "no-store, private, max-age=0");
+        response.setHeader("Pragma", "no-cache");
+        response.setHeader("Expires", "0");
+        response.setHeader("X-Content-Type-Options", "nosniff");
+    }
+}

+ 6 - 6
netflix-web/src/main/java/com/cyksj/web/controller/coupon/CouponFrontController.java

@@ -380,7 +380,7 @@ public class CouponFrontController {
 		String couponId = one.getSysValue();
 		CouponUserView couponUserView = couponFontService.getNewUserWelfareCoupons(userId, Long.valueOf(couponId));
 		if (couponUserView == null) {
-			CouponDetailView couponDetailView = beanSearcher.searchFirst(CouponDetailView.class, MapUtils.flatBuilder(request.getParameterMap()).field(CouponDetailView::getCouponId, couponId).build());
+			CouponDetailView couponDetailView = beanSearcher.searchFirst(CouponDetailView.class, MapUtils.builder().field(CouponDetailView::getCouponId, couponId).build());
 			return GatewayResponse.SUCCESS.newBuilder().toResult(couponDetailView);
 		}
 		return GatewayResponse.SUCCESS.newBuilder().toResult(couponUserView);
@@ -408,7 +408,7 @@ public class CouponFrontController {
 	 */
 	@GetMapping("/get/detail/{couponId}")
 	public Result<CouponDetailView> getCouponDetail(@PathVariable Long couponId) {
-		CouponDetailView couponDetailView = beanSearcher.searchFirst(CouponDetailView.class, MapUtils.flatBuilder(request.getParameterMap()).field(CouponDetailView::getCouponId, couponId).build());
+		CouponDetailView couponDetailView = beanSearcher.searchFirst(CouponDetailView.class, MapUtils.builder().field(CouponDetailView::getCouponId, couponId).build());
 		String str = couponSkuMapper.selectGoodsSkuStr(couponDetailView.getCouponId());
 		couponDetailView.setGoodsSkuStr(str.replaceAll(",", "/").replaceAll(";", ""));
 		List<CouponSkuView> couponSkus = couponSkuMapper.getGoodsSkuByCouponId(couponDetailView.getCouponId());
@@ -433,7 +433,7 @@ public class CouponFrontController {
 			return GatewayResponse.SUCCESS.newBuilder().toResult();
 		}
 		String couponId = one.getSysValue();
-		CouponDetailView couponDetailView = beanSearcher.searchFirst(CouponDetailView.class, MapUtils.flatBuilder(request.getParameterMap()).field(CouponDetailView::getCouponId, couponId).build());
+		CouponDetailView couponDetailView = beanSearcher.searchFirst(CouponDetailView.class, MapUtils.builder().field(CouponDetailView::getCouponId, couponId).build());
 		return GatewayResponse.SUCCESS.newBuilder().toResult(couponDetailView);
 	}
 
@@ -452,7 +452,7 @@ public class CouponFrontController {
 		String couponId = one.getSysValue();
 		CouponUserView couponUserView = couponFontService.receiveCoupon(userId, Long.valueOf(couponId), CouponUser.Channel.new_welfare);
 		if (couponUserView == null) {
-			couponUserView = beanSearcher.searchFirst(CouponUserView.class, MapUtils.flatBuilder(request.getParameterMap()).field(CouponNewUserView::getUserId, userId).field(CouponUserView::getCouponId, couponId).build());
+			couponUserView = beanSearcher.searchFirst(CouponUserView.class, MapUtils.builder().field(CouponUserView::getUserId, userId).field(CouponUserView::getCouponId, couponId).build());
 		}
 		if (couponUserView != null) {
 			String str = couponSkuMapper.selectGoodsSkuStr(couponUserView.getCouponId());
@@ -476,7 +476,7 @@ public class CouponFrontController {
 	public Result<List<CouponUserPopupsView>> getCouponPopups() {
 		long userId = StpUserUtil.getLoginIdAsLong();
 		DateTime now = DateTime.now();
-		List<CouponUserPopupsView> couponUserPopups = beanSearcher.searchAll(CouponUserPopupsView.class, MapUtils.flatBuilder(request.getParameterMap())
+		List<CouponUserPopupsView> couponUserPopups = beanSearcher.searchAll(CouponUserPopupsView.class, MapUtils.builder()
 				.field(CouponUserPopupsView::getStartTime, now).op(Operator.LessEqual)
 				.field(CouponUserPopupsView::getEndTime, now).op(Operator.GreaterThan)
 				.build());
@@ -537,7 +537,7 @@ public class CouponFrontController {
 		Long userId = StpUserUtil.getUserIdAfterLogin();
 		String dateStr = DateTime.now().toString();
 		String time = String.format(" and (ua.start_time is null or (ua.start_time < '%s' and ua.end_time > '%s'))", dateStr, dateStr);
-		UserActivityView userActivityView = beanSearcher.searchFirst(UserActivityView.class, MapUtils.flatBuilder(request.getParameterMap())
+		UserActivityView userActivityView = beanSearcher.searchFirst(UserActivityView.class, MapUtils.builder()
 				.field(UserActivityView::getType, 3)
 				.put("time", time)
 				.build());

+ 7 - 24
netflix-web/src/main/java/com/cyksj/web/controller/external/ExternalController.java

@@ -1,12 +1,5 @@
 package com.cyksj.web.controller.external;
 
-import cn.dev33.satoken.annotation.SaCheckLogin;
-import cn.dev33.satoken.annotation.SaCheckRole;
-import cn.dev33.satoken.annotation.SaCheckPermission;
-import cn.dev33.satoken.stp.StpUtil;
-import com.cyksj.common.annotation.Log;
-import com.cyksj.enums.BusinessType;
-
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
@@ -36,7 +29,10 @@ import com.ejlchina.searcher.SearchResult;
 import com.ejlchina.searcher.util.MapUtils;
 import lombok.RequiredArgsConstructor;
 import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.List;
@@ -72,7 +68,7 @@ public class ExternalController {
     /**
      * 知乎 特殊镜像规格 兑换码列表
      */
-    @GetMapping("/get/mirror")
+//    @GetMapping("/get/mirror")
     public Result<SearchResult<ExternalExchangeCodeView>> getMirror(@RequestParam(required = true, defaultValue = "18") Long gid) {
         //目前只展示规格id 444 GPT基础镜像2月
         List<Long> showSkuId = null;
@@ -86,13 +82,7 @@ public class ExternalController {
         if (CollUtil.isEmpty(showSkuId)) {
            return GatewayResponse.SUCCESS.newBuilder().toResult();
         }
-        int page = request.getParameter("start") == null ? 1 : Integer.parseInt(request.getParameter("start"));
-        int size = request.getParameter("limit") == null ? 10 : Integer.parseInt(request.getParameter("limit"));
-        if (page < 1 || size < 1 || size > 100) {
-            throw BusinessRuntimeException.getInstance("分页参数错误");
-        }
-        SearchResult<ExternalExchangeCodeView> search = beanSearcher.search(ExternalExchangeCodeView.class, MapUtils.builder()
-                .put("start", page).put("limit", size)
+        SearchResult<ExternalExchangeCodeView> search = beanSearcher.search(ExternalExchangeCodeView.class, MapUtils.flatBuilder(request.getParameterMap())
                 .field(ExternalExchangeCodeView::getSkuIds, showSkuId.toString())
                 .build());
         return GatewayResponse.SUCCESS.newBuilder().toResult(search);
@@ -101,11 +91,7 @@ public class ExternalController {
 	/**
 	 * 退款 成功 重新生成兑换码
 	 */
-	@SaCheckLogin
-	@SaCheckRole("root")
-	@PutMapping("/refund/{id}")
-	@SaCheckPermission("exchangeCode:refund")
-	@Log(module = "兑换码/外部退款重发", businessType = BusinessType.PUT, isSaveRequestData = true)
+//	@PutMapping("/refund/{id}")
 	@Transactional(rollbackFor = Throwable.class)
 	public Result<String> refund(@PathVariable Long id) throws Exception {
 		ExchangeCode exchangeCode = exchangeCodeMapper.selectById(id);
@@ -156,9 +142,6 @@ public class ExternalController {
 				ExchangeCodeReq exchangeCodeReq = new ExchangeCodeReq();
 				exchangeCodeReq.setNumber(1);
 				exchangeCodeReq.setSkuIds(List.of(orderDon.getSkuId()).toString());
-				exchangeCodeReq.setType(0);
-				exchangeCodeReq.setCodeType(1);
-				exchangeCodeReq.setOperator("admin:" + StpUtil.getLoginIdAsLong());
 				exchangeCodeService.createExchangeCode(exchangeCodeReq);
 			} catch (Exception e) {
 			}

+ 5 - 15
netflix-web/src/main/java/com/cyksj/web/controller/manage/exchange/ExchangeCodeController.java

@@ -26,7 +26,6 @@ import com.ejlchina.searcher.BeanSearcher;
 import com.ejlchina.searcher.SearchResult;
 import com.ejlchina.searcher.util.MapBuilder;
 import com.ejlchina.searcher.util.MapUtils;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -87,7 +86,6 @@ public class ExchangeCodeController {
 	@GetMapping("/get")
 	@SaCheckPermission("exchangeCode:view")
 	public Result<SearchResult<ExchangeCodeView>> getExchangeCodeView(Long goodsId, Long skuId) {
-		long adminId = StpUtil.getLoginIdAsLong();
 		String condition = StrUtil.EMPTY;
 		if (goodsId != null) {
 			condition = String.format(" and JSON_CONTAINS(goods_ids,'%s')", goodsId);
@@ -95,7 +93,7 @@ public class ExchangeCodeController {
 		if (skuId != null) {
 			condition += String.format(" and JSON_CONTAINS(sku_ids,'%s')", skuId);
 		}
-		MapBuilder builder = safeSearchBuilder(condition, adminId);
+		MapBuilder builder = safeSearchBuilder(condition);
 		SearchResult<ExchangeCodeView> search = beanSearcher.search(ExchangeCodeView.class, builder.build());
 		search.getDataList().forEach(data -> {
 			try {
@@ -119,7 +117,8 @@ public class ExchangeCodeController {
 	@SaCheckPermission("exchangeCode:export")
 	@Log(module = "兑换码/导出", businessType = BusinessType.POST, isSaveRequestData = true)
 	public void exportExchangeCode(HttpServletResponse response, Long goodsId, Long skuId) {
-		long adminId = StpUtil.getLoginIdAsLong();
+        response.setHeader("Cache-Control", "no-store, private");
+        response.setHeader("Pragma", "no-cache");
 		String condition = StrUtil.EMPTY;
 		if (goodsId != null) {
 			condition = String.format(" and JSON_CONTAINS(goods_ids,'%s')", goodsId);
@@ -127,7 +126,7 @@ public class ExchangeCodeController {
 		if (skuId != null) {
 			condition += String.format(" and JSON_CONTAINS(sku_ids,'%s')", skuId);
 		}
-		MapBuilder builder = safeSearchBuilder(condition, adminId);
+		MapBuilder builder = safeSearchBuilder(condition);
 		if (beanSearcher.searchCount(ExcelExchangeCodeData.class, builder.build()).longValue() > 10000) {
 			throw BusinessRuntimeException.getInstance("每次最多导出10000条,请缩小查询范围");
 		}
@@ -136,7 +135,7 @@ public class ExchangeCodeController {
 	}
 
 	// 不透传请求参数:condition、字段操作符、逻辑分组均不能由客户端指定。
-	private MapBuilder safeSearchBuilder(String condition, long adminId) {
+	private MapBuilder safeSearchBuilder(String condition) {
 		MapBuilder builder = MapUtils.builder().put("condition", condition);
 		String code = request.getParameter("code");
 		if (StrUtil.isNotBlank(code)) builder.field(ExchangeCodeView::getCode, code);
@@ -151,15 +150,6 @@ public class ExchangeCodeController {
 		}
 		String codeType = request.getParameter("codeType");
 		if (StrUtil.isNotBlank(codeType)) builder.field(ExchangeCodeView::getCodeType, Integer.valueOf(codeType));
-		if (!StpUtil.hasRole("root")) {
-			CmsUser admin = cmsUserMapper.selectById(adminId);
-			if (admin == null || StrUtil.isBlank(admin.getNickname())
-					|| cmsUserMapper.selectCount(Wrappers.lambdaQuery(CmsUser.class)
-					.eq(CmsUser::getNickname, admin.getNickname())) != 1) {
-				throw BusinessRuntimeException.getInstance("创建人信息不唯一,请联系管理员核实数据归属");
-			}
-			builder.field(ExchangeCodeView::getOperator, admin.getNickname());
-		}
 		int page = request.getParameter("start") == null ? 1 : Integer.parseInt(request.getParameter("start"));
 		int size = request.getParameter("limit") == null ? 10 : Integer.parseInt(request.getParameter("limit"));
 		if (page < 1 || size < 1 || size > 100) throw BusinessRuntimeException.getInstance("分页参数错误");

+ 5 - 3
netflix-web/src/main/java/com/cyksj/web/controller/payment/OrderController.java

@@ -538,7 +538,7 @@ public class OrderController {
      */
     @GetMapping("/get/{orderId}")
     public Result<OrderDetailView> detail(@PathVariable Long orderId, Boolean isNoLogin) {
-        Long userId = StpUserUtil.getUserIdAfterLogin();
+        Long userId = StpUserUtil.getLoginIdAsLong();
 
         OrderDetailView detail = orderDonService.getDetail(orderId, userId);
 
@@ -557,11 +557,12 @@ public class OrderController {
 
     @GetMapping("/get/order/status/{orderId}")
     public Result<OrderDonSearchView> getOrderStatusById(@PathVariable Long orderId) {
+        long userId = StpUserUtil.getLoginIdAsLong();
         if (orderId < 1) {
             throw BusinessRuntimeException.getInstance("订单不存在");
         }
         OrderDonSearchView orderDonView = beanSearcher.searchFirst(OrderDonSearchView.class, MapUtils.builder()
-                .put("orderId", String.format("where id = %s", orderId))
+                .put("orderId", String.format("where id = %s and user_id = %s", orderId, userId))
                 .build());
         if (orderDonView == null) {
             throw BusinessRuntimeException.getInstance("订单不存在");
@@ -599,11 +600,12 @@ public class OrderController {
      */
     @GetMapping("/get/upgrade/order/status/{orderId}")
     public Result<UpgradeOrderDonSearchView> getUpgradeOrderStatusById(@PathVariable Long orderId) {
+        long userId = StpUserUtil.getLoginIdAsLong();
         if (orderId < 1) {
             throw BusinessRuntimeException.getInstance("订单不存在");
         }
         UpgradeOrderDonSearchView orderDonView = beanSearcher.searchFirst(UpgradeOrderDonSearchView.class, MapUtils.builder()
-                .put("orderId", String.format("where id = %s", orderId))
+                .put("orderId", String.format("where id = %s and user_id = %s", orderId, userId))
                 .build());
         if (orderDonView == null) {
             throw BusinessRuntimeException.getInstance("订单不存在");

+ 35 - 10
netflix-web/src/main/java/com/cyksj/web/controller/register/RegisterController.java

@@ -185,7 +185,7 @@ public class RegisterController {
     public Result<RegisterOrderDon> getOrderStatusById(@PathVariable Long orderId) {
         long userId = StpUserUtil.getLoginIdAsLong();
         List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
-        RegisterOrderDon registerOrderDon = beanSearcher.searchFirst(RegisterOrderDon.class, MapUtils.flatBuilder(request.getParameterMap())
+        RegisterOrderDon registerOrderDon = beanSearcher.searchFirst(RegisterOrderDon.class, MapUtils.builder()
                 .field(RegisterOrderDon::getUserId, userIdList).op(Operator.InList)
                 .field(RegisterOrderDon::getId, orderId)
                 .selectExclude(RegisterOrderDon::getApplePwdId, RegisterOrderDon::getCountryOrArea, RegisterOrderDon::getRemark)
@@ -194,20 +194,44 @@ public class RegisterController {
     }
 
     @PutMapping("/put")
-    public Result<String> update(@RequestBody RegisterOrderDon req){
-        if (req.getId() == null) {
-            throw new BusinessRuntimeException("缺少必填参数");
-        }
-        RegisterOrderDon registerOrderDon = registerOrderDonService.getById(req.getId());
-        if (!registerOrderDon.getStatus().equals(RegisterOrderDon.Status.error)) {
+    public Result<String> update(@RequestBody RegisterOrderDon req) {
+        if (req == null || req.getId() == null) throw new BusinessRuntimeException("缺少必填参数");
+        RegisterOrderDon existing = requireOwnedOrder(req.getId());
+        if (existing.getStatus() != RegisterOrderDon.Status.error) {
             throw new BusinessRuntimeException("该状态下不可更改信息");
         }
-        req.setStatus(RegisterOrderDon.Status.hasPayment);
-        registerOrderDonService.updateById(req);
-
+        // Only registration input is editable; never bind payment/ownership fields.
+        RegisterOrderDon patch = new RegisterOrderDon();
+        patch.setNickname(req.getNickname());
+        patch.setUsername(req.getUsername());
+        patch.setPassword(req.getPassword());
+        patch.setEmail(req.getEmail());
+        patch.setBirthYear(req.getBirthYear());
+        patch.setBirthMonth(req.getBirthMonth());
+        patch.setBirthDay(req.getBirthDay());
+        patch.setGender(req.getGender());
+        patch.setPhone(req.getPhone());
+        patch.setCountry(req.getCountry());
+        patch.setStatus(RegisterOrderDon.Status.hasPayment);
+        boolean updated = registerOrderDonService.update(patch, Wrappers.lambdaQuery(RegisterOrderDon.class)
+                .eq(RegisterOrderDon::getId, existing.getId())
+                .eq(RegisterOrderDon::getUserId, existing.getUserId())
+                .eq(RegisterOrderDon::getStatus, RegisterOrderDon.Status.error));
+        if (!updated) throw new BusinessRuntimeException("订单状态已变化,请刷新后重试");
         return GatewayResponse.SUCCESS.newBuilder().toResult();
     }
 
+    private RegisterOrderDon requireOwnedOrder(Long orderId) {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        List<Long> allowed = userBindRelationService.getRelationUserIdList(userId, null);
+        RegisterOrderDon order = registerOrderDonService.getById(orderId);
+        if (order == null || !(java.util.Objects.equals(order.getUserId(), userId)
+                || (allowed != null && allowed.contains(order.getUserId())))) {
+            throw new BusinessRuntimeException("订单不存在或无权操作");
+        }
+        return order;
+    }
+
     @GetMapping("/get/appleId/area/detail")
     public Result<AppleIdCountryOrAreaDataView> getAppleIdCountryOrAreaData() {
         AppleIdCountryOrAreaDataView appleIdCountryOrAreaDataView = new AppleIdCountryOrAreaDataView();
@@ -223,6 +247,7 @@ public class RegisterController {
      */
     @PutMapping("/cancel/order/{orderId}")
     public Result<String> cancelOrder(@PathVariable Long orderId) {
+        requireOwnedOrder(orderId);
         registerOrderDonService.updateAppleAutoStatus(orderId);
         return GatewayResponse.SUCCESS.newBuilder().toResult("取消订单成功");
     }

+ 25 - 5
netflix-web/src/main/java/com/cyksj/web/controller/user/AuthorizationController.java

@@ -80,6 +80,10 @@ public class AuthorizationController {
 
     private final RedisService redisService;
 
+    // Socket peer, not user-controlled forwarded headers. Tune for reverse proxies.
+    @org.springframework.beans.factory.annotation.Value("${security.user-auth.send-peer-limit:1000}")
+    private int sendPeerLimit;
+
     private final BadIntentionOpService badIntentionOpService;
 
     private final WxOpenPlatYHLXLoginConfig wxOpenPlatYHLXLoginConfig;
@@ -434,9 +438,17 @@ public class AuthorizationController {
     @NoSubmit
     public Result<String> getPhoneCode(String phone, Integer digit, @RequestParam(defaultValue = "0") Boolean isFree, Integer cid, String verifyCode, String ticket,String randStr) throws Exception {
         if (cid == null) cid = 86;
-        if (StrUtil.isBlank(phone) || (86 == cid && !Validator.isMobile(phone))) {
+        if (StrUtil.isBlank(phone) || !phone.matches("[+]?[0-9]{5,20}") || cid < 1 || cid > 999 || (86 == cid && !Validator.isMobile(phone))) {
             throw BusinessRuntimeException.getInstance("请输入正确的手机号");
         }
+        String sendIdentity = phone.toLowerCase(java.util.Locale.ROOT);
+        if (redisService.securityAttempt("auth:send:account:" + cn.hutool.crypto.digest.DigestUtil.sha256Hex(sendIdentity)) > 5
+                || redisService.securityAttempt("auth:send:peer:" + cn.hutool.crypto.digest.DigestUtil.sha256Hex(request.getRemoteAddr())) > Math.max(1, sendPeerLimit)) {
+            throw BusinessRuntimeException.getInstance("验证码发送过于频繁,请稍后重试");
+        }
+        if (!redisService.setNx("auth:send:cooldown:" + cn.hutool.crypto.digest.DigestUtil.sha256Hex(sendIdentity), "1", 60L)) {
+            throw BusinessRuntimeException.getInstance("请60秒后重新获取验证码");
+        }
         //是否是虚拟手机号开头
         if (BlackOpsUtil.phoneIsStart(phone)) {
             throw BusinessRuntimeException.getInstance("请输入正确的手机号");
@@ -447,7 +459,7 @@ public class AuthorizationController {
         }
         //是否是恶意用户
         badIntentionOpService.checkHandleBadUserOp(phone, verifyCode, ticket, randStr, request);
-        String code = StringUtil.getRandomCodeStr(digit);
+        String code = StringUtil.getRandomCodeStr(6);
         Boolean isFlag;
         if (!isFree) {
             isFlag = SmsUtil.sendTencent(cid, phone, code);
@@ -484,12 +496,20 @@ public class AuthorizationController {
     @GetMapping("/get/email/code")
     @NoSubmit
     public Result<String> getEmailCode(String email, Integer digit, Integer cmt) {
-        if (email == null || !Validator.isEmail(email)) {
+        if (email == null || email.length() > 254 || !email.equals(email.trim()) || !Validator.isEmail(email)) {
             throw BusinessRuntimeException.getInstance("请输入正确邮箱");
         }
+        String sendIdentity = email.toLowerCase(java.util.Locale.ROOT);
+        if (redisService.securityAttempt("auth:send:account:" + cn.hutool.crypto.digest.DigestUtil.sha256Hex(sendIdentity)) > 5
+                || redisService.securityAttempt("auth:send:peer:" + cn.hutool.crypto.digest.DigestUtil.sha256Hex(request.getRemoteAddr())) > Math.max(1, sendPeerLimit)) {
+            throw BusinessRuntimeException.getInstance("验证码发送过于频繁,请稍后重试");
+        }
+        if (!redisService.setNx("auth:send:cooldown:" + cn.hutool.crypto.digest.DigestUtil.sha256Hex(sendIdentity), "1", 60L)) {
+            throw BusinessRuntimeException.getInstance("请60秒后重新获取验证码");
+        }
         if (envCommonService.isPrdEnv()) {
             //首次注册只能gmail邮箱
-            if (!email.contains("@gmail")) {
+            if (!email.toLowerCase(java.util.Locale.ROOT).endsWith("@gmail.com")) {
                 if (cmt == null || cmt != 2) {
                     User user = userService.getOne(Wrappers.lambdaQuery(User.class).eq(User::getEmail, email).last("limit 1"));
                     if (user == null) {
@@ -502,7 +522,7 @@ public class AuthorizationController {
         if (exists != null) {
             throw BusinessRuntimeException.getInstance("验证码未失效");
         }
-        String code = mailService.sendLoginCodeEmailToPeople(digit, email);
+        String code = mailService.sendLoginCodeEmailToPeople(6, email);
         if (code == null) {
             throw BusinessRuntimeException.getInstance("获取邮箱验证码错误");
         }