初始化天卡开放平台后端项目
包含完整功能模块: - 设备激活(直销激活码模式 + 代销免激活码模式) - 微信支付 + 天卡订单 - 天卡池管理 + 异步橘视厅SDK激活 - 第三方合作伙伴接口(代销模式) - 签名鉴权(appKey/appSecret + nonce防重放) - 天卡消耗(事务 + 幂等 + 并发控制) - 媒资接口(三级授权过滤) - 审计日志 - 定价策略(3层:门店 > 渠道 > 平台) - 分账(微信分账 + 分润规则) - Docker部署(含Redis容器)
This commit is contained in:
18
src/main/java/io/as/AsApplication.java
Normal file
18
src/main/java/io/as/AsApplication.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package io.as;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
/**
|
||||
* 天卡扫码付费开放API 启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
public class AsApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AsApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
22
src/main/java/io/as/common/annotation/AuditAction.java
Executable file
22
src/main/java/io/as/common/annotation/AuditAction.java
Executable file
@@ -0,0 +1,22 @@
|
||||
package io.as.common.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 审计日志注解:标记需写入 audit_log 的操作(分配/销售/转合同/绑定/解绑/激活校验/迁移/设备转门店/禁用等)
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface AuditAction {
|
||||
|
||||
/** 动作类型:如 ALLOCATE、TRANSFER、ORG_*、CARD_*、DEVICE_*、DEVICE_TRANSFER_STORE 等 */
|
||||
String value();
|
||||
|
||||
/** 中文描述,如 分配/销售、转合同 */
|
||||
String description() default "";
|
||||
}
|
||||
15
src/main/java/io/as/common/annotation/SysLog.java
Executable file
15
src/main/java/io/as/common/annotation/SysLog.java
Executable file
@@ -0,0 +1,15 @@
|
||||
package io.as.common.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface SysLog {
|
||||
|
||||
String value() default "";
|
||||
}
|
||||
121
src/main/java/io/as/common/aspect/ApiLogAspect.java
Executable file
121
src/main/java/io/as/common/aspect/ApiLogAspect.java
Executable file
@@ -0,0 +1,121 @@
|
||||
package io.as.common.aspect;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import io.as.common.utils.HttpContextUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 在 Controller 方法执行后把元数据放入 request;sys_api_log 在 ApiJstCallLogFilter#finally
|
||||
* 中写入。POST/PUT/PATCH 的 body 由 Filter 在进链前整段预读,见 {@link #ATTR_PREREAD_REQUEST_BODY}。
|
||||
* <p>入站 {@code /api/jst/*} 与其它 {@code io.as.modules.openapi.controller} 接口均落 sys_api_log;
|
||||
* {@code POST /api/jst/heartBeat} 不落库(轮询频繁);{@code POST /api/jst/tracking} 仅落 device_tracking_log。</p>
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class ApiLogAspect {
|
||||
|
||||
public static final String ATTR_SKIP_SYS_API_LOG = "io.as.api.SYS_API_LOG_SKIP";
|
||||
public static final String ATTR_PENDING_SYS_API_LOG = "io.as.api.SYS_API_LOG_PENDING";
|
||||
/** Filter 在进链前完整读取的 body,落 sys_api_log 时优先用,避免与 ContentCaching 时序/读空冲突 */
|
||||
public static final String ATTR_PREREAD_REQUEST_BODY = "io.as.api.PREREAD_REQUEST_BODY";
|
||||
|
||||
/** 供 Filter 落库;params 在 Filter 用 body 字节 */
|
||||
public static final class PendingSysApiLog {
|
||||
public long durationMs;
|
||||
public String resultJson;
|
||||
public String apiName;
|
||||
public String methodFull;
|
||||
/** body 仍为空时退路(可能少于原始 JSON) */
|
||||
public String gsonArgsFallback;
|
||||
}
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
@Pointcut("execution(* io.as.modules.openapi.controller..*.*(..))")
|
||||
public void logPointCut() {
|
||||
}
|
||||
|
||||
@Around("logPointCut()")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
|
||||
if (shouldSkipSysApiLogForJstInbound(request)) {
|
||||
request.setAttribute(ATTR_SKIP_SYS_API_LOG, Boolean.TRUE);
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
long beginTime = System.currentTimeMillis();
|
||||
Object result = point.proceed();
|
||||
long time = System.currentTimeMillis() - beginTime;
|
||||
|
||||
PendingSysApiLog pending = new PendingSysApiLog();
|
||||
pending.durationMs = time;
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
pending.apiName = method.getName();
|
||||
String className = point.getTarget().getClass().getName();
|
||||
pending.methodFull = className + "." + signature.getName() + "()";
|
||||
try {
|
||||
pending.resultJson = GSON.toJson(result);
|
||||
} catch (Exception e) {
|
||||
pending.resultJson = null;
|
||||
}
|
||||
pending.gsonArgsFallback = gsonToFilteredArgsJson(point.getArgs());
|
||||
request.setAttribute(ATTR_PENDING_SYS_API_LOG, pending);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 心跳/埋点:不落 sys_api_log(埋点数据写入 device_tracking_log) */
|
||||
private static boolean shouldSkipSysApiLogForJstInbound(HttpServletRequest request) {
|
||||
String rel = requestPathWithoutContext(request);
|
||||
return "/api/jst/heartBeat".equals(rel) || "/api/jst/tracking".equals(rel);
|
||||
}
|
||||
|
||||
private static String requestPathWithoutContext(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI();
|
||||
if (uri == null) {
|
||||
return "";
|
||||
}
|
||||
String ctx = request.getContextPath() == null ? "" : request.getContextPath();
|
||||
return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
|
||||
}
|
||||
|
||||
private static String gsonToFilteredArgsJson(Object[] args) {
|
||||
List<Object> filteredArgs = new ArrayList<>();
|
||||
if (args != null) {
|
||||
for (Object arg : args) {
|
||||
if (arg != null && !(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse)) {
|
||||
filteredArgs.add(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return GSON.toJson(filteredArgs);
|
||||
}
|
||||
|
||||
public static Charset requestCharsetOrUtf8(HttpServletRequest r) {
|
||||
if (r == null) {
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
String enc = r.getCharacterEncoding();
|
||||
if (StringUtils.isNotBlank(enc)) {
|
||||
try {
|
||||
return Charset.forName(enc);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
}
|
||||
128
src/main/java/io/as/common/aspect/AuditLogAspect.java
Executable file
128
src/main/java/io/as/common/aspect/AuditLogAspect.java
Executable file
@@ -0,0 +1,128 @@
|
||||
package io.as.common.aspect;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.utils.HttpContextUtils;
|
||||
import io.as.common.utils.IPUtils;
|
||||
import io.as.modules.sys.entity.AuditLogEntity;
|
||||
import io.as.modules.sys.entity.SysUserEntity;
|
||||
import io.as.modules.sys.service.AuditLogService;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审计日志切面:将标记了 @AuditAction 的接口调用记录到 audit_log 表
|
||||
* 用于:分配/销售、转合同、绑定、解绑、激活校验、迁移、设备转门店、禁用等
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class AuditLogAspect {
|
||||
|
||||
@Autowired
|
||||
private AuditLogService auditLogService;
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final int PARAM_MAX_LENGTH = 2000;
|
||||
private static final int RESULT_MAX_LENGTH = 500;
|
||||
|
||||
@Pointcut("@annotation(io.as.common.annotation.AuditAction)")
|
||||
public void auditPointCut() {
|
||||
}
|
||||
|
||||
@Around("auditPointCut()")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
AuditAction auditAction = method.getAnnotation(AuditAction.class);
|
||||
if (auditAction == null) {
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
String api = point.getTarget().getClass().getSimpleName() + "#" + method.getName();
|
||||
String paramJson = toParamJson(point.getArgs());
|
||||
String result = "success";
|
||||
try {
|
||||
Object ret = point.proceed();
|
||||
if (ret != null && ret.toString().length() <= RESULT_MAX_LENGTH) {
|
||||
result = ret.toString();
|
||||
}
|
||||
return ret;
|
||||
} catch (Throwable t) {
|
||||
result = "exception: " + (t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName());
|
||||
if (result.length() > RESULT_MAX_LENGTH) {
|
||||
result = result.substring(0, RESULT_MAX_LENGTH);
|
||||
}
|
||||
throw t;
|
||||
} finally {
|
||||
saveAuditLog(auditAction, api, paramJson, result, getRequest());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveAuditLog(AuditAction auditAction, String api, String paramJson, String result, HttpServletRequest request) {
|
||||
try {
|
||||
AuditLogEntity log = new AuditLogEntity();
|
||||
log.setHead(auditAction.description() != null && !auditAction.description().isEmpty()
|
||||
? auditAction.description() : auditAction.value());
|
||||
log.setMethod(auditAction.value());
|
||||
log.setApi(api);
|
||||
log.setParam(paramJson != null && paramJson.length() > PARAM_MAX_LENGTH
|
||||
? paramJson.substring(0, PARAM_MAX_LENGTH) : paramJson);
|
||||
log.setResult(result);
|
||||
|
||||
try {
|
||||
Object principal = SecurityUtils.getSubject().getPrincipal();
|
||||
if (principal instanceof SysUserEntity) {
|
||||
log.setActorUserId(((SysUserEntity) principal).getId());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
if (request != null) {
|
||||
log.setIp(IPUtils.getIpAddr(request));
|
||||
String ua = request.getHeader("User-Agent");
|
||||
if (ua != null && ua.length() > 255) {
|
||||
ua = ua.substring(0, 255);
|
||||
}
|
||||
log.setDeviceInfo(ua);
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
log.setCreatedAt(now);
|
||||
log.setUpdatedAt(now);
|
||||
auditLogService.save(log);
|
||||
} catch (Exception e) {
|
||||
// 审计写库失败不影响主流程
|
||||
org.slf4j.LoggerFactory.getLogger(AuditLogAspect.class).warn("audit_log save fail: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String toParamJson(Object[] args) {
|
||||
if (args == null || args.length == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// 简单序列化;可扩展为脱敏 password 等字段
|
||||
return GSON.toJson(args);
|
||||
} catch (Exception e) {
|
||||
return "[serialize error]";
|
||||
}
|
||||
}
|
||||
|
||||
private HttpServletRequest getRequest() {
|
||||
try {
|
||||
return HttpContextUtils.getHttpServletRequest();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/main/java/io/as/common/aspect/RedisAspect.java
Executable file
38
src/main/java/io/as/common/aspect/RedisAspect.java
Executable file
@@ -0,0 +1,38 @@
|
||||
package io.as.common.aspect;
|
||||
|
||||
import io.as.common.exception.ASException;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Redis切面处理类
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@Aspect
|
||||
@Configuration
|
||||
public class RedisAspect {
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
//是否开启redis缓存 true开启 false关闭
|
||||
@Value("${spring.redis.open: false}")
|
||||
private boolean open;
|
||||
|
||||
@Around("execution(* io.as.common.utils.RedisUtils.*(..))")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
Object result = null;
|
||||
if(open){
|
||||
try{
|
||||
result = point.proceed();
|
||||
}catch (Exception e){
|
||||
logger.error("redis error", e);
|
||||
throw new ASException("Redis服务异常");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
1
src/main/java/io/as/common/aspect/SysLogAspect.java
Executable file
1
src/main/java/io/as/common/aspect/SysLogAspect.java
Executable file
@@ -0,0 +1 @@
|
||||
|
||||
52
src/main/java/io/as/common/exception/ASException.java
Executable file
52
src/main/java/io/as/common/exception/ASException.java
Executable file
@@ -0,0 +1,52 @@
|
||||
package io.as.common.exception;
|
||||
|
||||
/**
|
||||
* 自定义异常
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class ASException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String msg;
|
||||
private int code = 500;
|
||||
|
||||
public ASException(String msg) {
|
||||
super(msg);
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public ASException(String msg, Throwable e) {
|
||||
super(msg, e);
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public ASException(String msg, int code) {
|
||||
super(msg);
|
||||
this.msg = msg;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public ASException(String msg, int code, Throwable e) {
|
||||
super(msg, e);
|
||||
this.msg = msg;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public void setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
}
|
||||
66
src/main/java/io/as/common/exception/ASExceptionHandler.java
Executable file
66
src/main/java/io/as/common/exception/ASExceptionHandler.java
Executable file
@@ -0,0 +1,66 @@
|
||||
package io.as.common.exception;
|
||||
|
||||
import io.as.common.utils.R;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
/**
|
||||
* 异常处理器
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class ASExceptionHandler {
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
/**
|
||||
* 处理自定义异常
|
||||
* 显式标记事务回滚,确保业务异常时数据库操作能正确回滚
|
||||
*/
|
||||
@ExceptionHandler(ASException.class)
|
||||
public R handleASException(ASException e){
|
||||
try {
|
||||
org.springframework.transaction.TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();
|
||||
if (status != null) {
|
||||
status.setRollbackOnly();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 无活跃事务时忽略
|
||||
}
|
||||
R r = new R();
|
||||
r.put("code", e.getCode());
|
||||
r.put("msg", e.getMessage());
|
||||
r.put("data", new java.util.HashMap<>());
|
||||
return r;
|
||||
}
|
||||
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public R handlerNoFoundException(Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
return R.error(404, "路径不存在,请检查路径是否正确");
|
||||
}
|
||||
|
||||
@ExceptionHandler(DuplicateKeyException.class)
|
||||
public R handleDuplicateKeyException(DuplicateKeyException e){
|
||||
logger.error(e.getMessage(), e);
|
||||
return R.error("数据库中已存在该记录");
|
||||
}
|
||||
|
||||
@ExceptionHandler(AuthorizationException.class)
|
||||
public R handleAuthorizationException(AuthorizationException e){
|
||||
logger.error(e.getMessage(), e);
|
||||
return R.error("没有权限,请联系管理员授权");
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public R handleException(Exception e){
|
||||
logger.error(e.getMessage(), e);
|
||||
return R.error();
|
||||
}
|
||||
}
|
||||
65
src/main/java/io/as/common/utils/BeanFindUtils.java
Executable file
65
src/main/java/io/as/common/utils/BeanFindUtils.java
Executable file
@@ -0,0 +1,65 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.core.type.filter.AssignableTypeFilter;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 查询特定的类,该工具下的方法应该在程序启动完成之后才可以使用
|
||||
*/
|
||||
public class BeanFindUtils {
|
||||
|
||||
private static ClassPathScanningCandidateComponentProvider scanner;
|
||||
|
||||
/**
|
||||
* 查询指定了特定注解的类
|
||||
* @param basePackage 类所存放的包名
|
||||
* @param annotation 特定的注解
|
||||
* @return 目标类
|
||||
*/
|
||||
public static Set<Class<?>> findAnnotation(String basePackage, Class<? extends Annotation> annotation) {
|
||||
ClassPathScanningCandidateComponentProvider scanner = getInstance();
|
||||
scanner.addIncludeFilter(new AnnotationTypeFilter(annotation));
|
||||
return getClasses(basePackage, scanner);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询实现了特定接口的类
|
||||
* @param basePackage 类所存放的包名
|
||||
* @param interfaceClass 特定的接口
|
||||
* @return 目标类
|
||||
*/
|
||||
public static Set<Class<?>> findInterface(String basePackage, Class<?> interfaceClass) {
|
||||
ClassPathScanningCandidateComponentProvider scanner = getInstance();
|
||||
scanner.addIncludeFilter(new AssignableTypeFilter(interfaceClass));
|
||||
return getClasses(basePackage, scanner);
|
||||
}
|
||||
|
||||
|
||||
private static Set<Class<?>> getClasses(String basePackage, ClassPathScanningCandidateComponentProvider scanner) {
|
||||
Set<BeanDefinition> beanDefinitions = scanner.findCandidateComponents(basePackage);
|
||||
return beanDefinitions.stream()
|
||||
.map(BeanDefinition::getBeanClassName)
|
||||
.map(className -> {
|
||||
try {
|
||||
return Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static ClassPathScanningCandidateComponentProvider getInstance(){
|
||||
if (scanner == null){
|
||||
scanner = new ClassPathScanningCandidateComponentProvider(false);
|
||||
}
|
||||
return scanner;
|
||||
}
|
||||
|
||||
}
|
||||
18
src/main/java/io/as/common/utils/ConfigConstant.java
Executable file
18
src/main/java/io/as/common/utils/ConfigConstant.java
Executable file
@@ -0,0 +1,18 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
/**
|
||||
* 系统参数相关Key
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class ConfigConstant {
|
||||
/**
|
||||
* 云存储配置KEY
|
||||
*/
|
||||
public final static String CLOUD_STORAGE_CONFIG_KEY = "CLOUD_STORAGE_CONFIG_KEY";
|
||||
|
||||
/**
|
||||
* 橘视厅/聚视厅第三方平台配置KEY(param_value 为 JSON)
|
||||
*/
|
||||
public final static String JST_PLATFORM_CONFIG = "JST_PLATFORM_CONFIG";
|
||||
}
|
||||
146
src/main/java/io/as/common/utils/Constant.java
Executable file
146
src/main/java/io/as/common/utils/Constant.java
Executable file
@@ -0,0 +1,146 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import io.as.common.validator.group.QcloudGroup;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 常量
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class Constant {
|
||||
/**
|
||||
* 超级管理员ID
|
||||
*/
|
||||
public static final int SUPER_ADMIN = 1;
|
||||
|
||||
/**
|
||||
* 超级管理员ID数组
|
||||
*/
|
||||
public static final long[] SUPER_ADMIN_IDS = {1};
|
||||
|
||||
/**
|
||||
* 判断是否为超级管理员(根据 orgId 是否为 null 判断)
|
||||
*/
|
||||
public static boolean isSuperAdmin(Long orgId) {
|
||||
return orgId == null;
|
||||
}
|
||||
/**
|
||||
* 当前页码
|
||||
*/
|
||||
public static final String PAGE = "page";
|
||||
/**
|
||||
* 每页显示记录数
|
||||
*/
|
||||
public static final String LIMIT = "limit";
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
public static final String ORDER_FIELD = "sidx";
|
||||
/**
|
||||
* 排序方式
|
||||
*/
|
||||
public static final String ORDER = "order";
|
||||
/**
|
||||
* 升序
|
||||
*/
|
||||
public static final String ASC = "asc";
|
||||
|
||||
/**
|
||||
* 菜单类型
|
||||
*
|
||||
* @author chenshun
|
||||
* @email sunlightcs@gmail.com
|
||||
* @date 2016年11月15日 下午1:24:29
|
||||
*/
|
||||
public enum MenuType {
|
||||
/**
|
||||
* 目录
|
||||
*/
|
||||
CATALOG(0),
|
||||
/**
|
||||
* 菜单
|
||||
*/
|
||||
MENU(1),
|
||||
/**
|
||||
* 按钮
|
||||
*/
|
||||
BUTTON(2);
|
||||
|
||||
private int value;
|
||||
|
||||
MenuType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务状态
|
||||
*
|
||||
* @author chenshun
|
||||
* @email sunlightcs@gmail.com
|
||||
* @date 2016年12月3日 上午12:07:22
|
||||
*/
|
||||
public enum ScheduleStatus {
|
||||
/**
|
||||
* 正常
|
||||
*/
|
||||
NORMAL(0),
|
||||
/**
|
||||
* 暂停
|
||||
*/
|
||||
PAUSE(1);
|
||||
|
||||
private int value;
|
||||
|
||||
ScheduleStatus(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 云服务商
|
||||
*/
|
||||
public enum CloudService {
|
||||
/**
|
||||
* 腾讯云 COS
|
||||
*/
|
||||
QCLOUD(3, QcloudGroup.class);
|
||||
|
||||
private int value;
|
||||
|
||||
private Class<?> validatorGroupClass;
|
||||
|
||||
CloudService(int value, Class<?> validatorGroupClass) {
|
||||
this.value = value;
|
||||
this.validatorGroupClass = validatorGroupClass;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Class<?> getValidatorGroupClass() {
|
||||
return this.validatorGroupClass;
|
||||
}
|
||||
|
||||
public static CloudService getByValue(Integer value) {
|
||||
Optional<CloudService> first = Stream.of(CloudService.values()).filter(cs -> value.equals(cs.value)).findFirst();
|
||||
if (!first.isPresent()) {
|
||||
throw new IllegalArgumentException("非法的枚举值:" + value);
|
||||
}
|
||||
return first.get();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
70
src/main/java/io/as/common/utils/CustomHeaderCaptureUtils.java
Executable file
70
src/main/java/io/as/common/utils/CustomHeaderCaptureUtils.java
Executable file
@@ -0,0 +1,70 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 采集请求中的自定义 header(X- 开头、sign 等),序列化为 JSON,供接口日志使用。
|
||||
*/
|
||||
public final class CustomHeaderCaptureUtils {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private CustomHeaderCaptureUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxJsonLength 整段 JSON 最大长度,超出截断
|
||||
*/
|
||||
public static String captureAsJson(HttpServletRequest request, int maxJsonLength, int maxValueLength) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Map<String, String> headers = new LinkedHashMap<>();
|
||||
Enumeration<String> names = request.getHeaderNames();
|
||||
if (names == null) {
|
||||
return null;
|
||||
}
|
||||
while (names.hasMoreElements()) {
|
||||
String name = names.nextElement();
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
String n = name.toLowerCase();
|
||||
if ("sign".equals(n) || n.startsWith("x-")) {
|
||||
String value = request.getHeader(name);
|
||||
if (value != null) {
|
||||
headers.put(name, truncate(value, maxValueLength));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (headers.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String json = MAPPER.writeValueAsString(headers);
|
||||
return truncate(json, maxJsonLength);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 与 api_jst_call_log.request_headers 一致的默认上限 */
|
||||
public static String captureAsJson(HttpServletRequest request) {
|
||||
return captureAsJson(request, 2000, 500);
|
||||
}
|
||||
|
||||
private static String truncate(String s, int maxLen) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
if (s.length() <= maxLen) {
|
||||
return s;
|
||||
}
|
||||
return s.substring(0, maxLen) + "...";
|
||||
}
|
||||
}
|
||||
158
src/main/java/io/as/common/utils/DateUtils.java
Executable file
158
src/main/java/io/as/common/utils/DateUtils.java
Executable file
@@ -0,0 +1,158 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.LocalDate;
|
||||
import org.joda.time.format.DateTimeFormat;
|
||||
import org.joda.time.format.DateTimeFormatter;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 日期处理
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class DateUtils {
|
||||
/** 时间格式(yyyy-MM-dd) */
|
||||
public final static String DATE_PATTERN = "yyyy-MM-dd";
|
||||
/** 时间格式(yyyy-MM-dd HH:mm:ss) */
|
||||
public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/**
|
||||
* 日期格式化 日期格式为:yyyy-MM-dd
|
||||
* @param date 日期
|
||||
* @return 返回yyyy-MM-dd格式日期
|
||||
*/
|
||||
public static String format(Date date) {
|
||||
return format(date, DATE_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期格式化 日期格式为:yyyy-MM-dd
|
||||
* @param date 日期
|
||||
* @param pattern 格式,如:DateUtils.DATE_TIME_PATTERN
|
||||
* @return 返回yyyy-MM-dd格式日期
|
||||
*/
|
||||
public static String format(Date date, String pattern) {
|
||||
if(date != null){
|
||||
SimpleDateFormat df = new SimpleDateFormat(pattern);
|
||||
return df.format(date);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串转换成日期
|
||||
* @param strDate 日期字符串
|
||||
* @param pattern 日期的格式,如:DateUtils.DATE_TIME_PATTERN
|
||||
*/
|
||||
public static Date stringToDate(String strDate, String pattern) {
|
||||
if (StringUtils.isBlank(strDate)){
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTimeFormatter fmt = DateTimeFormat.forPattern(pattern);
|
||||
return fmt.parseLocalDateTime(strDate).toDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据周数,获取开始日期、结束日期
|
||||
* @param week 周期 0本周,-1上周,-2上上周,1下周,2下下周
|
||||
* @return 返回date[0]开始日期、date[1]结束日期
|
||||
*/
|
||||
public static Date[] getWeekStartAndEnd(int week) {
|
||||
DateTime dateTime = new DateTime();
|
||||
LocalDate date = new LocalDate(dateTime.plusWeeks(week));
|
||||
|
||||
date = date.dayOfWeek().withMinimumValue();
|
||||
Date beginDate = date.toDate();
|
||||
Date endDate = date.plusDays(6).toDate();
|
||||
return new Date[]{beginDate, endDate};
|
||||
}
|
||||
|
||||
/**
|
||||
* 对日期的【秒】进行加/减
|
||||
*
|
||||
* @param date 日期
|
||||
* @param seconds 秒数,负数为减
|
||||
* @return 加/减几秒后的日期
|
||||
*/
|
||||
public static Date addDateSeconds(Date date, int seconds) {
|
||||
DateTime dateTime = new DateTime(date);
|
||||
return dateTime.plusSeconds(seconds).toDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对日期的【分钟】进行加/减
|
||||
*
|
||||
* @param date 日期
|
||||
* @param minutes 分钟数,负数为减
|
||||
* @return 加/减几分钟后的日期
|
||||
*/
|
||||
public static Date addDateMinutes(Date date, int minutes) {
|
||||
DateTime dateTime = new DateTime(date);
|
||||
return dateTime.plusMinutes(minutes).toDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对日期的【小时】进行加/减
|
||||
*
|
||||
* @param date 日期
|
||||
* @param hours 小时数,负数为减
|
||||
* @return 加/减几小时后的日期
|
||||
*/
|
||||
public static Date addDateHours(Date date, int hours) {
|
||||
DateTime dateTime = new DateTime(date);
|
||||
return dateTime.plusHours(hours).toDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对日期的【天】进行加/减
|
||||
*
|
||||
* @param date 日期
|
||||
* @param days 天数,负数为减
|
||||
* @return 加/减几天后的日期
|
||||
*/
|
||||
public static Date addDateDays(Date date, int days) {
|
||||
DateTime dateTime = new DateTime(date);
|
||||
return dateTime.plusDays(days).toDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对日期的【周】进行加/减
|
||||
*
|
||||
* @param date 日期
|
||||
* @param weeks 周数,负数为减
|
||||
* @return 加/减几周后的日期
|
||||
*/
|
||||
public static Date addDateWeeks(Date date, int weeks) {
|
||||
DateTime dateTime = new DateTime(date);
|
||||
return dateTime.plusWeeks(weeks).toDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对日期的【月】进行加/减
|
||||
*
|
||||
* @param date 日期
|
||||
* @param months 月数,负数为减
|
||||
* @return 加/减几月后的日期
|
||||
*/
|
||||
public static Date addDateMonths(Date date, int months) {
|
||||
DateTime dateTime = new DateTime(date);
|
||||
return dateTime.plusMonths(months).toDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对日期的【年】进行加/减
|
||||
*
|
||||
* @param date 日期
|
||||
* @param years 年数,负数为减
|
||||
* @return 加/减几年后的日期
|
||||
*/
|
||||
public static Date addDateYears(Date date, int years) {
|
||||
DateTime dateTime = new DateTime(date);
|
||||
return dateTime.plusYears(years).toDate();
|
||||
}
|
||||
}
|
||||
24
src/main/java/io/as/common/utils/HttpContextUtils.java
Executable file
24
src/main/java/io/as/common/utils/HttpContextUtils.java
Executable file
@@ -0,0 +1,24 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class HttpContextUtils {
|
||||
|
||||
public static HttpServletRequest getHttpServletRequest() {
|
||||
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
}
|
||||
|
||||
public static String getDomain(){
|
||||
HttpServletRequest request = getHttpServletRequest();
|
||||
StringBuffer url = request.getRequestURL();
|
||||
return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
|
||||
}
|
||||
|
||||
public static String getOrigin(){
|
||||
HttpServletRequest request = getHttpServletRequest();
|
||||
return request.getHeader("Origin");
|
||||
}
|
||||
}
|
||||
56
src/main/java/io/as/common/utils/IPUtils.java
Executable file
56
src/main/java/io/as/common/utils/IPUtils.java
Executable file
@@ -0,0 +1,56 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import com.alibaba.druid.util.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* IP地址
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class IPUtils {
|
||||
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
|
||||
|
||||
/**
|
||||
* 获取IP地址
|
||||
*
|
||||
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
|
||||
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
|
||||
*/
|
||||
public static String getIpAddr(HttpServletRequest request) {
|
||||
String ip = null;
|
||||
try {
|
||||
ip = request.getHeader("x-forwarded-for");
|
||||
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
}
|
||||
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("IPUtils ERROR ", e);
|
||||
}
|
||||
|
||||
// //使用代理,则获取第一个IP地址
|
||||
// if(StringUtils.isEmpty(ip) && ip.length() > 15) {
|
||||
// if(ip.indexOf(",") > 0) {
|
||||
// ip = ip.substring(0, ip.indexOf(","));
|
||||
// }
|
||||
// }
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
}
|
||||
151
src/main/java/io/as/common/utils/MacAddressUtils.java
Executable file
151
src/main/java/io/as/common/utils/MacAddressUtils.java
Executable file
@@ -0,0 +1,151 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
/**
|
||||
* MAC 地址工具类
|
||||
* 用于校验 MAC 地址的合法性
|
||||
*/
|
||||
public class MacAddressUtils {
|
||||
|
||||
/**
|
||||
* 校验 MAC 地址是否合法(单播地址,可作为源地址)
|
||||
*
|
||||
* 核心规则:
|
||||
* 1. 第一字节的最低位(I/G位)必须是 0 - 代表单播地址
|
||||
* - 0:单播(Individual),可作为源地址
|
||||
* - 1:组播(Group),只能作为目标地址,不能作为源地址
|
||||
* 2. 不能是全0地址(00:00:00:00:00:00)
|
||||
* 3. 不能是全F地址(FF:FF:FF:FF:FF:FF,广播地址)
|
||||
*
|
||||
* @param mac MAC 地址字符串(支持格式:AA:BB:CC:DD:EE:FF 或 AA-BB-CC-DD-EE-FF 或 AABBCCDDEEFF)
|
||||
* @return true-合法的单播MAC地址,false-非法
|
||||
*/
|
||||
public static boolean isValidUnicastMac(String mac) {
|
||||
if (StringUtils.isBlank(mac)) {
|
||||
return true; // 空值不校验,由调用方决定
|
||||
}
|
||||
|
||||
// 标准化 MAC 地址(去除分隔符,转为大写)
|
||||
String normalizedMac = normalizeMac(mac);
|
||||
if (normalizedMac == null || normalizedMac.length() != 12) {
|
||||
return false; // 格式错误
|
||||
}
|
||||
|
||||
// 检查是否全0
|
||||
if ("000000000000".equals(normalizedMac)) {
|
||||
return false; // 全0地址无效
|
||||
}
|
||||
|
||||
// 检查是否全F(广播地址)
|
||||
if ("FFFFFFFFFFFF".equalsIgnoreCase(normalizedMac)) {
|
||||
return false; // 广播地址无效
|
||||
}
|
||||
|
||||
// 获取第一字节
|
||||
int firstByte;
|
||||
try {
|
||||
firstByte = Integer.parseInt(normalizedMac.substring(0, 2), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
return false; // 解析错误
|
||||
}
|
||||
|
||||
// 检查 I/G 位(最低位):必须为0(单播)
|
||||
// 第一字节的二进制:xxxx xxx0
|
||||
if ((firstByte & 0x01) != 0) {
|
||||
return false; // I/G位为1,是组播地址,不能作为源地址
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MAC 地址类型描述
|
||||
*
|
||||
* @param mac MAC 地址字符串
|
||||
* @return 地址类型描述
|
||||
*/
|
||||
public static String getMacAddressType(String mac) {
|
||||
if (StringUtils.isBlank(mac)) {
|
||||
return "空地址";
|
||||
}
|
||||
|
||||
String normalizedMac = normalizeMac(mac);
|
||||
if (normalizedMac == null || normalizedMac.length() != 12) {
|
||||
return "格式错误";
|
||||
}
|
||||
|
||||
// 检查是否全0
|
||||
if ("000000000000".equals(normalizedMac)) {
|
||||
return "全0地址(无效)";
|
||||
}
|
||||
|
||||
// 检查是否全F(广播地址)
|
||||
if ("FFFFFFFFFFFF".equalsIgnoreCase(normalizedMac)) {
|
||||
return "广播地址(无效)";
|
||||
}
|
||||
|
||||
int firstByte;
|
||||
try {
|
||||
firstByte = Integer.parseInt(normalizedMac.substring(0, 2), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
return "格式错误";
|
||||
}
|
||||
|
||||
boolean isMulticast = (firstByte & 0x01) != 0;
|
||||
boolean isLocalAdmin = (firstByte & 0x02) != 0;
|
||||
|
||||
if (isMulticast) {
|
||||
return "组播地址(只能作为目标地址)";
|
||||
} else if (isLocalAdmin) {
|
||||
return "本地管理单播地址(LAA)";
|
||||
} else {
|
||||
return "全球唯一单播地址(UAA)";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化 MAC 地址
|
||||
* 去除分隔符(: - .),转为大写,保留12位十六进制字符
|
||||
*
|
||||
* @param mac 原始 MAC 地址
|
||||
* @return 标准化后的 MAC 地址(12位大写十六进制字符串),如果格式错误返回 null
|
||||
*/
|
||||
public static String normalizeMac(String mac) {
|
||||
if (StringUtils.isBlank(mac)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 去除所有分隔符和空白
|
||||
String cleaned = mac.replaceAll("[:.\\s-]", "").toUpperCase();
|
||||
|
||||
// 验证是否为有效的十六进制字符串
|
||||
if (!cleaned.matches("^[0-9A-F]{12}$")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 MAC 地址为冒号分隔格式
|
||||
*
|
||||
* @param mac 原始 MAC 地址
|
||||
* @return AA:BB:CC:DD:EE:FF 格式的 MAC 地址
|
||||
*/
|
||||
public static String formatMac(String mac) {
|
||||
String normalized = normalizeMac(mac);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < 12; i += 2) {
|
||||
if (i > 0) {
|
||||
sb.append(':');
|
||||
}
|
||||
sb.append(normalized.substring(i, i + 2));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
18
src/main/java/io/as/common/utils/MapUtils.java
Executable file
18
src/main/java/io/as/common/utils/MapUtils.java
Executable file
@@ -0,0 +1,18 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
|
||||
/**
|
||||
* Map工具类
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class MapUtils extends HashMap<String, Object> {
|
||||
|
||||
@Override
|
||||
public MapUtils put(String key, Object value) {
|
||||
super.put(key, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
102
src/main/java/io/as/common/utils/PageUtils.java
Executable file
102
src/main/java/io/as/common/utils/PageUtils.java
Executable file
@@ -0,0 +1,102 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页工具类
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class PageUtils implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
private int totalCount;
|
||||
/**
|
||||
* 每页记录数
|
||||
*/
|
||||
private int pageSize;
|
||||
/**
|
||||
* 总页数
|
||||
*/
|
||||
private int totalPage;
|
||||
/**
|
||||
* 当前页数
|
||||
*/
|
||||
private int currPage;
|
||||
/**
|
||||
* 列表数据
|
||||
*/
|
||||
private List<?> list;
|
||||
|
||||
/**
|
||||
* 分页
|
||||
* @param list 列表数据
|
||||
* @param totalCount 总记录数
|
||||
* @param pageSize 每页记录数
|
||||
* @param currPage 当前页数
|
||||
*/
|
||||
public PageUtils(List<?> list, int totalCount, int pageSize, int currPage) {
|
||||
this.list = list;
|
||||
this.totalCount = totalCount;
|
||||
this.pageSize = pageSize;
|
||||
this.currPage = currPage;
|
||||
this.totalPage = (int)Math.ceil((double)totalCount/pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
public PageUtils(IPage<?> page) {
|
||||
this.list = page.getRecords();
|
||||
this.totalCount = (int)page.getTotal();
|
||||
this.pageSize = (int)page.getSize();
|
||||
this.currPage = (int)page.getCurrent();
|
||||
this.totalPage = (int)page.getPages();
|
||||
}
|
||||
|
||||
public int getTotalCount() {
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
public void setTotalCount(int totalCount) {
|
||||
this.totalCount = totalCount;
|
||||
}
|
||||
|
||||
public int getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public void setPageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public int getTotalPage() {
|
||||
return totalPage;
|
||||
}
|
||||
|
||||
public void setTotalPage(int totalPage) {
|
||||
this.totalPage = totalPage;
|
||||
}
|
||||
|
||||
public int getCurrPage() {
|
||||
return currPage;
|
||||
}
|
||||
|
||||
public void setCurrPage(int currPage) {
|
||||
this.currPage = currPage;
|
||||
}
|
||||
|
||||
public List<?> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setList(List<?> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
}
|
||||
69
src/main/java/io/as/common/utils/Query.java
Executable file
69
src/main/java/io/as/common/utils/Query.java
Executable file
@@ -0,0 +1,69 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.as.common.xss.SQLFilter;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 查询参数
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class Query<T> {
|
||||
|
||||
public IPage<T> getPage(Map<String, Object> params) {
|
||||
return this.getPage(params, null, false);
|
||||
}
|
||||
|
||||
public IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
|
||||
//分页参数
|
||||
long curPage = 1;
|
||||
long limit = 10;
|
||||
|
||||
if(params.get(Constant.PAGE) != null){
|
||||
curPage = Long.parseLong((String)params.get(Constant.PAGE));
|
||||
}
|
||||
if(params.get(Constant.LIMIT) != null){
|
||||
limit = Long.parseLong((String)params.get(Constant.LIMIT));
|
||||
}
|
||||
|
||||
//分页对象
|
||||
Page<T> page = new Page<>(curPage, limit);
|
||||
|
||||
//分页参数
|
||||
params.put(Constant.PAGE, page);
|
||||
|
||||
//排序字段
|
||||
//防止SQL注入(因为sidx、order是通过拼接SQL实现排序的,会有SQL注入风险)
|
||||
String orderField = SQLFilter.sqlInject((String)params.get(Constant.ORDER_FIELD));
|
||||
String order = (String)params.get(Constant.ORDER);
|
||||
|
||||
|
||||
//前端字段排序
|
||||
if(StringUtils.isNotEmpty(orderField) && StringUtils.isNotEmpty(order)){
|
||||
if(Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.asc(orderField));
|
||||
}else {
|
||||
return page.addOrder(OrderItem.desc(orderField));
|
||||
}
|
||||
}
|
||||
|
||||
//没有排序字段,则不排序
|
||||
if(StringUtils.isBlank(defaultOrderField)){
|
||||
return page;
|
||||
}
|
||||
|
||||
//默认排序
|
||||
if(isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
}else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
}
|
||||
99
src/main/java/io/as/common/utils/R.java
Executable file
99
src/main/java/io/as/common/utils/R.java
Executable file
@@ -0,0 +1,99 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 返回数据
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class R extends HashMap<String, Object> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public R() {
|
||||
put("code", 0);
|
||||
put("msg", "success");
|
||||
}
|
||||
|
||||
public static R error() {
|
||||
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
|
||||
}
|
||||
|
||||
public static R error(String msg) {
|
||||
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
|
||||
}
|
||||
|
||||
public static R error(int code, String msg) {
|
||||
R r = new R();
|
||||
r.put("code", code);
|
||||
r.put("msg", msg);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static R ok(String msg) {
|
||||
R r = new R();
|
||||
r.put("msg", msg);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static R ok(Map<String, Object> map) {
|
||||
R r = new R();
|
||||
r.putAll(map);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static R ok() {
|
||||
return new R();
|
||||
}
|
||||
|
||||
/**
|
||||
* App 等统一响应:成功时 {@code { "code": 200, "msg": "", "data": {...} }}
|
||||
*/
|
||||
public static R dataOk() {
|
||||
return dataOk(new HashMap<String, Object>());
|
||||
}
|
||||
|
||||
/**
|
||||
* App 等统一响应:成功时 payload 放在 {@code data} 内。
|
||||
*/
|
||||
public static R dataOk(Map<String, Object> data) {
|
||||
R r = new R();
|
||||
r.clear();
|
||||
r.put("code", 200);
|
||||
r.put("msg", "");
|
||||
r.put("data", data != null ? data : new HashMap<String, Object>());
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一响应(code 为 0):成功时 {@code { "code": 0, "msg": "", "data": {...} }}
|
||||
*/
|
||||
public static R dataSuccess(Map<String, Object> data) {
|
||||
R r = new R();
|
||||
r.clear();
|
||||
r.put("code", 0);
|
||||
r.put("msg", "");
|
||||
r.put("data", data != null ? data : new HashMap<String, Object>());
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* App 等统一响应:失败时 {@code { "code": code, "msg": msg, "data": {} }}
|
||||
*/
|
||||
public static R dataError(int code, String msg) {
|
||||
R r = new R();
|
||||
r.clear();
|
||||
r.put("code", code);
|
||||
r.put("msg", msg != null ? msg : "");
|
||||
r.put("data", new HashMap<String, Object>());
|
||||
return r;
|
||||
}
|
||||
|
||||
public R put(String key, Object value) {
|
||||
super.put(key, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
13
src/main/java/io/as/common/utils/RedisKeys.java
Executable file
13
src/main/java/io/as/common/utils/RedisKeys.java
Executable file
@@ -0,0 +1,13 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
/**
|
||||
* Redis所有Keys
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class RedisKeys {
|
||||
|
||||
public static String getSysConfigKey(String key){
|
||||
return "sys:config:" + key;
|
||||
}
|
||||
}
|
||||
112
src/main/java/io/as/common/utils/RedisUtils.java
Executable file
112
src/main/java/io/as/common/utils/RedisUtils.java
Executable file
@@ -0,0 +1,112 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis工具类
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@Component
|
||||
public class RedisUtils {
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@Autowired
|
||||
private ValueOperations<String, String> valueOperations;
|
||||
@Autowired
|
||||
private HashOperations<String, String, Object> hashOperations;
|
||||
@Autowired
|
||||
private ListOperations<String, Object> listOperations;
|
||||
@Autowired
|
||||
private SetOperations<String, Object> setOperations;
|
||||
@Autowired
|
||||
private ZSetOperations<String, Object> zSetOperations;
|
||||
/** 默认过期时长,单位:秒 */
|
||||
public final static long DEFAULT_EXPIRE = 60 * 60 * 24;
|
||||
/** 不设置过期时长 */
|
||||
public final static long NOT_EXPIRE = -1;
|
||||
private final static Gson gson = new Gson();
|
||||
|
||||
public void set(String key, Object value, long expire){
|
||||
valueOperations.set(key, toJson(value));
|
||||
if(expire != NOT_EXPIRE){
|
||||
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public void set(String key, Object value){
|
||||
set(key, value, DEFAULT_EXPIRE);
|
||||
}
|
||||
|
||||
public <T> T get(String key, Class<T> clazz, long expire) {
|
||||
String value = valueOperations.get(key);
|
||||
if(expire != NOT_EXPIRE){
|
||||
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
|
||||
}
|
||||
return value == null ? null : fromJson(value, clazz);
|
||||
}
|
||||
|
||||
public <T> T get(String key, Class<T> clazz) {
|
||||
return get(key, clazz, NOT_EXPIRE);
|
||||
}
|
||||
|
||||
public String get(String key, long expire) {
|
||||
String value = valueOperations.get(key);
|
||||
if(expire != NOT_EXPIRE){
|
||||
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public String get(String key) {
|
||||
return get(key, NOT_EXPIRE);
|
||||
}
|
||||
|
||||
/** SETNX:仅当key不存在时设置值,同时设置过期时间(秒),返回是否设置成功 */
|
||||
public boolean setIfAbsent(String key, String value, long expire) {
|
||||
Boolean result = valueOperations.setIfAbsent(key, value, expire, TimeUnit.SECONDS);
|
||||
return result != null && result;
|
||||
}
|
||||
|
||||
public void delete(String key) {
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* SETNX:仅当 key 不存在时设置值,并设置过期时间
|
||||
*
|
||||
* @param key Redis key
|
||||
* @param value 值
|
||||
* @param expire 过期时间(秒)
|
||||
* @return true=设置成功(key 原先不存在),false=key 已存在
|
||||
*/
|
||||
public boolean setIfAbsent(String key, Object value, long expire) {
|
||||
if (expire == NOT_EXPIRE) {
|
||||
return valueOperations.setIfAbsent(key, toJson(value));
|
||||
}
|
||||
return valueOperations.setIfAbsent(key, toJson(value), expire, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Object转成JSON数据
|
||||
*/
|
||||
private String toJson(Object object){
|
||||
if(object instanceof Integer || object instanceof Long || object instanceof Float ||
|
||||
object instanceof Double || object instanceof Boolean || object instanceof String){
|
||||
return String.valueOf(object);
|
||||
}
|
||||
return gson.toJson(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON数据,转成Object
|
||||
*/
|
||||
private <T> T fromJson(String json, Class<T> clazz){
|
||||
return gson.fromJson(json, clazz);
|
||||
}
|
||||
}
|
||||
160
src/main/java/io/as/common/utils/RsaUtil.java
Executable file
160
src/main/java/io/as/common/utils/RsaUtil.java
Executable file
@@ -0,0 +1,160 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import java.security.*;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 纯 JDK 实现的 RSA 工具类
|
||||
* 不依赖任何第三方库
|
||||
*/
|
||||
public class RsaUtil {
|
||||
|
||||
// 加密算法
|
||||
private static final String CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding";
|
||||
// 密钥算法
|
||||
private static final String KEY_ALGORITHM = "RSA";
|
||||
|
||||
/**
|
||||
* 免登录接口加密demo,并发送 POST 请求
|
||||
* @param args
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
// 生成 timestamp 和 nonce,需与请求 URL 参数保持一致
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
HashMap<String, String> signMap = new HashMap<>();
|
||||
signMap.put("accesser", "ASJDTEST");
|
||||
signMap.put("businessLine", "4100450011967708592750001");
|
||||
signMap.put("uid", "device_77");
|
||||
signMap.put("mac", "device_77");
|
||||
signMap.put("nonce", nonce);
|
||||
signMap.put("timestamp", timestamp);
|
||||
signMap.put("serviceComboCode", "1100121010200492753300170");
|
||||
|
||||
Gson gson = new Gson();
|
||||
String plainData = gson.toJson(signMap);
|
||||
System.out.println("待加密字符串为:" + plainData);
|
||||
|
||||
String publicKeyStr = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnemyIjwf1S5PsqgPhwiPGcF2Efa4UvFiFpg34+d2ZHQ6mkTTaussXCWQQRZGUfGbvkhIJOspGxmxtaMYgNxMQv9BT5B6xcnMILqYzG40hc2N3EiLm4hDYP12f8pshikh5aNxolbgkdA+Y2vrMf2KNucArMh8gT1mUuQRyUMiefy+N/kk2nK2p6SSX0628/BtczNgj13MaMHMlr8krNTlzB/22M8WMSk10nFS4ULp1KOPia8VxqzMIFKeREPIHIUnarz+llFIj6zKGg0ocqS6KEml7jv1yCgBpmyA2kAGEgnuTreO/5h5DqXByKLBEW8bYDiMUb7H/58IRerrITXfVQIDAQAB";
|
||||
String encryptData = encrypt(publicKeyStr, plainData);
|
||||
System.out.println("加密后的encryptData为:" + encryptData);
|
||||
|
||||
String url = "https://external.future.jetsentv.com/ums_api/api/v1/third/login"
|
||||
+ "?timestamp=" + timestamp
|
||||
+ "&nonce=" + nonce
|
||||
+ "&serviceComboCode=1100121010200492753300170";
|
||||
|
||||
HttpResponse response = HttpRequest.post(url)
|
||||
.header("tenant", "huashitv")
|
||||
.header("encryptData", encryptData)
|
||||
.header("Cookie", "locale=en-us")
|
||||
.body("") // 空请求体,与 curl --data '' 一致
|
||||
.timeout(10000)
|
||||
.execute();
|
||||
|
||||
System.out.println("响应状态码:" + response.getStatus());
|
||||
System.out.println("响应内容:" + response.body());
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用公钥加密字符串
|
||||
*
|
||||
* @param key 公钥
|
||||
* @param plainText 明文字符串
|
||||
* @return 加密后的Base64编码字符串
|
||||
*/
|
||||
public static String encrypt(PublicKey key, String plainText) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
// 将字符串转换为字节数组
|
||||
byte[] plainTextBytes = plainText.getBytes("UTF-8");
|
||||
|
||||
// 分段加密(RSA有加密数据长度限制)
|
||||
int inputLen = plainTextBytes.length;
|
||||
int maxEncryptBlock = 245; // 2048位密钥的最大加密块大小
|
||||
|
||||
byte[] encryptedData = new byte[0];
|
||||
int offset = 0;
|
||||
|
||||
while (inputLen - offset > 0) {
|
||||
if (inputLen - offset > maxEncryptBlock) {
|
||||
byte[] encryptedBlock = cipher.doFinal(plainTextBytes, offset, maxEncryptBlock);
|
||||
encryptedData = concatenateByteArrays(encryptedData, encryptedBlock);
|
||||
offset += maxEncryptBlock;
|
||||
} else {
|
||||
byte[] encryptedBlock = cipher.doFinal(plainTextBytes, offset, inputLen - offset);
|
||||
encryptedData = concatenateByteArrays(encryptedData, encryptedBlock);
|
||||
offset = inputLen;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回Base64编码的加密数据
|
||||
return Base64.getEncoder().encodeToString(encryptedData);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException
|
||||
| InvalidKeyException | IllegalBlockSizeException
|
||||
| BadPaddingException | java.io.UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Base64编码的公钥字符串加密
|
||||
*
|
||||
* @param publicKeyStr Base64编码的公钥字符串
|
||||
* @param plainText 明文字符串
|
||||
* @return 加密后的Base64编码字符串
|
||||
*/
|
||||
public static String encrypt(String publicKeyStr, String plainText) {
|
||||
try {
|
||||
PublicKey publicKey = getPublicKeyFromString(publicKeyStr);
|
||||
return encrypt(publicKey, plainText);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Base64编码的字符串获取公钥
|
||||
*
|
||||
* @param publicKeyStr Base64编码的公钥字符串
|
||||
* @return PublicKey对象
|
||||
*/
|
||||
public static PublicKey getPublicKeyFromString(String publicKeyStr) {
|
||||
try {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(publicKeyStr);
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
|
||||
return keyFactory.generatePublic(keySpec);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("获取公钥失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并字节数组
|
||||
*
|
||||
* @param a 第一个字节数组
|
||||
* @param b 第二个字节数组
|
||||
* @return 合并后的字节数组
|
||||
*/
|
||||
private static byte[] concatenateByteArrays(byte[] a, byte[] b) {
|
||||
byte[] result = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, result, 0, a.length);
|
||||
System.arraycopy(b, 0, result, a.length, b.length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
53
src/main/java/io/as/common/utils/ShiroUtils.java
Executable file
53
src/main/java/io/as/common/utils/ShiroUtils.java
Executable file
@@ -0,0 +1,53 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.modules.sys.entity.SysUserEntity;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.session.Session;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
|
||||
/**
|
||||
* Shiro工具类
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class ShiroUtils {
|
||||
|
||||
public static Session getSession() {
|
||||
return SecurityUtils.getSubject().getSession();
|
||||
}
|
||||
|
||||
public static Subject getSubject() {
|
||||
return SecurityUtils.getSubject();
|
||||
}
|
||||
|
||||
public static SysUserEntity getUserEntity() {
|
||||
return (SysUserEntity)SecurityUtils.getSubject().getPrincipal();
|
||||
}
|
||||
|
||||
public static Long getUserId() {
|
||||
return getUserEntity().getId();
|
||||
}
|
||||
|
||||
public static void setSessionAttribute(Object key, Object value) {
|
||||
getSession().setAttribute(key, value);
|
||||
}
|
||||
|
||||
public static Object getSessionAttribute(Object key) {
|
||||
return getSession().getAttribute(key);
|
||||
}
|
||||
|
||||
public static boolean isLogin() {
|
||||
return SecurityUtils.getSubject().getPrincipal() != null;
|
||||
}
|
||||
|
||||
public static String getKaptcha(String key) {
|
||||
Object kaptcha = getSessionAttribute(key);
|
||||
if(kaptcha == null){
|
||||
throw new ASException("验证码已失效");
|
||||
}
|
||||
getSession().removeAttribute(key);
|
||||
return kaptcha.toString();
|
||||
}
|
||||
|
||||
}
|
||||
47
src/main/java/io/as/common/utils/SpringContextUtils.java
Executable file
47
src/main/java/io/as/common/utils/SpringContextUtils.java
Executable file
@@ -0,0 +1,47 @@
|
||||
package io.as.common.utils;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Spring Context 工具类
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@Component
|
||||
public class SpringContextUtils implements ApplicationContextAware {
|
||||
public static ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext)
|
||||
throws BeansException {
|
||||
SpringContextUtils.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public static Object getBean(String name) {
|
||||
return applicationContext.getBean(name);
|
||||
}
|
||||
|
||||
public static <T> T getBean(String name, Class<T> requiredType) {
|
||||
return applicationContext.getBean(name, requiredType);
|
||||
}
|
||||
|
||||
public static <T> T getBean(Class<T> requiredType) {
|
||||
return applicationContext.getBean(requiredType);
|
||||
}
|
||||
|
||||
public static boolean containsBean(String name) {
|
||||
return applicationContext.containsBean(name);
|
||||
}
|
||||
|
||||
public static boolean isSingleton(String name) {
|
||||
return applicationContext.isSingleton(name);
|
||||
}
|
||||
|
||||
public static Class<? extends Object> getType(String name) {
|
||||
return applicationContext.getType(name);
|
||||
}
|
||||
|
||||
}
|
||||
24
src/main/java/io/as/common/validator/Assert.java
Executable file
24
src/main/java/io/as/common/validator/Assert.java
Executable file
@@ -0,0 +1,24 @@
|
||||
package io.as.common.validator;
|
||||
|
||||
import io.as.common.exception.ASException;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
/**
|
||||
* 数据校验
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public abstract class Assert {
|
||||
|
||||
public static void isBlank(String str, String message) {
|
||||
if (StringUtils.isBlank(str)) {
|
||||
throw new ASException(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void isNull(Object object, String message) {
|
||||
if (object == null) {
|
||||
throw new ASException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/main/java/io/as/common/validator/ValidatorUtils.java
Executable file
46
src/main/java/io/as/common/validator/ValidatorUtils.java
Executable file
@@ -0,0 +1,46 @@
|
||||
package io.as.common.validator;
|
||||
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.Constant;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.Validator;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* hibernate-validator校验工具类
|
||||
*
|
||||
* 参考文档:http://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class ValidatorUtils {
|
||||
private static Validator validator;
|
||||
|
||||
static {
|
||||
validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验对象
|
||||
* @param object 待校验对象
|
||||
* @param groups 待校验的组
|
||||
* @throws ASException 校验不通过,则报ASException异常
|
||||
*/
|
||||
public static void validateEntity(Object object, Class<?>... groups)
|
||||
throws ASException {
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
|
||||
if (!constraintViolations.isEmpty()) {
|
||||
StringBuilder msg = new StringBuilder();
|
||||
for (ConstraintViolation<Object> constraint : constraintViolations) {
|
||||
msg.append(constraint.getMessage()).append("<br>");
|
||||
}
|
||||
throw new ASException(msg.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateEntity(Object object, Constant.CloudService type) {
|
||||
validateEntity(object, type.getValidatorGroupClass());
|
||||
}
|
||||
}
|
||||
9
src/main/java/io/as/common/validator/group/AddGroup.java
Executable file
9
src/main/java/io/as/common/validator/group/AddGroup.java
Executable file
@@ -0,0 +1,9 @@
|
||||
package io.as.common.validator.group;
|
||||
|
||||
/**
|
||||
* 新增数据 Group
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public interface AddGroup {
|
||||
}
|
||||
13
src/main/java/io/as/common/validator/group/Group.java
Executable file
13
src/main/java/io/as/common/validator/group/Group.java
Executable file
@@ -0,0 +1,13 @@
|
||||
package io.as.common.validator.group;
|
||||
|
||||
import javax.validation.GroupSequence;
|
||||
|
||||
/**
|
||||
* 定义校验顺序,如果AddGroup组失败,则UpdateGroup组不会再校验
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@GroupSequence({AddGroup.class, UpdateGroup.class})
|
||||
public interface Group {
|
||||
|
||||
}
|
||||
9
src/main/java/io/as/common/validator/group/QcloudGroup.java
Executable file
9
src/main/java/io/as/common/validator/group/QcloudGroup.java
Executable file
@@ -0,0 +1,9 @@
|
||||
package io.as.common.validator.group;
|
||||
|
||||
/**
|
||||
* 腾讯云
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public interface QcloudGroup {
|
||||
}
|
||||
11
src/main/java/io/as/common/validator/group/UpdateGroup.java
Executable file
11
src/main/java/io/as/common/validator/group/UpdateGroup.java
Executable file
@@ -0,0 +1,11 @@
|
||||
package io.as.common.validator.group;
|
||||
|
||||
/**
|
||||
* 更新数据 Group
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
|
||||
public interface UpdateGroup {
|
||||
|
||||
}
|
||||
530
src/main/java/io/as/common/xss/HTMLFilter.java
Executable file
530
src/main/java/io/as/common/xss/HTMLFilter.java
Executable file
@@ -0,0 +1,530 @@
|
||||
package io.as.common.xss;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
*
|
||||
* HTML filtering utility for protecting against XSS (Cross Site Scripting).
|
||||
*
|
||||
* This code is licensed LGPLv3
|
||||
*
|
||||
* This code is a Java port of the original work in PHP by Cal Hendersen.
|
||||
* http://code.iamcal.com/php/lib_filter/
|
||||
*
|
||||
* The trickiest part of the translation was handling the differences in regex handling
|
||||
* between PHP and Java. These resources were helpful in the process:
|
||||
*
|
||||
* http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html
|
||||
* http://us2.php.net/manual/en/reference.pcre.pattern.modifiers.php
|
||||
* http://www.regular-expressions.info/modifiers.html
|
||||
*
|
||||
* A note on naming conventions: instance variables are prefixed with a "v"; global
|
||||
* constants are in all caps.
|
||||
*
|
||||
* Sample use:
|
||||
* String input = ...
|
||||
* String clean = new HTMLFilter().filter( input );
|
||||
*
|
||||
* The class is not thread safe. Create a new instance if in doubt.
|
||||
*
|
||||
* If you find bugs or have suggestions on improvement (especially regarding
|
||||
* performance), please contact us. The latest version of this
|
||||
* source, and our contact details, can be found at http://xss-html-filter.sf.net
|
||||
*
|
||||
* @author Joseph O'Connell
|
||||
* @author Cal Hendersen
|
||||
* @author Michael Semb Wever
|
||||
*/
|
||||
public final class HTMLFilter {
|
||||
|
||||
/** regex flag union representing /si modifiers in php **/
|
||||
private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
|
||||
private static final Pattern P_COMMENTS = Pattern.compile("<!--(.*?)-->", Pattern.DOTALL);
|
||||
private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI);
|
||||
private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL);
|
||||
private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI);
|
||||
private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI);
|
||||
private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI);
|
||||
private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI);
|
||||
private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI);
|
||||
private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?");
|
||||
private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?");
|
||||
private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?");
|
||||
private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))");
|
||||
private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL);
|
||||
private static final Pattern P_END_ARROW = Pattern.compile("^>");
|
||||
private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)");
|
||||
private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)");
|
||||
private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)");
|
||||
private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)");
|
||||
private static final Pattern P_AMP = Pattern.compile("&");
|
||||
private static final Pattern P_QUOTE = Pattern.compile("<");
|
||||
private static final Pattern P_LEFT_ARROW = Pattern.compile("<");
|
||||
private static final Pattern P_RIGHT_ARROW = Pattern.compile(">");
|
||||
private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>");
|
||||
|
||||
// @xxx could grow large... maybe use sesat's ReferenceMap
|
||||
private static final ConcurrentMap<String,Pattern> P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<String, Pattern>();
|
||||
private static final ConcurrentMap<String,Pattern> P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<String, Pattern>();
|
||||
|
||||
/** set of allowed html elements, along with allowed attributes for each element **/
|
||||
private final Map<String, List<String>> vAllowed;
|
||||
/** counts of open tags for each (allowable) html element **/
|
||||
private final Map<String, Integer> vTagCounts = new HashMap<String, Integer>();
|
||||
|
||||
/** html elements which must always be self-closing (e.g. "<img />") **/
|
||||
private final String[] vSelfClosingTags;
|
||||
/** html elements which must always have separate opening and closing tags (e.g. "<b></b>") **/
|
||||
private final String[] vNeedClosingTags;
|
||||
/** set of disallowed html elements **/
|
||||
private final String[] vDisallowed;
|
||||
/** attributes which should be checked for valid protocols **/
|
||||
private final String[] vProtocolAtts;
|
||||
/** allowed protocols **/
|
||||
private final String[] vAllowedProtocols;
|
||||
/** tags which should be removed if they contain no content (e.g. "<b></b>" or "<b />") **/
|
||||
private final String[] vRemoveBlanks;
|
||||
/** entities allowed within html markup **/
|
||||
private final String[] vAllowedEntities;
|
||||
/** flag determining whether comments are allowed in input String. */
|
||||
private final boolean stripComment;
|
||||
private final boolean encodeQuotes;
|
||||
private boolean vDebug = false;
|
||||
/**
|
||||
* flag determining whether to try to make tags when presented with "unbalanced"
|
||||
* angle brackets (e.g. "<b text </b>" becomes "<b> text </b>"). If set to false,
|
||||
* unbalanced angle brackets will be html escaped.
|
||||
*/
|
||||
private final boolean alwaysMakeTags;
|
||||
|
||||
/** Default constructor.
|
||||
*
|
||||
*/
|
||||
public HTMLFilter() {
|
||||
vAllowed = new HashMap<>();
|
||||
|
||||
final ArrayList<String> a_atts = new ArrayList<String>();
|
||||
a_atts.add("href");
|
||||
a_atts.add("target");
|
||||
vAllowed.put("a", a_atts);
|
||||
|
||||
final ArrayList<String> img_atts = new ArrayList<String>();
|
||||
img_atts.add("src");
|
||||
img_atts.add("width");
|
||||
img_atts.add("height");
|
||||
img_atts.add("alt");
|
||||
vAllowed.put("img", img_atts);
|
||||
|
||||
final ArrayList<String> no_atts = new ArrayList<String>();
|
||||
vAllowed.put("b", no_atts);
|
||||
vAllowed.put("strong", no_atts);
|
||||
vAllowed.put("i", no_atts);
|
||||
vAllowed.put("em", no_atts);
|
||||
|
||||
vSelfClosingTags = new String[]{"img"};
|
||||
vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"};
|
||||
vDisallowed = new String[]{};
|
||||
vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp.
|
||||
vProtocolAtts = new String[]{"src", "href"};
|
||||
vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"};
|
||||
vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"};
|
||||
stripComment = true;
|
||||
encodeQuotes = true;
|
||||
alwaysMakeTags = true;
|
||||
}
|
||||
|
||||
/** Set debug flag to true. Otherwise use default settings. See the default constructor.
|
||||
*
|
||||
* @param debug turn debug on with a true argument
|
||||
*/
|
||||
public HTMLFilter(final boolean debug) {
|
||||
this();
|
||||
vDebug = debug;
|
||||
|
||||
}
|
||||
|
||||
/** Map-parameter configurable constructor.
|
||||
*
|
||||
* @param conf map containing configuration. keys match field names.
|
||||
*/
|
||||
public HTMLFilter(final Map<String,Object> conf) {
|
||||
|
||||
assert conf.containsKey("vAllowed") : "configuration requires vAllowed";
|
||||
assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags";
|
||||
assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags";
|
||||
assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed";
|
||||
assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols";
|
||||
assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts";
|
||||
assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks";
|
||||
assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities";
|
||||
|
||||
vAllowed = Collections.unmodifiableMap((HashMap<String, List<String>>) conf.get("vAllowed"));
|
||||
vSelfClosingTags = (String[]) conf.get("vSelfClosingTags");
|
||||
vNeedClosingTags = (String[]) conf.get("vNeedClosingTags");
|
||||
vDisallowed = (String[]) conf.get("vDisallowed");
|
||||
vAllowedProtocols = (String[]) conf.get("vAllowedProtocols");
|
||||
vProtocolAtts = (String[]) conf.get("vProtocolAtts");
|
||||
vRemoveBlanks = (String[]) conf.get("vRemoveBlanks");
|
||||
vAllowedEntities = (String[]) conf.get("vAllowedEntities");
|
||||
stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true;
|
||||
encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true;
|
||||
alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true;
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
vTagCounts.clear();
|
||||
}
|
||||
|
||||
private void debug(final String msg) {
|
||||
if (vDebug) {
|
||||
Logger.getAnonymousLogger().info(msg);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// my versions of some PHP library functions
|
||||
public static String chr(final int decimal) {
|
||||
return String.valueOf((char) decimal);
|
||||
}
|
||||
|
||||
public static String htmlSpecialChars(final String s) {
|
||||
String result = s;
|
||||
result = regexReplace(P_AMP, "&", result);
|
||||
result = regexReplace(P_QUOTE, """, result);
|
||||
result = regexReplace(P_LEFT_ARROW, "<", result);
|
||||
result = regexReplace(P_RIGHT_ARROW, ">", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
/**
|
||||
* given a user submitted input String, filter out any invalid or restricted
|
||||
* html.
|
||||
*
|
||||
* @param input text (i.e. submitted by a user) than may contain html
|
||||
* @return "clean" version of input, with only valid, whitelisted html elements allowed
|
||||
*/
|
||||
public String filter(final String input) {
|
||||
reset();
|
||||
String s = input;
|
||||
|
||||
debug("************************************************");
|
||||
debug(" INPUT: " + input);
|
||||
|
||||
s = escapeComments(s);
|
||||
debug(" escapeComments: " + s);
|
||||
|
||||
s = balanceHTML(s);
|
||||
debug(" balanceHTML: " + s);
|
||||
|
||||
s = checkTags(s);
|
||||
debug(" checkTags: " + s);
|
||||
|
||||
s = processRemoveBlanks(s);
|
||||
debug("processRemoveBlanks: " + s);
|
||||
|
||||
s = validateEntities(s);
|
||||
debug(" validateEntites: " + s);
|
||||
|
||||
debug("************************************************\n\n");
|
||||
return s;
|
||||
}
|
||||
|
||||
public boolean isAlwaysMakeTags(){
|
||||
return alwaysMakeTags;
|
||||
}
|
||||
|
||||
public boolean isStripComments(){
|
||||
return stripComment;
|
||||
}
|
||||
|
||||
private String escapeComments(final String s) {
|
||||
final Matcher m = P_COMMENTS.matcher(s);
|
||||
final StringBuffer buf = new StringBuffer();
|
||||
if (m.find()) {
|
||||
final String match = m.group(1); //(.*?)
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement("<!--" + htmlSpecialChars(match) + "-->"));
|
||||
}
|
||||
m.appendTail(buf);
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private String balanceHTML(String s) {
|
||||
if (alwaysMakeTags) {
|
||||
//
|
||||
// try and form html
|
||||
//
|
||||
s = regexReplace(P_END_ARROW, "", s);
|
||||
s = regexReplace(P_BODY_TO_END, "<$1>", s);
|
||||
s = regexReplace(P_XML_CONTENT, "$1<$2", s);
|
||||
|
||||
} else {
|
||||
//
|
||||
// escape stray brackets
|
||||
//
|
||||
s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s);
|
||||
s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s);
|
||||
|
||||
//
|
||||
// the last regexp causes '<>' entities to appear
|
||||
// (we need to do a lookahead assertion so that the last bracket can
|
||||
// be used in the next pass of the regexp)
|
||||
//
|
||||
s = regexReplace(P_BOTH_ARROWS, "", s);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private String checkTags(String s) {
|
||||
Matcher m = P_TAGS.matcher(s);
|
||||
|
||||
final StringBuffer buf = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String replaceStr = m.group(1);
|
||||
replaceStr = processTag(replaceStr);
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr));
|
||||
}
|
||||
m.appendTail(buf);
|
||||
|
||||
s = buf.toString();
|
||||
|
||||
// these get tallied in processTag
|
||||
// (remember to reset before subsequent calls to filter method)
|
||||
for (String key : vTagCounts.keySet()) {
|
||||
for (int ii = 0; ii < vTagCounts.get(key); ii++) {
|
||||
s += "</" + key + ">";
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private String processRemoveBlanks(final String s) {
|
||||
String result = s;
|
||||
for (String tag : vRemoveBlanks) {
|
||||
if(!P_REMOVE_PAIR_BLANKS.containsKey(tag)){
|
||||
P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?></" + tag + ">"));
|
||||
}
|
||||
result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result);
|
||||
if(!P_REMOVE_SELF_BLANKS.containsKey(tag)){
|
||||
P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>"));
|
||||
}
|
||||
result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) {
|
||||
Matcher m = regex_pattern.matcher(s);
|
||||
return m.replaceAll(replacement);
|
||||
}
|
||||
|
||||
private String processTag(final String s) {
|
||||
// ending tags
|
||||
Matcher m = P_END_TAG.matcher(s);
|
||||
if (m.find()) {
|
||||
final String name = m.group(1).toLowerCase();
|
||||
if (allowed(name)) {
|
||||
if (!inArray(name, vSelfClosingTags)) {
|
||||
if (vTagCounts.containsKey(name)) {
|
||||
vTagCounts.put(name, vTagCounts.get(name) - 1);
|
||||
return "</" + name + ">";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// starting tags
|
||||
m = P_START_TAG.matcher(s);
|
||||
if (m.find()) {
|
||||
final String name = m.group(1).toLowerCase();
|
||||
final String body = m.group(2);
|
||||
String ending = m.group(3);
|
||||
|
||||
//debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" );
|
||||
if (allowed(name)) {
|
||||
String params = "";
|
||||
|
||||
final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body);
|
||||
final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body);
|
||||
final List<String> paramNames = new ArrayList<String>();
|
||||
final List<String> paramValues = new ArrayList<String>();
|
||||
while (m2.find()) {
|
||||
paramNames.add(m2.group(1)); //([a-z0-9]+)
|
||||
paramValues.add(m2.group(3)); //(.*?)
|
||||
}
|
||||
while (m3.find()) {
|
||||
paramNames.add(m3.group(1)); //([a-z0-9]+)
|
||||
paramValues.add(m3.group(3)); //([^\"\\s']+)
|
||||
}
|
||||
|
||||
String paramName, paramValue;
|
||||
for (int ii = 0; ii < paramNames.size(); ii++) {
|
||||
paramName = paramNames.get(ii).toLowerCase();
|
||||
paramValue = paramValues.get(ii);
|
||||
|
||||
// debug( "paramName='" + paramName + "'" );
|
||||
// debug( "paramValue='" + paramValue + "'" );
|
||||
// debug( "allowed? " + vAllowed.get( name ).contains( paramName ) );
|
||||
|
||||
if (allowedAttribute(name, paramName)) {
|
||||
if (inArray(paramName, vProtocolAtts)) {
|
||||
paramValue = processParamProtocol(paramValue);
|
||||
}
|
||||
params += " " + paramName + "=\"" + paramValue + "\"";
|
||||
}
|
||||
}
|
||||
|
||||
if (inArray(name, vSelfClosingTags)) {
|
||||
ending = " /";
|
||||
}
|
||||
|
||||
if (inArray(name, vNeedClosingTags)) {
|
||||
ending = "";
|
||||
}
|
||||
|
||||
if (ending == null || ending.length() < 1) {
|
||||
if (vTagCounts.containsKey(name)) {
|
||||
vTagCounts.put(name, vTagCounts.get(name) + 1);
|
||||
} else {
|
||||
vTagCounts.put(name, 1);
|
||||
}
|
||||
} else {
|
||||
ending = " /";
|
||||
}
|
||||
return "<" + name + params + ending + ">";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// comments
|
||||
m = P_COMMENT.matcher(s);
|
||||
if (!stripComment && m.find()) {
|
||||
return "<" + m.group() + ">";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private String processParamProtocol(String s) {
|
||||
s = decodeEntities(s);
|
||||
final Matcher m = P_PROTOCOL.matcher(s);
|
||||
if (m.find()) {
|
||||
final String protocol = m.group(1);
|
||||
if (!inArray(protocol, vAllowedProtocols)) {
|
||||
// bad protocol, turn into local anchor link instead
|
||||
s = "#" + s.substring(protocol.length() + 1, s.length());
|
||||
if (s.startsWith("#//")) {
|
||||
s = "#" + s.substring(3, s.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private String decodeEntities(String s) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
|
||||
Matcher m = P_ENTITY.matcher(s);
|
||||
while (m.find()) {
|
||||
final String match = m.group(1);
|
||||
final int decimal = Integer.decode(match).intValue();
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
|
||||
}
|
||||
m.appendTail(buf);
|
||||
s = buf.toString();
|
||||
|
||||
buf = new StringBuffer();
|
||||
m = P_ENTITY_UNICODE.matcher(s);
|
||||
while (m.find()) {
|
||||
final String match = m.group(1);
|
||||
final int decimal = Integer.valueOf(match, 16).intValue();
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
|
||||
}
|
||||
m.appendTail(buf);
|
||||
s = buf.toString();
|
||||
|
||||
buf = new StringBuffer();
|
||||
m = P_ENCODE.matcher(s);
|
||||
while (m.find()) {
|
||||
final String match = m.group(1);
|
||||
final int decimal = Integer.valueOf(match, 16).intValue();
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
|
||||
}
|
||||
m.appendTail(buf);
|
||||
s = buf.toString();
|
||||
|
||||
s = validateEntities(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
private String validateEntities(final String s) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
|
||||
// validate entities throughout the string
|
||||
Matcher m = P_VALID_ENTITIES.matcher(s);
|
||||
while (m.find()) {
|
||||
final String one = m.group(1); //([^&;]*)
|
||||
final String two = m.group(2); //(?=(;|&|$))
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two)));
|
||||
}
|
||||
m.appendTail(buf);
|
||||
|
||||
return encodeQuotes(buf.toString());
|
||||
}
|
||||
|
||||
private String encodeQuotes(final String s){
|
||||
if(encodeQuotes){
|
||||
StringBuffer buf = new StringBuffer();
|
||||
Matcher m = P_VALID_QUOTES.matcher(s);
|
||||
while (m.find()) {
|
||||
final String one = m.group(1); //(>|^)
|
||||
final String two = m.group(2); //([^<]+?)
|
||||
final String three = m.group(3); //(<|$)
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, """, two) + three));
|
||||
}
|
||||
m.appendTail(buf);
|
||||
return buf.toString();
|
||||
}else{
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
private String checkEntity(final String preamble, final String term) {
|
||||
|
||||
return ";".equals(term) && isValidEntity(preamble)
|
||||
? '&' + preamble
|
||||
: "&" + preamble;
|
||||
}
|
||||
|
||||
private boolean isValidEntity(final String entity) {
|
||||
return inArray(entity, vAllowedEntities);
|
||||
}
|
||||
|
||||
private static boolean inArray(final String s, final String[] array) {
|
||||
for (String item : array) {
|
||||
if (item != null && item.equals(s)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean allowed(final String name) {
|
||||
return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed);
|
||||
}
|
||||
|
||||
private boolean allowedAttribute(final String name, final String paramName) {
|
||||
return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName));
|
||||
}
|
||||
}
|
||||
42
src/main/java/io/as/common/xss/SQLFilter.java
Executable file
42
src/main/java/io/as/common/xss/SQLFilter.java
Executable file
@@ -0,0 +1,42 @@
|
||||
package io.as.common.xss;
|
||||
|
||||
import io.as.common.exception.ASException;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
/**
|
||||
* SQL过滤
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class SQLFilter {
|
||||
|
||||
/**
|
||||
* SQL注入过滤
|
||||
* @param str 待验证的字符串
|
||||
*/
|
||||
public static String sqlInject(String str){
|
||||
if(StringUtils.isBlank(str)){
|
||||
return null;
|
||||
}
|
||||
//去掉'|"|;|\字符
|
||||
str = StringUtils.replace(str, "'", "");
|
||||
str = StringUtils.replace(str, "\"", "");
|
||||
str = StringUtils.replace(str, ";", "");
|
||||
str = StringUtils.replace(str, "\\", "");
|
||||
|
||||
//转换成小写
|
||||
str = str.toLowerCase();
|
||||
|
||||
//非法字符
|
||||
String[] keywords = {"master", "truncate", "insert", "select", "delete", "update", "declare", "alter", "drop"};
|
||||
|
||||
//判断是否包含非法字符
|
||||
for(String keyword : keywords){
|
||||
if(str.indexOf(keyword) != -1){
|
||||
throw new ASException("包含非法字符");
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
37
src/main/java/io/as/common/xss/XssFilter.java
Executable file
37
src/main/java/io/as/common/xss/XssFilter.java
Executable file
@@ -0,0 +1,37 @@
|
||||
package io.as.common.xss;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* XSS过滤
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class XssFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig config) throws ServletException {
|
||||
}
|
||||
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
// /api 需原样读 body 入 sys_api_log(ContentCachingRequestWrapper);Xss 对 JSON 会先读满内层流再换流,易导致下游 body 缓存为空,故 API 不走 XSS 包装
|
||||
String path = httpRequest.getRequestURI();
|
||||
String ctx = httpRequest.getContextPath() == null ? "" : httpRequest.getContextPath();
|
||||
String apiBase = ctx + "/api";
|
||||
if (path != null && (path.equals(apiBase) || path.startsWith(apiBase + "/"))) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(httpRequest);
|
||||
chain.doFilter(xssRequest, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
}
|
||||
139
src/main/java/io/as/common/xss/XssHttpServletRequestWrapper.java
Executable file
139
src/main/java/io/as/common/xss/XssHttpServletRequestWrapper.java
Executable file
@@ -0,0 +1,139 @@
|
||||
package io.as.common.xss;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XSS过滤处理
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
//没被包装过的HttpServletRequest(特殊场景,需要自己过滤)
|
||||
HttpServletRequest orgRequest;
|
||||
//html过滤
|
||||
private final static HTMLFilter htmlFilter = new HTMLFilter();
|
||||
|
||||
public XssHttpServletRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
orgRequest = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
//非json类型,直接返回
|
||||
if(!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(super.getHeader(HttpHeaders.CONTENT_TYPE))){
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
//为空,直接返回
|
||||
String json = IOUtils.toString(super.getInputStream(), "utf-8");
|
||||
if (StringUtils.isBlank(json)) {
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
//xss过滤
|
||||
json = xssEncode(json);
|
||||
final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes("utf-8"));
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return bis.read();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(xssEncode(name));
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
value = xssEncode(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] parameters = super.getParameterValues(name);
|
||||
if (parameters == null || parameters.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
parameters[i] = xssEncode(parameters[i]);
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String,String[]> getParameterMap() {
|
||||
Map<String,String[]> map = new LinkedHashMap<>();
|
||||
Map<String,String[]> parameters = super.getParameterMap();
|
||||
for (String key : parameters.keySet()) {
|
||||
String[] values = parameters.get(key);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = xssEncode(values[i]);
|
||||
}
|
||||
map.put(key, values);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(xssEncode(name));
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
value = xssEncode(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String xssEncode(String input) {
|
||||
return htmlFilter.filter(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最原始的request
|
||||
*/
|
||||
public HttpServletRequest getOrgRequest() {
|
||||
return orgRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最原始的request
|
||||
*/
|
||||
public static HttpServletRequest getOrgRequest(HttpServletRequest request) {
|
||||
if (request instanceof XssHttpServletRequestWrapper) {
|
||||
return ((XssHttpServletRequestWrapper) request).getOrgRequest();
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
36
src/main/java/io/as/config/CorsConfig.java
Executable file
36
src/main/java/io/as/config/CorsConfig.java
Executable file
@@ -0,0 +1,36 @@
|
||||
package io.as.config;
|
||||
|
||||
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 org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* 跨域配置:使用自定义 SimpleCorsFilter 在 Shiro 之前处理 OPTIONS 并回显 Origin(带凭证时不能用 *)。
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(false) // 仅用 Authorization,不涉及 Cookie
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||
.maxAge(3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 CORS 过滤器,最高优先级:OPTIONS 直接 200 + CORS 头,其它请求回显 Origin 以支持凭证。
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<SimpleCorsFilter> corsFilterRegistration() {
|
||||
FilterRegistrationBean<SimpleCorsFilter> registration = new FilterRegistrationBean<>(new SimpleCorsFilter());
|
||||
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||
registration.addUrlPatterns("/*");
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
41
src/main/java/io/as/config/FilterConfig.java
Executable file
41
src/main/java/io/as/config/FilterConfig.java
Executable file
@@ -0,0 +1,41 @@
|
||||
package io.as.config;
|
||||
|
||||
import io.as.common.xss.XssFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||
|
||||
import javax.servlet.DispatcherType;
|
||||
|
||||
/**
|
||||
* Filter配置
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@Configuration
|
||||
public class FilterConfig {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean shiroFilterRegistration() {
|
||||
FilterRegistrationBean registration = new FilterRegistrationBean();
|
||||
registration.setFilter(new DelegatingFilterProxy("shiroFilter"));
|
||||
//该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
|
||||
registration.addInitParameter("targetFilterLifecycle", "true");
|
||||
registration.setEnabled(true);
|
||||
registration.setOrder(Integer.MAX_VALUE - 1);
|
||||
registration.addUrlPatterns("/*");
|
||||
return registration;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean xssFilterRegistration() {
|
||||
FilterRegistrationBean registration = new FilterRegistrationBean();
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST);
|
||||
registration.setFilter(new XssFilter());
|
||||
registration.addUrlPatterns("/*");
|
||||
registration.setName("xssFilter");
|
||||
registration.setOrder(Integer.MAX_VALUE);
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
25
src/main/java/io/as/config/KaptchaConfig.java
Normal file
25
src/main/java/io/as/config/KaptchaConfig.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package io.as.config;
|
||||
|
||||
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||
import com.google.code.kaptcha.util.Config;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
@Configuration
|
||||
public class KaptchaConfig {
|
||||
|
||||
@Bean
|
||||
public DefaultKaptcha producer() {
|
||||
Properties properties = new Properties();
|
||||
properties.put("kaptcha.border", "no");
|
||||
properties.put("kaptcha.textproducer.font.color", "black");
|
||||
properties.put("kaptcha.textproducer.char.space", "5");
|
||||
properties.put("kaptcha.textproducer.font.names", "Arial,Courier,cmr10,宋体,楷体,微软雅黑");
|
||||
Config config = new Config(properties);
|
||||
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
|
||||
defaultKaptcha.setConfig(config);
|
||||
return defaultKaptcha;
|
||||
}
|
||||
}
|
||||
23
src/main/java/io/as/config/MybatisPlusConfig.java
Executable file
23
src/main/java/io/as/config/MybatisPlusConfig.java
Executable file
@@ -0,0 +1,23 @@
|
||||
package io.as.config;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* mybatis-plus配置
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 分页插件
|
||||
*/
|
||||
@Bean
|
||||
public PaginationInterceptor paginationInterceptor() {
|
||||
return new PaginationInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
55
src/main/java/io/as/config/RedisConfig.java
Executable file
55
src/main/java/io/as/config/RedisConfig.java
Executable file
@@ -0,0 +1,55 @@
|
||||
package io.as.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.*;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis配置
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
@Autowired
|
||||
private RedisConnectionFactory factory;
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate() {
|
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setKeySerializer(new StringRedisSerializer());
|
||||
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
|
||||
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
|
||||
redisTemplate.setValueSerializer(new StringRedisSerializer());
|
||||
redisTemplate.setConnectionFactory(factory);
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
|
||||
return redisTemplate.opsForHash();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
|
||||
return redisTemplate.opsForValue();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
|
||||
return redisTemplate.opsForList();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
|
||||
return redisTemplate.opsForSet();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
|
||||
return redisTemplate.opsForZSet();
|
||||
}
|
||||
}
|
||||
74
src/main/java/io/as/config/ShiroConfig.java
Executable file
74
src/main/java/io/as/config/ShiroConfig.java
Executable file
@@ -0,0 +1,74 @@
|
||||
package io.as.config;
|
||||
|
||||
import io.as.modules.sys.jwt.JWTFilter;
|
||||
import io.as.modules.sys.jwt.JWTRealm;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Configuration
|
||||
public class ShiroConfig {
|
||||
|
||||
@Bean("securityManager")
|
||||
public SecurityManager securityManager() {
|
||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
||||
securityManager.setRealm(jwtRealm());
|
||||
securityManager.setRememberMeManager(null);
|
||||
return securityManager;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JWTRealm jwtRealm() {
|
||||
return new JWTRealm();
|
||||
}
|
||||
|
||||
@Bean("shiroFilter")
|
||||
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
|
||||
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
|
||||
shiroFilter.setSecurityManager(securityManager);
|
||||
|
||||
Map<String, Filter> filters = new LinkedHashMap<>();
|
||||
filters.put("jwt", new JWTFilter());
|
||||
shiroFilter.setFilters(filters);
|
||||
|
||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||
// 免认证路径
|
||||
filterMap.put("/webjars/**", "anon");
|
||||
filterMap.put("/druid/**", "anon");
|
||||
filterMap.put("/sys/login", "anon");
|
||||
filterMap.put("/sys/refresh", "anon");
|
||||
filterMap.put("/captcha.jpg", "anon");
|
||||
filterMap.put("/openapi/**", "anon");
|
||||
filterMap.put("/api/jst/**", "anon");
|
||||
filterMap.put("/api/payment/**", "anon");
|
||||
filterMap.put("/swagger/**", "anon");
|
||||
filterMap.put("/v2/api-docs", "anon");
|
||||
filterMap.put("/swagger-ui.html", "anon");
|
||||
filterMap.put("/swagger-resources/**", "anon");
|
||||
// 其他路径需要 JWT 认证
|
||||
filterMap.put("/**", "jwt");
|
||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||
|
||||
return shiroFilter;
|
||||
}
|
||||
|
||||
@Bean("lifecycleBeanPostProcessor")
|
||||
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
||||
return new LifecycleBeanPostProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
|
||||
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
||||
advisor.setSecurityManager(securityManager);
|
||||
return advisor;
|
||||
}
|
||||
}
|
||||
49
src/main/java/io/as/config/SimpleCorsFilter.java
Executable file
49
src/main/java/io/as/config/SimpleCorsFilter.java
Executable file
@@ -0,0 +1,49 @@
|
||||
package io.as.config;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 跨域过滤器:在 Shiro 之前执行,对 OPTIONS 预检直接返回 200 并设置 CORS 头。
|
||||
* 带凭证(cookie/token)时须回显请求的 Origin,不能使用 *,否则浏览器会拒绝。
|
||||
*/
|
||||
public class SimpleCorsFilter implements Filter {
|
||||
|
||||
private static final String ORIGIN = "Origin";
|
||||
private static final String AC_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
|
||||
private static final String AC_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
|
||||
private static final String AC_ALLOW_METHODS = "Access-Control-Allow-Methods";
|
||||
private static final String AC_ALLOW_HEADERS = "Access-Control-Allow-Headers";
|
||||
private static final String AC_MAX_AGE = "Access-Control-Max-Age";
|
||||
private static final String AC_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
|
||||
|
||||
private static final String ALLOW_METHODS = "GET, POST, PUT, DELETE, OPTIONS, PATCH";
|
||||
private static final String ALLOW_HEADERS = "*";
|
||||
private static final String MAX_AGE = "3600";
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
HttpServletResponse resp = (HttpServletResponse) response;
|
||||
|
||||
String origin = req.getHeader(ORIGIN);
|
||||
if (origin != null && !origin.isEmpty()) {
|
||||
resp.setHeader(AC_ALLOW_ORIGIN, origin);
|
||||
resp.setHeader(AC_ALLOW_CREDENTIALS, "false"); // 仅用 Authorization,不涉及 Cookie
|
||||
}
|
||||
resp.setHeader(AC_ALLOW_METHODS, ALLOW_METHODS);
|
||||
resp.setHeader(AC_ALLOW_HEADERS, ALLOW_HEADERS);
|
||||
resp.setHeader(AC_MAX_AGE, MAX_AGE);
|
||||
resp.setHeader(AC_EXPOSE_HEADERS, ALLOW_HEADERS);
|
||||
|
||||
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
return;
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
53
src/main/java/io/as/config/SwaggerConfig.java
Executable file
53
src/main/java/io/as/config/SwaggerConfig.java
Executable file
@@ -0,0 +1,53 @@
|
||||
package io.as.config;
|
||||
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.PathSelectors;
|
||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||
import springfox.documentation.service.ApiInfo;
|
||||
import springfox.documentation.service.ApiKey;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
|
||||
@Configuration
|
||||
@EnableSwagger2
|
||||
public class SwaggerConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public Docket createRestApi() {
|
||||
return new Docket(DocumentationType.SWAGGER_2)
|
||||
.apiInfo(apiInfo())
|
||||
.select()
|
||||
//加了ApiOperation注解的类,才生成接口文档
|
||||
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
|
||||
//包下的类,才生成接口文档
|
||||
//.apis(RequestHandlerSelectors.basePackage("io.as.controller"))
|
||||
.paths(PathSelectors.any())
|
||||
.build()
|
||||
.securitySchemes(security());
|
||||
}
|
||||
|
||||
private ApiInfo apiInfo() {
|
||||
return new ApiInfoBuilder()
|
||||
.title("爱视文化科技")
|
||||
.description("爱视后台文档")
|
||||
.termsOfServiceUrl("https://www.renren.io")
|
||||
.version("3.0.0")
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<ApiKey> security() {
|
||||
return newArrayList(
|
||||
new ApiKey("Authorization", "Authorization", "header")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
16
src/main/java/io/as/datasource/annotation/DataSource.java
Executable file
16
src/main/java/io/as/datasource/annotation/DataSource.java
Executable file
@@ -0,0 +1,16 @@
|
||||
package io.as.datasource.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 多数据源注解
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
public @interface DataSource {
|
||||
String value() default "";
|
||||
}
|
||||
63
src/main/java/io/as/datasource/aspect/DataSourceAspect.java
Executable file
63
src/main/java/io/as/datasource/aspect/DataSourceAspect.java
Executable file
@@ -0,0 +1,63 @@
|
||||
package io.as.datasource.aspect;
|
||||
|
||||
|
||||
import io.as.datasource.annotation.DataSource;
|
||||
import io.as.datasource.config.DynamicContextHolder;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* 多数据源,切面处理类
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class DataSourceAspect {
|
||||
protected Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@Pointcut("@annotation(io.as.datasource.annotation.DataSource) " +
|
||||
"|| @within(io.as.datasource.annotation.DataSource)")
|
||||
public void dataSourcePointCut() {
|
||||
|
||||
}
|
||||
|
||||
@Around("dataSourcePointCut()")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Class targetClass = point.getTarget().getClass();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class);
|
||||
DataSource methodDataSource = method.getAnnotation(DataSource.class);
|
||||
if(targetDataSource != null || methodDataSource != null){
|
||||
String value;
|
||||
if(methodDataSource != null){
|
||||
value = methodDataSource.value();
|
||||
}else {
|
||||
value = targetDataSource.value();
|
||||
}
|
||||
|
||||
DynamicContextHolder.push(value);
|
||||
logger.debug("set datasource is {}", value);
|
||||
}
|
||||
|
||||
try {
|
||||
return point.proceed();
|
||||
} finally {
|
||||
DynamicContextHolder.poll();
|
||||
logger.debug("clean datasource");
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/main/java/io/as/datasource/config/DynamicContextHolder.java
Executable file
49
src/main/java/io/as/datasource/config/DynamicContextHolder.java
Executable file
@@ -0,0 +1,49 @@
|
||||
package io.as.datasource.config;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
/**
|
||||
* 多数据源上下文
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class DynamicContextHolder {
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() {
|
||||
@Override
|
||||
protected Object initialValue() {
|
||||
return new ArrayDeque();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获得当前线程数据源
|
||||
*
|
||||
* @return 数据源名称
|
||||
*/
|
||||
public static String peek() {
|
||||
return CONTEXT_HOLDER.get().peek();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前线程数据源
|
||||
*
|
||||
* @param dataSource 数据源名称
|
||||
*/
|
||||
public static void push(String dataSource) {
|
||||
CONTEXT_HOLDER.get().push(dataSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前线程数据源
|
||||
*/
|
||||
public static void poll() {
|
||||
Deque<String> deque = CONTEXT_HOLDER.get();
|
||||
deque.poll();
|
||||
if (deque.isEmpty()) {
|
||||
CONTEXT_HOLDER.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
17
src/main/java/io/as/datasource/config/DynamicDataSource.java
Executable file
17
src/main/java/io/as/datasource/config/DynamicDataSource.java
Executable file
@@ -0,0 +1,17 @@
|
||||
package io.as.datasource.config;
|
||||
|
||||
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
|
||||
|
||||
/**
|
||||
* 多数据源
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
public class DynamicDataSource extends AbstractRoutingDataSource {
|
||||
|
||||
@Override
|
||||
protected Object determineCurrentLookupKey() {
|
||||
return DynamicContextHolder.peek();
|
||||
}
|
||||
|
||||
}
|
||||
55
src/main/java/io/as/datasource/config/DynamicDataSourceConfig.java
Executable file
55
src/main/java/io/as/datasource/config/DynamicDataSourceConfig.java
Executable file
@@ -0,0 +1,55 @@
|
||||
package io.as.datasource.config;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import io.as.datasource.properties.DataSourceProperties;
|
||||
import io.as.datasource.properties.DynamicDataSourceProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 配置多数据源
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
|
||||
public class DynamicDataSourceConfig {
|
||||
@Autowired
|
||||
private DynamicDataSourceProperties properties;
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "spring.datasource.druid")
|
||||
public DataSourceProperties dataSourceProperties() {
|
||||
return new DataSourceProperties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
|
||||
DynamicDataSource dynamicDataSource = new DynamicDataSource();
|
||||
dynamicDataSource.setTargetDataSources(getDynamicDataSource());
|
||||
|
||||
//默认数据源
|
||||
DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
|
||||
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
|
||||
|
||||
return dynamicDataSource;
|
||||
}
|
||||
|
||||
private Map<Object, Object> getDynamicDataSource(){
|
||||
Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
|
||||
Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
|
||||
dataSourcePropertiesMap.forEach((k, v) -> {
|
||||
DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
|
||||
targetDataSources.put(k, druidDataSource);
|
||||
});
|
||||
|
||||
return targetDataSources;
|
||||
}
|
||||
|
||||
}
|
||||
46
src/main/java/io/as/datasource/config/DynamicDataSourceFactory.java
Executable file
46
src/main/java/io/as/datasource/config/DynamicDataSourceFactory.java
Executable file
@@ -0,0 +1,46 @@
|
||||
package io.as.datasource.config;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import io.as.datasource.properties.DataSourceProperties;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* DruidDataSource
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class DynamicDataSourceFactory {
|
||||
|
||||
public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
|
||||
DruidDataSource druidDataSource = new DruidDataSource();
|
||||
druidDataSource.setDriverClassName(properties.getDriverClassName());
|
||||
druidDataSource.setUrl(properties.getUrl());
|
||||
druidDataSource.setUsername(properties.getUsername());
|
||||
druidDataSource.setPassword(properties.getPassword());
|
||||
|
||||
druidDataSource.setInitialSize(properties.getInitialSize());
|
||||
druidDataSource.setMaxActive(properties.getMaxActive());
|
||||
druidDataSource.setMinIdle(properties.getMinIdle());
|
||||
druidDataSource.setMaxWait(properties.getMaxWait());
|
||||
druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
|
||||
druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
|
||||
druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
|
||||
druidDataSource.setValidationQuery(properties.getValidationQuery());
|
||||
druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
|
||||
druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
|
||||
druidDataSource.setTestOnReturn(properties.isTestOnReturn());
|
||||
druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
|
||||
druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
|
||||
druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());
|
||||
|
||||
try {
|
||||
druidDataSource.setFilters(properties.getFilters());
|
||||
druidDataSource.init();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return druidDataSource;
|
||||
}
|
||||
}
|
||||
194
src/main/java/io/as/datasource/properties/DataSourceProperties.java
Executable file
194
src/main/java/io/as/datasource/properties/DataSourceProperties.java
Executable file
@@ -0,0 +1,194 @@
|
||||
package io.as.datasource.properties;
|
||||
|
||||
/**
|
||||
* 多数据源属性
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class DataSourceProperties {
|
||||
private String driverClassName;
|
||||
private String url;
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* Druid默认参数
|
||||
*/
|
||||
private int initialSize = 2;
|
||||
private int maxActive = 10;
|
||||
private int minIdle = -1;
|
||||
private long maxWait = 60 * 1000L;
|
||||
private long timeBetweenEvictionRunsMillis = 60 * 1000L;
|
||||
private long minEvictableIdleTimeMillis = 1000L * 60L * 30L;
|
||||
private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7;
|
||||
private String validationQuery = "select 1";
|
||||
private int validationQueryTimeout = -1;
|
||||
private boolean testOnBorrow = false;
|
||||
private boolean testOnReturn = false;
|
||||
private boolean testWhileIdle = true;
|
||||
private boolean poolPreparedStatements = false;
|
||||
private int maxOpenPreparedStatements = -1;
|
||||
private boolean sharePreparedStatements = false;
|
||||
private String filters = "stat";
|
||||
|
||||
public String getDriverClassName() {
|
||||
return driverClassName;
|
||||
}
|
||||
|
||||
public void setDriverClassName(String driverClassName) {
|
||||
this.driverClassName = driverClassName;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public int getInitialSize() {
|
||||
return initialSize;
|
||||
}
|
||||
|
||||
public void setInitialSize(int initialSize) {
|
||||
this.initialSize = initialSize;
|
||||
}
|
||||
|
||||
public int getMaxActive() {
|
||||
return maxActive;
|
||||
}
|
||||
|
||||
public void setMaxActive(int maxActive) {
|
||||
this.maxActive = maxActive;
|
||||
}
|
||||
|
||||
public int getMinIdle() {
|
||||
return minIdle;
|
||||
}
|
||||
|
||||
public void setMinIdle(int minIdle) {
|
||||
this.minIdle = minIdle;
|
||||
}
|
||||
|
||||
public long getMaxWait() {
|
||||
return maxWait;
|
||||
}
|
||||
|
||||
public void setMaxWait(long maxWait) {
|
||||
this.maxWait = maxWait;
|
||||
}
|
||||
|
||||
public long getTimeBetweenEvictionRunsMillis() {
|
||||
return timeBetweenEvictionRunsMillis;
|
||||
}
|
||||
|
||||
public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
|
||||
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
|
||||
}
|
||||
|
||||
public long getMinEvictableIdleTimeMillis() {
|
||||
return minEvictableIdleTimeMillis;
|
||||
}
|
||||
|
||||
public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
|
||||
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
|
||||
}
|
||||
|
||||
public long getMaxEvictableIdleTimeMillis() {
|
||||
return maxEvictableIdleTimeMillis;
|
||||
}
|
||||
|
||||
public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) {
|
||||
this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
|
||||
}
|
||||
|
||||
public String getValidationQuery() {
|
||||
return validationQuery;
|
||||
}
|
||||
|
||||
public void setValidationQuery(String validationQuery) {
|
||||
this.validationQuery = validationQuery;
|
||||
}
|
||||
|
||||
public int getValidationQueryTimeout() {
|
||||
return validationQueryTimeout;
|
||||
}
|
||||
|
||||
public void setValidationQueryTimeout(int validationQueryTimeout) {
|
||||
this.validationQueryTimeout = validationQueryTimeout;
|
||||
}
|
||||
|
||||
public boolean isTestOnBorrow() {
|
||||
return testOnBorrow;
|
||||
}
|
||||
|
||||
public void setTestOnBorrow(boolean testOnBorrow) {
|
||||
this.testOnBorrow = testOnBorrow;
|
||||
}
|
||||
|
||||
public boolean isTestOnReturn() {
|
||||
return testOnReturn;
|
||||
}
|
||||
|
||||
public void setTestOnReturn(boolean testOnReturn) {
|
||||
this.testOnReturn = testOnReturn;
|
||||
}
|
||||
|
||||
public boolean isTestWhileIdle() {
|
||||
return testWhileIdle;
|
||||
}
|
||||
|
||||
public void setTestWhileIdle(boolean testWhileIdle) {
|
||||
this.testWhileIdle = testWhileIdle;
|
||||
}
|
||||
|
||||
public boolean isPoolPreparedStatements() {
|
||||
return poolPreparedStatements;
|
||||
}
|
||||
|
||||
public void setPoolPreparedStatements(boolean poolPreparedStatements) {
|
||||
this.poolPreparedStatements = poolPreparedStatements;
|
||||
}
|
||||
|
||||
public int getMaxOpenPreparedStatements() {
|
||||
return maxOpenPreparedStatements;
|
||||
}
|
||||
|
||||
public void setMaxOpenPreparedStatements(int maxOpenPreparedStatements) {
|
||||
this.maxOpenPreparedStatements = maxOpenPreparedStatements;
|
||||
}
|
||||
|
||||
public boolean isSharePreparedStatements() {
|
||||
return sharePreparedStatements;
|
||||
}
|
||||
|
||||
public void setSharePreparedStatements(boolean sharePreparedStatements) {
|
||||
this.sharePreparedStatements = sharePreparedStatements;
|
||||
}
|
||||
|
||||
public String getFilters() {
|
||||
return filters;
|
||||
}
|
||||
|
||||
public void setFilters(String filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
}
|
||||
25
src/main/java/io/as/datasource/properties/DynamicDataSourceProperties.java
Executable file
25
src/main/java/io/as/datasource/properties/DynamicDataSourceProperties.java
Executable file
@@ -0,0 +1,25 @@
|
||||
package io.as.datasource.properties;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 多数据源属性
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "dynamic")
|
||||
public class DynamicDataSourceProperties {
|
||||
private Map<String, DataSourceProperties> datasource = new LinkedHashMap<>();
|
||||
|
||||
public Map<String, DataSourceProperties> getDatasource() {
|
||||
return datasource;
|
||||
}
|
||||
|
||||
public void setDatasource(Map<String, DataSourceProperties> datasource) {
|
||||
this.datasource = datasource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package io.as.modules.daycard.constants;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 天卡业务相关常量
|
||||
*/
|
||||
public final class DayCardConstants {
|
||||
|
||||
private DayCardConstants() {
|
||||
}
|
||||
|
||||
// ==================== 订单状态 ====================
|
||||
/** 已创建 */
|
||||
public static final String ORDER_STATUS_CREATED = "PENDING";
|
||||
/** 支付中 */
|
||||
public static final String ORDER_STATUS_PAYING = "PAYING";
|
||||
/** 已支付 */
|
||||
public static final String ORDER_STATUS_PAID = "PAID";
|
||||
/** 支付失败 */
|
||||
public static final String ORDER_STATUS_PAY_FAILED = "PAY_FAILED";
|
||||
/** 已取消 */
|
||||
public static final String ORDER_STATUS_CANCELLED = "CANCELLED";
|
||||
/** 退款中 */
|
||||
public static final String ORDER_STATUS_REFUNDING = "REFUNDING";
|
||||
|
||||
// ==================== 天卡池状态 ====================
|
||||
/** 未使用 */
|
||||
public static final String POOL_STATUS_UNUSED = "UNUSED";
|
||||
/** 已预留 */
|
||||
public static final String POOL_STATUS_RESERVED = "RESERVED";
|
||||
/** 已使用 */
|
||||
public static final String POOL_STATUS_USED = "USED";
|
||||
/** 可复用(已解绑,橘视厅权益仍有效) */
|
||||
public static final String POOL_STATUS_REUSABLE = "REUSABLE";
|
||||
|
||||
// ==================== 导入批次状态 ====================
|
||||
/** 处理中 */
|
||||
public static final String BATCH_STATUS_PROCESSING = "PROCESSING";
|
||||
/** 成功 */
|
||||
public static final String BATCH_STATUS_SUCCEEDED = "SUCCEEDED";
|
||||
/** 部分失败 */
|
||||
public static final String BATCH_STATUS_PARTIAL_FAILED = "PARTIAL_FAILED";
|
||||
/** 失败 */
|
||||
public static final String BATCH_STATUS_FAILED = "FAILED";
|
||||
|
||||
// ==================== 激活状态 ====================
|
||||
/** 待激活 */
|
||||
public static final String ACTIVATE_STATUS_PENDING = "PENDING";
|
||||
/** 激活成功 */
|
||||
public static final String ACTIVATE_STATUS_SUCCESS = "SUCCESS";
|
||||
/** 激活失败 */
|
||||
public static final String ACTIVATE_STATUS_FAILED = "FAILED";
|
||||
|
||||
// ==================== 卡类型 ====================
|
||||
/** 天卡 */
|
||||
public static final String CARD_TYPE_DAY = "DAY";
|
||||
|
||||
// ==================== Redis Key 前缀 ====================
|
||||
/** 设备防重锁前缀 */
|
||||
public static final String REDIS_LOCK_PREFIX = "daycard:lock:device:";
|
||||
/** 订单前缀 */
|
||||
public static final String REDIS_ORDER_PREFIX = "daycard:order:";
|
||||
/** 设备订单前缀 */
|
||||
public static final String REDIS_DEVICE_ORDER_PREFIX = "daycard:device:";
|
||||
/** 代销天卡消耗防重锁前缀(按设备标识加锁) */
|
||||
public static final String REDIS_PARTNER_CONSUME_LOCK_PREFIX = "daycard:partner:consume:";
|
||||
/** 第三方激活Token缓存前缀(按thirdCardNo缓存) */
|
||||
public static final String REDIS_THIRD_PARTY_TOKEN_PREFIX = "api:thirdparty:activate:";
|
||||
/** 第三方激活任务锁前缀(按poolId加锁,防止重复激活) */
|
||||
public static final String REDIS_THIRD_PARTY_ACTIVATE_LOCK_PREFIX = "api:thirdparty:activate:lock:";
|
||||
|
||||
// ==================== 删除标记 ====================
|
||||
/** 未删除 */
|
||||
public static final String DELETE_FLAG_UNDEL = "UNDEL";
|
||||
/** 已删除 */
|
||||
public static final String DELETE_FLAG_DEL = "DEL";
|
||||
|
||||
// ==================== 分润规则范围 ====================
|
||||
/** 全局 */
|
||||
public static final String SCOPE_GLOBAL = "GLOBAL";
|
||||
/** 渠道 */
|
||||
public static final String SCOPE_CHANNEL = "CHANNEL";
|
||||
/** 代理商 */
|
||||
public static final String SCOPE_AGENT = "AGENT";
|
||||
/** 门店 */
|
||||
public static final String SCOPE_STORE = "STORE";
|
||||
|
||||
// ==================== 分润规则状态 ====================
|
||||
/** 启用 */
|
||||
public static final String PROFIT_RULE_STATUS_ACTIVE = "ACTIVE";
|
||||
/** 禁用 */
|
||||
public static final String PROFIT_RULE_STATUS_DISABLED = "DISABLED";
|
||||
|
||||
// ==================== 分润类型 ====================
|
||||
/** 按比例分润 */
|
||||
public static final String SHARE_TYPE_RATIO = "RATIO";
|
||||
/** 固定金额分润 */
|
||||
public static final String SHARE_TYPE_FIXED = "FIXED";
|
||||
|
||||
// ==================== 分账单状态 ====================
|
||||
/** 待处理 */
|
||||
public static final String SETTLE_STATUS_PENDING = "PENDING";
|
||||
/** 处理中 */
|
||||
public static final String SETTLE_STATUS_PROCESSING = "PROCESSING";
|
||||
/** 成功 */
|
||||
public static final String SETTLE_STATUS_SUCCESS = "SUCCESS";
|
||||
/** 失败 */
|
||||
public static final String SETTLE_STATUS_FAILED = "FAILED";
|
||||
/** 部分成功 */
|
||||
public static final String SETTLE_STATUS_PARTIAL = "PARTIAL";
|
||||
|
||||
// ==================== 接收方绑定状态 ====================
|
||||
/** 待绑定 */
|
||||
public static final String BIND_STATUS_PENDING = "PENDING";
|
||||
/** 已绑定 */
|
||||
public static final String BIND_STATUS_ACTIVE = "ACTIVE";
|
||||
/** 绑定失败 */
|
||||
public static final String BIND_STATUS_FAILED = "FAILED";
|
||||
|
||||
// ==================== 分账接收方类型 ====================
|
||||
/** 商户号 */
|
||||
public static final String RECEIVER_TYPE_MERCHANT_ID = "MERCHANT_ID";
|
||||
/** 个人OpenID */
|
||||
public static final String RECEIVER_TYPE_PERSONAL_OPENID = "PERSONAL_OPENID";
|
||||
|
||||
// ==================== 组织角色 ====================
|
||||
/** 平台 */
|
||||
public static final String ORG_ROLE_PLATFORM = "PLATFORM";
|
||||
/** 渠道 */
|
||||
public static final String ORG_ROLE_CHANNEL = "CHANNEL";
|
||||
/** 代理 */
|
||||
public static final String ORG_ROLE_AGENT = "AGENT";
|
||||
/** 门店 */
|
||||
public static final String ORG_ROLE_STORE = "STORE";
|
||||
|
||||
// ==================== 微信绑定状态 ====================
|
||||
/** 已绑定 */
|
||||
public static final String WX_BIND_STATUS_BINDED = "BINDED";
|
||||
/** 未绑定 */
|
||||
public static final String WX_BIND_STATUS_UNBOUND = "UNBOUND";
|
||||
|
||||
// ==================== 分润比例上限 ====================
|
||||
/** 分润比例上限:30% */
|
||||
public static final BigDecimal SHARE_RATIO_LIMIT = new BigDecimal("0.3000");
|
||||
/** 非平台方分润比例之和上限:30% */
|
||||
public static final double PROFIT_SHARE_LIMIT = 30.0;
|
||||
|
||||
// ==================== 退款状态 ====================
|
||||
/** 退款待处理 */
|
||||
public static final String REFUND_STATUS_PENDING = "PENDING";
|
||||
/** 退款处理中 */
|
||||
public static final String REFUND_STATUS_PROCESSING = "PROCESSING";
|
||||
/** 退款成功 */
|
||||
public static final String REFUND_STATUS_SUCCESS = "SUCCESS";
|
||||
/** 退款失败 */
|
||||
public static final String REFUND_STATUS_FAILED = "FAILED";
|
||||
/** 退款关闭 */
|
||||
public static final String REFUND_STATUS_CLOSED = "CLOSED";
|
||||
|
||||
// ==================== 支付渠道 ====================
|
||||
/** Mock支付 */
|
||||
public static final String PAY_CHANNEL_MOCK = "MOCK";
|
||||
/** 微信支付 */
|
||||
public static final String PAY_CHANNEL_WECHAT = "WECHAT";
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.DayCardActivationCodeEntity;
|
||||
import io.as.modules.daycard.service.DayCardActivationCodeService;
|
||||
import io.as.modules.sys.entity.SysOrgEntity;
|
||||
import io.as.modules.sys.service.SysOrgService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡设备激活码管理(管理后台用)
|
||||
* <p>管理后台按门店生成激活码,App 端 init 时输入激活码完成设备注册绑定</p>
|
||||
*/
|
||||
@Api(tags = "天卡设备激活码")
|
||||
@RestController
|
||||
@RequestMapping("/manage/daycard/activation-code")
|
||||
public class DayCardActivationCodeController {
|
||||
|
||||
@Autowired
|
||||
private DayCardActivationCodeService dayCardActivationCodeService;
|
||||
|
||||
@Autowired
|
||||
private SysOrgService sysOrgService;
|
||||
|
||||
/**
|
||||
* 按门店查询激活码列表
|
||||
*/
|
||||
@ApiOperation("按门店查询激活码列表")
|
||||
@GetMapping("/list/{storeOrgId}")
|
||||
public R listByStore(@PathVariable("storeOrgId") Long storeOrgId) {
|
||||
List<DayCardActivationCodeEntity> list = dayCardActivationCodeService.listByStoreOrgId(storeOrgId);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (DayCardActivationCodeEntity entity : list) {
|
||||
result.add(toMap(entity));
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("list", result);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为门店生成激活码(每门店一个,已存在则返回已有的)
|
||||
*/
|
||||
@ApiOperation("为门店生成激活码")
|
||||
@AuditAction(value = "ACTCODE_GENERATE", description = "生成门店激活码")
|
||||
@PostMapping("/generate")
|
||||
public R generate(@RequestBody GenerateRequest request) {
|
||||
if (request.getStoreOrgId() == null) {
|
||||
throw new ASException("门店组织ID不能为空");
|
||||
}
|
||||
// 校验门店存在
|
||||
SysOrgEntity storeOrg = sysOrgService.getById(request.getStoreOrgId());
|
||||
if (storeOrg == null) {
|
||||
throw new ASException("门店不存在");
|
||||
}
|
||||
// 从门店 mpath 解析渠道
|
||||
Long channelOrgId = resolveChannelOrgId(storeOrg);
|
||||
DayCardActivationCodeEntity entity = dayCardActivationCodeService.generateForStore(
|
||||
request.getStoreOrgId(), channelOrgId, request.getRemark());
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", toMap(entity));
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成激活码 code(旧 code 失效)
|
||||
*/
|
||||
@ApiOperation("重新生成激活码")
|
||||
@AuditAction(value = "ACTCODE_REGENERATE", description = "重新生成激活码")
|
||||
@PostMapping("/{id}/regenerate")
|
||||
public R regenerate(@PathVariable("id") Long id) {
|
||||
String newCode = dayCardActivationCodeService.regenerateCode(id);
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("code", newCode);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", item);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用激活码
|
||||
*/
|
||||
@ApiOperation("启用/禁用激活码")
|
||||
@AuditAction(value = "ACTCODE_UPDATE_STATUS", description = "修改激活码状态")
|
||||
@PutMapping("/{id}/status")
|
||||
public R updateStatus(@PathVariable("id") Long id, @RequestBody UpdateStatusRequest request) {
|
||||
dayCardActivationCodeService.updateStatus(id, request.getStatus());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除激活码(软删除)
|
||||
*/
|
||||
@ApiOperation("删除激活码")
|
||||
@AuditAction(value = "ACTCODE_DELETE", description = "删除激活码")
|
||||
@DeleteMapping("/{id}")
|
||||
public R delete(@PathVariable("id") Long id) {
|
||||
dayCardActivationCodeService.delete(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== 内部方法 ====================
|
||||
|
||||
/**
|
||||
* 从门店 mpath 解析渠道组织ID(mpath 第2段)
|
||||
*/
|
||||
private Long resolveChannelOrgId(SysOrgEntity storeOrg) {
|
||||
if (storeOrg == null || StringUtils.isBlank(storeOrg.getMpath())) {
|
||||
return null;
|
||||
}
|
||||
String[] segments = storeOrg.getMpath().split("\\.");
|
||||
if (segments.length >= 2) {
|
||||
try {
|
||||
return Long.parseLong(segments[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Object> toMap(DayCardActivationCodeEntity entity) {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", entity.getId());
|
||||
m.put("code", entity.getCode());
|
||||
m.put("channelOrgId", entity.getChannelOrgId());
|
||||
m.put("storeOrgId", entity.getStoreOrgId());
|
||||
m.put("status", entity.getStatus());
|
||||
m.put("maxDeviceCount", entity.getMaxDeviceCount());
|
||||
m.put("activatedCount", entity.getActivatedCount());
|
||||
m.put("remark", entity.getRemark());
|
||||
m.put("createdAt", entity.getCreatedAt());
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成激活码请求
|
||||
*/
|
||||
@lombok.Data
|
||||
public static class GenerateRequest {
|
||||
private Long storeOrgId;
|
||||
private String remark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态请求
|
||||
*/
|
||||
@lombok.Data
|
||||
public static class UpdateStatusRequest {
|
||||
private String status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import io.as.modules.daycard.service.DayCardOrderService;
|
||||
import io.as.modules.daycard.service.DayCardPoolService;
|
||||
import io.as.modules.daycard.vo.DayCardPoolStatsVo;
|
||||
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡管理后台接口(JWT 鉴权)
|
||||
* <p>供 Web 管理后台调用,与 /openapi/** 合作伙伴接口隔离</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/manage/daycard")
|
||||
@RequiresAuthentication
|
||||
public class DayCardManageController {
|
||||
|
||||
@Autowired
|
||||
private DayCardPoolService dayCardPoolService;
|
||||
|
||||
@Autowired
|
||||
private DayCardOrderService dayCardOrderService;
|
||||
|
||||
// ===================== 天卡池 =====================
|
||||
|
||||
/**
|
||||
* 天卡池列表
|
||||
*/
|
||||
@GetMapping("/pool/list")
|
||||
public R poolList(@RequestParam Map<String, Object> params) {
|
||||
Long partnerId = parsePartnerId(params);
|
||||
PageUtils page = dayCardPoolService.queryPage(params, partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 天卡池统计
|
||||
*/
|
||||
@GetMapping("/pool/stats")
|
||||
public R poolStats(@RequestParam(required = false) Long partnerId) {
|
||||
DayCardPoolStatsVo vo = buildStats(partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("stats", vo);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入天卡卡密
|
||||
*/
|
||||
@AuditAction(value = "DAYCARD_POOL_IMPORT", description = "导入天卡卡密")
|
||||
@PostMapping("/pool/import")
|
||||
public R poolImport(@RequestBody Map<String, Object> body) {
|
||||
Long partnerId = null;
|
||||
Object pid = body.get("partnerId");
|
||||
if (pid != null) {
|
||||
partnerId = Long.parseLong(String.valueOf(pid));
|
||||
}
|
||||
Object codesObj = body.get("cardCodes");
|
||||
if (codesObj == null) {
|
||||
return R.error("cardCodes 不能为空");
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> cardCodes = (List<String>) codesObj;
|
||||
if (cardCodes.isEmpty()) {
|
||||
return R.error("cardCodes 不能为空");
|
||||
}
|
||||
if (partnerId == null) {
|
||||
return R.error("partnerId 不能为空");
|
||||
}
|
||||
int success = dayCardPoolService.importCards(cardCodes, partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("successCount", success);
|
||||
data.put("totalCount", cardCodes.size());
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
// ===================== 天卡订单 =====================
|
||||
|
||||
/**
|
||||
* 天卡订单列表
|
||||
*/
|
||||
@GetMapping("/order/list")
|
||||
public R orderList(@RequestParam Map<String, Object> params) {
|
||||
Long partnerId = parsePartnerId(params);
|
||||
PageUtils page = dayCardOrderService.queryPage(params, partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
// ===================== 设备权益 =====================
|
||||
|
||||
/**
|
||||
* 查询设备天卡权益
|
||||
*/
|
||||
@GetMapping("/benefits/{deviceId}")
|
||||
public R deviceBenefits(@PathVariable Long deviceId) {
|
||||
DayCardPoolEntity pool = dayCardOrderService.getDeviceBenefits(deviceId);
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
if (pool != null) {
|
||||
item.put("hasBenefits", true);
|
||||
item.put("thirdCardNo", pool.getThirdCardNo());
|
||||
item.put("cardCode", pool.getCardCode());
|
||||
item.put("expireAt", pool.getExpireAt());
|
||||
} else {
|
||||
item.put("hasBenefits", false);
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", item);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑设备天卡权益(Web端手动操作)
|
||||
* <p>将设备绑定的 USED 卡转为 REUSABLE,释放设备。橘视厅权益保留,后续新设备下单可复用此卡。</p>
|
||||
*/
|
||||
@AuditAction(value = "DAYCARD_DEVICE_UNBIND", description = "解绑设备天卡权益")
|
||||
@PostMapping("/device/unbind")
|
||||
public R unbindDevice(@RequestParam Long deviceId) {
|
||||
dayCardOrderService.unbindDevice(deviceId);
|
||||
return R.ok("设备解绑成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备组织解绑(更换安装场所时调用)
|
||||
* <p>释放设备当前天卡权益并清除门店组织关联(storeOrgId),设备可在新场所用新激活码重新激活绑定。</p>
|
||||
*/
|
||||
@AuditAction(value = "DAYCARD_DEVICE_UNBIND_ORG", description = "设备组织解绑")
|
||||
@PostMapping("/device/unbind-org")
|
||||
public R unbindDeviceOrg(@RequestParam Long deviceId) {
|
||||
dayCardOrderService.unbindDeviceOrg(deviceId);
|
||||
return R.ok("设备组织解绑成功");
|
||||
}
|
||||
|
||||
// ===================== 内部方法 =====================
|
||||
|
||||
private Long parsePartnerId(Map<String, Object> params) {
|
||||
Object val = params.get("partnerId");
|
||||
if (val == null) {
|
||||
val = params.get("partner_id");
|
||||
}
|
||||
if (val == null || "".equals(val)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(val));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private DayCardPoolStatsVo buildStats(Long partnerId) {
|
||||
DayCardPoolStatsVo vo = new DayCardPoolStatsVo();
|
||||
|
||||
int available = dayCardPoolService.getAvailableCount(partnerId);
|
||||
vo.setAvailable(available);
|
||||
|
||||
QueryWrapper<DayCardPoolEntity> reservedWrapper = new QueryWrapper<>();
|
||||
reservedWrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.eq("status", DayCardConstants.POOL_STATUS_RESERVED);
|
||||
if (partnerId != null) {
|
||||
reservedWrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
int reserved = dayCardPoolService.count(reservedWrapper);
|
||||
vo.setReserved(reserved);
|
||||
|
||||
QueryWrapper<DayCardPoolEntity> usedWrapper = new QueryWrapper<>();
|
||||
usedWrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.eq("status", DayCardConstants.POOL_STATUS_USED);
|
||||
if (partnerId != null) {
|
||||
usedWrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
int used = dayCardPoolService.count(usedWrapper);
|
||||
vo.setUsed(used);
|
||||
|
||||
vo.setTotal(available + reserved + used);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.service.DayCardOrderService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡订单 Controller(管理后台用)
|
||||
* <p>App 端接口已迁移至 /api/jst/** (ApiJstController),本 Controller 仅保留管理后台用的分页查询和详情接口</p>
|
||||
*/
|
||||
@Api(tags = "天卡订单")
|
||||
@RestController
|
||||
@RequestMapping("/manage/daycard")
|
||||
public class DayCardOrderController {
|
||||
|
||||
@Autowired
|
||||
private DayCardOrderService dayCardOrderService;
|
||||
|
||||
/**
|
||||
* 分页查询订单
|
||||
*/
|
||||
@ApiOperation("分页查询天卡订单")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
Long partnerId = parsePartnerId(params);
|
||||
PageUtils page = dayCardOrderService.queryPage(params, partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单详情
|
||||
*/
|
||||
@ApiOperation("查询订单详情")
|
||||
@GetMapping("/info/{orderNo}")
|
||||
public R info(@PathVariable("orderNo") String orderNo) {
|
||||
DayCardOrderEntity order = dayCardOrderService.getByOrderNo(orderNo);
|
||||
if (order == null) {
|
||||
throw new ASException("订单不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("order", order);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求参数中解析 partnerId(兼容存量数据查询,新订单 partnerId 为 null)
|
||||
*/
|
||||
private Long parsePartnerId(Map<String, Object> params) {
|
||||
Object val = params.get("partnerId");
|
||||
if (val == null) {
|
||||
val = params.get("partner_id");
|
||||
}
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(val));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
import io.as.modules.daycard.service.DayCardPriceConfigService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡定价策略管理 Controller
|
||||
*/
|
||||
@Api(tags = "天卡定价策略管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/price")
|
||||
public class DayCardPriceConfigController {
|
||||
|
||||
@Autowired
|
||||
private DayCardPriceConfigService dayCardPriceConfigService;
|
||||
|
||||
/**
|
||||
* 分页查询定价策略(支持 partnerId/status 筛选)
|
||||
*/
|
||||
@ApiOperation("分页查询定价策略")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardPriceConfigService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询定价策略详情
|
||||
*/
|
||||
@ApiOperation("定价策略详情")
|
||||
@GetMapping("/info/{id}")
|
||||
public R info(@PathVariable("id") Long id) {
|
||||
DayCardPriceConfigEntity config = dayCardPriceConfigService.getById(id);
|
||||
if (config == null) {
|
||||
throw new ASException("定价策略不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("config", config);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询生效中的定价策略(3 层维度)
|
||||
* <p>优先级:门店配置价 > 渠道配置价 > 平台默认价兜底</p>
|
||||
*
|
||||
* @param channelOrgId 渠道组织ID(可空)
|
||||
* @param storeOrgId 门店组织ID(可空)
|
||||
*/
|
||||
@ApiOperation("查询生效中的定价策略")
|
||||
@GetMapping("/effective")
|
||||
public R effective(@RequestParam(value = "channelOrgId", required = false) Long channelOrgId,
|
||||
@RequestParam(value = "storeOrgId", required = false) Long storeOrgId) {
|
||||
DayCardPriceConfigEntity config = dayCardPriceConfigService.getEffectivePrice(channelOrgId, storeOrgId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("config", config);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增定价策略
|
||||
*/
|
||||
@ApiOperation("新增定价策略")
|
||||
@AuditAction(value = "PRICE_SAVE", description = "新增定价策略")
|
||||
@PostMapping("/save")
|
||||
public R save(@RequestBody DayCardPriceConfigEntity config) {
|
||||
Date now = new Date();
|
||||
config.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
config.setCreatedAt(now);
|
||||
config.setUpdatedAt(now);
|
||||
if (config.getStatus() == null) {
|
||||
config.setStatus("ACTIVE");
|
||||
}
|
||||
dayCardPriceConfigService.save(config);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改定价策略
|
||||
*/
|
||||
@ApiOperation("修改定价策略")
|
||||
@AuditAction(value = "PRICE_UPDATE", description = "修改定价策略")
|
||||
@PostMapping("/update")
|
||||
public R update(@RequestBody DayCardPriceConfigEntity config) {
|
||||
if (config.getId() == null) {
|
||||
throw new ASException("id不能为空");
|
||||
}
|
||||
config.setUpdatedAt(new Date());
|
||||
dayCardPriceConfigService.updateById(config);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除定价策略
|
||||
*/
|
||||
@ApiOperation("删除定价策略")
|
||||
@AuditAction(value = "PRICE_DELETE", description = "删除定价策略")
|
||||
@PostMapping("/delete")
|
||||
public R delete(@RequestBody Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
throw new ASException("ids不能为空");
|
||||
}
|
||||
List<DayCardPriceConfigEntity> list = dayCardPriceConfigService.listByIds(
|
||||
Arrays.asList(ids));
|
||||
Date now = new Date();
|
||||
for (DayCardPriceConfigEntity config : list) {
|
||||
config.setDeleteFlag(DayCardConstants.DELETE_FLAG_DEL);
|
||||
config.setUpdatedAt(now);
|
||||
}
|
||||
dayCardPriceConfigService.updateBatchById(list);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.DayCardRefundEntity;
|
||||
import io.as.modules.daycard.service.DayCardRefundService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡退款管理 Controller
|
||||
*/
|
||||
@Api(tags = "天卡退款管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/refund")
|
||||
public class DayCardRefundController {
|
||||
|
||||
@Autowired
|
||||
private DayCardRefundService dayCardRefundService;
|
||||
|
||||
/**
|
||||
* 分页查询退款记录
|
||||
*/
|
||||
@ApiOperation("分页查询退款记录")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardRefundService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款详情
|
||||
*/
|
||||
@ApiOperation("退款详情")
|
||||
@GetMapping("/info/{refundNo}")
|
||||
public R info(@PathVariable("refundNo") String refundNo) {
|
||||
DayCardRefundEntity refund = dayCardRefundService.getByRefundNo(refundNo);
|
||||
if (refund == null) {
|
||||
throw new ASException("退款记录不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("refund", refund);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起退款
|
||||
*
|
||||
* @param params 包含 orderNo(订单号)和 reason(退款原因)
|
||||
*/
|
||||
@ApiOperation("发起退款")
|
||||
@AuditAction(value = "REFUND_APPLY", description = "发起退款")
|
||||
@PostMapping("/apply")
|
||||
public R apply(@RequestBody Map<String, String> params) {
|
||||
String orderNo = params.get("orderNo");
|
||||
if (StringUtils.isBlank(orderNo)) {
|
||||
throw new ASException("orderNo不能为空");
|
||||
}
|
||||
String reason = params.get("reason");
|
||||
DayCardRefundEntity refund = dayCardRefundService.refund(orderNo, reason);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("refund", refund);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRecordEntity;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRuleEntity;
|
||||
import io.as.modules.daycard.service.DayCardSettleOrderService;
|
||||
import io.as.modules.daycard.service.DayCardSettleRecordService;
|
||||
import io.as.modules.daycard.service.DayCardSettleRuleService;
|
||||
import io.as.modules.daycard.service.ProfitCalcService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账管理 Controller
|
||||
*/
|
||||
@Api(tags = "天卡分账管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/settle")
|
||||
public class DayCardSettleController {
|
||||
|
||||
@Autowired
|
||||
private DayCardSettleRuleService dayCardSettleRuleService;
|
||||
|
||||
@Autowired
|
||||
private DayCardSettleRecordService dayCardSettleRecordService;
|
||||
|
||||
@Autowired
|
||||
private DayCardSettleOrderService dayCardSettleOrderService;
|
||||
|
||||
@Autowired
|
||||
private ProfitCalcService profitCalcService;
|
||||
|
||||
// ==================== 分账规则 ====================
|
||||
|
||||
/**
|
||||
* 分页查询分账规则
|
||||
*/
|
||||
@ApiOperation("分页查询分账规则")
|
||||
@GetMapping("/rule/list")
|
||||
public R ruleList(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardSettleRuleService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账规则详情
|
||||
*/
|
||||
@ApiOperation("分账规则详情")
|
||||
@GetMapping("/rule/info/{id}")
|
||||
public R ruleInfo(@PathVariable("id") Long id) {
|
||||
DayCardSettleRuleEntity rule = dayCardSettleRuleService.getById(id);
|
||||
if (rule == null) {
|
||||
throw new ASException("分账规则不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("rule", rule);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分账规则
|
||||
*/
|
||||
@ApiOperation("新增分账规则")
|
||||
@PostMapping("/rule/save")
|
||||
public R ruleSave(@RequestBody DayCardSettleRuleEntity rule) {
|
||||
if (rule.getName() == null || rule.getName().trim().isEmpty()) {
|
||||
throw new ASException("规则名称不能为空");
|
||||
}
|
||||
if (rule.getShareRatio() == null) {
|
||||
throw new ASException("分账比例不能为空");
|
||||
}
|
||||
dayCardSettleRuleService.saveRule(rule);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("rule", rule);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改分账规则
|
||||
*/
|
||||
@ApiOperation("修改分账规则")
|
||||
@PostMapping("/rule/update")
|
||||
public R ruleUpdate(@RequestBody DayCardSettleRuleEntity rule) {
|
||||
if (rule.getId() == null) {
|
||||
throw new ASException("id不能为空");
|
||||
}
|
||||
dayCardSettleRuleService.update(rule);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分账规则(软删除)
|
||||
*/
|
||||
@ApiOperation("删除分账规则")
|
||||
@PostMapping("/rule/delete")
|
||||
public R ruleDelete(@RequestBody Long[] ids) {
|
||||
dayCardSettleRuleService.delete(ids);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== 分账记录 ====================
|
||||
|
||||
/**
|
||||
* 分页查询分账记录
|
||||
*/
|
||||
@ApiOperation("分页查询分账记录")
|
||||
@GetMapping("/record/list")
|
||||
public R recordList(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardSettleRecordService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账记录详情
|
||||
*/
|
||||
@ApiOperation("分账记录详情")
|
||||
@GetMapping("/record/info/{id}")
|
||||
public R recordInfo(@PathVariable("id") Long id) {
|
||||
DayCardSettleRecordEntity record = dayCardSettleRecordService.getById(id);
|
||||
if (record == null) {
|
||||
throw new ASException("分账记录不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("record", record);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
// ==================== 分账单 ====================
|
||||
|
||||
/**
|
||||
* 分页查询分账单
|
||||
*/
|
||||
@ApiOperation("分页查询分账单")
|
||||
@GetMapping("/order/list")
|
||||
public R orderList(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardSettleOrderService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账单详情
|
||||
*/
|
||||
@ApiOperation("分账单详情")
|
||||
@GetMapping("/order/info/{id}")
|
||||
public R orderInfo(@PathVariable("id") Long id) {
|
||||
DayCardSettleOrderEntity settleOrder = dayCardSettleOrderService.getById(id);
|
||||
if (settleOrder == null) {
|
||||
throw new ASException("分账单不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("settleOrder", settleOrder);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
// ==================== 试算 ====================
|
||||
|
||||
/**
|
||||
* 分润试算
|
||||
*/
|
||||
@ApiOperation("分润试算")
|
||||
@GetMapping("/trial")
|
||||
public R trial(@RequestParam("partnerId") Long partnerId,
|
||||
@RequestParam(value = "channelOrgId", required = false) Long channelOrgId,
|
||||
@RequestParam(value = "storeOrgId", required = false) Long storeOrgId,
|
||||
@RequestParam("totalAmount") BigDecimal totalAmount) {
|
||||
if (partnerId == null) {
|
||||
throw new ASException("partnerId不能为空");
|
||||
}
|
||||
if (totalAmount == null || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new ASException("totalAmount必须大于0");
|
||||
}
|
||||
Map<String, Object> result = profitCalcService.calculate(partnerId, channelOrgId, storeOrgId, totalAmount);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", result);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.OrgSettlementAccountEntity;
|
||||
import io.as.modules.daycard.service.OrgSettlementAccountService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 组织分账接收方 Controller
|
||||
*/
|
||||
@Api(tags = "组织分账接收方管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/profit-receiver")
|
||||
public class OrgSettlementAccountController {
|
||||
|
||||
@Autowired
|
||||
private OrgSettlementAccountService orgSettlementAccountService;
|
||||
|
||||
/**
|
||||
* 分页查询分账接收方
|
||||
*/
|
||||
@ApiOperation("分页查询分账接收方")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = orgSettlementAccountService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账接收方详情
|
||||
*/
|
||||
@ApiOperation("分账接收方详情")
|
||||
@GetMapping("/info/{id}")
|
||||
public R info(@PathVariable("id") Long id) {
|
||||
OrgSettlementAccountEntity account = orgSettlementAccountService.getById(id);
|
||||
if (account == null) {
|
||||
throw new ASException("分账接收方不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("account", account);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分账接收方
|
||||
*/
|
||||
@ApiOperation("新增分账接收方")
|
||||
@AuditAction(value = "PROFIT_RECEIVER_SAVE", description = "新增分账接收方")
|
||||
@PostMapping("/save")
|
||||
public R save(@RequestBody OrgSettlementAccountEntity account) {
|
||||
if (account.getOrgId() == null) {
|
||||
throw new ASException("组织ID不能为空");
|
||||
}
|
||||
if (account.getReceiverAccount() == null || account.getReceiverAccount().trim().isEmpty()) {
|
||||
throw new ASException("接收方账号不能为空");
|
||||
}
|
||||
if (account.getReceiverType() == null || account.getReceiverType().trim().isEmpty()) {
|
||||
throw new ASException("接收方类型不能为空");
|
||||
}
|
||||
orgSettlementAccountService.saveAccount(account);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("account", account);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分账接收方(软删除)
|
||||
*/
|
||||
@ApiOperation("删除分账接收方")
|
||||
@AuditAction(value = "PROFIT_RECEIVER_DELETE", description = "删除分账接收方")
|
||||
@PostMapping("/delete")
|
||||
public R delete(@RequestBody Long[] ids) {
|
||||
orgSettlementAccountService.delete(ids);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步接收方到微信
|
||||
*/
|
||||
@ApiOperation("同步接收方到微信")
|
||||
@AuditAction(value = "PROFIT_RECEIVER_SYNC", description = "同步分账接收方到微信")
|
||||
@PostMapping("/sync/{id}")
|
||||
public R sync(@PathVariable("id") Long id) {
|
||||
orgSettlementAccountService.syncToWechat(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.ProfitSharingDetailDao;
|
||||
import io.as.modules.daycard.dao.ProfitSharingOrderDao;
|
||||
import io.as.modules.daycard.entity.ProfitSharingDetailEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingOrderEntity;
|
||||
import io.as.modules.sys.dao.SysOrgDao;
|
||||
import io.as.modules.sys.entity.SysOrgEntity;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 分账记录查询 Controller
|
||||
*/
|
||||
@Api(tags = "分账记录查询")
|
||||
@RestController
|
||||
@RequestMapping("/manage/profit-sharing")
|
||||
public class ProfitSharingQueryController {
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingOrderDao profitSharingOrderDao;
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingDetailDao profitSharingDetailDao;
|
||||
|
||||
@Autowired
|
||||
private SysOrgDao sysOrgDao;
|
||||
|
||||
/**
|
||||
* 分页查询分账单(支持 orderNo/status 筛选),返回详情列表
|
||||
*/
|
||||
@ApiOperation("分页查询分账单")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
IPage<ProfitSharingOrderEntity> page = new Query<ProfitSharingOrderEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<ProfitSharingOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
String orderNo = (String) params.get("orderNo");
|
||||
if (StringUtils.isNotBlank(orderNo)) {
|
||||
wrapper.eq("pay_order_no", orderNo);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
String outOrderNo = (String) params.get("outOrderNo");
|
||||
if (StringUtils.isNotBlank(outOrderNo)) {
|
||||
wrapper.eq("out_order_no", outOrderNo);
|
||||
}
|
||||
|
||||
IPage<ProfitSharingOrderEntity> result = profitSharingOrderDao.selectPage(page, wrapper);
|
||||
|
||||
// 填充明细列表
|
||||
for (ProfitSharingOrderEntity order : result.getRecords()) {
|
||||
QueryWrapper<ProfitSharingDetailEntity> detailWrapper = new QueryWrapper<>();
|
||||
detailWrapper.eq("sharing_order_id", order.getId());
|
||||
List<ProfitSharingDetailEntity> details = profitSharingDetailDao.selectList(detailWrapper);
|
||||
order.setDetails(details);
|
||||
}
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", new PageUtils(result));
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账单详情 + details 子表
|
||||
*/
|
||||
@ApiOperation("分账单详情")
|
||||
@GetMapping("/info/{id}")
|
||||
public R info(@PathVariable("id") Long id) {
|
||||
ProfitSharingOrderEntity order = profitSharingOrderDao.selectById(id);
|
||||
if (order == null) {
|
||||
throw new ASException("分账单不存在");
|
||||
}
|
||||
QueryWrapper<ProfitSharingDetailEntity> detailWrapper = new QueryWrapper<>();
|
||||
detailWrapper.eq("sharing_order_id", id);
|
||||
List<ProfitSharingDetailEntity> details = profitSharingDetailDao.selectList(detailWrapper);
|
||||
order.setDetails(details);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("order", order);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计(参数 startDate, endDate, groupBy)
|
||||
* 返回: { summary: {totalCount, totalAmount, successCount, successAmount}, groups: [{orgId, orgName, orgType, count, amount}] }
|
||||
*/
|
||||
@ApiOperation("分账统计")
|
||||
@GetMapping("/stats")
|
||||
public R stats(@RequestParam(value = "startDate", required = false) String startDate,
|
||||
@RequestParam(value = "endDate", required = false) String endDate,
|
||||
@RequestParam(value = "groupBy", required = false) String groupBy) {
|
||||
QueryWrapper<ProfitSharingOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
if (StringUtils.isNotBlank(startDate)) {
|
||||
wrapper.ge("created_at", startDate + " 00:00:00");
|
||||
}
|
||||
if (StringUtils.isNotBlank(endDate)) {
|
||||
wrapper.le("created_at", endDate + " 23:59:59");
|
||||
}
|
||||
|
||||
List<ProfitSharingOrderEntity> orders = profitSharingOrderDao.selectList(wrapper);
|
||||
|
||||
// 汇总
|
||||
int totalCount = orders.size();
|
||||
int totalAmount = 0;
|
||||
int successCount = 0;
|
||||
int successAmount = 0;
|
||||
for (ProfitSharingOrderEntity order : orders) {
|
||||
if (order.getTotalAmount() != null) {
|
||||
totalAmount += order.getTotalAmount();
|
||||
}
|
||||
if (DayCardConstants.SETTLE_STATUS_SUCCESS.equals(order.getStatus())) {
|
||||
successCount++;
|
||||
if (order.getTotalAmount() != null) {
|
||||
successAmount += order.getTotalAmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> summary = new HashMap<>();
|
||||
summary.put("totalCount", totalCount);
|
||||
summary.put("totalAmount", totalAmount);
|
||||
summary.put("successCount", successCount);
|
||||
summary.put("successAmount", successAmount);
|
||||
|
||||
// 分组统计
|
||||
List<Map<String, Object>> groups = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(groupBy) && "org".equals(groupBy)) {
|
||||
// 按组织分组:查询所有明细,按 orgId 聚合
|
||||
Map<Long, Map<String, Object>> orgMap = new HashMap<>();
|
||||
for (ProfitSharingOrderEntity order : orders) {
|
||||
QueryWrapper<ProfitSharingDetailEntity> detailWrapper = new QueryWrapper<>();
|
||||
detailWrapper.eq("sharing_order_id", order.getId());
|
||||
List<ProfitSharingDetailEntity> details = profitSharingDetailDao.selectList(detailWrapper);
|
||||
for (ProfitSharingDetailEntity detail : details) {
|
||||
Long orgId = detail.getOrgId();
|
||||
if (orgId == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> group = orgMap.get(orgId);
|
||||
if (group == null) {
|
||||
group = new HashMap<>();
|
||||
group.put("orgId", orgId);
|
||||
group.put("count", 0);
|
||||
group.put("amount", 0);
|
||||
group.put("orgRole", detail.getOrgRole());
|
||||
// 查询组织名称
|
||||
SysOrgEntity org = sysOrgDao.selectById(orgId);
|
||||
if (org != null) {
|
||||
group.put("orgName", org.getName());
|
||||
group.put("orgType", org.getType());
|
||||
}
|
||||
orgMap.put(orgId, group);
|
||||
}
|
||||
group.put("count", (Integer) group.get("count") + 1);
|
||||
if (detail.getAmount() != null) {
|
||||
group.put("amount", (Integer) group.get("amount") + detail.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
groups.addAll(orgMap.values());
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("summary", summary);
|
||||
result.put("groups", groups);
|
||||
return R.dataSuccess(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleItemEntity;
|
||||
import io.as.modules.daycard.service.ProfitSharingRuleService;
|
||||
import io.as.modules.sys.entity.SysOrgEntity;
|
||||
import io.as.modules.sys.service.SysOrgService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分润规则 Controller
|
||||
*/
|
||||
@Api(tags = "分润规则管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/profit-rule")
|
||||
public class ProfitSharingRuleController {
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingRuleService profitSharingRuleService;
|
||||
|
||||
@Autowired
|
||||
private SysOrgService sysOrgService;
|
||||
|
||||
/**
|
||||
* 分页查询分润规则
|
||||
*/
|
||||
@ApiOperation("分页查询分润规则")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = profitSharingRuleService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分润规则详情(含 items 子表)
|
||||
*/
|
||||
@ApiOperation("分润规则详情")
|
||||
@GetMapping("/info/{id}")
|
||||
public R info(@PathVariable("id") Long id) {
|
||||
ProfitSharingRuleEntity rule = profitSharingRuleService.getInfo(id);
|
||||
if (rule == null) {
|
||||
throw new ASException("分润规则不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("rule", rule);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分润规则
|
||||
*/
|
||||
@ApiOperation("新增分润规则")
|
||||
@AuditAction(value = "PROFIT_RULE_SAVE", description = "新增分润规则")
|
||||
@PostMapping("/save")
|
||||
public R save(@RequestBody ProfitSharingRuleEntity rule) {
|
||||
if (StringUtils.isBlank(rule.getRuleName())) {
|
||||
throw new ASException("规则名称不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(rule.getScopeType())) {
|
||||
throw new ASException("范围类型不能为空");
|
||||
}
|
||||
profitSharingRuleService.saveRule(rule);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("rule", rule);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改分润规则
|
||||
*/
|
||||
@ApiOperation("修改分润规则")
|
||||
@AuditAction(value = "PROFIT_RULE_UPDATE", description = "修改分润规则")
|
||||
@PostMapping("/update")
|
||||
public R update(@RequestBody ProfitSharingRuleEntity rule) {
|
||||
if (rule.getId() == null) {
|
||||
throw new ASException("id不能为空");
|
||||
}
|
||||
profitSharingRuleService.updateRule(rule);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分润规则(逻辑删除)
|
||||
*/
|
||||
@ApiOperation("删除分润规则")
|
||||
@AuditAction(value = "PROFIT_RULE_DELETE", description = "删除分润规则")
|
||||
@PostMapping("/delete")
|
||||
public R delete(@RequestBody Map<String, Object> body) {
|
||||
Long id = Long.parseLong(body.get("id").toString());
|
||||
profitSharingRuleService.delete(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 试算:根据门店和金额预览分润结果
|
||||
*/
|
||||
@ApiOperation("分润试算")
|
||||
@AuditAction(value = "PROFIT_RULE_PREVIEW", description = "分润试算")
|
||||
@PostMapping("/preview")
|
||||
public R preview(@RequestBody Map<String, Object> body) {
|
||||
Long storeOrgId = Long.parseLong(body.get("storeOrgId").toString());
|
||||
Long amount = Long.parseLong(body.get("amount").toString());
|
||||
|
||||
// 1. 从 storeOrgId 查 sys_org 获取 mpath,解析出 channelOrgId 和 agentOrgId
|
||||
SysOrgEntity storeOrg = sysOrgService.getById(storeOrgId);
|
||||
if (storeOrg == null) {
|
||||
throw new ASException("门店组织不存在");
|
||||
}
|
||||
String mpath = storeOrg.getMpath();
|
||||
if (StringUtils.isBlank(mpath)) {
|
||||
throw new ASException("门店组织 mpath 为空");
|
||||
}
|
||||
String[] segments = mpath.split("\\.");
|
||||
// segments[0] = PLATFORM, segments[1] = CHANNEL, segments[n-1] = STORE
|
||||
Long platformOrgId = null;
|
||||
Long channelOrgId = null;
|
||||
Long agentOrgId = null;
|
||||
if (segments.length >= 1) {
|
||||
platformOrgId = Long.parseLong(segments[0]);
|
||||
}
|
||||
if (segments.length >= 2) {
|
||||
channelOrgId = Long.parseLong(segments[1]);
|
||||
}
|
||||
// segments.length >= 4 表示路径中有代理商(PLATFORM.CHANNEL.AGENT.STORE)
|
||||
if (segments.length >= 4) {
|
||||
agentOrgId = Long.parseLong(segments[segments.length - 2]);
|
||||
}
|
||||
|
||||
// 2. 调用 matchRule 匹配规则
|
||||
ProfitSharingRuleEntity rule = profitSharingRuleService.matchRule(storeOrgId, agentOrgId, channelOrgId);
|
||||
if (rule == null) {
|
||||
throw new ASException("未匹配到分润规则");
|
||||
}
|
||||
|
||||
// 3. 按 items 计算各方分账金额
|
||||
List<ProfitSharingRuleItemEntity> items = rule.getItems();
|
||||
if (items == null || items.isEmpty()) {
|
||||
throw new ASException("分润规则未配置明细");
|
||||
}
|
||||
|
||||
// 按 sortOrder 排序
|
||||
items.sort((a, b) -> {
|
||||
int sa = a.getSortOrder() == null ? 0 : a.getSortOrder();
|
||||
int sb = b.getSortOrder() == null ? 0 : b.getSortOrder();
|
||||
return Integer.compare(sa, sb);
|
||||
});
|
||||
|
||||
// orgRole -> orgId 映射
|
||||
Map<String, Long> orgIdMap = new HashMap<>();
|
||||
orgIdMap.put(DayCardConstants.ORG_ROLE_PLATFORM, platformOrgId);
|
||||
orgIdMap.put(DayCardConstants.ORG_ROLE_CHANNEL, channelOrgId);
|
||||
orgIdMap.put(DayCardConstants.ORG_ROLE_AGENT, agentOrgId);
|
||||
orgIdMap.put(DayCardConstants.ORG_ROLE_STORE, storeOrgId);
|
||||
|
||||
// 找到最后一个 PLATFORM 项(尾差归平台)
|
||||
int lastPlatformIndex = -1;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (DayCardConstants.ORG_ROLE_PLATFORM.equals(items.get(i).getOrgRole())) {
|
||||
lastPlatformIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算非平台方 RATIO 比例之和
|
||||
BigDecimal nonPlatformTotalRatio = BigDecimal.ZERO;
|
||||
|
||||
// 计算各方分账金额
|
||||
List<Map<String, Object>> details = new ArrayList<>();
|
||||
long allocatedTotal = 0;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
ProfitSharingRuleItemEntity item = items.get(i);
|
||||
Map<String, Object> detail = new HashMap<>();
|
||||
detail.put("orgRole", item.getOrgRole());
|
||||
detail.put("shareType", item.getShareType());
|
||||
detail.put("shareValue", item.getShareValue());
|
||||
|
||||
Long orgId = orgIdMap.get(item.getOrgRole());
|
||||
detail.put("orgId", orgId);
|
||||
|
||||
// 查询组织名称
|
||||
String orgName = null;
|
||||
if (orgId != null) {
|
||||
SysOrgEntity org = sysOrgService.getById(orgId);
|
||||
if (org != null) {
|
||||
orgName = org.getName();
|
||||
}
|
||||
}
|
||||
detail.put("orgName", orgName);
|
||||
|
||||
long itemAmount;
|
||||
if (i == lastPlatformIndex) {
|
||||
// 最后一方 PLATFORM 用减法(尾差归平台)
|
||||
itemAmount = amount - allocatedTotal;
|
||||
} else if (DayCardConstants.SHARE_TYPE_FIXED.equals(item.getShareType())) {
|
||||
// FIXED: shareValue 直接作为分
|
||||
itemAmount = item.getShareValue().longValue();
|
||||
allocatedTotal += itemAmount;
|
||||
} else {
|
||||
// RATIO: amount * shareValue / 100
|
||||
BigDecimal ratioAmount = BigDecimal.valueOf(amount)
|
||||
.multiply(item.getShareValue())
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.DOWN);
|
||||
itemAmount = ratioAmount.longValue();
|
||||
allocatedTotal += itemAmount;
|
||||
}
|
||||
|
||||
// 统计非平台方 RATIO 比例之和
|
||||
if (!DayCardConstants.ORG_ROLE_PLATFORM.equals(item.getOrgRole())
|
||||
&& DayCardConstants.SHARE_TYPE_RATIO.equals(item.getShareType())
|
||||
&& item.getShareValue() != null) {
|
||||
nonPlatformTotalRatio = nonPlatformTotalRatio.add(item.getShareValue());
|
||||
}
|
||||
|
||||
detail.put("amount", itemAmount);
|
||||
details.add(detail);
|
||||
}
|
||||
|
||||
// 5. warning 非 null 时表示超30%
|
||||
String warning = null;
|
||||
if (nonPlatformTotalRatio.compareTo(BigDecimal.valueOf(DayCardConstants.PROFIT_SHARE_LIMIT)) > 0) {
|
||||
warning = "非平台方分润比例之和" + nonPlatformTotalRatio + "%超过上限" + DayCardConstants.PROFIT_SHARE_LIMIT + "%";
|
||||
}
|
||||
|
||||
// 4. 返回结果
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("ruleId", rule.getId());
|
||||
result.put("ruleName", rule.getRuleName());
|
||||
result.put("details", details);
|
||||
result.put("nonPlatformTotalRatio", nonPlatformTotalRatio);
|
||||
result.put("warning", warning);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", result);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardActivationCodeEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡设备激活码 DAO
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardActivationCodeDao extends BaseMapper<DayCardActivationCodeEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardImportBatchEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡导入批次
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardImportBatchDao extends BaseMapper<DayCardImportBatchEntity> {
|
||||
}
|
||||
12
src/main/java/io/as/modules/daycard/dao/DayCardOrderDao.java
Normal file
12
src/main/java/io/as/modules/daycard/dao/DayCardOrderDao.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡订单
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardOrderDao extends BaseMapper<DayCardOrderEntity> {
|
||||
}
|
||||
22
src/main/java/io/as/modules/daycard/dao/DayCardPoolDao.java
Normal file
22
src/main/java/io/as/modules/daycard/dao/DayCardPoolDao.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 天卡池
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardPoolDao extends BaseMapper<DayCardPoolEntity> {
|
||||
|
||||
/**
|
||||
* 使用 SELECT ... FOR UPDATE SKIP LOCKED 抢占一张未使用的天卡,
|
||||
* 避免多线程/多实例并发下单时分配到同一张卡。
|
||||
* <p>天卡池为通用池,不再按 partner 过滤</p>
|
||||
*
|
||||
* @return 被当前事务锁定的可用天卡行,无可用时返回 null
|
||||
*/
|
||||
DayCardPoolEntity selectOneForUpdateSkipLocked();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡定价策略 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardPriceConfigDao extends BaseMapper<DayCardPriceConfigEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardProfitReceiverEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分账接收方 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardProfitReceiverDao extends BaseMapper<DayCardProfitReceiverEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardProfitRuleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分润规则 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardProfitRuleDao extends BaseMapper<DayCardProfitRuleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardRefundEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡退款单 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardRefundDao extends BaseMapper<DayCardRefundEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分账单 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardSettleOrderDao extends BaseMapper<DayCardSettleOrderEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRecordEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分账记录 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardSettleRecordDao extends BaseMapper<DayCardSettleRecordEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRuleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分账规则 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardSettleRuleDao extends BaseMapper<DayCardSettleRuleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.OrgSettlementAccountEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 组织分账接收方 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface OrgSettlementAccountDao extends BaseMapper<OrgSettlementAccountEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.ProfitSharingDetailEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 分账明细 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProfitSharingDetailDao extends BaseMapper<ProfitSharingDetailEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.ProfitSharingOrderEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 分账单 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProfitSharingOrderDao extends BaseMapper<ProfitSharingOrderEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 分润规则 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProfitSharingRuleDao extends BaseMapper<ProfitSharingRuleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleItemEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 分润规则明细 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProfitSharingRuleItemDao extends BaseMapper<ProfitSharingRuleItemEntity> {
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡设备激活码
|
||||
* <p>管理后台按门店生成,App 端 init 时输入激活码完成设备注册绑定</p>
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_activation_code")
|
||||
public class DayCardActivationCodeEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 激活码(全局唯一) */
|
||||
private String code;
|
||||
|
||||
/** 渠道组织ID */
|
||||
private Long channelOrgId;
|
||||
|
||||
/** 门店组织ID */
|
||||
private Long storeOrgId;
|
||||
|
||||
/** 状态:ACTIVE/DISABLED */
|
||||
private String status;
|
||||
|
||||
/** 最大可激活设备数(0=不限) */
|
||||
private Integer maxDeviceCount;
|
||||
|
||||
/** 已激活设备数 */
|
||||
private Integer activatedCount;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 删除标记:UNDEL/DEL */
|
||||
private String deleteFlag;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡导入批次
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_import_batch")
|
||||
public class DayCardImportBatchEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 批次号 */
|
||||
private String batchNo;
|
||||
/** 合作商ID */
|
||||
private Long partnerId;
|
||||
/** 状态:PROCESSING/SUCCEEDED/PARTIAL_FAILED/FAILED */
|
||||
private String status;
|
||||
/** 总数 */
|
||||
private Integer totalCount;
|
||||
/** 成功数 */
|
||||
private Integer successCount;
|
||||
/** 失败数 */
|
||||
private Integer failCount;
|
||||
/** 结果报告URL */
|
||||
private String resultReportUrl;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
/** 创建人 */
|
||||
private Long createdBy;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡订单
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_order")
|
||||
public class DayCardOrderEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 订单号 */
|
||||
private String orderNo;
|
||||
/** 合作商ID */
|
||||
private Long partnerId;
|
||||
/** 设备ID */
|
||||
private Long deviceId;
|
||||
/** 门店组织ID */
|
||||
private Long storeOrgId;
|
||||
/** 渠道组织ID */
|
||||
private Long channelOrgId;
|
||||
/** 支付后写入的卡号 */
|
||||
private String cardNo;
|
||||
/** 订单状态:CREATED/PAYING/PAID/PAY_FAILED/CANCELLED */
|
||||
private String status;
|
||||
/** 订单金额 */
|
||||
private BigDecimal amount;
|
||||
/** 支付单号 */
|
||||
private String payNo;
|
||||
/** 支付金额 */
|
||||
private BigDecimal payAmount;
|
||||
/** 支付时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date payTime;
|
||||
/** 订单过期时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date expireTime;
|
||||
/** 取消时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date cancelTime;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
/** 支付渠道 MOCK/WECHAT */
|
||||
private String payChannel;
|
||||
/** 微信预支付ID */
|
||||
private String prepayId;
|
||||
/** 微信扫码支付链接 */
|
||||
private String codeUrl;
|
||||
/** 设备SN(冗余,便于排查) */
|
||||
private String deviceSn;
|
||||
/** 设备MAC(冗余) */
|
||||
private String mac;
|
||||
/** 代理商组织ID */
|
||||
private Long agentOrgId;
|
||||
/** 组织链路快照(如 1.5.12.) */
|
||||
private String orgPathSnapshot;
|
||||
/** 微信支付订单号 */
|
||||
private String channelOrderNo;
|
||||
|
||||
/** 合作商名称(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private String partnerName;
|
||||
/** 设备MAC地址(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private String deviceMac;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡池
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_pool")
|
||||
public class DayCardPoolEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 卡密 */
|
||||
private String cardCode;
|
||||
/** 第三方引用标识 */
|
||||
private String thirdPartyRef;
|
||||
/** 第三方卡号 */
|
||||
private String thirdCardNo;
|
||||
/** 卡类型,默认DAY */
|
||||
private String cardType;
|
||||
/** 合作商ID */
|
||||
private Long partnerId;
|
||||
/** 状态:UNUSED/RESERVED/USED */
|
||||
private String status;
|
||||
/** 激活状态:PENDING/SUCCESS/FAILED */
|
||||
private String activateStatus;
|
||||
/** 激活失败原因 */
|
||||
private String activateError;
|
||||
/** 激活成功时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date activatedAt;
|
||||
/** 关联订单号 */
|
||||
private String orderNo;
|
||||
/** 设备ID */
|
||||
private Long deviceId;
|
||||
/** 导入批次ID */
|
||||
private Long importBatchId;
|
||||
/** 卡过期时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date expireAt;
|
||||
/** 使用时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date usedAt;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡定价策略
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_price_config")
|
||||
public class DayCardPriceConfigEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 渠道组织ID(可空=全渠道默认) */
|
||||
private Long channelOrgId;
|
||||
/** 门店组织ID(空=渠道级或平台级) */
|
||||
private Long storeOrgId;
|
||||
/** 天卡价格(元) */
|
||||
private BigDecimal price;
|
||||
/** 策略名称 */
|
||||
private String name;
|
||||
/** 状态 ACTIVE/DISABLED */
|
||||
private String status;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分账接收方
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_profit_receiver")
|
||||
public class DayCardProfitReceiverEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 名称 */
|
||||
private String name;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 接收方类型 MERCHANT_ID/PERSONAL_OPENID */
|
||||
private String receiverType;
|
||||
/** 分账接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 微信绑定状态 BINDED/UNBOUND */
|
||||
private String wxBindStatus;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分润规则
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_profit_rule")
|
||||
public class DayCardProfitRuleEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 规则名称 */
|
||||
private String name;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 渠道组织ID */
|
||||
private Long channelOrgId;
|
||||
/** 门店组织ID(空=渠道级或合作商级) */
|
||||
private Long storeOrgId;
|
||||
/** 接收方类型 MERCHANT_ID/PERSONAL_OPENID */
|
||||
private String receiverType;
|
||||
/** 分账接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 分润比例(如0.3000=30%)或固定金额 */
|
||||
private BigDecimal shareRatio;
|
||||
/** 分润类型 RATIO/FIXED */
|
||||
private String shareType;
|
||||
/** 优先级(数字越大优先级越高) */
|
||||
private Integer priority;
|
||||
/** 状态 ACTIVE/DISABLED */
|
||||
private String status;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡退款单
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_refund")
|
||||
public class DayCardRefundEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 退款单号 */
|
||||
private String refundNo;
|
||||
/** 关联订单号 */
|
||||
private String orderNo;
|
||||
/** 微信退款单号 */
|
||||
private String wxRefundId;
|
||||
/** 退款金额 */
|
||||
private BigDecimal refundAmount;
|
||||
/** 退款原因 */
|
||||
private String reason;
|
||||
/** 状态 PENDING/PROCESSING/SUCCESS/FAILED/CLOSED */
|
||||
private String status;
|
||||
/** 错误信息 */
|
||||
private String errorMessage;
|
||||
/** 退款完成时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date settledAt;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分账单
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_settle_order")
|
||||
public class DayCardSettleOrderEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 分账单号 */
|
||||
private String settleNo;
|
||||
/** 订单号 */
|
||||
private String orderNo;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 分润规则ID */
|
||||
private Long ruleId;
|
||||
/** 接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 分润比例/金额 */
|
||||
private BigDecimal shareRatio;
|
||||
/** 分润类型 RATIO/FIXED */
|
||||
private String shareType;
|
||||
/** 分账金额 */
|
||||
private BigDecimal settleAmount;
|
||||
/** 订单总金额 */
|
||||
private BigDecimal totalAmount;
|
||||
/** 状态 PENDING/PROCESSING/SUCCESS/FAILED */
|
||||
private String status;
|
||||
/** 错误信息 */
|
||||
private String errorMessage;
|
||||
/** 分账完成时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date settledAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分账记录
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_settle_record")
|
||||
public class DayCardSettleRecordEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 订单号 */
|
||||
private String orderNo;
|
||||
/** 分账规则ID */
|
||||
private Long settleRuleId;
|
||||
/** 订单总金额 */
|
||||
private BigDecimal totalAmount;
|
||||
/** 分账比例 */
|
||||
private BigDecimal shareRatio;
|
||||
/** 分账金额 */
|
||||
private BigDecimal settleAmount;
|
||||
/** 接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 状态 PENDING/SUCCESS/FAILED */
|
||||
private String status;
|
||||
/** 错误信息 */
|
||||
private String errorMessage;
|
||||
/** 分账完成时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date settledAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分账规则
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_settle_rule")
|
||||
public class DayCardSettleRuleEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 规则名称 */
|
||||
private String name;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 渠道组织ID */
|
||||
private Long channelOrgId;
|
||||
/** 门店组织ID(空=渠道级) */
|
||||
private Long storeOrgId;
|
||||
/** 分账接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 分账比例(如0.3000=30%) */
|
||||
private BigDecimal shareRatio;
|
||||
/** 接收方类型 MERCHANT_ID/PERSONAL_OPENID */
|
||||
private String receiverType;
|
||||
/** 状态 ACTIVE/DISABLED */
|
||||
private String status;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 组织分账接收方
|
||||
*/
|
||||
@Data
|
||||
@TableName("org_settlement_account")
|
||||
public class OrgSettlementAccountEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 关联组织 */
|
||||
private Long orgId;
|
||||
/** 支付渠道 */
|
||||
private String payChannel;
|
||||
/** 接收方类型 MERCHANT_ID / PERSONAL_OPENID */
|
||||
private String receiverType;
|
||||
/** 接收方账号(商户号或OpenID) */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 绑定状态 PENDING/ACTIVE/FAILED */
|
||||
private String bindStatus;
|
||||
/** 微信侧绑定ID */
|
||||
private String externalBindId;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
|
||||
/** 组织名称(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private String orgName;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 分账明细
|
||||
*/
|
||||
@Data
|
||||
@TableName("profit_sharing_detail")
|
||||
public class ProfitSharingDetailEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 关联分账单 */
|
||||
private Long sharingOrderId;
|
||||
/** 接收方组织ID */
|
||||
private Long orgId;
|
||||
/** 组织角色 PLATFORM/CHANNEL/AGENT/STORE */
|
||||
private String orgRole;
|
||||
/** 接收方账号 */
|
||||
private String receiverAccount;
|
||||
/** 接收方类型 */
|
||||
private String receiverType;
|
||||
/** 分账金额(分) */
|
||||
private Integer amount;
|
||||
/** 分账描述 */
|
||||
private String description;
|
||||
/** 状态 PENDING/SUCCESS/FAILED */
|
||||
private String status;
|
||||
/** 微信侧分账单号 */
|
||||
private String externalOrderId;
|
||||
/** 成功时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date successTime;
|
||||
/** 失败原因 */
|
||||
private String failReason;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分账单
|
||||
*/
|
||||
@Data
|
||||
@TableName("profit_sharing_order")
|
||||
public class ProfitSharingOrderEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 关联支付订单号 */
|
||||
private String payOrderNo;
|
||||
/** 微信支付订单号 */
|
||||
private String transactionId;
|
||||
/** 分账方内部单号 */
|
||||
private String outOrderNo;
|
||||
/** 状态 PENDING/SUCCESS/FAILED/PARTIAL */
|
||||
private String status;
|
||||
/** 分账总金额(分) */
|
||||
private Integer totalAmount;
|
||||
/** 命中的规则ID */
|
||||
private Long ruleId;
|
||||
/** 规则快照(JSON) */
|
||||
private String ruleSnapshot;
|
||||
/** 组织链路快照 */
|
||||
private String orgPathSnapshot;
|
||||
/** 分账渠道 */
|
||||
private String sharingChannel;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
|
||||
/** 分账明细列表(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private List<ProfitSharingDetailEntity> details;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分润规则(主表)
|
||||
*/
|
||||
@Data
|
||||
@TableName("profit_sharing_rule")
|
||||
public class ProfitSharingRuleEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 规则名称 */
|
||||
private String ruleName;
|
||||
|
||||
/** 范围类型 GLOBAL/CHANNEL/AGENT/STORE */
|
||||
private String scopeType;
|
||||
|
||||
/** 关联组织ID(GLOBAL时为空) */
|
||||
private Long scopeOrgId;
|
||||
|
||||
/** 优先级 */
|
||||
private Integer priority;
|
||||
|
||||
/** 状态 ACTIVE/DISABLED */
|
||||
private String status;
|
||||
|
||||
/** 生效开始时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date effectiveFrom;
|
||||
|
||||
/** 生效结束时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date effectiveTo;
|
||||
|
||||
/** 删除标记 UNDEL/DEL */
|
||||
private String deleteFlag;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
|
||||
/** 子表:分润规则明细(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private List<ProfitSharingRuleItemEntity> items;
|
||||
|
||||
/** 关联组织名称(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private String scopeOrgName;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 分润规则明细(子表)
|
||||
*/
|
||||
@Data
|
||||
@TableName("profit_sharing_rule_item")
|
||||
public class ProfitSharingRuleItemEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 关联规则ID */
|
||||
private Long ruleId;
|
||||
|
||||
/** 组织角色 PLATFORM/CHANNEL/AGENT/STORE */
|
||||
private String orgRole;
|
||||
|
||||
/** 分润类型 RATIO/FIXED */
|
||||
private String shareType;
|
||||
|
||||
/** 分润值(比例:百分比;固定:分) */
|
||||
private BigDecimal shareValue;
|
||||
|
||||
/** 排序 */
|
||||
private Integer sortOrder;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.as.modules.daycard.form;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 天卡订单创建请求
|
||||
*/
|
||||
@Data
|
||||
public class DayCardOrderCreateRequest implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 设备MAC(优先) */
|
||||
private String mac;
|
||||
/** 设备SN(次优) */
|
||||
private String deviceSn;
|
||||
/** 设备UID(兜底) */
|
||||
private String deviceUid;
|
||||
/** 媒资ID */
|
||||
private String mediaId;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.modules.daycard.entity.DayCardActivationCodeEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 天卡设备激活码服务
|
||||
*/
|
||||
public interface DayCardActivationCodeService extends IService<DayCardActivationCodeEntity> {
|
||||
|
||||
/**
|
||||
* 根据激活码查询有效记录(未删除、状态为 ACTIVE)
|
||||
*
|
||||
* @param code 激活码字符串
|
||||
* @return 激活码实体,不存在或无效返回 null
|
||||
*/
|
||||
DayCardActivationCodeEntity getByCode(String code);
|
||||
|
||||
/**
|
||||
* 按门店查询激活码列表
|
||||
*
|
||||
* @param storeOrgId 门店组织ID
|
||||
* @return 激活码列表
|
||||
*/
|
||||
List<DayCardActivationCodeEntity> listByStoreOrgId(Long storeOrgId);
|
||||
|
||||
/**
|
||||
* 为门店生成激活码(每门店一个,已存在则返回已有的)
|
||||
*
|
||||
* @param storeOrgId 门店组织ID
|
||||
* @param channelOrgId 渠道组织ID
|
||||
* @param remark 备注(可选)
|
||||
* @return 激活码实体
|
||||
*/
|
||||
DayCardActivationCodeEntity generateForStore(Long storeOrgId, Long channelOrgId, String remark);
|
||||
|
||||
/**
|
||||
* 重新生成激活码 code(旧 code 失效)
|
||||
*
|
||||
* @param id 激活码记录ID
|
||||
* @return 新激活码字符串
|
||||
*/
|
||||
String regenerateCode(Long id);
|
||||
|
||||
/**
|
||||
* 启用/禁用激活码
|
||||
*
|
||||
* @param id 激活码记录ID
|
||||
* @param status 目标状态:ACTIVE/DISABLED
|
||||
*/
|
||||
void updateStatus(Long id, String status);
|
||||
|
||||
/**
|
||||
* 删除激活码(软删除)
|
||||
*
|
||||
* @param id 激活码记录ID
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
/**
|
||||
* 增加已激活设备计数
|
||||
*
|
||||
* @param id 激活码记录ID
|
||||
*/
|
||||
void incrementActivatedCount(Long id);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡订单 Service
|
||||
*/
|
||||
public interface DayCardOrderService extends IService<DayCardOrderEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询天卡订单
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @param partnerId 合作商ID
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params, Long partnerId);
|
||||
|
||||
/**
|
||||
* 创建天卡订单(从 sign 鉴权上下文获取 deviceId)
|
||||
* <p>流程:Redis防重 -> 解析设备组织链路 -> 按渠道定价 -> 生成订单号 -> 插入订单 -> SKIP LOCKED抢卡 -> 乐观锁更新卡状态 -> 返回订单</p>
|
||||
*
|
||||
* @param deviceId 设备ID(从 sign 解析)
|
||||
*/
|
||||
DayCardOrderEntity createOrder(Long deviceId);
|
||||
|
||||
/**
|
||||
* 创建天卡订单(通过设备标识,扫码后调用)
|
||||
* <p>流程:Redis防重(mac/deviceSn/deviceUid)-> 解析设备组织链路 -> 服务端定价 -> 生成订单号 -> 插入订单 -> SKIP LOCKED抢卡 -> 乐观锁更新卡状态 -> 返回订单</p>
|
||||
*
|
||||
* @param mac 设备MAC(优先)
|
||||
* @param deviceSn 设备SN(次优)
|
||||
* @param deviceUid 设备UID(兜底)
|
||||
* @param partnerId 合作商ID
|
||||
*/
|
||||
DayCardOrderEntity createOrder(String mac, String deviceSn, String deviceUid, Long partnerId);
|
||||
|
||||
/**
|
||||
* 支付回调
|
||||
* <p>流程:幂等检查 -> 乐观锁更新订单PAID -> 更新卡状态USED -> 异步激活 -> 清理Redis</p>
|
||||
*
|
||||
* @param orderNo 订单号
|
||||
* @param payNo 支付单号
|
||||
* @param payAmount 支付金额
|
||||
*/
|
||||
void payCallback(String orderNo, String payNo, BigDecimal payAmount);
|
||||
|
||||
/**
|
||||
* 取消超时未支付的订单
|
||||
* <p>查询超时订单(状态CREATED, expire_time < now, LIMIT 200)→ 逐个取消(释放卡+更新订单CANCELLED)</p>
|
||||
*
|
||||
* @return 取消的订单数量
|
||||
*/
|
||||
int cancelTimeoutOrders();
|
||||
|
||||
/**
|
||||
* 根据订单号查询订单
|
||||
*
|
||||
* @param orderNo 订单号
|
||||
*/
|
||||
DayCardOrderEntity getByOrderNo(String orderNo);
|
||||
|
||||
/**
|
||||
* 查询设备的天卡权益(激活后 App 获取播放凭证)
|
||||
* <p>从 day_card_pool 表查询 status=USED 且 expire_at > NOW() 的卡,返回 thirdCardNo 和 expireAt</p>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 天卡池实体,无有效权益时返回 null
|
||||
*/
|
||||
DayCardPoolEntity getDeviceBenefits(Long deviceId);
|
||||
|
||||
/**
|
||||
* 查询设备生效的天卡定价(供前端二维码展示)
|
||||
* <p>按 3 层维度匹配:门店 > 渠道 > 平台默认</p>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 定价策略,无匹配时返回 null
|
||||
*/
|
||||
DayCardPriceConfigEntity getDevicePrice(Long deviceId);
|
||||
|
||||
/**
|
||||
* 解绑设备天卡权益(Web端手动操作)
|
||||
* <p>将 USED 卡转为 REUSABLE,释放设备绑定,保留橘视厅权益供后续设备复用</p>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
void unbindDevice(Long deviceId);
|
||||
|
||||
/**
|
||||
* 设备组织解绑(更换安装场所时使用)
|
||||
* <p>释放设备当前天卡权益并清除门店组织关联(storeOrgId),
|
||||
* 设备可在新场所用新激活码重新激活绑定。</p>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
void unbindDeviceOrg(Long deviceId);
|
||||
|
||||
/**
|
||||
* 异步激活天卡:调用橘视厅接口
|
||||
* <p>通过 Spring AOP 代理调用以确保 @Async 生效</p>
|
||||
*
|
||||
* @param order 天卡订单
|
||||
*/
|
||||
void asyncActivateCard(DayCardOrderEntity order);
|
||||
|
||||
/**
|
||||
* 重试激活失败的天卡
|
||||
* <p>补偿定时任务调用:查询 activate_status=FAILED 的卡,重新激活</p>
|
||||
*
|
||||
* @return 重试的卡数量
|
||||
*/
|
||||
int retryFailedActivations();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡池 Service
|
||||
*/
|
||||
public interface DayCardPoolService extends IService<DayCardPoolEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询天卡池
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @param partnerId 合作商ID
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params, Long partnerId);
|
||||
|
||||
/**
|
||||
* 获取可用天卡数量
|
||||
*
|
||||
* @param partnerId 合作商ID
|
||||
*/
|
||||
int getAvailableCount(Long partnerId);
|
||||
|
||||
/**
|
||||
* 批量导入天卡卡密
|
||||
* <p>只需提供 cardCode(橘视厅激活码),thirdPartyRef 和 thirdCardNo 由服务端自动生成</p>
|
||||
*
|
||||
* @param cardCodes 激活码列表(从橘视厅/供应商获取的真实卡密)
|
||||
* @param partnerId 合作商ID
|
||||
* @return 成功导入数量
|
||||
*/
|
||||
int importCards(java.util.List<String> cardCodes, Long partnerId);
|
||||
|
||||
/**
|
||||
* 生成全局唯一的第三方引用号(uid):yyyyMMdd-HHmmss-xxxx
|
||||
*/
|
||||
String generateUniqueThirdPartyRef();
|
||||
|
||||
/**
|
||||
* 生成 24 位随机第三方卡号(0-9 A-Z a-z)
|
||||
*/
|
||||
String generateThirdCardNo();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡定价策略 Service
|
||||
*/
|
||||
public interface DayCardPriceConfigService extends IService<DayCardPriceConfigEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询定价策略
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 获取有效定价策略(3 层维度,优先级从高到低)
|
||||
* <p>1. 门店配置价(storeOrgId 命中)</p>
|
||||
* <p>2. 渠道配置价(storeOrgId 为空,channelOrgId 命中)</p>
|
||||
* <p>3. 平台默认价兜底(storeOrgId、channelOrgId 均为空)</p>
|
||||
*
|
||||
* @param channelOrgId 渠道组织ID(可空)
|
||||
* @param storeOrgId 门店组织ID(可空)
|
||||
* @return 定价策略,无匹配时返回 null
|
||||
*/
|
||||
DayCardPriceConfigEntity getEffectivePrice(Long channelOrgId, Long storeOrgId);
|
||||
|
||||
/**
|
||||
* 根据组织链路获取价格金额
|
||||
*
|
||||
* @param channelOrgId 渠道组织ID(可空)
|
||||
* @param storeOrgId 门店组织ID(可空)
|
||||
* @return 价格金额,无匹配时返回 null
|
||||
*/
|
||||
BigDecimal getPrice(Long channelOrgId, Long storeOrgId);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardProfitReceiverEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账接收方 Service
|
||||
*/
|
||||
public interface DayCardProfitReceiverService extends IService<DayCardProfitReceiverEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询分账接收方
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据ID获取分账接收方
|
||||
*/
|
||||
DayCardProfitReceiverEntity getById(Long id);
|
||||
|
||||
/**
|
||||
* 新增分账接收方
|
||||
*/
|
||||
void saveReceiver(DayCardProfitReceiverEntity receiver);
|
||||
|
||||
/**
|
||||
* 修改分账接收方
|
||||
*/
|
||||
void updateReceiver(DayCardProfitReceiverEntity receiver);
|
||||
|
||||
/**
|
||||
* 批量删除分账接收方(软删除)
|
||||
*/
|
||||
void delete(Long[] ids);
|
||||
|
||||
/**
|
||||
* 同步接收方到微信(占位方法,实际微信API待对接)
|
||||
*
|
||||
* @param id 接收方ID
|
||||
*/
|
||||
void syncToWechat(Long id);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user