初始化天卡开放平台后端项目

包含完整功能模块:
- 设备激活(直销激活码模式 + 代销免激活码模式)
- 微信支付 + 天卡订单
- 天卡池管理 + 异步橘视厅SDK激活
- 第三方合作伙伴接口(代销模式)
  - 签名鉴权(appKey/appSecret + nonce防重放)
  - 天卡消耗(事务 + 幂等 + 并发控制)
  - 媒资接口(三级授权过滤)
  - 审计日志
- 定价策略(3层:门店 > 渠道 > 平台)
- 分账(微信分账 + 分润规则)
- Docker部署(含Redis容器)
This commit is contained in:
zhaogd
2026-07-26 12:34:13 +08:00
commit 30198c7191
349 changed files with 28837 additions and 0 deletions

View 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);
}
}

View 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 "";
}

View 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 "";
}

View 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;
}
}

View 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;
}
}
}

View 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;
}
}

View File

@@ -0,0 +1 @@

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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";
}

View 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();
}
}
}

View 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) + "...";
}
}

View 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();
}
}

View 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");
}
}

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
}
}

View 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());
}
}

View File

@@ -0,0 +1,9 @@
package io.as.common.validator.group;
/**
* 新增数据 Group
*
* @author Mark sunlightcs@gmail.com
*/
public interface AddGroup {
}

View 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 {
}

View File

@@ -0,0 +1,9 @@
package io.as.common.validator.group;
/**
* 腾讯云
*
* @author Mark sunlightcs@gmail.com
*/
public interface QcloudGroup {
}

View File

@@ -0,0 +1,11 @@
package io.as.common.validator.group;
/**
* 更新数据 Group
*
* @author Mark sunlightcs@gmail.com
*/
public interface UpdateGroup {
}

View 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, "&amp;", result);
result = regexReplace(P_QUOTE, "&quot;", result);
result = regexReplace(P_LEFT_ARROW, "&lt;", result);
result = regexReplace(P_RIGHT_ARROW, "&gt;", 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, "&lt;$1", s);
s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2&gt;<", 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, "&quot;", 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
: "&amp;" + 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));
}
}

View 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;
}
}

View 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() {
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}

View 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);
}
}

View 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")
);
}
}

View 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 "";
}

View 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");
}
}
}

View 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();
}
}
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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";
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View 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> {
}

View 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();
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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 &lt; 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();
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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