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:
@@ -0,0 +1,39 @@
|
|||||||
|
package com.cool.store.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖
|
||||||
|
*/
|
||||||
|
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Inherited
|
||||||
|
@Documented
|
||||||
|
public @interface Debounce {
|
||||||
|
/**
|
||||||
|
* 防抖时间窗口(毫秒)
|
||||||
|
*/
|
||||||
|
long timeMs() default 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key 的 SpEL
|
||||||
|
* 例:"'order:' + #req.orderId"
|
||||||
|
*/
|
||||||
|
String key() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否根据用户区分
|
||||||
|
*/
|
||||||
|
boolean diffUser() default true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为pc端,pc端使用CurrentUserHolder获取用户信息,否则使用PartnerUserHolder获取
|
||||||
|
*/
|
||||||
|
boolean pc() default true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* true: 在窗口内触发则抛异常
|
||||||
|
* false: 在窗口内触发则直接返回上一次结果
|
||||||
|
*/
|
||||||
|
boolean throwOnDebounce() default true;
|
||||||
|
}
|
||||||
@@ -384,4 +384,24 @@ public class RedisConstant {
|
|||||||
* 钱包分账支付锁
|
* 钱包分账支付锁
|
||||||
*/
|
*/
|
||||||
public static final String WALLET_ALLOCATION_PAY_LOCK_KEY = "wallet_allocation_pay_lock_key:{0}";
|
public static final String WALLET_ALLOCATION_PAY_LOCK_KEY = "wallet_allocation_pay_lock_key:{0}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私域设备SN->绑定二维码地址
|
||||||
|
*/
|
||||||
|
public static final String PRIVATE_SN_QR_CODE = "private_sphere_qr";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私域设备SN->门店id
|
||||||
|
*/
|
||||||
|
public static final String PRIVATE_STORE_SN_BIND = "private_sphere_store_sn";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私域设备SN->门店私域二维码地址
|
||||||
|
*/
|
||||||
|
public static final String PRIVATE_STORE_SN_QR_CODE = "private_sphere_store_sn_qr_code";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IP限制key
|
||||||
|
*/
|
||||||
|
public static final String IP_LIMIT = "ip_limit";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -452,6 +452,8 @@ public enum ErrorCodeEnum {
|
|||||||
WALLET_RE_PAY_FAIL(1830019, "重新支付异常,请联系管理员", null),
|
WALLET_RE_PAY_FAIL(1830019, "重新支付异常,请联系管理员", null),
|
||||||
WALLET_PAY_CANNOT_CANCEL(1830020, "该交易无法取消", null),
|
WALLET_PAY_CANNOT_CANCEL(1830020, "该交易无法取消", null),
|
||||||
TOTAL_FEES_NEED_EQUAL(1830021, "分账总金额需与合计缴费金额一致", null),
|
TOTAL_FEES_NEED_EQUAL(1830021, "分账总金额需与合计缴费金额一致", null),
|
||||||
|
|
||||||
|
IP_LIMIT(1840000, "IP访问次数超限", null),
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -247,4 +247,11 @@ public class StoreDao {
|
|||||||
}
|
}
|
||||||
return storeMapper.batchUpdateAddress(storeList);
|
return storeMapper.batchUpdateAddress(storeList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取私域二维码
|
||||||
|
*/
|
||||||
|
public String getPrivateSphereQrCode(String storeId){
|
||||||
|
return storeMapper.getPrivateSphereQrCode(storeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,4 +153,6 @@ public interface StoreMapper {
|
|||||||
List<StoreAddressDTO> getStoreAddress(@Param("flag") Integer flag, @Param("storeCode") String storeCode);
|
List<StoreAddressDTO> getStoreAddress(@Param("flag") Integer flag, @Param("storeCode") String storeCode);
|
||||||
|
|
||||||
int batchUpdateAddress(@Param("list") List<StoreDO> storeList);
|
int batchUpdateAddress(@Param("list") List<StoreDO> storeList);
|
||||||
|
|
||||||
|
String getPrivateSphereQrCode(@Param("storeId") String storeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -421,4 +421,8 @@
|
|||||||
</foreach>
|
</foreach>
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<select id="getPrivateSphereQrCode" resultType="java.lang.String">
|
||||||
|
SELECT private_sphere_qr FROM store_extend_info_${enterpriseId}
|
||||||
|
WHERE store_id = #{storeId}
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.cool.store.request.privatesphere;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 门店设备sn绑定
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author wangff
|
||||||
|
* @since 2026/4/2
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PrivateSphereBindRequest {
|
||||||
|
@ApiModelProperty("门店id")
|
||||||
|
@NotBlank(message = "门店id不能为空")
|
||||||
|
private String storeId;
|
||||||
|
|
||||||
|
@ApiModelProperty("设备sn")
|
||||||
|
@NotBlank(message = "设备sn不能为空")
|
||||||
|
private String sn;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.cool.store.request.privatesphere;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 设备sn
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author wangff
|
||||||
|
* @since 2026/4/2
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PrivateSphereSnRequest {
|
||||||
|
@ApiModelProperty("设备sn")
|
||||||
|
@NotBlank(message = "设备sn不能为空")
|
||||||
|
private String sn;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "客户端IP", hidden = true)
|
||||||
|
private String clientIp;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.cool.store.vo.privatesphere;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 私域二维码VO
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author wangff
|
||||||
|
* @since 2026/4/2
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PrivateSphereVO {
|
||||||
|
@ApiModelProperty("状态,0正常,1未绑定sn,2门店未维护私域二维码,3IP限制")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@ApiModelProperty("二维码地址")
|
||||||
|
private String qrCodeUrl;
|
||||||
|
|
||||||
|
public PrivateSphereVO(String url) {
|
||||||
|
this.qrCodeUrl = url;
|
||||||
|
this.status = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PrivateSphereVO unbind() {
|
||||||
|
return new PrivateSphereVO(1, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PrivateSphereVO noMaintain() {
|
||||||
|
return new PrivateSphereVO(2, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PrivateSphereVO ipLimit() {
|
||||||
|
return new PrivateSphereVO(3, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,6 +127,11 @@
|
|||||||
<groupId>com.aliyun</groupId>
|
<groupId>com.aliyun</groupId>
|
||||||
<artifactId>alibabacloud-dysmsapi20170525</artifactId>
|
<artifactId>alibabacloud-dysmsapi20170525</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- 二维码生成 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>core</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.cool.store.service.privatesphere;
|
||||||
|
|
||||||
|
import com.cool.store.request.privatesphere.PrivateSphereBindRequest;
|
||||||
|
import com.cool.store.request.privatesphere.PrivateSphereSnRequest;
|
||||||
|
import com.cool.store.vo.privatesphere.PrivateSphereVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 私域二维码服务
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author wangff
|
||||||
|
* @since 2026/4/1
|
||||||
|
*/
|
||||||
|
public interface PrivateSphereQrService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成二维码并上传到OSS
|
||||||
|
* 相同sn优先从redis中获取二维码图片路径
|
||||||
|
*
|
||||||
|
* @param request 设备sn
|
||||||
|
* @return 二维码图片的OSS路径
|
||||||
|
*/
|
||||||
|
PrivateSphereVO generateQrCode(PrivateSphereSnRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门店绑定设备sn
|
||||||
|
* @param request 门店设备sn绑定
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
Boolean bindStoreSn(PrivateSphereBindRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备sn获取门店私域二维码
|
||||||
|
* @param request 设备sn
|
||||||
|
* @return 私域二维码
|
||||||
|
*/
|
||||||
|
PrivateSphereVO getPrivateSphereQr(PrivateSphereSnRequest request);
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.cool.store.service.privatesphere.impl;
|
||||||
|
|
||||||
|
import cn.hutool.extra.qrcode.QrCodeUtil;
|
||||||
|
import cn.hutool.extra.qrcode.QrConfig;
|
||||||
|
import com.cool.store.constants.RedisConstant;
|
||||||
|
import com.cool.store.dao.StoreDao;
|
||||||
|
import com.cool.store.enums.ErrorCodeEnum;
|
||||||
|
import com.cool.store.exception.ServiceException;
|
||||||
|
import com.cool.store.oss.OssClientService;
|
||||||
|
import com.cool.store.request.privatesphere.PrivateSphereBindRequest;
|
||||||
|
import com.cool.store.request.privatesphere.PrivateSphereSnRequest;
|
||||||
|
import com.cool.store.service.privatesphere.PrivateSphereQrService;
|
||||||
|
import com.cool.store.utils.RedisUtilPool;
|
||||||
|
import com.cool.store.utils.poi.StringUtils;
|
||||||
|
import com.cool.store.vo.privatesphere.PrivateSphereVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 私域二维码服务实现
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author wangff
|
||||||
|
* @since 2026/4/1
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PrivateSphereQrServiceImpl implements PrivateSphereQrService {
|
||||||
|
|
||||||
|
private final RedisUtilPool redisUtilPool;
|
||||||
|
private final OssClientService ossClientService;
|
||||||
|
private final StoreDao storeDao;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二维码尺寸
|
||||||
|
*/
|
||||||
|
private static final int QR_CODE_SIZE = 1280;
|
||||||
|
/**
|
||||||
|
* sn缓存时间
|
||||||
|
*/
|
||||||
|
private static final int SN_CACHE_TIME = 86400;
|
||||||
|
/**
|
||||||
|
* IP限制时间(1小时)
|
||||||
|
*/
|
||||||
|
private static final int IP_LIMIT_TIME = 3600;
|
||||||
|
/**
|
||||||
|
* IP每小时最大生成次数
|
||||||
|
*/
|
||||||
|
private static final int IP_MAX_COUNT = 5;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrivateSphereVO generateQrCode(PrivateSphereSnRequest request) {
|
||||||
|
// 先从Redis中获取缓存
|
||||||
|
String cachedUrl = redisUtilPool.hashGet(RedisConstant.PRIVATE_SN_QR_CODE, request.getSn());
|
||||||
|
if (StringUtils.isNotBlank(cachedUrl)) {
|
||||||
|
return new PrivateSphereVO(cachedUrl);
|
||||||
|
}
|
||||||
|
verify(request.getClientIp(), "rq_ge_limit");
|
||||||
|
// 生成二维码并上传到OSS
|
||||||
|
try {
|
||||||
|
String qrCodeUrl = generateAndUploadQrCode(request.getSn());
|
||||||
|
// 存储到Redis
|
||||||
|
redisUtilPool.hashSet(RedisConstant.PRIVATE_SN_QR_CODE, request.getSn(), qrCodeUrl, SN_CACHE_TIME);
|
||||||
|
log.info("生成二维码并上传成功,sn: {}, url: {}", request.getSn(), qrCodeUrl);
|
||||||
|
return new PrivateSphereVO(qrCodeUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成二维码失败,sn: {}", request.getSn(), e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verify(String ip, String suffix) {
|
||||||
|
// IP限制检查:同一IP一小时内最多生成5次
|
||||||
|
if (StringUtils.isNotBlank(ip)) {
|
||||||
|
String countStr = redisUtilPool.hashGet(RedisConstant.IP_LIMIT + "_" + suffix, ip);
|
||||||
|
int count = StringUtils.isNotBlank(countStr) ? Integer.parseInt(countStr) : 0;
|
||||||
|
if (count >= IP_MAX_COUNT) {
|
||||||
|
log.error("IP生成次数超限,ip: {}, count: {}", ip, count);
|
||||||
|
throw new ServiceException(ErrorCodeEnum.IP_LIMIT);
|
||||||
|
}
|
||||||
|
// 增加计数
|
||||||
|
redisUtilPool.hashSet(RedisConstant.IP_LIMIT + "_" + suffix, ip, String.valueOf(count + 1), IP_LIMIT_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean bindStoreSn(PrivateSphereBindRequest request) {
|
||||||
|
redisUtilPool.hashSet(RedisConstant.PRIVATE_STORE_SN_BIND, request.getSn(), request.getStoreId(), SN_CACHE_TIME);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrivateSphereVO getPrivateSphereQr(PrivateSphereSnRequest request) {
|
||||||
|
String storeId = redisUtilPool.hashGet(RedisConstant.PRIVATE_STORE_SN_BIND, request.getSn());
|
||||||
|
if (StringUtils.isBlank(storeId)) {
|
||||||
|
return PrivateSphereVO.unbind();
|
||||||
|
}
|
||||||
|
String privateSphereQrCode = redisUtilPool.hashGet(RedisConstant.PRIVATE_STORE_SN_QR_CODE, request.getSn());
|
||||||
|
if (privateSphereQrCode == null) {
|
||||||
|
privateSphereQrCode = storeDao.getPrivateSphereQrCode(storeId);
|
||||||
|
redisUtilPool.hashSet(RedisConstant.PRIVATE_STORE_SN_QR_CODE, request.getSn(), privateSphereQrCode == null ? "" : privateSphereQrCode, 120);
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(privateSphereQrCode)) {
|
||||||
|
return PrivateSphereVO.noMaintain();
|
||||||
|
}
|
||||||
|
return new PrivateSphereVO(privateSphereQrCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成二维码并上传到OSS
|
||||||
|
*
|
||||||
|
* @param sn 二维码内容
|
||||||
|
* @return OSS图片路径
|
||||||
|
*/
|
||||||
|
private String generateAndUploadQrCode(String sn) throws Exception {
|
||||||
|
// 配置二维码
|
||||||
|
QrConfig qrConfig = new QrConfig(QR_CODE_SIZE, QR_CODE_SIZE);
|
||||||
|
qrConfig.setMargin(2);
|
||||||
|
|
||||||
|
// 生成二维码图片到内存
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
QrCodeUtil.generate(sn, qrConfig, "png", outputStream);
|
||||||
|
|
||||||
|
String fileName = "qrcode" + sn + ".png";
|
||||||
|
|
||||||
|
// 上传到OSS
|
||||||
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
|
||||||
|
long contentLength = outputStream.size();
|
||||||
|
|
||||||
|
return ossClientService.putObjectWithoutPrefix(fileName, inputStream, contentLength, "image/png");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,9 @@ public class OpenApiValidateFilter implements Filter {
|
|||||||
"/zxjp/open/v1/statusRefresh",
|
"/zxjp/open/v1/statusRefresh",
|
||||||
"/zxjp/open/v1/getStoreUser",
|
"/zxjp/open/v1/getStoreUser",
|
||||||
"/zxjp/open/v1/callback",
|
"/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(
|
private static final List<String> oldUrlMapping = new ArrayList<>(Arrays.asList(
|
||||||
"/zxjp/open/v1/statusRefresh","/zxjp/open/v1/changePaymentStatus",
|
"/zxjp/open/v1/statusRefresh","/zxjp/open/v1/changePaymentStatus",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.cool.store.controller.webb;
|
package com.cool.store.controller.webb;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.cool.store.annotation.Debounce;
|
||||||
import com.cool.store.dto.*;
|
import com.cool.store.dto.*;
|
||||||
import com.cool.store.dto.region.BigRegionDTO;
|
import com.cool.store.dto.region.BigRegionDTO;
|
||||||
import com.cool.store.dto.store.StoreUserPositionDTO;
|
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.ShopListRequest;
|
||||||
import com.cool.store.request.openapi.StoreRequest;
|
import com.cool.store.request.openapi.StoreRequest;
|
||||||
import com.cool.store.request.openapi.SubRegionRequest;
|
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.AccountTradeCallbackRequest;
|
||||||
import com.cool.store.request.wallet.AddTagCallbackNoticeRequest;
|
import com.cool.store.request.wallet.AddTagCallbackNoticeRequest;
|
||||||
import com.cool.store.request.wallet.OnlineCommercialBankCallbackRequest;
|
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.ShopResponse;
|
||||||
import com.cool.store.response.bigdata.ApiResponse;
|
import com.cool.store.response.bigdata.ApiResponse;
|
||||||
import com.cool.store.service.*;
|
import com.cool.store.service.*;
|
||||||
|
import com.cool.store.service.privatesphere.PrivateSphereQrService;
|
||||||
import com.cool.store.service.wallet.WalletService;
|
import com.cool.store.service.wallet.WalletService;
|
||||||
|
import com.cool.store.utils.HttpHelper;
|
||||||
import com.cool.store.utils.poi.StringUtils;
|
import com.cool.store.utils.poi.StringUtils;
|
||||||
import com.cool.store.service.close.CloseStoreService;
|
import com.cool.store.service.close.CloseStoreService;
|
||||||
|
import com.cool.store.vo.privatesphere.PrivateSphereVO;
|
||||||
import com.github.pagehelper.PageInfo;
|
import com.github.pagehelper.PageInfo;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
@@ -39,6 +44,7 @@ import org.springframework.validation.annotation.Validated;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,6 +86,8 @@ public class OpenApiController {
|
|||||||
BigRegionService bigRegionService;
|
BigRegionService bigRegionService;
|
||||||
@Resource
|
@Resource
|
||||||
RegionService regionService;
|
RegionService regionService;
|
||||||
|
@Resource
|
||||||
|
PrivateSphereQrService privateSphereQrService;
|
||||||
|
|
||||||
@PostMapping("/statusRefresh")
|
@PostMapping("/statusRefresh")
|
||||||
public ApiResponse<Boolean> statusRefresh(@RequestBody StatusRefreshDTO statusRefreshDTO){
|
public ApiResponse<Boolean> statusRefresh(@RequestBody StatusRefreshDTO statusRefreshDTO){
|
||||||
@@ -280,4 +288,19 @@ public class OpenApiController {
|
|||||||
return ApiResponse.success(regionService.getSubRegionByParentId(request.getRegionId()));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
6
pom.xml
6
pom.xml
@@ -238,6 +238,12 @@
|
|||||||
<version>1.70</version>
|
<version>1.70</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- XML处理 -->
|
<!-- XML处理 -->
|
||||||
|
<!-- 二维码生成 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>core</artifactId>
|
||||||
|
<version>3.4.1</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user