Merge #88 into master from cc_20260401_private_sphere_qr

fix:私域二维码

* cc_20260401_private_sphere_qr: (3 commits squashed)

  - fix:新增防抖

  - Merge branch 'refs/heads/cc_20260302_debounce' into cc_20260401_private_sphere_qr

  - fix:私域二维码

Signed-off-by: 王非凡 <accounts_67eba0c5fee9c49c80c8e2b4@mail.teambition.com>
Merged-by: 正新 <accounts_6964c7bcd2a2c377c5bbd01b@mail.teambition.com>

CR-link: https://codeup.aliyun.com/692ea314dec569489f6f167c/hangzhou/java/custom_zxjp/change/88
This commit is contained in:
王非凡
2026-04-08 10:44:13 +00:00
committed by 正新
parent 5ff5673914
commit 68cef4b83a
17 changed files with 562 additions and 1 deletions

View File

@@ -0,0 +1,150 @@
package com.cool.store.aspect;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.cool.store.annotation.Debounce;
import com.cool.store.context.CurrentUserHolder;
import com.cool.store.context.PartnerUserHolder;
import com.cool.store.enums.ErrorCodeEnum;
import com.cool.store.exception.ServiceException;
import com.cool.store.utils.RedisUtilPool;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;
/**
* <p>
* 防抖切面
* </p>
*
* @author wangff
* @since 2026/2/7
*/
@Aspect
@Component
@Slf4j
public class DebounceAspect {
@Resource
private RedisUtilPool redisUtilPool;
private static final String KEY_PREFIX = "debounce:";
private static final String FIELD_NEXT_ALLOWED_AT = "nextAllowedAt";
private static final String FIELD_LAST_RESULT = "lastResult";
private final ExpressionParser spelParser = new SpelExpressionParser();
@Around("execution(public * *(..)) && (@within(com.cool.store.annotation.Debounce) || @annotation(com.cool.store.annotation.Debounce))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Debounce debounce = AnnotatedElementUtils.findMergedAnnotation(method, Debounce.class);
if (debounce == null) {
debounce = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Debounce.class);
}
if (debounce == null || debounce.timeMs() <= 0) {
return pjp.proceed();
}
String key = KEY_PREFIX + buildKey(debounce, pjp, method);
long now = System.currentTimeMillis();
long ttlMs = debounce.timeMs();
Long nextAllowedAt = readNextAllowedAt(key);
if (nextAllowedAt != null && now < nextAllowedAt) {
if (debounce.throwOnDebounce()) {
throw new ServiceException(ErrorCodeEnum.OPERATION_OVER_TIME);
}
String val = redisUtilPool.getString(key + ":" + FIELD_LAST_RESULT);
return parseToReturnType(val, method);
}
boolean allowed = trySetNextAllowedAt(key, now + ttlMs, ttlMs);
if (!allowed) {
if (debounce.throwOnDebounce()) {
throw new ServiceException(ErrorCodeEnum.OPERATION_OVER_TIME);
}
String val = redisUtilPool.getString(key + ":" + FIELD_LAST_RESULT);
return parseToReturnType(val, method);
}
Object result = null;
try {
result = pjp.proceed();
return result;
} finally {
if (!isVoidReturn(method) && Objects.nonNull(result)) {
redisUtilPool.setNxExpire(key + ":" + FIELD_LAST_RESULT, JSONObject.toJSONString(result), (int) ttlMs * 2);
}
}
}
private Long readNextAllowedAt(String key) {
String val = redisUtilPool.getString(key + ":" + FIELD_NEXT_ALLOWED_AT);
if (StringUtils.isBlank(val)) {
return null;
}
Object obj = JSONObject.parse(val);
if (obj instanceof Number) {
return ((Number) obj).longValue();
}
try {
return Long.parseLong(val);
} catch (Exception e) {
return null;
}
}
private boolean trySetNextAllowedAt(String key, long nextAllowedAt, long ttlMs) {
return redisUtilPool.setNxExpire(key + ":" + FIELD_NEXT_ALLOWED_AT, String.valueOf(nextAllowedAt), (int) ttlMs);
}
private boolean isVoidReturn(Method method) {
return Void.TYPE.equals(method.getReturnType()) || Void.class.equals(method.getReturnType());
}
private Object parseToReturnType(String json, Method method) {
if (StringUtils.isBlank(json)) {
return null;
}
Class<?> returnType = method.getReturnType();
if (returnType == null || Object.class.equals(returnType)) {
return JSONObject.parse(json);
}
if (String.class.equals(returnType)) {
return json;
}
return JSON.parseObject(json, returnType);
}
private String buildKey(Debounce debounce, ProceedingJoinPoint pjp, Method method) {
String spel = debounce.key();
String suffix = debounce.diffUser() ? ":" + (debounce.pc() ? CurrentUserHolder.getUserId() : PartnerUserHolder.getUser().getPartnerId()) : "";
if (spel != null && !spel.isEmpty()) {
EvaluationContext ctx = new StandardEvaluationContext();
Object[] args = pjp.getArgs();
String[] paramNames = ((MethodSignature) pjp.getSignature()).getParameterNames();
if (paramNames != null) {
for (int i = 0; i < paramNames.length; i++) {
ctx.setVariable(paramNames[i], args[i]);
}
}
Object val = spelParser.parseExpression(spel).getValue(ctx);
return "spel:" + method.getDeclaringClass().getName() + "#" + method.getName() + ":" + val + suffix;
}
return "default:" + method.getDeclaringClass().getName() + "#" + method.getName() + ":" + Arrays.deepHashCode(pjp.getArgs()) + suffix;
}
}

View File

@@ -52,7 +52,9 @@ public class OpenApiValidateFilter implements Filter {
"/zxjp/open/v1/statusRefresh",
"/zxjp/open/v1/getStoreUser",
"/zxjp/open/v1/callback",
"/zxjp/open/v1/wallet/onlineCommercialBankCallback"
"/zxjp/open/v1/wallet/onlineCommercialBankCallback",
"/zxjp/open/v1/ps/geBindQr",
"/zxjp/open/v1/ps/getPrivateSphereQr"
));
private static final List<String> oldUrlMapping = new ArrayList<>(Arrays.asList(
"/zxjp/open/v1/statusRefresh","/zxjp/open/v1/changePaymentStatus",

View File

@@ -1,6 +1,7 @@
package com.cool.store.controller.webb;
import com.alibaba.fastjson.JSONObject;
import com.cool.store.annotation.Debounce;
import com.cool.store.dto.*;
import com.cool.store.dto.region.BigRegionDTO;
import com.cool.store.dto.store.StoreUserPositionDTO;
@@ -17,6 +18,7 @@ import com.cool.store.request.notice.ThirdMatterRequest;
import com.cool.store.request.openapi.ShopListRequest;
import com.cool.store.request.openapi.StoreRequest;
import com.cool.store.request.openapi.SubRegionRequest;
import com.cool.store.request.privatesphere.PrivateSphereSnRequest;
import com.cool.store.request.wallet.AccountTradeCallbackRequest;
import com.cool.store.request.wallet.AddTagCallbackNoticeRequest;
import com.cool.store.request.wallet.OnlineCommercialBankCallbackRequest;
@@ -27,9 +29,12 @@ import com.cool.store.response.ResponseResult;
import com.cool.store.response.ShopResponse;
import com.cool.store.response.bigdata.ApiResponse;
import com.cool.store.service.*;
import com.cool.store.service.privatesphere.PrivateSphereQrService;
import com.cool.store.service.wallet.WalletService;
import com.cool.store.utils.HttpHelper;
import com.cool.store.utils.poi.StringUtils;
import com.cool.store.service.close.CloseStoreService;
import com.cool.store.vo.privatesphere.PrivateSphereVO;
import com.github.pagehelper.PageInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -39,6 +44,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
@@ -80,6 +86,8 @@ public class OpenApiController {
BigRegionService bigRegionService;
@Resource
RegionService regionService;
@Resource
PrivateSphereQrService privateSphereQrService;
@PostMapping("/statusRefresh")
public ApiResponse<Boolean> statusRefresh(@RequestBody StatusRefreshDTO statusRefreshDTO){
@@ -280,4 +288,19 @@ public class OpenApiController {
return ApiResponse.success(regionService.getSubRegionByParentId(request.getRegionId()));
}
@ApiOperation("私域二维码-生成绑定二维码")
@PostMapping("/ps/geBindQr")
@Debounce(key = "#request.sn", diffUser = false, timeMs = 5000)
public ApiResponse<PrivateSphereVO> geBindQr(@RequestBody @Validated PrivateSphereSnRequest request, HttpServletRequest httpRequest) {
request.setClientIp(HttpHelper.getIpAddr(httpRequest));
return ApiResponse.success(privateSphereQrService.generateQrCode(request));
}
@ApiOperation("私域二维码-根据设备sn获取门店私域二维码")
@PostMapping("/ps/getPrivateSphereQr")
@Debounce(key = "#request.sn", diffUser = false, timeMs = 5000)
public ApiResponse<PrivateSphereVO> getPrivateSphereQr(@RequestBody @Validated PrivateSphereSnRequest request, HttpServletRequest httpRequest) {
request.setClientIp(HttpHelper.getIpAddr(httpRequest));
return ApiResponse.success(privateSphereQrService.getPrivateSphereQr(request));
}
}

View File

@@ -0,0 +1,35 @@
package com.cool.store.controller.webc;
import com.cool.store.request.privatesphere.PrivateSphereBindRequest;
import com.cool.store.response.ResponseResult;
import com.cool.store.service.privatesphere.PrivateSphereQrService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* Mini门店私域二维码
* </p>
*
* @author wangff
* @since 2026/4/2
*/
@RestController
@Api(tags = "Mini门店私域二维码")
@RequestMapping({"/mini/sp"})
@RequiredArgsConstructor
public class MiniPrivateSphereQrController {
private final PrivateSphereQrService privateSphereQrService;
@ApiOperation("门店绑定设备sn")
@PostMapping("/bind")
public ResponseResult<Boolean> bindStoreSn(@RequestBody @Validated PrivateSphereBindRequest request) {
return ResponseResult.success(privateSphereQrService.bindStoreSn(request));
}
}