Ver código fonte

first commit

linhui.li 1 mês atrás
commit
0da7c752da
100 arquivos alterados com 5761 adições e 0 exclusões
  1. 39 0
      .editorconfig
  2. 16 0
      .gitignore
  3. 27 0
      PULL_REQUEST_TEMPLATE.md
  4. 3 0
      README.md
  5. 74 0
      auc/pom.xml
  6. 31 0
      auc/src/main/java/cn/hobbystocks/auc/AucApplication.java
  7. 66 0
      auc/src/main/java/cn/hobbystocks/auc/aop/AppRoleAspect.java
  8. 42 0
      auc/src/main/java/cn/hobbystocks/auc/aop/ControllerLoggingInterceptor.java
  9. 73 0
      auc/src/main/java/cn/hobbystocks/auc/app/AppClient.java
  10. 72 0
      auc/src/main/java/cn/hobbystocks/auc/config/MyFlywayMigrationStrategy.java
  11. 57 0
      auc/src/main/java/cn/hobbystocks/auc/config/ReadinessHealthIndicator.java
  12. 39 0
      auc/src/main/java/cn/hobbystocks/auc/config/SecurityConfig.java
  13. 19 0
      auc/src/main/java/cn/hobbystocks/auc/config/WebConfig.java
  14. 24 0
      auc/src/main/java/cn/hobbystocks/auc/web/AdminBaseController.java
  15. 124 0
      auc/src/main/java/cn/hobbystocks/auc/web/AuctionController.java
  16. 59 0
      auc/src/main/java/cn/hobbystocks/auc/web/BidController.java
  17. 63 0
      auc/src/main/java/cn/hobbystocks/auc/web/LocalController.java
  18. 183 0
      auc/src/main/java/cn/hobbystocks/auc/web/LotController.java
  19. 316 0
      auc/src/main/java/cn/hobbystocks/auc/web/LotGroupController.java
  20. 44 0
      auc/src/main/java/cn/hobbystocks/auc/web/SyncController.java
  21. 28 0
      auc/src/main/resources/application-dev.yml
  22. 23 0
      auc/src/main/resources/application-local.yml
  23. 26 0
      auc/src/main/resources/application-prod.yml
  24. 100 0
      auc/src/main/resources/application.yml
  25. 160 0
      auc/src/main/resources/db/migration/V1.1_Init_.sql
  26. 7 0
      auc/src/main/resources/db/migration/V1.2_lot_goods_type_.sql
  27. 3 0
      auc/src/main/resources/db/migration/V1.3_bid_mod_.sql
  28. 4 0
      auc/src/main/resources/db/migration/V1.4_carousel_imgs_.sql
  29. 2 0
      auc/src/main/resources/db/migration/V1.5_bid_avatar_.sql
  30. 3 0
      auc/src/main/resources/db/migration/V1.6_lot_avatar_.sql
  31. 2 0
      auc/src/main/resources/db/migration/V1.7_lot_goods_.sql
  32. 2 0
      auc/src/main/resources/db/migration/V1.8_lot_merchant_.sql
  33. 20 0
      auc/src/main/resources/db/migration/V1.9_error_.sql
  34. 7 0
      auc/src/main/resources/db/migration/V2.0_index_.sql
  35. 2 0
      auc/src/main/resources/db/migration/V2.1_bid_no_.sql
  36. 2 0
      auc/src/main/resources/db/migration/V2.2_bid_user_code_.sql
  37. 16 0
      auc/src/main/resources/db/migration/V2.3_bid_record_.sql
  38. 1 0
      auc/src/main/resources/db/migration/V2.4_update_lot_goods_type_.sql
  39. 18 0
      auc/src/main/resources/db/migration/V2.5_sold_order_record_.sql
  40. 4 0
      auc/src/main/resources/db/migration/V2.6_private_domain_.sql
  41. 17 0
      auc/src/main/resources/db/migration/V2.7_nickname_too_long_.sql
  42. 5 0
      auc/src/main/resources/db/migration/V2.8_img_too_long_.sql
  43. 2 0
      auc/src/main/resources/db/migration/V2.9_delay_publish_.sql
  44. 26 0
      auc/src/main/resources/db/migration/V3.0_lot_fans_.sql
  45. 19 0
      auc/src/main/resources/db/migration/V3.1_lot_fans_push_record_.sql
  46. 2 0
      auc/src/main/resources/db/migration/V3.2_manual_return_point_.sql
  47. 152 0
      auc/src/main/resources/db/migration/V3.3_new_v2_auction_.sql
  48. 2 0
      auc/src/main/resources/db/migration/V3.4_merch_.sql
  49. 4 0
      auc/src/main/resources/db/migration/V3.5_lot_group_edit.sql
  50. 5 0
      auc/src/main/resources/db/migration/V3.6_bid_index_.sql
  51. 4 0
      auc/src/main/resources/db/migration/V3.7_lot_edit.sql
  52. 4 0
      auc/src/main/resources/db/migration/V3.8_lot_fans_edit.sql
  53. 21 0
      auc/src/main/resources/logback-fluentd.xml
  54. 43 0
      auc/src/main/resources/logback-spring.xml
  55. 169 0
      bid/pom.xml
  56. 34 0
      bid/src/main/java/cn/hobbystocks/auc/BidApplication.java
  57. 398 0
      bid/src/main/java/cn/hobbystocks/auc/app/AppClient.java
  58. 66 0
      bid/src/main/java/cn/hobbystocks/auc/aspectj/AppRoleAspect.java
  59. 42 0
      bid/src/main/java/cn/hobbystocks/auc/aspectj/ControllerLoggingInterceptor.java
  60. 26 0
      bid/src/main/java/cn/hobbystocks/auc/aspectj/SensitiveDataAspect.java
  61. 52 0
      bid/src/main/java/cn/hobbystocks/auc/config/BeeConfig.java
  62. 35 0
      bid/src/main/java/cn/hobbystocks/auc/config/JacksonConfig.java
  63. 60 0
      bid/src/main/java/cn/hobbystocks/auc/config/ReadinessHealthIndicator.java
  64. 39 0
      bid/src/main/java/cn/hobbystocks/auc/config/SecurityConfig.java
  65. 19 0
      bid/src/main/java/cn/hobbystocks/auc/config/WebConfig.java
  66. 71 0
      bid/src/main/java/cn/hobbystocks/auc/delegate/HongKongPlatformApi.java
  67. 57 0
      bid/src/main/java/cn/hobbystocks/auc/handle/CouponSoldHandler.java
  68. 98 0
      bid/src/main/java/cn/hobbystocks/auc/handle/SkuSoldHandler.java
  69. 11 0
      bid/src/main/java/cn/hobbystocks/auc/handle/SoldHandler.java
  70. 23 0
      bid/src/main/java/cn/hobbystocks/auc/handle/SoldHandlerHolder.java
  71. 36 0
      bid/src/main/java/cn/hobbystocks/auc/listener/ChangeListener.java
  72. 23 0
      bid/src/main/java/cn/hobbystocks/auc/listener/JPushListener.java
  73. 78 0
      bid/src/main/java/cn/hobbystocks/auc/listener/LiveEndListener.java
  74. 60 0
      bid/src/main/java/cn/hobbystocks/auc/listener/SoldListener.java
  75. 22 0
      bid/src/main/java/cn/hobbystocks/auc/listener/StartBiddingListener.java
  76. 22 0
      bid/src/main/java/cn/hobbystocks/auc/listener/SyncListener.java
  77. 370 0
      bid/src/main/java/cn/hobbystocks/auc/task/BidTask.java
  78. 693 0
      bid/src/main/java/cn/hobbystocks/auc/web/BidingController.java
  79. 50 0
      bid/src/main/java/cn/hobbystocks/auc/web/FansController.java
  80. 88 0
      bid/src/main/java/cn/hobbystocks/auc/web/InController.java
  81. 139 0
      bid/src/main/java/cn/hobbystocks/auc/web/LocalController.java
  82. 34 0
      bid/src/main/java/cn/hobbystocks/auc/web/SelfController.java
  83. 69 0
      bid/src/main/java/cn/hobbystocks/auc/web/ShippingBiddingController.java
  84. 38 0
      bid/src/main/resources/application-dev.yml
  85. 33 0
      bid/src/main/resources/application-local.yml
  86. 31 0
      bid/src/main/resources/application-prod.yml
  87. 79 0
      bid/src/main/resources/application.yml
  88. 1 0
      bid/src/main/resources/bee.properties
  89. 21 0
      bid/src/main/resources/logback-fluentd.xml
  90. 57 0
      bid/src/main/resources/logback-spring.xml
  91. 20 0
      bid/src/main/resources/mybatis/mybatis-config.xml
  92. 208 0
      lot/pom.xml
  93. 14 0
      lot/src/main/java/cn/hobbystocks/auc/annotation/RequireRoles.java
  94. 13 0
      lot/src/main/java/cn/hobbystocks/auc/annotation/Sensitive.java
  95. 11 0
      lot/src/main/java/cn/hobbystocks/auc/annotation/SensitiveData.java
  96. 15 0
      lot/src/main/java/cn/hobbystocks/auc/annotation/View.java
  97. 18 0
      lot/src/main/java/cn/hobbystocks/auc/cache/CacheMap.java
  98. 71 0
      lot/src/main/java/cn/hobbystocks/auc/common/config/FastJson2JsonRedisSerializer.java
  99. 13 0
      lot/src/main/java/cn/hobbystocks/auc/common/config/HttpConfig.java
  100. 127 0
      lot/src/main/java/cn/hobbystocks/auc/common/config/RedisConfig.java

+ 39 - 0
.editorconfig

@@ -0,0 +1,39 @@
+# EditorConfig 官网: https://EditorConfig.org
+# 表示这是顶级配置文件
+root = true
+
+# 匹配所有文件
+[*]
+# 编码为utf-8
+charset = utf-8
+# 自动删除行尾的空白字符
+trim_trailing_whitespace = true
+# 在文件末尾插入一个空行
+insert_final_newline = true
+
+# 匹配Java源文件
+[*.java]
+# 缩进风格为空格
+indent_style = space
+# 缩进大小为4个空格
+indent_size = 4
+# 最大行长度,一般设置为120较为常见
+max_line_length = 120
+# 换行符使用操作系统默认的换行符(Windows下为CRLF,Linux和macOS下为LF)
+end_of_line = lf
+# Java代码中的花括号换行风格
+brace_style = next_line
+# 强制使用4个空格的缩进,即使在连续的缩进级别中
+continuation_indent_size = 4
+# 注释中的缩进与代码的缩进保持一致
+comment_indent_size = 4
+# 类、方法和变量的命名风格遵循Java的命名约定
+# 例如,类名大写驼峰,变量和方法名小写驼峰
+# 这里没有具体的设置项,主要靠开发者遵循约定
+# 导入包时,按照字母顺序排列
+sort_imports = true
+# 控制是否在导入包时使用静态导入的星号形式
+# 例如,import static java.util.Arrays.*;
+# 一般不推荐使用星号形式,除非有特殊需求
+# 这里设置为false,即不使用星号形式的静态导入
+java_import_alias = false

+ 16 - 0
.gitignore

@@ -0,0 +1,16 @@
+# ---> Java
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+.idea
+target

+ 27 - 0
PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,27 @@
+# Pull Request模板
+
+## 拉取请求概述
+- **PR标题**:[具体标题]
+- **PR描述**:简要说明本次拉取请求的目的和主要变更内容。## 变更内容详情
+- **新增文件**:列出本次新增的所有文件及简要说明其功能。
+- **修改文件**:详细列出修改的文件路径及对应的修改内容简述。
+- **删除文件**:如有删除的文件,请列出并说明原因。
+
+## 代码规范检查
+- ( ) 我的代码遵循了项目的代码风格指南。
+- ( ) 我已经对自己的代码进行了自我审查。
+- ( ) 我在代码中添加了必要的注释,特别是在难以理解的区域。
+-  
+## 测试情况说明
+- **测试类型**:[单元测试/集成测试/系统测试等]
+- **测试用例**:列出你执行的测试用例编号或名称,并附上测试结果截图或详细的测试报告链接。
+- **测试环境**:描述测试所使用的环境,包括操作系统、数据库版本、依赖库版本等。
+
+## 相关文档更新
+- ( ) 我已经对相关文档进行了更新,包括但不限于README、API文档、用户手册等。
+- 如果有更新的文档,请列出具体的文档路径和更新内容简述。
+
+## 合并前确认事项
+- ( ) 所有相关的问题都已经在本次PR中得到解决或处理。
+- ( ) 代码已经通过了所有的质量检查,如代码静态分析、安全扫描等。
+- ( ) 我已经与相关的团队成员或利益相关者进行了沟通,并获得了他们的认可或同意。

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# Auction
+
+Auction

+ 74 - 0
auc/pom.xml

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>auction</artifactId>
+		<groupId>cn.hobbystocks</groupId>
+		<version>0.1.0</version>
+	</parent>
+
+	<artifactId>auc</artifactId>
+
+	<dependencies>
+		<dependency>
+			<groupId>cn.hobbystocks</groupId>
+			<artifactId>lot</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.flywaydb</groupId>
+			<artifactId>flyway-core</artifactId>
+			<version>5.2.1</version>
+		</dependency>
+		<dependency>
+			<groupId>org.flywaydb</groupId>
+			<artifactId>flyway-mysql</artifactId>
+			<version>8.5.11</version>
+		</dependency>
+		<!-- redis 缓存操作 -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-redis</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-pool2</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.google.guava</groupId>
+			<artifactId>guava</artifactId>
+			<version>33.3.1-jre</version>
+			<scope>compile</scope>
+		</dependency>
+	</dependencies>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+				<version>2.1.1.RELEASE</version>
+				<configuration>
+					<fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+				</configuration>
+				<executions>
+					<execution>
+						<goals>
+							<goal>repackage</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-war-plugin</artifactId>
+				<version>3.1.0</version>
+				<configuration>
+					<failOnMissingWebXml>false</failOnMissingWebXml>
+					<warName>${project.artifactId}</warName>
+				</configuration>
+			</plugin>
+		</plugins>
+		<finalName>${project.artifactId}</finalName>
+	</build>
+</project>

+ 31 - 0
auc/src/main/java/cn/hobbystocks/auc/AucApplication.java

@@ -0,0 +1,31 @@
+package cn.hobbystocks.auc;
+
+import java.util.TimeZone;
+
+import com.dtflys.forest.springboot.annotation.ForestScan;
+import lombok.extern.slf4j.Slf4j;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import springfox.documentation.oas.annotations.EnableOpenApi;
+
+@ForestScan(basePackages = "cn.hobbystocks.auc.forest")
+@MapperScan("cn/hobbystocks/auc/mapper")
+@SpringBootApplication
+@EnableScheduling
+@Slf4j
+@EnableOpenApi
+public class AucApplication {
+	public static void main(String[] args) {
+		SpringApplication.run(AucApplication.class, args);
+		log.info("(♥◠‿◠)ノ゙ app启动成功   ლ(´ڡ`ლ)゙");
+	}
+
+	@Bean
+	public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() {
+		return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getTimeZone("GMT+8"));
+	}
+}

+ 66 - 0
auc/src/main/java/cn/hobbystocks/auc/aop/AppRoleAspect.java

@@ -0,0 +1,66 @@
+package cn.hobbystocks.auc.aop;
+
+import cn.hobbystocks.auc.annotation.RequireRoles;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.user.UserInfo;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+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 org.springframework.util.CollectionUtils;
+
+import java.lang.reflect.Method;
+import java.util.*;
+
+@Slf4j
+@Aspect
+@Component
+public class AppRoleAspect {
+	// 配置织入点
+	@Pointcut("@annotation(cn.hobbystocks.auc.annotation.RequireRoles)")
+	public void rolePointCut() {
+	}
+
+	//@RequireRoles({"admin"})
+	@Around("cn.hobbystocks.auc.aop.AppRoleAspect.rolePointCut()")
+	public Object roleBefore(ProceedingJoinPoint pjp) throws Throwable {
+		UserInfo user = UserUtils.getSimpleUserInfo();
+		Signature signature = pjp.getSignature();
+		MethodSignature methodSignature = (MethodSignature) signature;
+		Method targetMethod = methodSignature.getMethod();
+		RequireRoles annotation = targetMethod.getAnnotation(RequireRoles.class);
+		String[] roles = annotation.value();
+		List<String> requirePermissions = Arrays.asList(roles);
+		List<String> userPermissions = new ArrayList<>();
+		List<String> roleList = user.getRoleCodeList() != null ? user.getRoleCodeList() : Collections.emptyList();
+		userPermissions.addAll(roleList);
+		List<String> permissionsList = user.getPermissionsList();
+		if (!CollectionUtils.isEmpty(permissionsList)) {
+			userPermissions.addAll(permissionsList);
+		}
+		Set<String> commonRole = getCommonElements(requirePermissions, userPermissions);
+		if (CollectionUtils.isEmpty(commonRole)) {
+			log.info("权限不足,用户信息:{}",user);
+			return AjaxResult.error("权限不足!");
+		}
+		return pjp.proceed();
+
+	}
+
+	private Set<String> getCommonElements(List<String> list1, List<String> list2) {
+		if (CollectionUtils.isEmpty(list1) || CollectionUtils.isEmpty(list2)) {
+			return null;
+		}
+		Set<String> set1 = new HashSet<>(list1);
+		Set<String> set2 = new HashSet<>(list2);
+		Set<String> commonElements = new HashSet<>(set1);
+		commonElements.retainAll(set2);
+		return commonElements;
+	}
+
+}

+ 42 - 0
auc/src/main/java/cn/hobbystocks/auc/aop/ControllerLoggingInterceptor.java

@@ -0,0 +1,42 @@
+package cn.hobbystocks.auc.aop;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.UUID;
+
+@Component
+public class ControllerLoggingInterceptor implements HandlerInterceptor {
+
+    private static final Logger logger = LoggerFactory.getLogger(ControllerLoggingInterceptor.class);
+    private static final ThreadLocal<Long> startTime = new ThreadLocal<>();
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        String requestURI = request.getRequestURI();
+        String method = request.getMethod();
+        String traceId = UUID.randomUUID().toString();
+        request.setAttribute("traceId", traceId);
+        MDC.put("traceId", traceId);
+        logger.info("Controller method call start: {}", requestURI);
+        startTime.set(System.currentTimeMillis());
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+                                Object handler, Exception exception) throws Exception {
+        long endTime = System.currentTimeMillis();
+        long duration = endTime - startTime.get();
+        int responseCode = response.getStatus();
+        String requestURI = request.getRequestURI();
+        logger.info("Controller method called:{} resp {} in {} ms",requestURI, responseCode, duration);
+        startTime.remove();
+        MDC.remove("traceId");
+    }
+}

+ 73 - 0
auc/src/main/java/cn/hobbystocks/auc/app/AppClient.java

@@ -0,0 +1,73 @@
+package cn.hobbystocks.auc.app;
+
+import cn.hobbystocks.auc.domain.Bid;
+import cn.hobbystocks.auc.forest.CommonForestClient;
+import cn.hobbystocks.auc.vo.CardGroupLivesConfigVO;
+import cn.hobbystocks.auc.vo.LivingExplainDTO;
+import cn.hobbystocks.auc.vo.SkuDTO;
+import com.alibaba.fastjson.JSON;
+import com.dtflys.forest.http.ForestResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import com.google.common.collect.Maps;
+import java.util.*;
+
+@Component
+@Slf4j
+public class AppClient {
+    @Autowired
+    private CommonForestClient client;
+    @Value("${hobbystocks.host.pointUrl}")
+    private String pointUrl;
+    @Value(("${hobbystocks.host.getSku}"))
+    private String getSku;
+
+
+    public SkuDTO getSku(Integer skuId) {
+
+        ForestResponse<CommonForestClient.Response<Object>> response =
+                client.sendGet(getSku + skuId);
+        return JSON.parseObject(JSON.toJSONString(response.getResult().getData()), SkuDTO.class);
+    }
+
+
+
+    public boolean returnPoint(Bid bid, Long recordId) {
+        ForestResponse<CommonForestClient.Response<Object>> response;
+        try {
+            Map<String, Object> params = new HashMap<>();
+            params.put("userId", bid.getAccountId());
+            List<Map<String, Object>> pointRecords = new ArrayList<>();
+            params.put("pointRecords", pointRecords);
+            pointRecords.add(getReturnPointRecord(bid, recordId));
+            response = client.sendPost(pointUrl, getDefaultHeaderMap(), params);
+            boolean result = Objects.equals(response.getStatusCode(), 200) && Objects.equals(response.getResult().getCode(), 0);
+            if (!result) {
+                log.error("return point error, {} , {}  {}", pointUrl, bid, response.getResult().getMsg());
+            }else {
+                log.info("return point {} {}", bid,  response.getContent());
+            }
+            return result;
+        }catch (Exception e) {
+            log.error("return point error, {} , {}", pointUrl, bid);
+            return false;
+        }
+    }
+
+    private Map<String, Object> getReturnPointRecord(Bid bid, Long recordId) {
+        Map<String, Object> pointRecord = new HashMap<>();
+        pointRecord.put("changePoint", bid.getAmount().longValue() * 100);
+        pointRecord.put("orderId", bid.getLotId());
+        pointRecord.put("refId", bid.getBidNo() + "-" + recordId);
+        return pointRecord;
+    }
+    private Map<String, Object> getDefaultHeaderMap() {
+        Map<String, Object> headerMap = Maps.newHashMap();
+        String traceId = MDC.get("traceId");
+        headerMap.put("traceid", traceId);
+        return headerMap;
+    }
+}

+ 72 - 0
auc/src/main/java/cn/hobbystocks/auc/config/MyFlywayMigrationStrategy.java

@@ -0,0 +1,72 @@
+package cn.hobbystocks.auc.config;
+
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import cn.hobbystocks.auc.domain.Auction;
+import cn.hobbystocks.auc.service.IAuctionService;
+import cn.hobbystocks.auc.service.ILotService;
+import cn.hobbystocks.auc.vo.AuctionVO;
+import lombok.SneakyThrows;
+import org.flywaydb.core.Flyway;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
+import org.springframework.stereotype.Component;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Objects;
+import java.util.TimeZone;
+
+@Component
+public class MyFlywayMigrationStrategy implements FlywayMigrationStrategy {
+
+    @Value("${user.info-url:http://coresvc2/user/}")
+    private String userUrl;
+
+    @Autowired
+    protected IAuctionService auctionService;
+    @Autowired
+    private ILotService lotService;
+
+    @SneakyThrows
+    @Override
+    public void migrate(Flyway flyway)  {
+        try {
+            flyway.migrate();
+        }
+        catch (NoSuchMethodError ex) {
+            // Flyway < 7.0
+            flyway.getClass().getMethod("migrate").invoke(flyway);
+        }
+        init();
+    }
+
+    private void init() throws Exception {
+        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
+        UserUtils.setUrl(userUrl);
+
+        Auction auction = auctionService.selectAuctionById(1L);
+        if (Objects.isNull(auction)) {
+            auction = new AuctionVO();
+            auction.setStatus(Constants.GROUP_STATUS_STARTING);
+            auction.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+            auction.setName("双十一拍卖活动");
+            auction.setDetail("双十一拍卖活动");
+            auction.setPubTime(new Date());
+            auction.setStartTime(new Date());
+            auction.setEndTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2025-01-03 00:00:00"));
+            auction.setCreateTime(new Date());
+            auction.setUpdateTime(new Date());
+            auctionService.insertAuction(auction);
+            auctionService.pubAuction((AuctionVO)auction);
+        }else {
+            AuctionVO auctionVO = new AuctionVO();
+            BeanUtils.copyProperties(auction, auctionVO);
+            auctionService.pubAuction(auctionVO);
+        }
+        lotService.dynamicTasks();
+    }
+
+}

+ 57 - 0
auc/src/main/java/cn/hobbystocks/auc/config/ReadinessHealthIndicator.java

@@ -0,0 +1,57 @@
+package cn.hobbystocks.auc.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.HealthIndicator;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+@Component
+public class ReadinessHealthIndicator implements HealthIndicator {
+    @Autowired
+    private DataSource dataSource;
+    @Autowired
+    private RedisConnectionFactory redisConnectionFactory;
+
+    @Override
+    public Health health() {
+        // 可以检查各种依赖服务状态,比如数据库连接、Redis、外部API等
+        if (!checkDB()) {
+            return Health.down().withDetail("database", "Connection is closed").build();
+        }else if (!checkRedis()) {
+            Health.down().withDetail("redis", "Cannot connect to Redis").build();
+        }
+        return Health.up().build();
+    }
+
+    private boolean checkDB() {
+        try (Connection connection = dataSource.getConnection()) {
+            // 如果连接成功,则返回健康状态
+            if (!connection.isClosed()) {
+                return true;
+            } else {
+                return false;
+            }
+        } catch (SQLException e) {
+            // 如果连接失败,则返回不健康状态并包含错误信息
+            return false;
+        }
+    }
+
+    private boolean checkRedis() {
+        try (RedisConnection connection = redisConnectionFactory.getConnection()) {
+            // 检查连接是否有效
+            connection.ping();  // 如果能成功ping通,则Redis连接正常
+            return true;
+        } catch (Exception e) {
+            // 连接异常则返回不健康状态
+            return false;
+        }
+    }
+
+}

+ 39 - 0
auc/src/main/java/cn/hobbystocks/auc/config/SecurityConfig.java

@@ -0,0 +1,39 @@
+package cn.hobbystocks.auc.config;
+
+import cn.hobbystocks.auc.common.filter.AuthenticationFilter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+    private final AuthenticationFilter authenticationFilter;
+
+    private String [] ignoreUrl={"/actuator/**","/api/local/**","/api-docs/*","/doc.html","/webjars/**","/swagger-resources/**","/v3/api-docs/**","/swagger-ui/**"};
+
+    public SecurityConfig(AuthenticationFilter authenticationFilter) {
+        this.authenticationFilter = authenticationFilter;
+    }
+
+    @Bean
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        http
+                .csrf(AbstractHttpConfigurer::disable)  // 禁用 CSRF
+                .sessionManagement(session -> session
+                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 使用无状态会话
+                .authorizeHttpRequests(auth -> auth
+                        .antMatchers(ignoreUrl).permitAll()
+                        .anyRequest().authenticated()  // 其他请求需要身份验证
+                )
+                .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class); // 添加自定义过滤器
+
+        return http.build();
+    }
+}

+ 19 - 0
auc/src/main/java/cn/hobbystocks/auc/config/WebConfig.java

@@ -0,0 +1,19 @@
+package cn.hobbystocks.auc.config;
+
+import cn.hobbystocks.auc.aop.ControllerLoggingInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Autowired
+    private ControllerLoggingInterceptor controllerLoggingInterceptor;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(controllerLoggingInterceptor).addPathPatterns("/**");
+    }
+}

+ 24 - 0
auc/src/main/java/cn/hobbystocks/auc/web/AdminBaseController.java

@@ -0,0 +1,24 @@
+package cn.hobbystocks.auc.web;
+
+
+import cn.hobbystocks.auc.common.core.controller.BaseController;
+import cn.hobbystocks.auc.service.IAuctionService;
+import cn.hobbystocks.auc.service.IBidService;
+import cn.hobbystocks.auc.service.ILotService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * web层通用数据处理
+ */
+public abstract class AdminBaseController extends BaseController {
+
+	@Autowired
+	protected ILotService lotService;
+
+	@Autowired
+	protected IAuctionService auctionService;
+
+	@Autowired
+	protected IBidService bidService;
+
+}

+ 124 - 0
auc/src/main/java/cn/hobbystocks/auc/web/AuctionController.java

@@ -0,0 +1,124 @@
+package cn.hobbystocks.auc.web;
+
+import java.util.List;
+import java.util.Objects;
+
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.core.redis.RedisCache;
+import cn.hobbystocks.auc.vo.AuctionVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import cn.hobbystocks.auc.domain.Auction;
+
+/**
+ * 拍卖会Controller
+ */
+@RestController
+@RequestMapping("/auction/admin/auction")
+@Slf4j
+@Api(tags = "拍卖会相关接口")
+public class AuctionController extends AdminBaseController {
+	@Autowired
+	private RedisCache redisCache;
+	@ApiOperation(value = "test", notes = "just for test", response = AjaxResult.class, responseContainer = "AjaxResult")
+	@GetMapping("/test")
+	public AjaxResult test() {
+		return AjaxResult.success();
+	}
+
+	/**
+	 * 查询拍卖会列表
+	 */
+	@ApiOperation(value = "查询拍卖会列表", notes = "分页查询拍卖会列表", response = Auction.class, responseContainer = "List<Auction>")
+	@PostMapping("/list")
+	public AjaxResult list(@RequestBody AuctionVO auction) {
+		startPage(auction);
+		List<Auction> list = auctionService.selectAuctionList(auction);
+		return AjaxResult.successPage(list);
+	}
+
+	/**
+	 * 新增保存拍卖会
+	 */
+	@ApiOperation(value = "新增保存拍卖会", notes = "新增保存拍卖会", response = AjaxResult.class, responseContainer = "int")
+	@PostMapping("/add")
+	public AjaxResult addSave(@RequestBody AuctionVO auction) {
+		Auction dbAuc = auctionService.selectAuctionByNo(auction.getNo());
+		if (Objects.nonNull(dbAuc))
+			return AjaxResult.error("拍卖会编号已存在");
+		auction.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+		auction.setStatus(Constants.GROUP_STATUS_WAITING);
+		auction.setPubStatus(Constants.PUB_STATUS_NO_PUBLISHED);
+		auction.setCreateBy(getUsername());
+		return AjaxResult.success(auctionService.insertAuction(auction));
+	}
+
+	/**
+	 * 根据拍卖会编号获取拍卖会详情
+	 */
+	@ApiOperation(value = "根据拍卖会编号获取拍卖会详情", notes = "根据拍卖会编号获取拍卖会详情", response = Auction.class, responseContainer = "Auction")
+	@PostMapping(value = "/get")
+	public AjaxResult getInfo(@RequestBody AuctionVO auction) {
+		return AjaxResult.success(auctionService.selectAuctionByNo(auction.getNo()));
+	}
+
+	/**
+	 * 修改保存拍卖会
+	 */
+	@ApiOperation(value = "修改保存拍卖会", notes = "修改保存拍卖会", response = Auction.class, responseContainer = "Auction")
+	@PostMapping("/edit")
+	public AjaxResult editSave(@RequestBody AuctionVO auction) {
+		Auction dbAuc = auctionService.selectAuctionById(auction.getId());
+		if (Objects.isNull(dbAuc))
+			return AjaxResult.error("没有找到拍卖会");
+		if (Objects.equals(Constants.PUB_STATUS_PUBLISHED, dbAuc.getPubStatus())) {
+			return AjaxResult.error("拍卖会已发布不能修改");
+		}
+		auction.setUpdateBy(getUsername());
+		return AjaxResult.success(auctionService.updateAuction(auction));
+	}
+
+	/**
+	 * 根据拍卖会编号删除拍卖会及其拍品
+	 */
+	@ApiOperation(value = "删除拍卖会", notes = "根据拍卖会编号删除拍卖会及其拍品", response = AjaxResult.class, responseContainer = "AjaxResult.success")
+	@PostMapping(value = "/delete")
+	public AjaxResult remove(@RequestBody AuctionVO auction) {
+		Auction dbAuc = auctionService.selectAuctionById(auction.getId());
+		if (Objects.isNull(dbAuc))
+			return AjaxResult.error("没有找到拍卖会");
+		if (Objects.equals(Constants.PUB_STATUS_PUBLISHED, dbAuc.getPubStatus())) {
+			return AjaxResult.error("拍卖会已发布不能修改");
+		}
+		auction.setUpdateBy(getUsername());
+		auction.setDelFlag(Constants.DEL_FLAG_DELETED);
+		redisCache.deleteObject(String.format(Constants.REDIS_AUC_TEMPLATE, auction.getId()));
+		auctionService.updateAuction(auction);
+		return AjaxResult.success();
+	}
+
+	/**
+	 * 根据拍卖会编号发布拍卖会
+	 */
+	@ApiOperation(value = "发布", notes = "根据拍卖会编号发布拍卖会", response = AjaxResult.class, responseContainer = "AjaxResult.success")
+	@PostMapping(value = "/pub")
+	public AjaxResult pub(@RequestBody AuctionVO auction) throws Exception {
+		Auction dbAuc = auctionService.selectAuctionById(auction.getId());
+		if (Objects.isNull(dbAuc))
+			return AjaxResult.error("没有找到拍卖会");
+		if (Objects.equals(Constants.PUB_STATUS_PUBLISHED, dbAuc.getPubStatus())) {
+			return AjaxResult.error("拍卖会已发布不能修改");
+		}
+		auction.setUpdateBy(getUsername());
+		auctionService.pubAuction(auction);
+		return AjaxResult.success();
+	}
+
+}

+ 59 - 0
auc/src/main/java/cn/hobbystocks/auc/web/BidController.java

@@ -0,0 +1,59 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import cn.hobbystocks.auc.common.utils.SensitiveDataUtils;
+import cn.hobbystocks.auc.common.utils.StringUtils;
+import cn.hobbystocks.auc.domain.Bid;
+import cn.hobbystocks.auc.vo.BidVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 拍卖出价后台查看
+ */
+@RestController
+@RequestMapping("/auction/admin/bid")
+@Api(tags = "后台查看拍卖出价")
+public class BidController extends AdminBaseController {
+
+    /**
+     * 查询出价列表
+     * foreach
+     */
+    @ApiOperation(value = "list", notes = "查询出价列表", response = Bid.class, responseContainer = "List<Bid>")
+    @PostMapping("/list")
+    public AjaxResult list(@RequestBody BidVO bidVO) {
+        bidVO.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+        startPage(bidVO);
+        List<Bid> bids =bidService.selectBidList(bidVO);
+        AjaxResult ajaxResult = AjaxResult.successPage(bids);
+
+        bids.forEach(bid -> {
+            if (StringUtils.isEmpty(bid.getUserCode()) && Objects.nonNull(bid.getAccountId())) {
+                String code = UserUtils.getCode(Integer.parseInt(bid.getAccountId()));
+                if (!StringUtils.isEmpty(code)) {
+                    bid.setUserCode(code);
+                    //???update do wat
+                    Bid update = new Bid();
+                    update.setId(bid.getId());
+                    update.setUserCode(code);
+                    bidService.updateBid(bid);
+                }
+            }
+            bid.setAccount(SensitiveDataUtils.maskString(bid.getAccount(), 4));
+        });
+
+        if (!CollectionUtils.isEmpty(bids) && Objects.equals(1, bids.get(0).getStatus())) {
+            ajaxResult.put("win", bids.get(0));
+        }
+        return ajaxResult;
+    }
+
+}

+ 63 - 0
auc/src/main/java/cn/hobbystocks/auc/web/LocalController.java

@@ -0,0 +1,63 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.annotation.RequireRoles;
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import cn.hobbystocks.auc.common.utils.StringUtils;
+import cn.hobbystocks.auc.common.utils.UserType;
+import cn.hobbystocks.auc.domain.LotGroup;
+import cn.hobbystocks.auc.vo.LotGroupVO;
+import cn.hobbystocks.auc.vo.SkuDTO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Objects;
+
+@RestController
+@Api(tags = "本地服务接口")
+@RequestMapping("/api/local")
+public class LocalController extends AdminBaseController{
+    @Autowired
+    private AppClient appClient;
+
+    @ApiOperation(value = "根据businessCode查询商品", notes = "根据商品名称,审核状态查询商品列表", response = LotGroup.class, responseContainer = "List<LotGroup>")
+    @PostMapping("/lot_group/list")
+    public AjaxResult list(@RequestBody LotGroupVO lotGroup) {
+        if (StringUtils.isEmpty(lotGroup.getBusinessCode())){
+            AjaxResult.error("业务系统编码不能为空");
+        }
+        startPage(lotGroup);
+        lotGroup.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+        if (Objects.nonNull(UserUtils.getSimpleUserInfo()) && Objects.nonNull(UserUtils.getSimpleUserInfo().getMerchantId())) {
+            lotGroup.setMerchantId(UserUtils.getSimpleUserInfo().getMerchantId().longValue());
+        }
+        if (Objects.equals(0, lotGroup.getNewStatus())) {
+            lotGroup.setPubStatus(Constants.PUB_STATUS_NO_PUBLISHED);
+        }else if (Objects.equals(1, lotGroup.getNewStatus())) {
+            lotGroup.setPubStatus(Constants.PUB_STATUS_PUBLISHED);
+        }else if (Objects.equals(2, lotGroup.getNewStatus())) {
+            lotGroup.setPubStatus(Constants.PUB_STATUS_PUBLISHED);
+            lotGroup.setStatus(Constants.GROUP_STATUS_WAITING);
+        }else if (Objects.equals(3, lotGroup.getNewStatus())) {
+            lotGroup.setStatus(Constants.GROUP_STATUS_STARTING);
+        }
+
+        List<LotGroup> data = lotService.selectLotGroupList(lotGroup);
+        data.forEach(group -> {
+            SkuDTO sku = appClient.getSku(Integer.parseInt(group.getGoodsId()));
+            if (Objects.nonNull(sku)) {
+                group.setSkuStock(sku.getSkuStock());
+                group.setSkuCode(sku.getCode());
+            }
+        });
+        return AjaxResult.successPage(data);
+    }
+}

+ 183 - 0
auc/src/main/java/cn/hobbystocks/auc/web/LotController.java

@@ -0,0 +1,183 @@
+package cn.hobbystocks.auc.web;
+
+import java.util.*;
+
+
+import cn.hobbystocks.auc.annotation.RequireRoles;
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.utils.StringUtils;
+import cn.hobbystocks.auc.common.utils.UserType;
+import cn.hobbystocks.auc.domain.Bid;
+import cn.hobbystocks.auc.domain.Lot;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.mapper.BidMapper;
+import cn.hobbystocks.auc.service.SyncService;
+import cn.hobbystocks.auc.task.DynamicTaskService;
+import cn.hobbystocks.auc.vo.LotVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.*;
+
+import cn.hobbystocks.auc.domain.Auction;
+
+import javax.validation.Valid;
+
+/**
+ * 拍品Controller
+ */
+@RestController
+@RequestMapping("/auction/admin/lot")
+@Api(tags = "拍品管理")
+public class LotController extends AdminBaseController {
+	@Autowired
+	private SyncService syncService;
+	@Autowired
+	private AppClient appClient;
+	@Autowired
+	private BidMapper bidMapper;
+
+	/**
+	 * 查询拍品列表
+	 */
+	@ApiOperation(value = "查询拍品列表", notes = "分页查询拍品列表", response = Lot.class, responseContainer = "List<Lot>")
+	@PostMapping("/list")
+	@RequireRoles({UserType.USER_ROLE_ADMIN})
+	public AjaxResult list(@RequestBody LotVO lot) {
+	    if (StringUtils.isEmpty(lot.getBusinessCode())){
+	        return AjaxResult.error("业务系统编码不能为空");
+        }
+		startPage(lot);
+		lot.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+		lot.setAuctionId(1L);
+		List<Lot> lotList = lotService.selectLotList(lot);
+		lotList.forEach(l -> {
+			l.setDelay(StringUtils.isEmpty(l.getDelayPublish()) ? 0 : 1);
+			if (Constants.LOT_STATUS_CANCELLED.equals(l.getStatus())) {
+				List<Bid> bids = bidMapper.selectBidListLimit(l.getId(), 1L);
+				if (!CollectionUtils.isEmpty(bids)) {
+					Bid bid = bids.get(0);
+					l.setManualReturnPoint(bid.getManualReturnPoint());
+				}
+			}
+		});
+		return AjaxResult.successPage(lotList);
+	}
+
+	/**
+	 * 新增保存拍品
+	 */
+	@ApiOperation(value = "新增保存拍品", notes = "如拍卖会已经结束不可添加", response = AjaxResult.class, responseContainer = "AjaxResult.success")
+	@PostMapping("/add")
+	@RequireRoles({UserType.USER_ROLE_ADMIN})
+	public AjaxResult addSave(@RequestBody @Valid LotVO lot) {
+		Auction dbAuction = auctionService.selectAuctionById(lot.getAuctionId());
+		if (Objects.equals(Constants.GROUP_STATUS_FINISH, dbAuction.getStatus()))
+			return AjaxResult.error("拍卖会已经结束");
+		lot.setCreateBy(getUsername());
+		lot.setStatus(Constants.LOT_STATUS_WAITING);
+		lot.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+		lot.setGoodsType(StringUtils.isEmpty(lot.getGoodsType()) ? "sku" : lot.getGoodsType());
+		lot.setPubStatus(Constants.PUB_STATUS_NO_PUBLISHED);
+		lotService.insertLot(lot);
+		return AjaxResult.success();
+	}
+
+	/**
+	 * 修改保存拍品
+	 */
+	@ApiOperation(value = "修改保存拍品", notes = "修改保存拍品", response = AjaxResult.class, responseContainer = "AjaxResult.success")
+	@PostMapping("/edit")
+	@RequireRoles({UserType.USER_ROLE_ADMIN})
+	public AjaxResult editSave(@RequestBody @Valid LotVO lot) {
+		Lot dbLot = lotService.selectLotById(lot.getId());
+		lot.setUpdateBy(getUsername());
+		if (Constants.PUB_STATUS_PUBLISHED.equals(dbLot.getPubStatus())) {
+			lotService.updateLotEx(lot);
+			syncService.syncLot(lot.getId());
+		}else {
+			lotService.updateLotView(lot);
+		}
+		return AjaxResult.success();
+	}
+
+	/**
+	 * 根据拍品编号删除拍品
+	 */
+	@ApiOperation(value = "根据拍品编号删除拍品", notes = "拍品已发布不能删除", response = AjaxResult.class, responseContainer = "AjaxResult.success")
+	@PostMapping(value = "/remove")
+	@RequireRoles({UserType.USER_ROLE_ADMIN})
+	public AjaxResult remove(@RequestBody LotVO lot) {
+		Lot dbLot = lotService.selectLotById(lot.getId());
+		if (Objects.equals(Constants.PUB_STATUS_PUBLISHED, dbLot.getPubStatus()))
+			return AjaxResult.error("拍品已发布不能删除");
+		lotService.removeLot(lot);
+		return AjaxResult.success();
+	}
+
+	/**
+	 * 发布拍品
+	 */
+	@ApiOperation(value = "发布拍品", notes = "拍品已发布不能再次发布\n" +
+			"拍卖会还未发布不能发布\n" +
+			"拍卖会已结束不能发布\n", response = AjaxResult.class, responseContainer = "AjaxResult.success")
+	@PostMapping(value = "/pub")
+	@RequireRoles({UserType.USER_ROLE_ADMIN})
+	public AjaxResult pub(@RequestBody LotVO lot) {
+		Lot dbLot = lotService.selectLotById(lot.getId());
+		if (Objects.equals(Constants.PUB_STATUS_PUBLISHED, dbLot.getPubStatus()))
+			return AjaxResult.error("拍品已发布不能再次发布");
+		Auction dbAuction = auctionService.selectAuctionById(lot.getAuctionId());
+		if (Objects.equals(Constants.PUB_STATUS_NO_PUBLISHED, dbAuction.getPubStatus()))
+			return AjaxResult.error("拍卖会还未发布");
+		if (Objects.equals(Constants.GROUP_STATUS_FINISH, dbAuction.getStatus()))
+			return AjaxResult.error("拍卖会已结束");
+		BeanUtils.copyProperties(dbLot, lot);
+		lot.setUpdateBy(getUsername());
+		lot.setStatus(Constants.LOT_STATUS_WAITING);
+		lotService.pubLot(lot);
+		return AjaxResult.success();
+	}
+
+	/**
+	 * 根据拍品编号撤回拍品
+	 */
+	@ApiOperation(value = "根据拍品编号撤回拍品", notes = "拍品未发布不能撤回\n", response = AjaxResult.class, responseContainer = "AjaxResult.success")
+	@PostMapping(value = "/cancel")
+	@RequireRoles({UserType.USER_ROLE_ADMIN})
+	public AjaxResult callback(@RequestBody LotVO lot) {
+		Lot dbLot = lotService.selectLotById(lot.getId());
+		if (Objects.equals(Constants.PUB_STATUS_NO_PUBLISHED, dbLot.getPubStatus()))
+			return AjaxResult.error("拍品未发布不能撤回");
+		dbLot.setUpdateBy(getUsername());
+		lotService.cancelLot(dbLot);
+		return AjaxResult.success();
+	}
+
+	@ApiOperation(value = "返还积分", notes = "下架不返还积分\n" +
+			"竞价没积分不返还\n" +
+			"竞价为空不返还\n", response = AjaxResult.class, responseContainer = "AjaxResult.success")
+	@PostMapping(value = "/returnPoint")
+	@RequireRoles({UserType.USER_ROLE_ADMIN})
+	public AjaxResult returnPoint(@RequestBody LotVO lot) {
+		Lot l = lotService.selectLotById(lot.getId());
+		if (!Constants.LOT_STATUS_CANCELLED.equals(l.getStatus())) {
+			return AjaxResult.error();
+		}
+		List<Bid> bids = bidService.selectBidList(Bid.builder().lotId(lot.getId()).build());
+		if (!CollectionUtils.isEmpty(bids)) {
+			Bid bid = bids.get(0);
+			if (Objects.nonNull(bid.getManualReturnPoint())) {
+				return AjaxResult.error();
+			}
+			appClient.returnPoint(bid, -1L);
+			bidService.updateBid(Bid.builder().id(bid.getId()).manualReturnPoint(bid.getAmount()).build());
+		}else {
+			return AjaxResult.error();
+		}
+		return AjaxResult.success();
+	}
+}

+ 316 - 0
auc/src/main/java/cn/hobbystocks/auc/web/LotGroupController.java

@@ -0,0 +1,316 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.annotation.RequireRoles;
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import cn.hobbystocks.auc.common.utils.SensitiveDataUtils;
+import cn.hobbystocks.auc.common.utils.StringUtils;
+import cn.hobbystocks.auc.common.utils.UserType;
+import cn.hobbystocks.auc.domain.Bid;
+import cn.hobbystocks.auc.domain.Lot;
+import cn.hobbystocks.auc.domain.LotFans;
+import cn.hobbystocks.auc.domain.LotGroup;
+import cn.hobbystocks.auc.service.ILotFansService;
+import cn.hobbystocks.auc.service.IOrderStatusService;
+import cn.hobbystocks.auc.vo.LotGroupVO;
+import cn.hobbystocks.auc.vo.SkuDTO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 拍品Controller
+ */
+@RestController
+@RequestMapping("/auction/admin/shipping/lot")
+@Slf4j
+@Api(tags = "商家拍品管理")
+public class LotGroupController extends AdminBaseController {
+	@Autowired
+	private AppClient appClient;
+	@Autowired
+	ILotFansService lotFansService;
+
+    //根据businessCode查询拍品组信息
+    @ApiOperation(value = "根据businessCode查询商品", notes = "根据商品名称,审核状态查询商品列表", response = LotGroup.class, responseContainer = "List<LotGroup>")
+    @PostMapping("/list")
+    @RequireRoles({UserType.USER_ROLE_SHIPPING})
+    public AjaxResult list(@RequestBody LotGroupVO lotGroup) {
+        if (StringUtils.isEmpty(lotGroup.getBusinessCode())){
+            AjaxResult.error("业务系统编码不能为空");
+        }
+        startPage(lotGroup);
+        lotGroup.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+        if (Objects.nonNull(UserUtils.getSimpleUserInfo()) && Objects.nonNull(UserUtils.getSimpleUserInfo().getMerchantId())) {
+            lotGroup.setMerchantId(UserUtils.getSimpleUserInfo().getMerchantId().longValue());
+        }
+        if (Objects.equals(0, lotGroup.getNewStatus())) {
+            lotGroup.setPubStatus(Constants.PUB_STATUS_NO_PUBLISHED);
+        }else if (Objects.equals(1, lotGroup.getNewStatus())) {
+            lotGroup.setPubStatus(Constants.PUB_STATUS_PUBLISHED);
+        }else if (Objects.equals(2, lotGroup.getNewStatus())) {
+            lotGroup.setPubStatus(Constants.PUB_STATUS_PUBLISHED);
+            lotGroup.setStatus(Constants.GROUP_STATUS_WAITING);
+        }else if (Objects.equals(3, lotGroup.getNewStatus())) {
+            lotGroup.setStatus(Constants.GROUP_STATUS_STARTING);
+        }
+
+        List<LotGroup> data = lotService.selectLotGroupList(lotGroup);
+        data.forEach(group -> {
+            SkuDTO sku = appClient.getSku(Integer.parseInt(group.getGoodsId()));
+            if (Objects.nonNull(sku)) {
+                group.setSkuStock(sku.getSkuStock());
+                group.setSkuCode(sku.getCode());
+            }
+        });
+        return AjaxResult.successPage(data);
+    }
+	@ApiOperation(value = "克隆商品", notes = "insertLotGroup with no id", response = AjaxResult.class, responseContainer = "AjaxResult")
+	@PostMapping("/clone")
+	@RequireRoles({UserType.USER_ROLE_SHIPPING})
+	public AjaxResult clone(@RequestBody LotGroupVO lotGroupVO) {
+		LotGroup lotGroup = lotService.selectLotGroupById(lotGroupVO.getId());
+		String error = checkUser(lotGroup.getMerchantId());
+		if (!StringUtils.isEmpty(error)) {
+			return AjaxResult.error(error);
+		}
+		lotGroup.setId(null);
+		lotGroup.setCreateTime(new Date());
+		lotGroup.setCreateBy(getUsername());
+		lotGroup.setUpdateTime(null);
+		lotGroup.setUpdateBy(null);
+		lotGroup.setStatus(Constants.GROUP_STATUS_WAITING);
+		lotGroup.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+		lotGroup.setGoodsType(lotGroup.getGoodsType());
+		lotGroup.setPubStatus(Constants.PUB_STATUS_NO_PUBLISHED);
+		lotGroup.setAuctionId(2L);
+		lotGroup.setFinishNum(0L);
+
+		lotGroup.setStartNum(0L);
+		lotGroup.setSoldNum(0L);
+		lotGroup.setPaid(0L);
+		lotGroup.setOrderId(null);
+		lotGroup.setPubTime(null);
+		lotGroup.setLotId(null);
+		lotGroup.setNextLotId(null);
+		lotService.insertLotGroup(lotGroup);
+		return AjaxResult.success();
+	}
+
+
+	@ApiOperation(value = "添加拍卖商品", notes = "insertLotGroup", response = AjaxResult.class, responseContainer = "AjaxResult")
+	@PostMapping("/add")
+	@RequireRoles({UserType.USER_ROLE_SHIPPING})
+	public AjaxResult addSave(@RequestBody @Valid LotGroupVO lotGroup) {
+		if (lotGroup.getNum() <= 0) {
+			return AjaxResult.error("商品数量必须大于0");
+		}
+		String error = checkUser(lotGroup.getMerchantId());
+		if (!StringUtils.isEmpty(error)) {
+			return AjaxResult.error(error);
+		}
+		lotGroup.setId(null);
+		lotGroup.setCreateTime(new Date());
+		lotGroup.setCreateBy(getUsername());
+		lotGroup.setStatus(Constants.GROUP_STATUS_WAITING);
+		lotGroup.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+		lotGroup.setPubStatus(Constants.PUB_STATUS_NO_PUBLISHED);
+		lotGroup.setAuctionId(2L);
+		lotGroup.setFinishNum(0L);
+
+		lotGroup.setStartNum(0L);
+		lotGroup.setSoldNum(0L);
+		lotGroup.setPaid(0L);
+		lotGroup.setOrderId(null);
+		lotGroup.setPubTime(null);
+		lotGroup.setLotId(null);
+
+		SkuDTO sku = appClient.getSku(Integer.parseInt(lotGroup.getGoodsId()));
+		if (Objects.isNull(sku)) {
+			return AjaxResult.error("未找到商品!");
+		}
+		lotGroup.setImgs(sku.getCarouselImgUrl());
+
+		lotService.insertLotGroup(lotGroup);
+		return AjaxResult.success();
+	}
+
+	@ApiOperation(value = "修改商品基础信息", notes = "竞价已开启不能改只能改name rule detail sort", response = AjaxResult.class, responseContainer = "AjaxResult")
+	@PostMapping("/editLot")
+	@RequireRoles({UserType.USER_ROLE_SHIPPING})
+	public AjaxResult editLot(@RequestBody LotGroupVO lotGroup) {
+		Long lotId = lotGroup.getId();
+		Lot lot = lotService.selectLotById(lotId);
+		if (!Constants.LOT_STATUS_WAITING.equals(lot.getStatus())) {
+			return AjaxResult.error("竞价已开启");
+		}
+		lotService.updateLot(Lot.builder()
+				.id(lot.getId())
+				.name(lotGroup.getName())
+				.ruleContent(lotGroup.getRuleContent())
+				.detail(lotGroup.getDetail())
+				.sort(lotGroup.getSort()).build());
+		return AjaxResult.success();
+	}
+
+	/**
+	 * 修改保存拍品
+	 */
+	@ApiOperation(value = "修改保存拍品 ", notes = "竞价正在进行中的不能改", response = AjaxResult.class, responseContainer = "AjaxResult")
+	@PostMapping("/edit")
+	@RequireRoles({UserType.USER_ROLE_SHIPPING})
+	public AjaxResult editSave(@RequestBody LotGroupVO lotGroup) {
+		if (Objects.nonNull(lotGroup.getNum()) &&lotGroup.getNum() <= 0) {
+			return AjaxResult.error("商品数量必须大于0");
+		}
+		String error = checkUser(lotGroup.getMerchantId());
+		if (!StringUtils.isEmpty(error)) {
+			return AjaxResult.error(error);
+		}
+		LotGroup dbLotGroup = lotService.selectLotGroupById(lotGroup.getId());
+		if (Objects.nonNull(lotGroup.getGoodsId()) && Constants.GROUP_STATUS_STARTING.equals(dbLotGroup.getStatus())) {
+			return AjaxResult.error("竞价正在进行中");
+		}
+		error = checkUser(dbLotGroup.getMerchantId());
+		if (!StringUtils.isEmpty(error)) {
+			return AjaxResult.error(error);
+		}
+		if (Objects.nonNull(lotGroup.getGoodsId())) {
+			lotGroup.setAuctionId(2L);
+			SkuDTO sku = appClient.getSku(Integer.parseInt(lotGroup.getGoodsId()));
+			if (Objects.isNull(sku)) {
+				return AjaxResult.error("未找到商品!");
+			}
+			if (Objects.equals(Constants.PUB_STATUS_PUBLISHED, dbLotGroup.getPubStatus())) {
+				List<LotGroup> list =
+						lotService.selectLotGroupList(LotGroup.builder().goodsId(dbLotGroup.getGoodsId()).pubStatus(Constants.PUB_STATUS_PUBLISHED).delFlag(Constants.DEL_FLAG_NO_DELETE).build());
+				long num = 0;
+				for (LotGroup group : list) {
+					if (!Objects.equals(group.getId(), dbLotGroup.getId())) {
+						List<Lot> lotList = lotService.selectLotList(Lot.builder().delFlag(Constants.DEL_FLAG_NO_DELETE).status(Constants.LOT_STATUS_PASS).groupId(group.getId()).build());
+						num += (group.getNum() - lotList.size());
+					}
+				}
+				List<Lot> lotList =
+						lotService.selectLotList(Lot.builder().delFlag(Constants.DEL_FLAG_NO_DELETE).status(Constants.LOT_STATUS_PASS).groupId(dbLotGroup.getId()).build());
+				num += (lotGroup.getNum() - lotList.size());
+				long skuStock = Objects.nonNull(sku.getSkuStock()) ? sku.getSkuStock() : 0L;
+				if (skuStock < num) {
+					return AjaxResult.error("商品数量超过当前库存!");
+				}
+			}
+			if (StringUtils.isNotEmpty(lotGroup.getGoodsId()) &&
+					!Objects.equals(lotGroup.getGoodsId(), dbLotGroup.getGoodsId()) ) {
+				lotGroup.setImgs(sku.getCarouselImgUrl());
+			}
+		}
+		lotService.updateLotGroup(lotGroup);
+		return AjaxResult.success();
+	}
+
+
+
+
+	/**
+	 * 发布拍品
+	 */
+	@ApiOperation(value = "发布竞价商品", notes = "已审核不能再次审核 有sku库存校验", response = AjaxResult.class, responseContainer = "AjaxResult.success()")
+	@PostMapping(value = "/pub")
+	@RequireRoles({UserType.USER_ROLE_SHIPPING})
+	public AjaxResult pub(@RequestBody LotGroupVO lotGroup) {
+		LotGroup dbLotGroup = lotService.selectLotGroupById(lotGroup.getId());
+		String error = checkUser(dbLotGroup.getMerchantId());
+		if (!StringUtils.isEmpty(error)) {
+			return AjaxResult.error(error);
+		}
+		if (Objects.equals(Constants.PUB_STATUS_PUBLISHED, dbLotGroup.getPubStatus()))
+			return AjaxResult.error("已审核不能再次审核");
+		//查询关联商品的id
+		String goodsId = dbLotGroup.getGoodsId();
+		//查询所有使用此商品的拍卖
+		List<LotGroup> list =
+				lotService.selectLotGroupList(LotGroup.builder().goodsId(dbLotGroup.getGoodsId()).pubStatus(Constants.PUB_STATUS_PUBLISHED).delFlag(Constants.DEL_FLAG_NO_DELETE).build());
+		long num = 0;
+		for (LotGroup group : list) {
+			List<Lot> lotList = lotService.selectLotList(Lot.builder().delFlag(Constants.DEL_FLAG_NO_DELETE).status(Constants.LOT_STATUS_PASS).groupId(group.getId()).build());
+			num += (group.getNum() - lotList.size());
+		}
+		//计算库存
+		num += dbLotGroup.getNum();
+		SkuDTO sku = appClient.getSku(Integer.parseInt(goodsId));
+		long skuStock = Objects.nonNull(sku.getSkuStock()) ? sku.getSkuStock() : 0L;
+		if (skuStock < num) {
+			return AjaxResult.error("商品数量超过当前库存!");
+		}
+		dbLotGroup.setPubStatus(Constants.PUB_STATUS_PUBLISHED);
+		lotService.updateLotGroup0(LotGroup.builder()
+				.id(dbLotGroup.getId())
+				.pubStatus(Constants.PUB_STATUS_PUBLISHED)
+				.pubTime(new Date())
+				.build());
+		return AjaxResult.success();
+	}
+
+
+	//此处查询未指定方法,导致swagger会重复
+	@ApiOperation(value = "商家拍品列表", notes = "selectLotList with lotGroupids", response = Lot.class, responseContainer = "List<Lot>")
+	@RequestMapping("/liveLot")
+	@RequireRoles({UserType.USER_ROLE_ADMIN,UserType.USER_ROLE_SHIPPING})
+	public AjaxResult listLot(@RequestBody LotGroupVO lotGroup){
+		startPage(lotGroup);
+		Lot lot = new Lot();
+		BeanUtils.copyProperties(lotGroup, lot, "id");
+		lot.setGroupId(lotGroup.getId());
+		List<Lot> lotList = lotService.selectLotList(lot);
+		lotList.forEach(lot1 -> {
+			lot1.setDealAccount(SensitiveDataUtils.maskString(lot1.getDealAccount(), 4));
+			lot1.setAnonymous(0);
+			List<Bid> bids = bidService.selectBidList(Bid.builder().lotId(lot1.getId()).build());
+			if (!CollectionUtils.isEmpty(bids)) {
+				lot1.setAnonymous(bids.get(0).getAnonymous());
+			}
+			/*LotFans lotFans = lotFansService.selectLotFansByLotIdAndType(lot1.getId(), "pay_expire");
+			if(lotFans != null) {
+				lot1.setLotFansId(lotFans.getId());
+			}*/
+//            List<Order> orderList = orderStatusService.selectOrderStatusList(Order.builder().lotId(lot1.getId()).flag(1).build());
+
+        });
+		return AjaxResult.successPage(lotList);
+	}
+
+	public String checkUser(Long merchantId) {
+		Integer merchantIdReq = UserUtils.getSimpleUserInfo().getMerchantId();
+		if (Objects.nonNull(merchantIdReq) && Objects.nonNull(merchantId)) {
+			if (!Objects.equals(merchantIdReq.longValue(), merchantId)) {
+				return  "您无权新增或者修改此商品";
+			}
+		}
+		return null;
+	}
+
+    /*@ApiOperation(value = "删除订单逾期未支付的用户记录", notes = "delFans with id", response = AjaxResult.class, responseContainer = "AjaxResult.success")
+    @PostMapping("/delFans/{id}")
+	public AjaxResult delExpire(@PathVariable("id") Long id){
+		int rs = lotFansService.updateLotFansDel(id);
+		if(rs == 0) {
+			return AjaxResult.error();
+		}
+		return AjaxResult.success();
+	}*/
+}

+ 44 - 0
auc/src/main/java/cn/hobbystocks/auc/web/SyncController.java

@@ -0,0 +1,44 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.annotation.RequireRoles;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.core.redis.Locker;
+import cn.hobbystocks.auc.common.utils.UserType;
+import cn.hobbystocks.auc.domain.Lot;
+import cn.hobbystocks.auc.service.SyncService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/auction/admin/sync")
+@Api(tags = "同步拍品直播数据和缓存")
+public class SyncController {
+    @Autowired
+    private SyncService syncService;
+    @Autowired
+    private Locker locker;
+
+    @ApiOperation(value = "同步全部", notes = "同步全部", response = AjaxResult.class, responseContainer = "AjaxResult")
+    @PostMapping("/all")
+    @RequireRoles({UserType.USER_ROLE_ADMIN})
+    public AjaxResult syncAll() {
+        locker.tryLock(Constants.REDIS_LOCK_SYNC_LOT_TEMPLATE, () -> {
+            syncService.syncAll();
+        }, true);
+        return AjaxResult.success();
+    }
+    @ApiOperation(value = "同步单个", notes = "同步单个", response = AjaxResult.class, responseContainer = "AjaxResult")
+    @PostMapping("/lot/{lotId}")
+    @RequireRoles({UserType.USER_ROLE_ADMIN})
+    public AjaxResult syncLot(@PathVariable Long lotId) {
+        syncService.syncLot(lotId);
+        return AjaxResult.success();
+    }
+
+}

+ 28 - 0
auc/src/main/resources/application-dev.yml

@@ -0,0 +1,28 @@
+spring:
+  redis:
+    sentinel:
+      master: ${SENTINEL_MASTER:}
+    password: ${REDIS_PASSWORD:}
+    database: 9
+    timeout: 3000
+    lettuce:
+      pool:
+        max-active: 1000
+        max-wait: -1ms
+        max-idle: 10
+        min-idle: 5
+
+hobbystocks:
+  redis:
+    sentinel:
+      nodes: ${SENTINEL_NODES:}
+  host:
+    pointUrl: https://app/api/local/v1/point/operate
+    getSku: http://app/api/local/v1/items/sku/
+user:
+  info-url: http://app/api/local/v1/user/auction/check-msg/
+knife4j:
+  enable: true
+  setting:
+    language: zh-CN
+  production: false

+ 23 - 0
auc/src/main/resources/application-local.yml

@@ -0,0 +1,23 @@
+redis:
+  database: 9
+  host: ${REDIS_HOST:127.0.0.1}
+  port: ${REDIS_PORT:6379}
+  password: #Pass2021    # 密码(默认为空)
+  timeout: 60000  # 连接超时时长(毫秒)
+  pool:
+    max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
+    max-wait: -1ms    # 连接池最大阻塞等待时间(使用负值表示没有限制)
+    max-idle: 10      # 连接池中的最大空闲连接
+    min-idle: 5       # 连接池中的最小空闲连接
+
+hobbystocks:
+  host:
+    pointUrl: https://m-dev.hobbystocks.net/app/api/local/v1/point/operate
+    getSku: https://m-dev.hobbystocks.net/app/api/local/v1/items/sku/
+user:
+  info-url: https://m-dev.hobbystocks.net/app/api/local/v4/user/auction/check-msg/
+knife4j:
+  enable: true
+  setting:
+    language: zh-CN
+  production: false

+ 26 - 0
auc/src/main/resources/application-prod.yml

@@ -0,0 +1,26 @@
+spring:
+  redis:
+    sentinel:
+      master: ${SENTINEL_MASTER:}
+    password: ${REDIS_PASSWORD:}
+    database: 9
+    timeout: 3000
+    lettuce:
+      pool:
+        max-active: 1000
+        max-wait: -1ms
+        max-idle: 10
+        min-idle: 5
+
+hobbystocks:
+  host:
+    getSku: http://app//api/local/v1/items/sku/
+    pointUrl: http://app//api/local/v1/point/operate
+  redis:
+    sentinel:
+      nodes: ${SENTINEL_NODES:}
+knife4j:
+  enable: true
+  setting:
+    language: zh-CN
+  production: true

+ 100 - 0
auc/src/main/resources/application.yml

@@ -0,0 +1,100 @@
+spring:
+  profiles:
+    active: local
+  application:
+    name: auction-hk
+  datasource:
+    url: ${DB_URL:jdbc:postgresql://192.168.50.10:15432/hobby_auction}
+    username: ${DB_USERNAME:poyee_auction}
+    password: ${DB_PASSWORD:Pass2025}
+  mvc:
+    pathmatch:
+      matching-strategy: ant_path_matcher
+  flyway:
+    # 是否启用flyway
+    enabled: false
+    # 指定表
+    table: flyway_schema_history_auction
+    # 禁止清理数据库表
+    clean-disabled: true
+    # 编码格式,默认UTF-8
+    encoding: UTF-8
+    # 迁移sql脚本文件存放路径,默认db/migration
+    locations: classpath:db/migration
+    # 迁移sql脚本文件名称的前缀,默认V
+    sql-migration-prefix: V
+    # 迁移sql脚本文件名称的分隔符,默认2个下划线__
+    sql-migration-separator: _
+    # 迁移sql脚本文件名称的后缀
+    sql-migration-suffixes: .sql
+    # 迁移时是否进行校验,默认true
+    validate-on-migrate: true
+    # 当迁移发现数据库非空且存在没有元数据的表时,自动执行基准迁移,新建schema_version表
+    baseline-on-migrate: true
+    url: ${DB_URL:jdbc:postgresql://localhost:5432/auction}
+    user: ${DB_USERNAME:hobby_auction}
+    password: ${DB_PASSWORD:Pass2025}
+
+
+
+
+server:
+  # 服务器的HTTP端口,默认为8080
+  port: 8080
+
+swagger:
+  # 是否开启swagger
+  enabled: true
+  # 请求前缀
+  pathMapping: /
+
+# 日志配置
+logging:
+  console.enabled: ${CONSOLE_ENABLED:true}
+  file.enabled: ${FILE_ENABLED:false}
+  level:
+    cn.hobbystocks: info
+    org.springframework: warn
+  fluentd:
+    enabled: ${FLUENTD_ENABLED:false}
+    host: ${FLUENTD_HOST:127.0.0.1}
+    port: ${FLUENTD_PORT:24225}
+
+hobbystocks:
+  app-version: ${spring.profiles.active}
+  white:
+    uris:
+      - /auc/auction/admin/sync
+
+
+#Forest
+forest:
+  #bean-id: config0 # 在spring上下文中bean的id(默认为 forestConfiguration)
+  #backend: okhttp3 # 后端HTTP框架(默认为 okhttp3)
+  #max-connections: 1000 # 连接池最大连接数(默认为 500)
+  #max-route-connections: 500 # 每个路由的最大连接数(默认为 500)你这
+  timeout: 30000 # 请求超时时间,单位为毫秒(默认为 3000)
+  connect-timeout: 30000 # 连接超时时间,单位为毫秒(默认为 timeout)
+  read-timeout: 300000 # 数据读取超时时间,单位为毫秒(默认为 timeout)
+  #max-retry-count: 0 # 请求失败后重试次数(默认为 0 次不重试)
+  #ssl-protocol: SSLv3 # 单向验证的HTTPS的默认SSL协议(默认为 SSLv3)
+  #logEnabled: true # 打开或关闭日志(默认为 true)
+  #log-request: true # 打开/关闭Forest请求日志(默认为 true)
+  #log-response-status: true # 打开/关闭Forest响应状态日志(默认为 true)
+  #log-response-content: true # 打开/关闭Forest响应内容日志(默认为 false)
+  variables:
+    appleUrl: https://appleid.apple.com/auth
+
+management:
+  endpoints:
+    web:
+      exposure:
+        include: health
+  endpoint:
+    health:
+      enabled: true
+      show-details: always
+mybatis-plus:
+  mapper-locations: classpath*:mapper/**/*Mapper.xml
+  type-aliases-package: cn.hobbystocks.auc.domain
+  config-location: classpath:mybatis/mybatis-config.xml

+ 160 - 0
auc/src/main/resources/db/migration/V1.1_Init_.sql

@@ -0,0 +1,160 @@
+CREATE TABLE "public"."auction" (
+  "id" int4 NOT NULL GENERATED ALWAYS AS IDENTITY (
+INCREMENT 1
+MINVALUE  1
+MAXVALUE 2147483647
+START 1
+CACHE 1
+),
+  "no" varchar(50) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "name" varchar(200) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "site" varchar(50) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "imgs" varchar(2000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "banner" varchar(200) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "attachment" varchar(2000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "detail" varchar(2000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "des_data" varchar(2000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "labels" varchar(500) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "pub_status" int4 DEFAULT 0,
+  "pub_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "status" varchar(10) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "start_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "end_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "del_flag" int4 DEFAULT 0,
+  "create_by" varchar(30) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "create_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "update_by" varchar(30) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "update_time" timestamp(6) DEFAULT NULL::timestamp without time zone
+)
+;
+COMMENT ON COLUMN "public"."auction"."no" IS '拍卖会编号';
+COMMENT ON COLUMN "public"."auction"."name" IS '拍卖会名称';
+COMMENT ON COLUMN "public"."auction"."pub_status" IS '发布状态';
+COMMENT ON COLUMN "public"."auction"."pub_time" IS '发布时间';
+COMMENT ON COLUMN "public"."auction"."status" IS '拍卖会状态';
+COMMENT ON COLUMN "public"."auction"."start_time" IS '拍卖会开始时间';
+COMMENT ON COLUMN "public"."auction"."end_time" IS '拍卖会结束时间';
+COMMENT ON COLUMN "public"."auction"."del_flag" IS '删除标志';
+
+-- ----------------------------
+-- Primary Key structure for table auction
+-- ----------------------------
+ALTER TABLE "public"."auction" ADD CONSTRAINT "auction_pkey" PRIMARY KEY ("id");
+
+
+CREATE TABLE "public"."lot" (
+  "id" int4 NOT NULL GENERATED ALWAYS AS IDENTITY (
+INCREMENT 1
+MINVALUE  1
+MAXVALUE 2147483647
+START 1
+CACHE 1
+),
+  "goods_id" varchar(32) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "auction_id" int4 NOT NULL,
+  "name" varchar(200) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "num" numeric(20,4) DEFAULT NULL::numeric,
+  "unit" varchar(20) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "imgs" varchar(2000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "detail" varchar(2000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "pub_status" int4 DEFAULT 0,
+  "pub_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "status" varchar(10) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "start_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "end_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "real_end_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "rule_type" varchar(10) COLLATE "pg_catalog"."default" NOT NULL DEFAULT NULL::character varying,
+  "last_price" numeric(20,2) DEFAULT NULL::numeric,
+  "last_price_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "deal_price" numeric(20,2) DEFAULT NULL::numeric,
+  "deal_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "deal_account_id" varchar(40) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "deal_account" varchar(40) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "paid" int4 DEFAULT 0,
+  "order_id" varchar(32) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "bid_count" int4 DEFAULT 0,
+  "bid_persion_count" int4 DEFAULT 0,
+  "del_flag" int4 DEFAULT 0,
+  "create_by" varchar(30) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "create_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "update_by" varchar(30) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "update_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "sort" int8,
+  "rule_content" varchar(2000) COLLATE "pg_catalog"."default" NOT NULL DEFAULT NULL::numeric
+)
+;
+COMMENT ON COLUMN "public"."lot"."goods_id" IS '商品ID';
+COMMENT ON COLUMN "public"."lot"."auction_id" IS '拍卖会ID';
+COMMENT ON COLUMN "public"."lot"."name" IS '拍品名称';
+COMMENT ON COLUMN "public"."lot"."num" IS '拍品数量';
+COMMENT ON COLUMN "public"."lot"."unit" IS '数量单位';
+COMMENT ON COLUMN "public"."lot"."imgs" IS '拍品图片';
+COMMENT ON COLUMN "public"."lot"."detail" IS '拍品介绍';
+COMMENT ON COLUMN "public"."lot"."pub_status" IS '是否发布(0:未发布;1:已发布)';
+COMMENT ON COLUMN "public"."lot"."pub_time" IS '发布时间';
+COMMENT ON COLUMN "public"."lot"."status" IS '拍卖状态(Waiting:未开始;Starting:开启中;Bidding:进行中Finished:拍卖结束;Cancelled:撤拍;Pass:流拍;Sold:成交;Regret :悔拍)';
+COMMENT ON COLUMN "public"."lot"."start_time" IS '拍卖开始时间';
+COMMENT ON COLUMN "public"."lot"."end_time" IS '拍卖结束时间';
+COMMENT ON COLUMN "public"."lot"."real_end_time" IS '实际结束时间';
+COMMENT ON COLUMN "public"."lot"."rule_type" IS '规则类型';
+COMMENT ON COLUMN "public"."lot"."last_price" IS '最新出价';
+COMMENT ON COLUMN "public"."lot"."last_price_time" IS '最新出价时间';
+COMMENT ON COLUMN "public"."lot"."deal_price" IS '成交价';
+COMMENT ON COLUMN "public"."lot"."deal_time" IS '成交时间';
+COMMENT ON COLUMN "public"."lot"."deal_account_id" IS '成交用过id';
+COMMENT ON COLUMN "public"."lot"."deal_account" IS '成交用户';
+COMMENT ON COLUMN "public"."lot"."paid" IS '已支付';
+COMMENT ON COLUMN "public"."lot"."order_id" IS '订单id';
+COMMENT ON COLUMN "public"."lot"."bid_count" IS '出价次数';
+COMMENT ON COLUMN "public"."lot"."bid_persion_count" IS '出价人数';
+COMMENT ON COLUMN "public"."lot"."del_flag" IS '0:未删除;1:已删除';
+COMMENT ON COLUMN "public"."lot"."sort" IS '排序';
+COMMENT ON COLUMN "public"."lot"."rule_content" IS '规则';
+
+-- ----------------------------
+-- Primary Key structure for table lot
+-- ----------------------------
+ALTER TABLE "public"."lot" ADD CONSTRAINT "lot_pkey" PRIMARY KEY ("id");
+
+
+
+CREATE TABLE "public"."bid" (
+  "id" int4 NOT NULL GENERATED ALWAYS AS IDENTITY (
+INCREMENT 1
+MINVALUE  1
+MAXVALUE 2147483647
+START 1
+CACHE 1
+),
+  "lot_id" int4 NOT NULL,
+  "round" int4,
+  "account" varchar(40) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "account_id" varchar(40) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "amount" numeric(20,2) DEFAULT NULL::numeric,
+  "device_type" varchar(50) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "ip" varchar(200) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "del_flag" int4 DEFAULT 0,
+  "create_by" varchar(40) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "create_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "update_by" varchar(40) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "update_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "remark" varchar(500) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "status" int2 DEFAULT 0,
+  "anonymous" int2,
+  "curr" int2
+)
+;
+COMMENT ON COLUMN "public"."bid"."lot_id" IS '拍品ID';
+COMMENT ON COLUMN "public"."bid"."round" IS '轮次';
+COMMENT ON COLUMN "public"."bid"."account" IS '昵称';
+COMMENT ON COLUMN "public"."bid"."account_id" IS '用户id';
+COMMENT ON COLUMN "public"."bid"."amount" IS '消耗积分/金额';
+COMMENT ON COLUMN "public"."bid"."device_type" IS '设备类型';
+COMMENT ON COLUMN "public"."bid"."status" IS '状态:0未中标 1中标';
+COMMENT ON COLUMN "public"."bid"."anonymous" IS '是否匿名:0否 1是';
+COMMENT ON COLUMN "public"."bid"."curr" IS '是否是当前最高价';
+
+-- ----------------------------
+-- Primary Key structure for table bid
+-- ----------------------------
+ALTER TABLE "public"."bid" ADD CONSTRAINT "bid_pkey" PRIMARY KEY ("id");

+ 7 - 0
auc/src/main/resources/db/migration/V1.2_lot_goods_type_.sql

@@ -0,0 +1,7 @@
+ALTER TABLE "public"."lot"
+ADD COLUMN "goods_type" varchar(32);
+
+COMMENT ON COLUMN "public"."lot"."goods_type" IS '商品类型';
+
+ALTER TABLE "public"."lot"
+ALTER COLUMN "rule_type" TYPE varchar(200) COLLATE "pg_catalog"."default";

+ 3 - 0
auc/src/main/resources/db/migration/V1.3_bid_mod_.sql

@@ -0,0 +1,3 @@
+ALTER TABLE "public"."bid"
+ALTER COLUMN "anonymous" SET DEFAULT 0,
+ALTER COLUMN "curr" SET DEFAULT 0;

+ 4 - 0
auc/src/main/resources/db/migration/V1.4_carousel_imgs_.sql

@@ -0,0 +1,4 @@
+ALTER TABLE "public"."lot"
+  ADD COLUMN "carousel_imgs" varchar(2000);
+
+COMMENT ON COLUMN "public"."lot"."carousel_imgs" IS '轮播图片 逗号分割';

+ 2 - 0
auc/src/main/resources/db/migration/V1.5_bid_avatar_.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "public"."bid"
+  ADD COLUMN "avatar" varchar(255);

+ 3 - 0
auc/src/main/resources/db/migration/V1.6_lot_avatar_.sql

@@ -0,0 +1,3 @@
+ALTER TABLE "public"."lot"
+  ADD COLUMN "merchant_name" varchar(255),
+  ADD COLUMN "merchant_avatar" varchar(2000);

+ 2 - 0
auc/src/main/resources/db/migration/V1.7_lot_goods_.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "public"."lot"
+  ADD COLUMN "goods_name" varchar(255);

+ 2 - 0
auc/src/main/resources/db/migration/V1.8_lot_merchant_.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "public"."lot"
+  ADD COLUMN "merchant_id" int4;

+ 20 - 0
auc/src/main/resources/db/migration/V1.9_error_.sql

@@ -0,0 +1,20 @@
+CREATE TABLE "public"."lot_error" (
+  "id" int4 NOT NULL GENERATED ALWAYS AS IDENTITY (
+INCREMENT 1
+MINVALUE  1
+MAXVALUE 2147483647
+START 1
+CACHE 1
+),
+  "create_time" timestamp(6) NOT NULL,
+  "type" varchar(255) NOT NULL,
+  "lot_id" int4 NOT NULL ,
+  "bid_id" int4,
+  "point" int4,
+  "msg" varchar(255),
+  "order_id" varchar(255),
+  PRIMARY KEY ("id")
+)
+;
+
+COMMENT ON COLUMN "public"."lot_error"."type" IS '错误维度';

+ 7 - 0
auc/src/main/resources/db/migration/V2.0_index_.sql

@@ -0,0 +1,7 @@
+CREATE INDEX "auctionId" ON "public"."lot" USING btree (
+  "auction_id"
+);
+
+CREATE INDEX "lotId" ON "public"."bid" USING btree (
+  "lot_id"
+);

+ 2 - 0
auc/src/main/resources/db/migration/V2.1_bid_no_.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "public"."bid"
+  ADD COLUMN "bid_no" varchar(255);

+ 2 - 0
auc/src/main/resources/db/migration/V2.2_bid_user_code_.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "public"."bid"
+  ADD COLUMN "user_code" varchar(255);

+ 16 - 0
auc/src/main/resources/db/migration/V2.3_bid_record_.sql

@@ -0,0 +1,16 @@
+CREATE TABLE "public"."bid_record" (
+  "id" int4 NOT NULL GENERATED ALWAYS AS IDENTITY (
+INCREMENT 1
+MINVALUE  1
+MAXVALUE 2147483647
+START 1
+CACHE 1
+),
+  "user_id" int4,
+  "amount" numeric(20,2),
+  "lot_id" int4 NOT NULL,
+  "create_time" timestamp(6),
+  "bid_no" varchar(255),
+  PRIMARY KEY ("id")
+)
+;

+ 1 - 0
auc/src/main/resources/db/migration/V2.4_update_lot_goods_type_.sql

@@ -0,0 +1 @@
+UPDATE lot set goods_type = 'sku' WHERE goods_type is NULL;

+ 18 - 0
auc/src/main/resources/db/migration/V2.5_sold_order_record_.sql

@@ -0,0 +1,18 @@
+CREATE TABLE "public"."sold_order_record" (
+    "id" int4 NOT NULL GENERATED ALWAYS AS IDENTITY (
+  INCREMENT 1
+  MINVALUE  1
+  MAXVALUE 2147483647
+  START 1
+  CACHE 1
+  ),
+  "lot_id" int4 NOT NULL,
+  "bid_id" int4 NOT NULL,
+  "create_time" timestamp(6) NOT NULL,
+  "order_id" varchar(255)
+)
+;
+
+CREATE UNIQUE INDEX "lotid" ON "public"."sold_order_record" USING btree (
+  "lot_id"
+);

+ 4 - 0
auc/src/main/resources/db/migration/V2.6_private_domain_.sql

@@ -0,0 +1,4 @@
+ALTER TABLE "public"."lot"
+  ADD COLUMN "private_domain" int2 NOT NULL DEFAULT 0;
+
+COMMENT ON COLUMN "public"."lot"."private_domain" IS '是否是私域 0 不是  1 是';

+ 17 - 0
auc/src/main/resources/db/migration/V2.7_nickname_too_long_.sql

@@ -0,0 +1,17 @@
+ALTER TABLE "public"."lot" 
+  ALTER COLUMN "deal_account" TYPE varchar(64) COLLATE "pg_catalog"."default",
+  ALTER COLUMN "create_by" TYPE varchar(64) COLLATE "pg_catalog"."default",
+  ALTER COLUMN "update_by" TYPE varchar(64) COLLATE "pg_catalog"."default";
+
+
+ALTER TABLE "public"."bid"
+  ALTER COLUMN "account" TYPE varchar(64) COLLATE "pg_catalog"."default",
+  ALTER COLUMN "create_by" TYPE varchar(64) COLLATE "pg_catalog"."default",
+  ALTER COLUMN "update_by" TYPE varchar(64) COLLATE "pg_catalog"."default";
+
+
+ALTER TABLE "public"."auction"
+  ALTER COLUMN "create_by" TYPE varchar(64) COLLATE "pg_catalog"."default",
+  ALTER COLUMN "update_by" TYPE varchar(64) COLLATE "pg_catalog"."default";
+
+

+ 5 - 0
auc/src/main/resources/db/migration/V2.8_img_too_long_.sql

@@ -0,0 +1,5 @@
+ALTER TABLE "public"."lot"
+  ALTER COLUMN "imgs" TYPE varchar(5000) COLLATE "pg_catalog"."default",
+  ALTER COLUMN "detail" TYPE varchar(5000) COLLATE "pg_catalog"."default",
+  ALTER COLUMN "rule_content" TYPE varchar(5000) COLLATE "pg_catalog"."default",
+  ALTER COLUMN "carousel_imgs" TYPE varchar(5000) COLLATE "pg_catalog"."default";

+ 2 - 0
auc/src/main/resources/db/migration/V2.9_delay_publish_.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "public"."lot"
+  ADD COLUMN "delay_publish" varchar(255);

+ 26 - 0
auc/src/main/resources/db/migration/V3.0_lot_fans_.sql

@@ -0,0 +1,26 @@
+CREATE TABLE "public"."lot_fans" (
+  "id" int4 NOT NULL GENERATED ALWAYS AS IDENTITY (
+INCREMENT 1
+MINVALUE  1
+MAXVALUE 2147483647
+START 1
+CACHE 1
+),
+  "user_id" int4 NOT NULL,
+  "lot_id" int4 NOT NULL,
+  "create_time" timestamp(6) NOT NULL,
+  "type" varchar(64) COLLATE "pg_catalog"."default" NOT NULL
+)
+;
+
+CREATE INDEX "lot_id_type" ON "public"."lot_fans" USING btree (
+  "lot_id",
+  "type"
+);
+
+CREATE INDEX "user_id_lot_id" ON "public"."lot_fans" USING btree (
+  "user_id",
+  "lot_id"
+);
+
+COMMENT ON COLUMN "public"."lot_fans"."type" IS '类型,user_like';

+ 19 - 0
auc/src/main/resources/db/migration/V3.1_lot_fans_push_record_.sql

@@ -0,0 +1,19 @@
+CREATE TABLE "public"."lot_fans_push_record" (
+  "id" int4 NOT NULL GENERATED ALWAYS AS IDENTITY (
+INCREMENT 1
+MINVALUE  1
+MAXVALUE 2147483647
+START 1
+CACHE 1
+),
+  "lot_id" int4 NOT NULL,
+  "create_time" timestamp(6) NOT NULL,
+  "type" varchar(255) NOT NULL,
+  "content" varchar(5000)
+)
+;
+
+CREATE UNIQUE INDEX "lot_id_type_record" ON "public"."lot_fans_push_record" USING btree (
+  "lot_id",
+  "type"
+);

+ 2 - 0
auc/src/main/resources/db/migration/V3.2_manual_return_point_.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "public"."bid"
+  ADD COLUMN "manual_return_point" numeric(20,2);

+ 152 - 0
auc/src/main/resources/db/migration/V3.3_new_v2_auction_.sql

@@ -0,0 +1,152 @@
+/*
+ Navicat Premium Data Transfer
+
+ Source Server         : sh.light.ipangyou.com
+ Source Server Type    : PostgreSQL
+ Source Server Version : 130012
+ Source Host           : sh.light.ipangyou.com:5432
+ Source Catalog        : auction
+ Source Schema         : public
+
+ Target Server Type    : PostgreSQL
+ Target Server Version : 130012
+ File Encoding         : 65001
+
+ Date: 05/12/2024 11:37:36
+*/
+
+
+-- ----------------------------
+-- Table structure for lot_group
+-- ----------------------------
+DROP TABLE IF EXISTS "public"."lot_group";
+CREATE TABLE "public"."lot_group" (
+  "id" int4 NOT NULL GENERATED ALWAYS AS IDENTITY (
+INCREMENT 1
+MINVALUE  1
+MAXVALUE 2147483647
+START 1
+CACHE 1
+),
+  "goods_id" varchar(32) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "auction_id" int4 NOT NULL,
+  "name" varchar(200) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "num" numeric(20,4) DEFAULT NULL::numeric,
+  "unit" varchar(20) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "imgs" varchar(5000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "detail" varchar(5000) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "pub_status" int4 DEFAULT 0,
+  "pub_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "status" varchar(10) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "start_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "end_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "real_end_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "rule_type" varchar(200) COLLATE "pg_catalog"."default" NOT NULL DEFAULT NULL::character varying,
+  "last_price" numeric(20,2) DEFAULT NULL::numeric,
+  "last_price_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "deal_price" numeric(20,2) DEFAULT NULL::numeric,
+  "deal_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "deal_account_id" varchar(40) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "deal_account" varchar(64) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "paid" int4 DEFAULT 0,
+  "order_id" varchar(32) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "bid_count" int4 DEFAULT 0,
+  "bid_persion_count" int4 DEFAULT 0,
+  "del_flag" int4 DEFAULT 0,
+  "create_by" varchar(64) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "create_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "update_by" varchar(64) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
+  "update_time" timestamp(6) DEFAULT NULL::timestamp without time zone,
+  "sort" int8,
+  "rule_content" varchar(5000) COLLATE "pg_catalog"."default" NOT NULL DEFAULT NULL::numeric,
+  "goods_type" varchar(32) COLLATE "pg_catalog"."default",
+  "carousel_imgs" varchar(5000) COLLATE "pg_catalog"."default",
+  "merchant_name" varchar(255) COLLATE "pg_catalog"."default",
+  "merchant_avatar" varchar(2000) COLLATE "pg_catalog"."default",
+  "goods_name" varchar(255) COLLATE "pg_catalog"."default",
+  "merchant_id" int4,
+  "private_domain" int2 NOT NULL DEFAULT 0,
+  "delay_publish" varchar(255) COLLATE "pg_catalog"."default",
+  "live_id" int4,
+  "finish_num" int4 NOT NULL DEFAULT 0,
+  "sold_num" int4 NOT NULL DEFAULT 0,
+  "start_num" int4 NOT NULL DEFAULT 0,
+  "lot_id" int4,
+  "sell_point" varchar(255) COLLATE "pg_catalog"."default",
+  "next_lot_id" int4
+)
+;
+COMMENT ON COLUMN "public"."lot_group"."goods_id" IS '商品ID';
+COMMENT ON COLUMN "public"."lot_group"."auction_id" IS '拍卖会ID';
+COMMENT ON COLUMN "public"."lot_group"."name" IS '拍品名称';
+COMMENT ON COLUMN "public"."lot_group"."num" IS '拍品数量';
+COMMENT ON COLUMN "public"."lot_group"."unit" IS '数量单位';
+COMMENT ON COLUMN "public"."lot_group"."imgs" IS '拍品图片';
+COMMENT ON COLUMN "public"."lot_group"."detail" IS '拍品介绍';
+COMMENT ON COLUMN "public"."lot_group"."pub_status" IS '是否发布(0:未发布;1:已发布)';
+COMMENT ON COLUMN "public"."lot_group"."pub_time" IS '发布时间';
+COMMENT ON COLUMN "public"."lot_group"."status" IS '拍卖状态(Waiting:未开始;Starting:开启中;Bidding:进行中Finished:拍卖结束;Cancelled:撤拍;Pass:流拍;Sold:成交;Regret :悔拍)';
+COMMENT ON COLUMN "public"."lot_group"."start_time" IS '拍卖开始时间';
+COMMENT ON COLUMN "public"."lot_group"."end_time" IS '拍卖结束时间';
+COMMENT ON COLUMN "public"."lot_group"."real_end_time" IS '实际结束时间';
+COMMENT ON COLUMN "public"."lot_group"."rule_type" IS '规则类型';
+COMMENT ON COLUMN "public"."lot_group"."last_price" IS '最新出价';
+COMMENT ON COLUMN "public"."lot_group"."last_price_time" IS '最新出价时间';
+COMMENT ON COLUMN "public"."lot_group"."deal_price" IS '成交价';
+COMMENT ON COLUMN "public"."lot_group"."deal_time" IS '成交时间';
+COMMENT ON COLUMN "public"."lot_group"."deal_account_id" IS '成交用过id';
+COMMENT ON COLUMN "public"."lot_group"."deal_account" IS '成交用户';
+COMMENT ON COLUMN "public"."lot_group"."paid" IS '已支付';
+COMMENT ON COLUMN "public"."lot_group"."order_id" IS '订单id';
+COMMENT ON COLUMN "public"."lot_group"."bid_count" IS '出价次数';
+COMMENT ON COLUMN "public"."lot_group"."bid_persion_count" IS '出价人数';
+COMMENT ON COLUMN "public"."lot_group"."del_flag" IS '0:未删除;1:已删除';
+COMMENT ON COLUMN "public"."lot_group"."sort" IS '排序';
+COMMENT ON COLUMN "public"."lot_group"."rule_content" IS '规则';
+COMMENT ON COLUMN "public"."lot_group"."goods_type" IS '商品类型';
+COMMENT ON COLUMN "public"."lot_group"."carousel_imgs" IS '轮播图片 逗号分割';
+COMMENT ON COLUMN "public"."lot_group"."private_domain" IS '是否是私域 0 不是  1 是';
+
+-- ----------------------------
+-- Indexes structure for table lot_group
+-- ----------------------------
+CREATE INDEX "LotStatus_copy1" ON "public"."lot_group" USING hash (
+  "status" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops"
+);
+CREATE INDEX "create_time" ON "public"."lot_group" USING btree (
+  "create_time" "pg_catalog"."timestamp_ops" ASC NULLS LAST
+);
+CREATE INDEX "live_id" ON "public"."lot_group" USING btree (
+  "live_id" "pg_catalog"."int4_ops" ASC NULLS LAST
+);
+
+-- ----------------------------
+-- Primary Key structure for table lot_group
+-- ----------------------------
+ALTER TABLE "public"."lot_group" ADD CONSTRAINT "lot_copy1_pkey" PRIMARY KEY ("id");
+
+ALTER TABLE "public"."lot_fans"
+  ADD COLUMN "expire" timestamp(6);
+
+ALTER TABLE "public"."lot"
+  ADD COLUMN "live_id" int4,
+  ADD COLUMN "group_id" int4;
+
+ALTER TABLE "public"."lot"
+  ALTER COLUMN "order_id" TYPE varchar(1000) COLLATE "pg_catalog"."default";
+
+CREATE INDEX "expire" ON "public"."lot_fans" USING btree (
+  "expire"
+);
+
+CREATE INDEX "lot_create_time" ON "public"."lot" USING btree (
+  "create_time"
+);
+
+CREATE INDEX "lot_group_id" ON "public"."lot" USING btree (
+  "group_id"
+);
+
+CREATE INDEX "lot_live_id" ON "public"."lot" USING btree (
+  "live_id"
+);

+ 2 - 0
auc/src/main/resources/db/migration/V3.4_merch_.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "public"."lot_fans"
+  ADD COLUMN "merchant_id" int4;

+ 4 - 0
auc/src/main/resources/db/migration/V3.5_lot_group_edit.sql

@@ -0,0 +1,4 @@
+ALTER TABLE "lot_group"
+    ADD COLUMN "live_name" varchar(255);
+
+COMMENT ON COLUMN "lot_group"."live_name" IS '直播间名称';

+ 5 - 0
auc/src/main/resources/db/migration/V3.6_bid_index_.sql

@@ -0,0 +1,5 @@
+CREATE INDEX "lotId_accountId" ON "public"."bid" USING btree (
+  "lot_id",
+  "account_id",
+  "status"
+);

+ 4 - 0
auc/src/main/resources/db/migration/V3.7_lot_edit.sql

@@ -0,0 +1,4 @@
+ALTER TABLE "lot"
+    ADD COLUMN "live_name" varchar(255);
+
+COMMENT ON COLUMN "lot"."live_name" IS '直播间名称';

+ 4 - 0
auc/src/main/resources/db/migration/V3.8_lot_fans_edit.sql

@@ -0,0 +1,4 @@
+ALTER TABLE "lot_fans"
+    ADD COLUMN "del_flag" int2 DEFAULT 0;
+
+COMMENT ON COLUMN "lot_fans"."del_flag" IS '是否删除 默认0否 1是';

+ 21 - 0
auc/src/main/resources/logback-fluentd.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<included>
+    <appender name="FLUENT_SYNC" class="ch.qos.logback.more.appenders.DataFluentAppender">
+        <tag>logback</tag>
+        <label>${spring.application.name}.${hostname}</label>
+        <remoteHost>${logging.fluentd.host}</remoteHost>
+        <port>${logging.fluentd.port}</port>
+
+        <encoder charset="UTF-8">
+            <pattern>%logger{15}:%L - %msg</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="FLUENT" class="ch.qos.logback.classic.AsyncAppender">
+        <queueSize>1000</queueSize>
+        <neverBlock>true</neverBlock>
+        <maxFlushTime>5000</maxFlushTime>
+        <includeCallerData>false</includeCallerData>
+        <appender-ref ref="FLUENT_SYNC" />
+    </appender>
+</included>

+ 43 - 0
auc/src/main/resources/logback-spring.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+    <property name="log.path" value="../logs" />
+    <!-- 日志输出格式 -->
+    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+    <springProperty scope="context" name="spring.application.name" source="spring.application.name"/>
+    <springProperty scope="context" name="logging.fluentd.enabled" source="logging.fluentd.enabled"/>
+    <springProperty scope="context" name="logging.console.enabled" source="logging.console.enabled"/>
+    <springProperty scope="context" name="logging.fluentd.host" source="logging.fluentd.host"/>
+    <springProperty scope="context" name="logging.fluentd.port" source="logging.fluentd.port"/>
+    <define name="hostname" class="cn.hobbystocks.auc.common.utils.CanonicalHostNamePropertyDefiner"/>
+
+    <if condition='p("logging.fluentd.enabled").equals("true")'>
+        <then>
+            <include resource="logback-fluentd.xml" />
+        </then>
+    </if>
+
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <!-- Spring日志级别控制  -->
+    <logger name="org.springframework" level="warn" />
+    <!--系统操作日志-->
+    <root level="info">
+        <if condition='p("logging.console.enabled").equals("true")'>
+            <then>
+                <appender-ref ref="console" />
+            </then>
+        </if>
+        <if condition='p("logging.fluentd.enabled").equals("true")'>
+            <then>
+                <appender-ref ref="FLUENT"/>
+            </then>
+        </if>
+    </root>
+</configuration>

+ 169 - 0
bid/pom.xml

@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>auction</artifactId>
+		<groupId>cn.hobbystocks</groupId>
+		<version>0.1.0</version>
+	</parent>
+
+	<artifactId>bid</artifactId>
+
+	<dependencies>
+		<dependency>
+			<groupId>com.google.guava</groupId>
+			<artifactId>guava</artifactId>
+			<version>33.3.0-jre</version>
+		</dependency>
+		<dependency>
+			<groupId>cn.hobbystocks</groupId>
+			<artifactId>lot</artifactId>
+		</dependency>
+		<!-- web -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		<!-- actuator -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
+		</dependency>
+		<!-- 数据库连接驱动 -->
+
+		<dependency>
+			<groupId>com.baomidou</groupId>
+			<artifactId>mybatis-plus-boot-starter</artifactId>
+		</dependency>
+		<!-- Druid 数据库连接池 -->
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>druid-spring-boot-starter</artifactId>
+		</dependency>
+		<!-- AOP(Druid监控Spring时需要依赖) -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-aop</artifactId>
+		</dependency>
+
+		<!-- swagger3 -->
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-boot-starter</artifactId>
+			<version>3.0.0</version>
+		</dependency>
+		<!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
+		<dependency>
+			<groupId>io.swagger</groupId>
+			<artifactId>swagger-models</artifactId>
+			<version>1.6.2</version>
+		</dependency>
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+			<optional>true</optional>
+		</dependency>
+		<!--常用工具类 -->
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-lang3</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.github.pagehelper</groupId>
+			<artifactId>pagehelper-spring-boot-starter</artifactId>
+		</dependency>
+		<!-- 阿里JSON解析器 -->
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+			<version>1.2.80</version>
+		</dependency>
+		<!-- redis 缓存操作 -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-redis</artifactId>
+		</dependency>
+		<!-- pool 对象池 -->
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-pool2</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+			<version>4.5.10</version>
+		</dependency>
+		<dependency>
+			<groupId>org.codehaus.janino</groupId>
+			<artifactId>janino</artifactId>
+			<version>3.0.12</version>
+		</dependency>
+		<dependency>
+			<groupId>com.sndyuk</groupId>
+			<artifactId>logback-more-appenders</artifactId>
+			<version>1.8.5</version>
+		</dependency>
+		<dependency>
+			<groupId>org.fluentd</groupId>
+			<artifactId>fluent-logger</artifactId>
+			<version>0.3.4</version>
+		</dependency>
+		<!--<dependency>
+			<groupId>org.apache.poi</groupId>
+			<artifactId>poi-ooxml</artifactId>
+		</dependency>-->
+		<!-- 自定义验证注解 -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-validation</artifactId>
+		</dependency>
+		<!--bee-->
+		<dependency>
+			<groupId>org.teasoft</groupId>
+			<artifactId>bee</artifactId>
+			<version>${bee.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.teasoft</groupId>
+			<artifactId>honey</artifactId>
+			<version>${bee.version}</version>
+		</dependency>
+		<!--for log framework,Excel(poi) -->
+		<dependency>
+			<groupId>org.teasoft</groupId>
+			<artifactId>bee-ext</artifactId>
+			<version>${bee.version}</version>
+		</dependency>
+	</dependencies>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+				<version>2.1.1.RELEASE</version>
+				<configuration>
+					<fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+				</configuration>
+				<executions>
+					<execution>
+						<goals>
+							<goal>repackage</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-war-plugin</artifactId>
+				<version>3.1.0</version>
+				<configuration>
+					<failOnMissingWebXml>false</failOnMissingWebXml>
+					<warName>${project.artifactId}</warName>
+				</configuration>
+			</plugin>
+		</plugins>
+		<finalName>${project.artifactId}</finalName>
+	</build>
+</project>

+ 34 - 0
bid/src/main/java/cn/hobbystocks/auc/BidApplication.java

@@ -0,0 +1,34 @@
+package cn.hobbystocks.auc;
+
+import java.util.TimeZone;
+
+import com.dtflys.forest.springboot.annotation.ForestScan;
+import lombok.extern.slf4j.Slf4j;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import springfox.documentation.oas.annotations.EnableOpenApi;
+
+@ForestScan(basePackages = "cn.hobbystocks.auc.forest")
+@MapperScan("cn/hobbystocks/auc/mapper")
+@EnableScheduling
+@EnableAsync
+@SpringBootApplication
+@Slf4j
+@EnableOpenApi
+public class BidApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(BidApplication.class, args);
+		log.info("(♥◠‿◠)ノ゙ app启动成功   ლ(´ڡ`ლ)゙");
+	}
+
+	@Bean
+	public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() {
+		return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getTimeZone("GMT+8"));
+	}
+}

+ 398 - 0
bid/src/main/java/cn/hobbystocks/auc/app/AppClient.java

@@ -0,0 +1,398 @@
+package cn.hobbystocks.auc.app;
+
+import cn.hobbystocks.auc.common.utils.CloneUtils;
+import cn.hobbystocks.auc.common.utils.DateUtils;
+import cn.hobbystocks.auc.common.utils.StringUtils;
+import cn.hobbystocks.auc.domain.Bid;
+import cn.hobbystocks.auc.domain.Lot;
+import cn.hobbystocks.auc.domain.LotFans;
+import cn.hobbystocks.auc.domain.LotFansPushRecord;
+import cn.hobbystocks.auc.dto.PointDTO;
+import cn.hobbystocks.auc.event.SoldEvent;
+import cn.hobbystocks.auc.forest.CommonForestClient;
+import cn.hobbystocks.auc.handle.context.Live;
+import cn.hobbystocks.auc.mapper.LotFansMapper;
+import cn.hobbystocks.auc.mapper.LotFansPushRecordMapper;
+import cn.hobbystocks.auc.vo.*;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.dtflys.forest.http.ForestResponse;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Component
+@Slf4j
+public class AppClient {
+    @Autowired
+    private CommonForestClient client;
+    @Value("${hobbystocks.host.pointUrl}")
+    private String pointUrl;
+    @Value("${hobbystocks.host.orderUrl}")
+    private String orderUrl;
+    @Value("${hobbystocks.host.couponUrl}")
+    private String couponUrl;
+    @Value("${hobbystocks.host.noticeUrl}")
+    private String noticeUrl;
+    @Value("${hobbystocks.host.jPushUrl}")
+    private String jPushUrl;
+    @Value("${hobbystocks.host.imUrl}")
+    private String liveWebSocketUrl;
+    @Value("${hobbystocks.host.notifyUrl}")
+    private String notifyUrl;
+
+    @Autowired
+    private LotFansMapper lotFansMapper;
+    @Autowired
+    private LotFansPushRecordMapper lotFansPushRecordMapper;
+
+    private final static String winTemplate = "恭喜您以%s积分获得%s!";
+    private final static String noEinTemplate = "您参与的%s结果已揭晓";
+
+    public void jPush(Live live) {
+
+        Long lotId = live.getLot().getId();
+        ForestResponse<CommonForestClient.Response<Map<String, Object>>> response = null;
+        try {
+            LotFansPushRecord record = LotFansPushRecord.builder().lotId(lotId).type("jPush").build();
+            record.setCreateTime(new Date());
+            lotFansPushRecordMapper.insertRecord(record);
+            LotFans lotFans = LotFans.builder().lotId(lotId).type("user_like").build();
+            Set<Long> userList = lotFansMapper.selectLotFansList(lotFans).stream().mapToLong(LotFans::getUserId).boxed().collect(Collectors.toSet());
+            userList.addAll(live.getAccountList().stream().mapToLong(Long::parseLong).boxed().collect(Collectors.toSet()));
+            if (!CollectionUtils.isEmpty(userList)) {
+                HashMap<String, Object> extrasMap = new HashMap<>();
+                extrasMap.put("url","/pageA/20241111/auctionInfo?id="+ lotId);
+                AuctionJPushDTO auctionJPushDTO = AuctionJPushDTO.builder()
+                    .params(Lists.newArrayList(live.getLot().getName()))
+                    .userIds(Lists.newArrayList(userList))
+                    .templateId(19)
+                    .extrasMap(extrasMap)
+                    .build();
+                response = client.sendGet(jPushUrl+"?pushUserParam=" +JSON.toJSONString(auctionJPushDTO));
+            }
+        } catch (Exception e) {
+            log.error("jPush fail lotId : {}", lotId, e);
+            throw new RuntimeException("jPush fail");
+        }
+        if (Objects.nonNull(response) && Objects.equals(response.getStatusCode(), 200) && Objects.equals(response.getResult().getCode(), 200)) {
+            log.info("jPush success lotId {} {}", lotId, response.getContent());
+        } else if (Objects.nonNull(response) && Objects.equals(response.getStatusCode(), 500)) {
+            log.error("jPush fail lotId {} {}", lotId, response.getResult().getMsg());
+            throw new RuntimeException("jPush fail");
+        } else {
+            throw new RuntimeException("jPush fail");
+        }
+
+    }
+
+    /**
+     * 通知app后台更新拍品组状态
+     *
+     * @param lotGroupId
+     */
+    public void notifyLotGroup(Long lotGroupId,int status) {
+        ForestResponse<CommonForestClient.Response<Object>> response = client.sendGet(notifyUrl + lotGroupId+"?status="+status);
+        if (Objects.nonNull(response) && Objects.equals(response.getStatusCode(), 200) && Objects.equals(response.getResult().getCode(), 0)) {
+            log.info("notify app success lotGroupId {} {}", lotGroupId, response.getContent());
+        } else {
+            log.error("notify app error {}", lotGroupId);
+        }
+    }
+
+    @Async
+    public void noticeLive(Live live) {
+        log.info("notice live {}", live.getLot().getId());
+        Map<String, Object> data = new HashMap<>();
+        data.put("code", "chat");
+        data.put("messageType", "no_record");
+        Map<String, Object> messageParam = new HashMap<>();
+        data.put("messageParam", messageParam);
+        messageParam.put("receiver", "LOT-" + live.getLot().getId());
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("contentType", "TEXT");
+        payload.put("sender", 1L);
+        try {
+//            String content = new ObjectMapper().writeValueAsString(SensitiveDataUtils.handleViewDataSafe(live));
+            payload.put("content", live.getLot().getId());
+            payload.put("extra", "{}");
+            messageParam.put("payload", payload);
+            data.put("messageParam", messageParam);
+            ForestResponse<CommonForestClient.Response<Map<String, Object>>> response = client.sendPost(liveWebSocketUrl, getDefaultHeaderMap(), data);
+            if (Objects.isNull(response) || !Objects.equals(response.getStatusCode(), 200)) {
+                log.error("notice live error. {}", live);
+            }
+        } catch (Exception e) {
+            log.error("notice live error. {}", live, e);
+        }
+    }
+
+    @Async
+    public void noticeLiveLot(Long lotId) {
+        log.info("notice live {}", lotId);
+        Map<String, Object> data = new HashMap<>();
+        data.put("code", "chat");
+        data.put("messageType", "no_record");
+        Map<String, Object> messageParam = new HashMap<>();
+        data.put("messageParam", messageParam);
+        messageParam.put("receiver", "LOT-" + lotId);
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("contentType", "TEXT");
+        payload.put("sender", 1L);
+        try {
+//            String content = new ObjectMapper().writeValueAsString(SensitiveDataUtils.handleViewDataSafe(live));
+            payload.put("content", lotId);
+            payload.put("extra", "{}");
+            messageParam.put("payload", payload);
+            data.put("messageParam", messageParam);
+            ForestResponse<CommonForestClient.Response<Map<String, Object>>> response = client.sendPost(liveWebSocketUrl, getDefaultHeaderMap(), data);
+            if (Objects.isNull(response) || !Objects.equals(response.getStatusCode(), 200)) {
+                log.error("notice live error. {}", lotId);
+            }
+        } catch (Exception e) {
+            log.error("notice live error. {}", lotId, e);
+        }
+
+    }
+
+    /**
+     * 最新出价产生,拍品状态变化,发送通知
+     *
+     * @param lotId       拍品id
+     * @param contentType 通知内容类型:modify_price 最新出价,lot_state
+     */
+    @Async
+    public void notice(String lotId, String msg, String dataJsonStr, String contentType, String sender) {
+        JSONObject params = new JSONObject();
+        params.put("code", "chat");
+        params.put("protocol", "MQTT");
+        params.put("messageType", "no_record");
+        JSONObject messageParam = new JSONObject();
+        messageParam.put("receiver", "LOT-" + lotId);
+        messageParam.put("messageId", 1L);//唯一随机数
+        params.put("messageParam", messageParam);
+        JSONObject payload = new JSONObject();
+        payload.put("contentType", contentType);
+        payload.put("sender", sender);
+        payload.put("content", msg);
+        payload.put("extra", dataJsonStr);
+        messageParam.put("payload",payload);
+        try {
+            log.info("notice param {}",params.toJSONString());
+            ForestResponse<CommonForestClient.Response<Map<String, Object>>> response = client.sendPost(liveWebSocketUrl, getDefaultHeaderMap(), params);
+            if (Objects.isNull(response) || !Objects.equals(response.getStatusCode(), 200)) {
+                log.error("notice error. {}", lotId);
+            }
+        } catch (Exception e) {
+            log.error("notice error. {}", lotId);
+        }
+    }
+
+    public String createCoupon(SoldEvent soldEvent) {
+        Map<String, Object> params = Maps.newHashMap();
+        params.put("userId", soldEvent.getUserId());
+        params.put("couponId", soldEvent.getGoodsId());
+        params.put("point", soldEvent.getPoint() * 100);
+        params.put("auctionId", soldEvent.getLotId());
+        params.put("startTime", soldEvent.getStartTime());
+        params.put("type", "point");
+        ForestResponse<CommonForestClient.Response<Map<String, Object>>> response = null;
+        try {
+            response = client.sendPost(couponUrl, getDefaultHeaderMap(), params);
+        } catch (Exception e) {
+            log.error("create coupon fail", e);
+            throw new RuntimeException("create coupon fail, " + soldEvent.toString());
+        }
+        if (Objects.nonNull(response) && Objects.equals(response.getStatusCode(), 200) && Objects.equals(response.getResult().getCode(), 200)) {
+            log.info("create coupon {} {}", soldEvent, response.getContent());
+            return soldEvent.getGoodsId().toString();
+        } else if (Objects.nonNull(response) && !Objects.equals(response.getStatusCode(), 200)) {
+            log.error("create coupon fail {} {}", soldEvent, response.getResult().getMsg());
+            throw new RuntimeException("create coupon fail, " + soldEvent);
+        } else {
+            throw new RuntimeException("create coupon fail, " + soldEvent);
+        }
+    }
+
+
+    public OrderVO createOrder(SoldEvent soldEvent) {
+        Map<String, Object> params = Maps.newHashMap();
+        params.put("userId", soldEvent.getUserId());
+        params.put("skuId", soldEvent.getGoodsId());
+        Lot lot = soldEvent.getLot();
+        //根据拍卖会id区分积分抢拍和竞价拍卖
+        if (lot.getAuctionId()==2L) {
+            params.put("type", "amount");
+            params.put("amount", soldEvent.getBids().get(0).getAmount());
+        } else {
+            params.put("amount", soldEvent.getPoint() * 100);
+            params.put("type", "point");
+        }
+        params.put("auctionId", soldEvent.getLotId());
+        params.put("startTime", soldEvent.getStartTime());
+        ForestResponse<CommonForestClient.Response<Object>> response = null;
+        try {
+            response = client.sendPost(orderUrl, getDefaultHeaderMap(), params);
+        } catch (Exception e) {
+            log.error("create order fail", e);
+            throw new RuntimeException("create order fail, " + soldEvent.toString());
+        }
+        if (Objects.nonNull(response) && Objects.equals(response.getStatusCode(), 200) && Objects.equals(response.getResult().getCode(), 200)) {
+            log.info("create order {} {}", soldEvent, response.getContent());
+            OrderVO orderVO = JSON.parseObject(JSON.toJSONString(response.getResult().getData()), OrderVO.class);
+            if (Objects.nonNull(orderVO) && Objects.nonNull(orderVO.getExpireTime())) {
+                if (DateUtils.addHours(new Date(), 8).getTime() < orderVO.getExpireTime().getTime()) {
+                    orderVO.setExpireTime(DateUtils.addHours(orderVO.getExpireTime(), -8));
+                }
+            }
+            return orderVO;
+        } else if (Objects.nonNull(response) && !Objects.equals(response.getStatusCode(), 200)) {
+            log.error("create order fail {} {}", soldEvent, response.getResult().getMsg());
+            throw new RuntimeException("create order fail, " + soldEvent.toString());
+        } else {
+            throw new RuntimeException("create order fail, " + soldEvent.toString());
+        }
+    }
+
+    public boolean notice(String lotName, Bid bid, String url) {
+        ForestResponse<CommonForestClient.Response<Object>> response;
+        try {
+            Map<String, Object> params = Maps.newHashMap();
+            params.put("title", Objects.equals(bid.getStatus(), 1) ? "抢拍活动结果通知" : "抢拍活动结果通知");
+            params.put("msg", Objects.equals(bid.getStatus(), 1) ?
+                String.format(winTemplate, bid.getAmount().longValue(), lotName) : String.format(noEinTemplate, lotName));
+            params.put("toUserId", bid.getAccountId());
+            params.put("type","ACT");
+            params.put("fromUser","HOBBY STOCKS");
+            params.put("toUser", bid.getAccount());
+            params.put("contentId", bid.getAccountId());
+            params.put("contentUrl", url);
+            response = client.sendGet(noticeUrl+"?notice="+ JSON.toJSONString(params));
+            boolean result = Objects.equals(response.getStatusCode(), 200) && Objects.equals(response.getResult().getCode(), 200);
+            if (!result) {
+                log.error("notice error, {} , {}  {}", noticeUrl, bid, response.getResult().getMsg());
+            } else {
+                log.info("notice {} {}", JSON.toJSONString(bid), response.getContent());
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("notice error, {} , {}", noticeUrl, bid, e);
+            return false;
+        }
+    }
+
+
+    private Map<String, Object> getDefaultHeaderMap() {
+        Map<String, Object> headerMap = Maps.newHashMap();
+        String traceId = MDC.get("traceId");
+        headerMap.put("traceid", traceId);
+        return headerMap;
+    }
+
+    /**
+     * 操作积分构建请求参数
+     *
+     * @param bid         出价记录
+     * @param operateType 操作类型:CONFIRMED:确认,FROZEN:冻结,CANCELED:
+     * @return
+     */
+    private Map<String, Object> operatePointParam(Bid bid, String operateType, String orderNo) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("type", "common");//积分类型:商家积分:merchant,商城积分:goods,平台积分:common
+        map.put("userId", Long.valueOf(bid.getAccountId()));//用户id
+        map.put("changePoint", bid.getAmount().longValue() * -100);//积分
+        map.put("orderId", bid.getId());
+        map.put("orderNo", orderNo);
+        map.put("bizId", "AUCTION_RECORD:" + bid.getId());
+        map.put("status", operateType);//
+        map.put("bizParentId", bid.getLotId());
+        map.put("lockTime", new Date());
+        return map;
+    }
+
+    /**
+     * 冻结积分
+     *
+     * @return
+     */
+    public boolean frozenPoint(Bid bid) {
+        Map<String, Object> frozen = operatePointParam(bid,"FROZEN", "");
+
+        try {
+            String response = client.sendPost(pointUrl, frozen);
+            PointDTO pointDTO = JSON.parseObject(response, PointDTO.class);
+            if (pointDTO.getCode()==200){
+                return pointDTO.isData();
+            }
+            return false;
+        } catch (Exception e) {
+            return false;
+        }
+
+    }
+
+    /**
+     * 解冻出价用户积分
+     *
+     * @param bid
+     * @return
+     */
+    public boolean canceledPoint(Bid bid) {
+        Map<String, Object> canceled = operatePointParam(bid,"CANCELED", "");
+
+        try {
+            String response = client.sendPost(pointUrl, canceled);
+            PointDTO pointDTO = JSON.parseObject(response, PointDTO.class);
+            if (pointDTO.getCode()==200){
+                return pointDTO.isData();
+            }
+            return false;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    public boolean confirmPoint(Bid bid, String orderNo) {
+        Map<String, Object> confirmed = operatePointParam(bid,"CONFIRMED", orderNo);
+        try {
+            String response = client.sendPost(pointUrl, confirmed);
+            PointDTO pointDTO = JSON.parseObject(response, PointDTO.class);
+            if (pointDTO.getCode()==200){
+                return pointDTO.isData();
+            }
+            return false;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * @param nowBid     当前出价
+     * @param lastBid    上一个出价
+     * @return 是否操作成功
+     */
+    public boolean operatePoint(Bid nowBid, Bid lastBid) {
+        if (Objects.isNull(lastBid)) {
+            //第一个出价人,冻结当前出价用户积分
+            return frozenPoint(nowBid);
+        } else {
+            //冻结当前出价积分,解冻上一次出价积分
+            if (frozenPoint(nowBid)) {
+                if (canceledPoint(lastBid)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+    }
+}

+ 66 - 0
bid/src/main/java/cn/hobbystocks/auc/aspectj/AppRoleAspect.java

@@ -0,0 +1,66 @@
+package cn.hobbystocks.auc.aspectj;
+
+import cn.hobbystocks.auc.annotation.RequireRoles;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.user.UserInfo;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+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 org.springframework.util.CollectionUtils;
+
+import java.lang.reflect.Method;
+import java.util.*;
+
+@Slf4j
+@Aspect
+@Component
+public class AppRoleAspect {
+	// 配置织入点
+	@Pointcut("@annotation(cn.hobbystocks.auc.annotation.RequireRoles)")
+	public void rolePointCut() {
+	}
+
+	//@RequireRoles({"admin"})
+	@Around("cn.hobbystocks.auc.aspectj.AppRoleAspect.rolePointCut()")
+	public Object roleBefore(ProceedingJoinPoint pjp) throws Throwable {
+		UserInfo user = UserUtils.getSimpleUserInfo();
+		Signature signature = pjp.getSignature();
+		MethodSignature methodSignature = (MethodSignature) signature;
+		Method targetMethod = methodSignature.getMethod();
+		RequireRoles annotation = targetMethod.getAnnotation(RequireRoles.class);
+		String[] roles = annotation.value();
+		List<String> requirePermissions = Arrays.asList(roles);
+		List<String> userPermissions = new ArrayList<>();
+        List<String> roleList = user.getRoleCodeList() != null ? user.getRoleCodeList() : Collections.emptyList();
+        userPermissions.addAll(roleList);
+		List<String> permissionsList = user.getPermissionsList();
+		if (!CollectionUtils.isEmpty(permissionsList)) {
+			userPermissions.addAll(permissionsList);
+		}
+		Set<String> commonRole = getCommonElements(requirePermissions, userPermissions);
+		if (CollectionUtils.isEmpty(commonRole)) {
+			log.info("权限不足,用户信息:{}",user);
+			return AjaxResult.error("权限不足!");
+		}
+		return pjp.proceed();
+
+	}
+
+	private Set<String> getCommonElements(List<String> list1, List<String> list2) {
+		if (CollectionUtils.isEmpty(list1) || CollectionUtils.isEmpty(list2)) {
+			return null;
+		}
+		Set<String> set1 = new HashSet<>(list1);
+		Set<String> set2 = new HashSet<>(list2);
+		Set<String> commonElements = new HashSet<>(set1);
+		commonElements.retainAll(set2);
+		return commonElements;
+	}
+
+}

+ 42 - 0
bid/src/main/java/cn/hobbystocks/auc/aspectj/ControllerLoggingInterceptor.java

@@ -0,0 +1,42 @@
+package cn.hobbystocks.auc.aspectj;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.UUID;
+
+@Component
+public class ControllerLoggingInterceptor implements HandlerInterceptor {
+
+    private static final Logger logger = LoggerFactory.getLogger(ControllerLoggingInterceptor.class);
+    private static final ThreadLocal<Long> startTime = new ThreadLocal<>();
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        String requestURI = request.getRequestURI();
+        String method = request.getMethod();
+        String traceId = UUID.randomUUID().toString();
+        request.setAttribute("traceId", traceId);
+        MDC.put("traceId", traceId);
+        logger.info("Controller method call start: {}", requestURI);
+        startTime.set(System.currentTimeMillis());
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+                                Object handler, Exception exception) throws Exception {
+        long endTime = System.currentTimeMillis();
+        long duration = endTime - startTime.get();
+        int responseCode = response.getStatus();
+        String requestURI = request.getRequestURI();
+        logger.info("Controller method called:{} resp {} in {} ms",requestURI, responseCode, duration);
+        startTime.remove();
+        MDC.remove("traceId");
+    }
+}

+ 26 - 0
bid/src/main/java/cn/hobbystocks/auc/aspectj/SensitiveDataAspect.java

@@ -0,0 +1,26 @@
+package cn.hobbystocks.auc.aspectj;
+
+import cn.hobbystocks.auc.common.utils.SensitiveDataUtils;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+
+/**
+ * 敏感数据处理切面
+ */
+@Aspect
+@Component
+public class SensitiveDataAspect {
+
+    @Pointcut("@annotation(cn.hobbystocks.auc.annotation.SensitiveData)")
+    public void sensitiveDataPointcut() {
+    }
+
+    @AfterReturning(pointcut = "sensitiveDataPointcut()", returning = "result")
+    public void handleSensitiveData(Object result) {
+        if (result != null) {
+            SensitiveDataUtils.handleViewData(result);
+        }
+    }
+}

+ 52 - 0
bid/src/main/java/cn/hobbystocks/auc/config/BeeConfig.java

@@ -0,0 +1,52 @@
+package cn.hobbystocks.auc.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
+import org.teasoft.bee.osql.PreparedSql;
+import org.teasoft.bee.osql.Suid;
+import org.teasoft.bee.osql.SuidRich;
+import org.teasoft.honey.osql.core.BeeFactory;
+import org.teasoft.honey.osql.core.BeeFactoryHelper;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+
+/**
+ * @author by po'yi
+ * @Classname BeeConfig
+ * @Description 通用dao,实体类尽量使用pojo包下,其他实体类个别字段未和表对应
+ * @Date 2021/12/8 13:01
+ */
+@Configuration
+@Slf4j
+public class BeeConfig {
+	@Resource
+	private DataSource dataSource;
+
+	@Bean
+	public BeeFactory beeFactory() throws Exception {
+		BeeFactory beeFactory = BeeFactory.getInstance();
+		beeFactory.setDataSource(dataSource);
+		return beeFactory;
+	}
+
+	@Bean("suid")
+	@DependsOn("beeFactory")
+	public Suid beeSuid() {
+		return BeeFactoryHelper.getSuid();
+	}
+
+	@Bean("suidRich")
+	@DependsOn("beeFactory")
+	public SuidRich beeSuidRich() {
+		return BeeFactory.getHoneyFactory().getSuidRich();
+	}
+
+	@Bean("preparedSql")
+	@DependsOn("beeFactory")
+	public PreparedSql beePreparedSql() {
+		return BeeFactory.getHoneyFactory().getPreparedSql();
+	}
+}

+ 35 - 0
bid/src/main/java/cn/hobbystocks/auc/config/JacksonConfig.java

@@ -0,0 +1,35 @@
+package cn.hobbystocks.auc.config;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+
+import java.text.SimpleDateFormat;
+
+@Configuration
+public class JacksonConfig {
+
+    @Bean
+    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
+        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
+
+        // 配置日期格式
+        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
+
+        // 序列化时,忽略值为 NULL 的字段
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+        // 序列化时,忽略空 Bean 的错误
+        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+
+        // 禁用未知属性错误
+        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+        return objectMapper;
+    }
+
+}

+ 60 - 0
bid/src/main/java/cn/hobbystocks/auc/config/ReadinessHealthIndicator.java

@@ -0,0 +1,60 @@
+package cn.hobbystocks.auc.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.HealthIndicator;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+//准备就绪
+@Component
+public class ReadinessHealthIndicator implements HealthIndicator {
+    @Autowired
+    private DataSource dataSource;
+    @Autowired
+    private RedisConnectionFactory redisConnectionFactory;
+
+    @Override
+    public Health health() {
+        // 可以检查各种依赖服务状态,比如数据库连接、Redis、外部API等
+        if (!checkDB()) {
+            return Health.down().withDetail("database", "Connection is closed").build();
+        }else if (!checkRedis()) {
+            Health.down().withDetail("redis", "Cannot connect to Redis").build();
+        }
+        return Health.up().build();
+    }
+
+    private boolean checkDB() {
+        try (Connection connection = dataSource.getConnection()) {
+            // 如果连接成功,则返回健康状态
+            if (!connection.isClosed()) {
+                return true;
+            } else {
+                return false;
+            }
+        } catch (SQLException e) {
+            // 如果连接失败,则返回不健康状态并包含错误信息
+            return false;
+        }
+
+
+
+    }
+
+    private boolean checkRedis() {
+        try (RedisConnection connection = redisConnectionFactory.getConnection()) {
+            // 检查连接是否有效
+            connection.ping();  // 如果能成功ping通,则Redis连接正常
+            return true;
+        } catch (Exception e) {
+            // 连接异常则返回不健康状态
+            return false;
+        }
+    }
+
+}

+ 39 - 0
bid/src/main/java/cn/hobbystocks/auc/config/SecurityConfig.java

@@ -0,0 +1,39 @@
+package cn.hobbystocks.auc.config;
+
+import cn.hobbystocks.auc.common.filter.AuthenticationFilter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+    private final AuthenticationFilter authenticationFilter;
+
+    private String [] ignoreUrl={"/actuator/**","/api/local/**","/api-docs/*","/doc.html","/webjars/**","/swagger-resources/**","/v3/api-docs/**","/swagger-ui/**"};
+
+    public SecurityConfig(AuthenticationFilter authenticationFilter) {
+        this.authenticationFilter = authenticationFilter;
+    }
+
+    @Bean
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        http
+                .csrf(AbstractHttpConfigurer::disable)  // 禁用 CSRF
+                .sessionManagement(session -> session
+                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 使用无状态会话
+                .authorizeHttpRequests(auth -> auth
+                        .antMatchers(ignoreUrl).permitAll()
+                        .anyRequest().authenticated()  // 其他请求需要身份验证
+                )
+                .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class); // 添加自定义过滤器
+
+        return http.build();
+    }
+}

+ 19 - 0
bid/src/main/java/cn/hobbystocks/auc/config/WebConfig.java

@@ -0,0 +1,19 @@
+package cn.hobbystocks.auc.config;
+
+import cn.hobbystocks.auc.aspectj.ControllerLoggingInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Autowired
+    private ControllerLoggingInterceptor controllerLoggingInterceptor;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(controllerLoggingInterceptor).addPathPatterns("/**");
+    }
+}

+ 71 - 0
bid/src/main/java/cn/hobbystocks/auc/delegate/HongKongPlatformApi.java

@@ -0,0 +1,71 @@
+package cn.hobbystocks.auc.delegate;
+
+import cn.hobbystocks.auc.common.user.UserInfo;
+import cn.hobbystocks.auc.domain.Bid;
+import cn.hobbystocks.auc.forest.CommonForestClient;
+import cn.hobbystocks.auc.vo.SkuDTO;
+import com.alibaba.fastjson.JSONObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * 香港平台api地址
+ */
+@Component
+public class HongKongPlatformApi extends AbstractPlatformApi{
+    @Value("${hobbystocks.hongkong.host.pointUrl}")
+    private String pointUrl;
+    @Value("${hobbystocks.hongkong.host.orderUrl}")
+    private String orderUrl;
+    private String noticeUrl;
+
+    @Autowired
+    private CommonForestClient client;
+    @Override
+    public boolean jPush(JSONObject jsonObject) {
+        return false;
+    }
+
+    @Override
+    public UserInfo getUserInfo() {
+        return null;
+    }
+
+    @Override
+    public SkuDTO getSku(long skuId) {
+        return null;
+    }
+
+    @Override
+    public boolean notice(JSONObject jsonObject,String type){
+        client.sendPost("",getDefaultHeaderMap(),jsonObject);
+        return false;
+    }
+
+    @Override
+    public boolean createOrder(JSONObject jsonObject) {
+
+        return false;
+    }
+
+    @Override
+    public boolean createCoupon(JSONObject jsonObject) {
+        return false;
+    }
+
+    @Override
+    public boolean frozenPoint(Bid bid, String merchantId) {
+        return false;
+    }
+
+    @Override
+    public boolean canceledPoint(Bid bid, String merchantId) {
+        return false;
+    }
+
+    @Override
+    public boolean confirmPoint(Bid bid, Long merchantId, String orderNo) {
+        return false;
+    }
+}

+ 57 - 0
bid/src/main/java/cn/hobbystocks/auc/handle/CouponSoldHandler.java

@@ -0,0 +1,57 @@
+package cn.hobbystocks.auc.handle;
+
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.utils.StringUtils;
+import cn.hobbystocks.auc.domain.Bid;
+import cn.hobbystocks.auc.event.SoldEvent;
+import cn.hobbystocks.auc.mapper.LotMapper;
+import com.google.common.collect.Sets;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Set;
+
+@Component
+@Slf4j
+public class CouponSoldHandler implements SoldHandler {
+
+    @Autowired
+    private LotMapper lotMapper;
+    @Autowired
+    private AppClient appClient;
+
+    public static final String noticeUrl = "/pageC/myCoupon/couponList";
+
+    @Override
+    public boolean match(SoldEvent soldEvent) {
+        return Constants.LOT_FROM_TYPE_COUPON.equals(soldEvent.getGoodsType());
+    }
+
+    @Override
+    public boolean handle(SoldEvent soldEvent) {
+        final String coupon = appClient.createCoupon(soldEvent);
+        if (!StringUtils.isEmpty(coupon) &&
+                lotMapper.updateOrder(soldEvent.getLotId() , coupon) > 0 ) {
+            Set<String> bidAccount = Sets.newHashSet();
+            //确认扣减积分
+            List<Bid> bids = soldEvent.getBids();
+
+            appClient.confirmPoint(bids.get(0),null);
+            soldEvent.getBids().forEach(bid -> {
+                if (!bidAccount.contains(bid.getAccountId())) {
+                    //发送站内信,暂不发送
+                    appClient.notice(soldEvent.getName(), bid, noticeUrl);
+                    bidAccount.add(bid.getAccountId());
+                }
+            });
+            soldEvent.setOrderId(coupon);
+            return true;
+        }else {
+            log.error("coupon sold fail  {}", soldEvent);
+            return false;
+        }
+    }
+}

+ 98 - 0
bid/src/main/java/cn/hobbystocks/auc/handle/SkuSoldHandler.java

@@ -0,0 +1,98 @@
+package cn.hobbystocks.auc.handle;
+
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.utils.StringUtils;
+import cn.hobbystocks.auc.domain.Bid;
+import cn.hobbystocks.auc.domain.Order;
+import cn.hobbystocks.auc.event.SoldEvent;
+import cn.hobbystocks.auc.mapper.LotMapper;
+import cn.hobbystocks.auc.service.IOrderStatusService;
+import cn.hobbystocks.auc.task.DynamicTaskService;
+import cn.hobbystocks.auc.vo.OrderVO;
+import com.alibaba.fastjson.JSON;
+import com.google.common.collect.Sets;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Isolation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+/**
+ * 所有的拍品成交都会走这里,包含积分抢拍商品
+ */
+@Component
+@Slf4j
+public class SkuSoldHandler implements SoldHandler {
+
+    @Autowired
+    private LotMapper lotMapper;
+    @Autowired
+    private AppClient appClient;
+    @Autowired
+    private DynamicTaskService dynamicTaskService;
+
+    @Autowired
+    IOrderStatusService orderStatusService;
+
+    public static final String noticeUrl = "/pageA/20241111/auctionInfo?id=%s";
+
+    @Override
+    public boolean match(SoldEvent soldEvent) {
+        return Constants.LOT_FROM_TYPE_SKU.equals(soldEvent.getGoodsType());
+    }
+
+    @Override
+    @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
+    public boolean handle(SoldEvent soldEvent) {
+        OrderVO orderVO = appClient.createOrder(soldEvent);
+        String order = JSON.toJSONString(orderVO);
+        log.info("submit order {}"+order);
+        soldEvent.getLot().setOrderId(order);
+        if (Objects.nonNull(orderVO.getExpireTime())) {
+            //确认扣减积分
+            List<Bid> bids = soldEvent.getBids();
+            Long merchantId = soldEvent.getLot().getMerchantId();
+            if (1L == soldEvent.getLot().getAuctionId()){
+                //如果为积分抢拍
+                appClient.confirmPoint(bids.get(0),orderVO.getOrderNo());
+            }
+
+            lotMapper.updatePay(soldEvent.getLotId(), -1);
+            soldEvent.getLot().setPaid(-1L);
+
+            //保存订单相关信息
+            Order dbOrder = new Order();
+            BeanUtils.copyProperties(orderVO,dbOrder);
+            dbOrder.setFlag(0);
+            dbOrder.setLotId(soldEvent.getLotId());
+            dbOrder.setUserId(soldEvent.getUserId().longValue());
+            dbOrder.setMerchantId(merchantId);
+            orderStatusService.addOrder(dbOrder);
+        }
+        if (!StringUtils.isEmpty(order) &&
+                lotMapper.updateOrder(soldEvent.getLotId() , order) > 0 ) {
+            //积分抢拍
+            if (!Objects.equals(2L, soldEvent.getLot().getAuctionId())) {
+
+                Set<String> bidAccount = Sets.newHashSet();
+                soldEvent.getBids().forEach(bid -> {
+                    if (!bidAccount.contains(bid.getAccountId())) {
+                        //发送站内信
+                        appClient.notice(soldEvent.getName(), bid, String.format(noticeUrl, bid.getLotId()));
+                        bidAccount.add(bid.getAccountId());
+                    }
+                });
+            }
+            soldEvent.setOrderId(order);
+            return true;
+        } else {
+            log.error("sku sold fail  {}", soldEvent);
+            return false;
+        }
+    }
+}

+ 11 - 0
bid/src/main/java/cn/hobbystocks/auc/handle/SoldHandler.java

@@ -0,0 +1,11 @@
+package cn.hobbystocks.auc.handle;
+
+import cn.hobbystocks.auc.event.SoldEvent;
+
+public interface SoldHandler {
+
+    boolean match(SoldEvent soldEvent);
+
+    boolean handle(SoldEvent soldEvent);
+
+}

+ 23 - 0
bid/src/main/java/cn/hobbystocks/auc/handle/SoldHandlerHolder.java

@@ -0,0 +1,23 @@
+package cn.hobbystocks.auc.handle;
+
+import cn.hobbystocks.auc.event.SoldEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class SoldHandlerHolder {
+    @Autowired
+    private List<SoldHandler> soldHandlers;
+
+    public boolean handle(SoldEvent soldEvent) {
+        for (SoldHandler soldHandler : soldHandlers) {
+            if (soldHandler.match(soldEvent)) {
+                return soldHandler.handle(soldEvent);
+            }
+        }
+        return false;
+    }
+
+}

+ 36 - 0
bid/src/main/java/cn/hobbystocks/auc/listener/ChangeListener.java

@@ -0,0 +1,36 @@
+package cn.hobbystocks.auc.listener;
+
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.cache.CacheMap;
+import cn.hobbystocks.auc.event.ChangeEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+/**
+ * 拍品状态改变事件监听
+ */
+@Component
+@Slf4j
+public class ChangeListener {
+    @Autowired
+    private CacheMap cacheMap;
+    @Autowired
+    private AppClient appClient;
+
+
+    @EventListener
+    public void handleChange(ChangeEvent changeEvent){
+        if (Objects.nonNull(changeEvent.getLive())) {
+            cacheMap.putLive(changeEvent.getLive());
+            //最新出价发送通知
+//            appClient.noticeLive(changeEvent.getLive());
+            appClient.notice(changeEvent.getLive().getLot().getId()+"","1","{}","lot_auction","");
+        }
+    }
+
+
+}

+ 23 - 0
bid/src/main/java/cn/hobbystocks/auc/listener/JPushListener.java

@@ -0,0 +1,23 @@
+package cn.hobbystocks.auc.listener;
+
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.event.JPushEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class JPushListener {
+    @Autowired
+    private AppClient appClient;
+
+    @EventListener
+    @Async
+    public void handleJPush(JPushEvent jPushEvent){
+        appClient.jPush(jPushEvent.getLive());
+    }
+
+}

+ 78 - 0
bid/src/main/java/cn/hobbystocks/auc/listener/LiveEndListener.java

@@ -0,0 +1,78 @@
+package cn.hobbystocks.auc.listener;
+
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.utils.DateUtils;
+import cn.hobbystocks.auc.domain.LotGroup;
+import cn.hobbystocks.auc.event.EndEvent;
+import cn.hobbystocks.auc.forest.CommonForestClient;
+import cn.hobbystocks.auc.handle.context.Live;
+import cn.hobbystocks.auc.service.ILotService;
+import cn.hobbystocks.auc.task.DynamicTaskService;
+import cn.hobbystocks.auc.vo.CardGroupLivesConfigVO;
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 拍品拍卖结束事件监听
+ */
+@Component
+@Slf4j
+public class LiveEndListener {
+    @Autowired
+    private ILotService lotService;
+    @Autowired
+    private DynamicTaskService dynamicTaskService;
+    @Autowired
+    private AppClient appClient;
+
+
+    @EventListener
+    public void handleEndEvent(EndEvent endEvent) {
+        Live live = endEvent.getLive();
+        final String status = live.getLot().getStatus();
+        Long groupId = live.getLot().getGroupId();
+        if (Objects.nonNull(groupId)) {
+            lotService.handleEndLotGroup(LotGroup.builder().id(groupId).build(), status);
+
+            try {
+                LotGroup lotGroup = lotService.selectLotGroupById(groupId);
+                appClient.notice(live.getLot().getId().toString(),"2","{}","lot_auction","");
+                dynamicTaskService.updateTask("3m_lot_"+ groupId, DateUtils.addMinutes(new Date(), 3), () ->{
+                    try {
+
+                        if (Constants.LOT_STATUS_WAITING.equals(lotGroup.getStatus())) {
+                            appClient.notice(live.getLot().getId().toString(),"","{}","auction_end","");
+                        }
+                    }catch (Exception e) {
+                        log.error("lot_auction group fail {}" ,  live.getLot().getId(), e);
+                    }
+                });
+                //每当一个拍品结束,检查拍品对应的拍品组的拍卖进度,如果一组拍卖已完成,发送通知给app后台
+                long finishNum = lotGroup.getFinishNum();
+                long totalNum = lotGroup.getNum();
+                if (finishNum==totalNum){
+                    appClient.notifyLotGroup(groupId,20);
+                }else if (finishNum<totalNum){
+                    appClient.notifyLotGroup(groupId,15);
+                }
+            }catch (Exception e) {
+                log.error("lot_auction group fail {}" ,  live.getLot().getId(), e);
+            }
+        }
+    }
+
+
+
+
+}

+ 60 - 0
bid/src/main/java/cn/hobbystocks/auc/listener/SoldListener.java

@@ -0,0 +1,60 @@
+package cn.hobbystocks.auc.listener;
+
+import cn.hobbystocks.auc.common.utils.StringUtils;
+import cn.hobbystocks.auc.domain.Lot;
+import cn.hobbystocks.auc.domain.SoldOrderRecord;
+import cn.hobbystocks.auc.event.SoldEvent;
+import cn.hobbystocks.auc.handle.SoldHandlerHolder;
+import cn.hobbystocks.auc.mapper.LotGroupMapper;
+import cn.hobbystocks.auc.mapper.LotMapper;
+import cn.hobbystocks.auc.mapper.SoldOrderRecordMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * 拍品成交事件监听器
+ */
+@Component
+@Slf4j
+public class SoldListener {
+    @Autowired
+    private LotMapper lotMapper;
+    @Autowired
+    private SoldHandlerHolder soldHandlerHolder;
+    @Autowired
+    private SoldOrderRecordMapper soldOrderRecordMapper;
+
+    @Async
+    @EventListener
+    public void handleSoldEvent(SoldEvent soldEvent) {
+        log.info("handle sold event {}", soldEvent);
+        try {
+            int count = soldOrderRecordMapper.insertRecord(
+                    SoldOrderRecord.builder()
+                            .lotId(soldEvent.getLotId())
+                            .bidId(soldEvent.getBidId())
+                            .createTime(new Date())
+                            .build());
+            if (count <= 0) {
+                log.error("handled sold event {}", soldEvent);
+                return;
+            }
+            Lot lot = lotMapper.selectLotById(soldEvent.getLotId());
+            if (StringUtils.isEmpty(lot.getOrderId())) {
+                if(soldHandlerHolder.handle(soldEvent)) {
+                    soldOrderRecordMapper.updateOrder(soldEvent.getOrderId(), soldEvent.getLotId());
+                }
+            }else {
+                log.error("handled sold event {}", soldEvent);
+            }
+        }catch (Exception e) {
+            log.error("handle sold event {}", soldEvent, e);
+        }
+    }
+
+}

+ 22 - 0
bid/src/main/java/cn/hobbystocks/auc/listener/StartBiddingListener.java

@@ -0,0 +1,22 @@
+package cn.hobbystocks.auc.listener;
+
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.event.StartBiddingEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * 开始竞价监听器
+ */
+@Component
+public class StartBiddingListener {
+
+    @Autowired
+    AppClient appClient;
+    @EventListener
+    public void handle(StartBiddingEvent startBiddingEvent){
+        //开始竞价通知
+        appClient.notice(startBiddingEvent.getLotId()+"","3","{}","lot_auction","");
+    }
+}

+ 22 - 0
bid/src/main/java/cn/hobbystocks/auc/listener/SyncListener.java

@@ -0,0 +1,22 @@
+package cn.hobbystocks.auc.listener;
+
+import cn.hobbystocks.auc.cache.CacheMap;
+import cn.hobbystocks.auc.event.SyncEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class SyncListener {
+
+    @Autowired
+    private CacheMap cacheMap;
+
+    @EventListener
+    public void handleChange(SyncEvent syncEvent){
+        cacheMap.sync();
+    }
+
+}

+ 370 - 0
bid/src/main/java/cn/hobbystocks/auc/task/BidTask.java

@@ -0,0 +1,370 @@
+package cn.hobbystocks.auc.task;
+
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.cache.CacheMap;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.core.redis.Locker;
+import cn.hobbystocks.auc.common.core.redis.RedisCache;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import cn.hobbystocks.auc.common.utils.CloneUtils;
+import cn.hobbystocks.auc.common.utils.DateUtils;
+import cn.hobbystocks.auc.domain.*;
+import cn.hobbystocks.auc.handle.RuleHandlerHolder;
+import cn.hobbystocks.auc.handle.context.Live;
+import cn.hobbystocks.auc.mapper.*;
+import cn.hobbystocks.auc.service.IAuctionService;
+import cn.hobbystocks.auc.service.ILotService;
+import cn.hobbystocks.auc.service.IOrderStatusService;
+import cn.hobbystocks.auc.vo.CardGroupLivesConfigVO;
+import cn.hobbystocks.auc.vo.OrderVO;
+import com.alibaba.fastjson.JSON;
+import com.google.common.collect.Lists;
+import io.micrometer.core.instrument.util.NamedThreadFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+@Component
+@Slf4j
+public class BidTask implements CacheMap {
+
+    // region params
+	private static final ConcurrentHashMap<String, ConcurrentHashMap<String, Live>> liveCacheMap = new ConcurrentHashMap<>();
+
+	@Autowired
+	private RedisCache redisCache;
+	@Autowired
+	private RuleHandlerHolder ruleHandlerHolder;
+	@Autowired
+	private Locker locker;
+	@Autowired
+	private ILotService lotService;
+
+	@Autowired
+	private IAuctionService auctionService;
+
+	@Value("${auction.thread.corePoolSize:50}")
+	private Integer corePoolSize;
+	@Value("${auction.thread.maximumPoolSize:100}")
+	private Integer maximumPoolSize;
+	@Value("${auction.thread.queueSize:50}")
+	private Integer queueSize;
+
+	@Value("${user.info-url:http://coresvc2/user/}")
+	private String userUrl;
+
+	private ThreadPoolExecutor threadPool;
+	@Autowired
+	private LotFansMapper lotFansMapper;
+	@Autowired
+	private AppClient appClient;
+	@Autowired
+	private LotGroupMapper lotGroupMapper;
+    @Autowired
+    private LotMapper lotMapper;
+    @Autowired
+    private BidMapper bidMapper;
+    @Autowired
+    private BidRecordMapper bidRecordMapper;
+    @Autowired
+    IOrderStatusService orderStatusService;
+
+    // endregion
+
+    // region init
+
+	@PostConstruct
+	public void init() {
+		TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
+		UserUtils.setUrl(userUrl);
+		sync();
+		this.threadPool = new ThreadPoolExecutor(corePoolSize,
+				maximumPoolSize,
+				60L, TimeUnit.SECONDS,
+				new ArrayBlockingQueue<>(queueSize),
+				new NamedThreadFactory("tradition-live-lot"),
+				(r, e) -> log.error("thread pool rejected execution")
+		);
+	}
+
+    // endregion
+
+    // region methods
+
+    /**
+     * 同步 Redis 缓存中的数据到本地的 liveCacheMap 中
+     */
+    @Override
+	public void sync() {
+        // 遍历所有以 Constants.REDIS_MAP_AUC_LOT_PREFIX 为前缀的 Redis 键
+		redisCache.keys(Constants.REDIS_MAP_AUC_LOT_PREFIX).forEach(aucKey -> {
+            // 清空本地缓存
+            liveCacheMap.clear();
+            // 将 Redis 中对应键的值(一个 Map)放入本地缓存
+            liveCacheMap.put(aucKey, new ConcurrentHashMap<>(redisCache.getCacheMap(aucKey)));
+		});
+	}
+
+    /**
+     * 更新缓存到本地liveCacheMap
+     */
+    @Override
+    public void putLive(Live live){
+        live = CloneUtils.clone(live);
+        ConcurrentHashMap<String, Live> cacheMap = liveCacheMap.get(String.format(Constants.REDIS_MAP_AUC_LOT_TEMPLATE, live.getLot().getAuctionId()));
+        if (Objects.isNull(cacheMap)) {
+            cacheMap = new ConcurrentHashMap<>();
+        }
+        cacheMap.put(live.getLot().getId().toString(), live);
+        liveCacheMap.put(String.format(Constants.REDIS_MAP_AUC_LOT_TEMPLATE, live.getLot().getAuctionId()), cacheMap);
+    }
+
+    /**
+     * 根据拍卖 ID 获取本地缓存中的全部直播数据
+     */
+    @Override
+    public Map<String, Live> viewAuction(Long auctionId) {
+        ConcurrentHashMap<String, Live> map = liveCacheMap.get(String.format(Constants.REDIS_MAP_AUC_LOT_TEMPLATE, auctionId));
+        return Objects.isNull(map) ?  new HashMap<>() : new HashMap<>(map);
+    }
+
+    /**
+     * 根据拍品 ID 获取本地缓存中的直播数据
+     */
+    @Override
+    public Live viewLot(Long auctionId, Long lotId) {
+        ConcurrentHashMap<String, Live> map = liveCacheMap.get(String.format(Constants.REDIS_MAP_AUC_LOT_TEMPLATE, auctionId));
+        if (CollectionUtils.isEmpty(map)) {
+            return null;
+        }
+        return map.get(lotId.toString());
+    }
+
+    /**
+     * 本地缓存删除直播数据
+     */
+    @Override
+    public void end(Long auctionId) {
+        // 本地缓存删除直播数据
+        liveCacheMap.remove(String.format(Constants.REDIS_MAP_AUC_LOT_TEMPLATE, auctionId));
+        Lot cond = new Lot();
+        cond.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+        cond.setAuctionId(auctionId);
+    }
+
+    // endregion
+
+    // region schedule tasks
+
+    /// 5s一次检查并处理即将过期的订单
+	/*@Scheduled(fixedRate = 5000)
+	public void expire() {
+		lotFansMapper.selectLotFansList(LotFans.builder().type("pre_pay_expire").build()).forEach(fans -> {
+			Lot lot = lotService.selectLotById(fans.getLotId());
+			/// 如果当前时间大于 fans 对象的过期时间
+            if (System.currentTimeMillis() > fans.getExpire().getTime()) {
+                ///更新过期未支付
+                lotService.updateExpire(fans);
+                /// 通知直播平台更新商品状态
+                appClient.noticeLiveLot(lot.getId());
+            }else {
+                lotService.updateSoldAndPaid(fans, lot.getGroupId());
+                // 通知app更新商品
+                appClient.noticeLiveLot(lot.getId());
+			}
+		});
+	}*/
+
+    /**
+     * 每5秒检查下订单状态,如订单已过期则更新拍品状态为
+     */
+    @Scheduled(fixedRate = 5000)
+    public void expireOrder(){
+        //查询所有订单状态,如果订单超时未支付,更新拍品状态为流拍
+        List<Order> orderList = orderStatusService.list();
+        for (Order order : orderList) {
+            Lot lot = lotMapper.selectLotById(order.getLotId());
+            if (System.currentTimeMillis() > order.getExpireTime().getTime()) {
+                //当前时间超过订单过期时间
+                if (!Objects.equals(order.getStatus(), 101L) &&
+                    !Objects.equals(order.getStatus(), 102L) &&
+                    !Objects.equals(order.getStatus(), 103L) &&
+                    !Objects.equals(order.getStatus(), 104L) &&
+                    !Objects.equals(order.getStatus(), 105L) &&
+                    !Objects.equals(order.getStatus(), 106L) &&
+                    !Objects.equals(order.getStatus(), 301L)) {
+                    //订单状态未支付
+                    //更新拍品状态为流拍
+                    lotMapper.updateLot(Lot.builder().id(order.getLotId()).status(Constants.LOT_STATUS_PASS).build());
+                    //更新订单标识为已处理
+                    orderStatusService.modifyOrder(Order.builder().orderNo(order.getOrderNo()).flag(1).expireTime(DateUtils.addDays(new Date(),1)).build());
+                }else{
+                    //订单状态已支付,更新拍品为成交状态
+                    lotMapper.updatePay(order.getLotId(), 1);
+                    lotGroupMapper.addSold(lot.getGroupId());
+                    orderStatusService.modifyOrder(Order.builder().orderNo(order.getOrderNo()).flag(2).build());
+                    // 通知更新拍品信息
+                }
+                appClient.notice(lot.getId().toString(),"","{}","lot_auction","");
+            }
+
+        }
+    }
+    /// 每5s检查一次order_status查询flag=1,订单超时未支付状态并且已超过过期的时间的orderStatus然后将其移除
+	@Scheduled(fixedRate = 5000)
+	public void expirePay() {
+        orderStatusService.orderStatusCheck();
+	}
+
+    /**
+     * 每5秒检查一次直播状态,并更新数据库中的直播信息
+     */
+	/*@Scheduled(fixedDelay = 5000)
+	private void liveCheck(){
+        // 查询需要检查直播状态为结束的直播ID列表
+		List<Long> ids = lotGroupMapper.selectLiveCheck().stream().mapToLong(LotGroup::getLiveId).distinct().boxed().collect(Collectors.toList());
+        if (!CollectionUtils.isEmpty(ids)) {
+            // 通过App获取直播状态信息
+			List<CardGroupLivesConfigVO> list = appClient.getLive(ids);
+			if (!CollectionUtils.isEmpty(list)) {
+                // 过滤出有效的直播ID
+				List<Long> liveIds = list.stream()
+						.filter(cardGroupLivesConfigVO -> Objects.nonNull(cardGroupLivesConfigVO.getId()))
+						.mapToLong(CardGroupLivesConfigVO::getId)
+						.boxed()
+						.collect(Collectors.toList());
+				if (!CollectionUtils.isEmpty(liveIds)) {
+                    // 更新数据库中的直播信息
+					lotGroupMapper.updateClearLive(liveIds);
+				}
+			}
+		}
+	}*/
+
+    /**
+     * 每1秒检查一次正在竞拍的拍品,并更新直播状态
+     */
+	@Scheduled(fixedRate = 1000)
+	public void live() {
+        // 遍历所有正在竞拍的拍品
+		lotService.selectBidding().forEach(lot -> {
+            // 根据拍品的拍卖ID生成Redis缓存键
+			String aucKey = String.format(Constants.REDIS_MAP_AUC_LOT_TEMPLATE, lot.getAuctionId());
+            // 从Redis缓存中获取对应拍品的直播数据
+			Live live = redisCache.getCacheMapValue(aucKey, lot.getId().toString());
+			if (Objects.nonNull(live)) {
+                // 将直播数据放入本地缓存中
+				liveCacheMap.computeIfAbsent(aucKey, s -> new ConcurrentHashMap<>())
+						.put(live.getLot().getId().toString(), live);
+                // 将直播任务提交到线程池中执行
+				threadPool.submit(() -> {
+					String currentStatus = ruleHandlerHolder.getCurrentStatus(live);
+                    // 如果当前状态不是拍品状态未开始,则返回
+					if (Objects.equals(Constants.LOT_STATUS_WAITING, currentStatus))
+						return;
+                    // 尝试获取锁,并执行直播服务
+					locker.tryLock(String.format(Constants.REDIS_LOCK_LOT_TEMPLATE, live.getLot().getId()), () -> lotService.live(live));
+				});
+			}
+		});
+        // 遍历所有拍品状态撤拍的拍品
+		lotService.selectCancel().forEach(lot -> {
+            // 根据拍品的拍卖ID生成Redis缓存键
+			String aucKey = String.format(Constants.REDIS_MAP_AUC_LOT_TEMPLATE, lot.getAuctionId());
+            // 从本地缓存中移除已取消的拍品的直播数据
+			liveCacheMap.computeIfAbsent(aucKey, s -> new ConcurrentHashMap<>())
+					.remove(lot.getId().toString());
+		});
+        // 遍历本地缓存中的所有直播数据
+		for (String aucKey : liveCacheMap.keySet()) {
+			ConcurrentHashMap<String, Live> map = liveCacheMap.get(aucKey);
+			List<String> lotIds = Lists.newArrayList();
+            // 遍历所有直播数据,检查是否有过期的直播
+			map.values().forEach(live -> {
+                // 如果当前时间超过直播结束时间3秒,则将该拍品ID添加到移除列表中
+				if (live.getCurrentEndTime() + 3000 < System.currentTimeMillis()) {
+					lotIds.add(live.getLot().getId().toString());
+				}
+			});
+            // 移除过期的直播数据
+			lotIds.forEach(map::remove);
+		}
+	}
+
+    /**
+     * 每5分钟检查一次已结束的直播,并清理相关数据
+     */
+	@Scheduled(fixedRate = 5 * 60 * 1000)
+	public void delFinishLive() {
+        // 遍历所有正在直播的拍卖
+		auctionService.live().forEach(auction -> {
+            // 根据拍卖ID生成Redis缓存键
+			String aucKey = String.format(Constants.REDIS_MAP_AUC_LOT_TEMPLATE, auction.getId());
+            // 从Redis缓存中获取对应拍卖的直播数据Map
+			Map<String, Live> auctionMap = redisCache.getCacheMap(aucKey);
+            // 遍历所有直播数据
+			auctionMap.values().forEach(live -> {
+                // 获取拍品的状态
+				String status = live.getLot().getStatus();
+                // 如果拍品状态为拍品状态流拍  或 已售出并且直播结束时间超过5分钟
+				if ((Constants.LOT_STATUS_PASS.equals(status) || Constants.LOT_STATUS_SOLD.equals(status)) &&
+						(live.getCurrentEndTime() + 5 * 60 * 1000) < System.currentTimeMillis()) {
+                    // 取消拍卖,删除redis删除锁
+					ruleHandlerHolder.cancelLot(live.getLot());
+                    //liveCacheMap 中没有aucKey时插入一个ConcurrentHashMap,再remove掉
+					liveCacheMap.computeIfAbsent(aucKey, s -> new ConcurrentHashMap<>())
+							.remove(live.getLot().getId().toString());
+                    // 清缓存中记录的竞价记录
+                    redisCache.delList(String.format(Constants.REDIS_MAP_AUC_LOT_BID_LIST_PREFIX, live.getLot().getId()));
+                    redisCache.delList(String.format(Constants.REDIS_MAP_AUC_LOT_BIDRECORD_LIST_PREFIX, live.getLot().getId()));
+                    redisCache.delList(String.format(Constants.REDIS_MAP_AUC_LOT_BID_USER_TEMPLATE, live.getLot().getId()));
+				}
+			});
+		});
+	}
+
+
+
+    /**
+     * 每1秒检查一次拍卖状态,并将缓存中未写入数据库的bid数据写入数据库
+     */
+    @Scheduled(fixedDelay = 1 * 1 * 1000)
+    private void auctionLotCheck(){
+        lotService.selectBidding().forEach(lot -> {
+            List<Bid> bidList = redisCache.getCacheList(String.format(Constants.REDIS_MAP_AUC_LOT_BID_LIST_PREFIX, lot.getId()));
+            List<BidRecord> bidRecordList = redisCache.getCacheList(String.format(Constants.REDIS_MAP_AUC_LOT_BIDRECORD_LIST_PREFIX, lot.getId()));
+            redisCache.delList(String.format(Constants.REDIS_MAP_AUC_LOT_BID_LIST_PREFIX, lot.getId()));
+            redisCache.delList(String.format(Constants.REDIS_MAP_AUC_LOT_BIDRECORD_LIST_PREFIX, lot.getId()));
+
+            if(!Objects.isNull(bidList)&&!bidList.isEmpty()){
+                int size = bidList.size();
+                for (int i = 0; i < size; i += 100) {
+                    int endIndex = Math.min(i + 100, size);
+                    List<Bid> batch = bidList.subList(i, endIndex);
+                    bidMapper.batchInsertBid(batch);
+                }
+                bidMapper.clearCurrentBid(lot.getId());
+                bidMapper.setCurrentBid(lot.getId());
+
+            }
+            if(!Objects.isNull(bidRecordList)&&!bidRecordList.isEmpty()){
+                int size = bidRecordList.size();
+                for (int i = 0; i < size; i += 100) {
+                    int endIndex = Math.min(i + 100, size);
+                    List<BidRecord> batch = bidRecordList.subList(i, endIndex);
+                    bidRecordMapper.batchInsertBidRecord(batch);
+                }
+            }
+        });
+    }
+
+    // endregion
+}

+ 693 - 0
bid/src/main/java/cn/hobbystocks/auc/web/BidingController.java

@@ -0,0 +1,693 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.cache.CacheMap;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.core.controller.BaseController;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.core.redis.RedisCache;
+import cn.hobbystocks.auc.common.core.text.Convert;
+import cn.hobbystocks.auc.common.exception.AddPriceException;
+import cn.hobbystocks.auc.common.exception.LockException;
+import cn.hobbystocks.auc.common.user.UserInfo;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import cn.hobbystocks.auc.common.utils.CloneUtils;
+import cn.hobbystocks.auc.common.utils.PaginationUtil;
+import cn.hobbystocks.auc.common.utils.SensitiveDataUtils;
+import cn.hobbystocks.auc.domain.*;
+import cn.hobbystocks.auc.event.EventPublisher;
+import cn.hobbystocks.auc.forest.CommonForestClient;
+import cn.hobbystocks.auc.handle.RuleHandlerHolder;
+import cn.hobbystocks.auc.handle.context.Live;
+import cn.hobbystocks.auc.handle.context.LiveContext;
+import cn.hobbystocks.auc.mapper.BidRecordMapper;
+import cn.hobbystocks.auc.service.IBidService;
+import cn.hobbystocks.auc.service.ILotService;
+import cn.hobbystocks.auc.service.IOrderStatusService;
+import cn.hobbystocks.auc.vo.BidVO;
+import com.alibaba.fastjson.JSON;
+import com.dtflys.forest.http.ForestResponse;
+import com.github.pagehelper.Page;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 出价
+ */
+@RestController
+@RequestMapping({"/bid/bidding"})
+@Slf4j
+@Api(tags = "出价相关接口")
+public class BidingController extends BaseController {
+
+    // region Params
+
+    @Value("${hobbystocks.host.pointUrl}")
+    private String pointUrl;
+    @Autowired
+    private CommonForestClient client;
+    @Autowired
+    private IBidService bidService;
+    @Autowired
+    private ILotService lotService;
+    @Autowired
+    private AppClient appClient;
+    @Autowired
+    private EventPublisher eventPublisher;
+    @Autowired
+    private CacheMap cacheMap;
+    @Autowired
+    private BidRecordMapper bidRecordMapper;
+    @Autowired
+    private RedisCache redisCache;
+    @Autowired
+    private RuleHandlerHolder ruleHandlerHolder;
+
+
+    @Autowired
+    private IOrderStatusService orderStatusService;
+    // endregion
+
+    // region Controllers
+
+    @ApiOperation("同步数据")
+    @PostMapping("/sync")
+    public AjaxResult sync() {
+        eventPublisher.sync();
+        return AjaxResult.success();
+    }
+
+    @ApiOperation("调用积分接口处理积分变动")
+    @PostMapping("/point")
+    public AjaxResult point(@RequestBody Map<String, Object> map) {
+        ForestResponse<CommonForestClient.Response<Map<String, Object>>>  response =
+            client.sendPost(pointUrl, Maps.newHashMap(), map);
+        if (Objects.nonNull(response) && Objects.equals(response.getStatusCode(), 200) && Objects.equals(response.getResult().getCode(), 0)) {
+            return AjaxResult.success();
+        }else {
+            throw new RuntimeException();
+        }
+    }
+
+    @ApiOperation("积分拍卖增加价格")
+    @PostMapping("/addPrice")
+    public AjaxResult addPrice(@RequestBody BidVO bid) {
+        log.info("add price {}", bid);
+
+        UserInfo userInfo = UserUtils.getUserInfo();
+
+        // 创建bidRecord
+        bid.setCreateBy(userInfo.getSub());
+        bid.setAccount(userInfo.getAccount());
+        String accountId = String.valueOf(userInfo.getId());
+        bid.setAccountId(accountId);
+        bid.setAvatar(userInfo.getAvatar());
+        bid.setBidNo(UUID.randomUUID().toString());
+        Long recordId = createBidRecord(bid);
+        Lot lot = lotService.selectLotById(bid.getLotId());
+        // 判断拍卖状态
+        String error = checkLotStatus(bid);
+        if (StringUtils.hasLength(error)) {
+            return AjaxResult.error(error);
+        }
+
+        if(!userInfo.isHasAddress()) {
+            return AjaxResult.error("请先添加地址");
+        }
+
+        try {
+            bidService.addPrice(bid,
+                () -> point(bid) // 这里处理积分相关的
+            );
+        }catch (RuntimeException e) {
+            log.error("addPrice error {}", bid, e);
+            if (e instanceof LockException) {
+                return AjaxResult.error("商品当前积分已变动");
+            }else if (e instanceof AddPriceException) {
+                return AjaxResult.error(e.getMessage());
+            }else {
+                return AjaxResult.error("出价失败");
+            }
+        }
+        return AjaxResult.success();
+    }
+
+
+
+    @ApiOperation("出价拍卖增加价格")
+    @PostMapping({"/v2/addPrice"})
+    public AjaxResult addPriceV2(@RequestBody BidVO bid) {
+        bid.setAuctionId(2L);
+        log.info("add price {}", bid);
+        // 创建bidRecord
+        UserInfo userInfo = UserUtils.getUserInfo();
+        // 创建bidRecord
+        bid.setCreateBy(userInfo.getSub());
+        bid.setAccount(userInfo.getAccount());
+        String accountId = String.valueOf(userInfo.getId());
+        bid.setAccountId(accountId);
+        bid.setAvatar(userInfo.getAvatar());
+        bid.setBidNo(UUID.randomUUID().toString());
+        Long recordId = createBidRecord(bid);
+
+        if (Objects.equals(2, userInfo.getAccountStatus())) {
+            return AjaxResult.error("暂时无法参与当前竞价");
+        }
+        if (!Objects.equals(1, userInfo.getFaceVerify()) && !bid.getTest()) {
+            return AjaxResult.error("请先进行实名认证");
+        }
+
+        if(!userInfo.isHasAddress()) {
+            return AjaxResult.error("请先添加地址");
+        }
+
+        // 判断拍卖状态
+        String error = checkLotStatus(bid);
+        if (StringUtils.hasLength(error)) {
+            log.warn("出价失败:{}", error);
+            return AjaxResult.error("出价失败");
+        }
+        Lot lot = cacheMap.viewLot(bid.getAuctionId(), bid.getLotId()).getLot();
+        if (Objects.equals(lot.getMerchantId().intValue(), userInfo.getMerchantId())) {
+            log.warn("出价失败,商家无法参与自己的竞价拍卖");
+            return AjaxResult.error("出价失败,商家无法参与自己的竞价拍卖");
+        }
+        List<Order> orderList = orderStatusService.selectOrderStatusList(Order.builder().flag(1).merchantId(lot.getMerchantId()).userId(userInfo.getId().longValue()).build());
+        /*if (!CollectionUtils.isEmpty(orderList)) {
+            return AjaxResult.error("因您存在超时支付,暂时无法参与当前竞价");
+        }
+        orderList=orderStatusService.selectOrderStatusList(Order.builder().flag(0).merchantId(lot.getMerchantId()).status(100L).userId(userInfo.getId().longValue()).build());
+        if (!CollectionUtils.isEmpty(orderList)) {
+            return AjaxResult.error("支付当前待支付订单才可继续参与竞价");
+        }*/
+        try {
+            bidService.addPrice(bid,() ->{ });
+        }catch (RuntimeException e) {
+            log.error("addPrice error {}", bid, e);
+            return AjaxResult.error("当前价格已变动");
+        }
+        return AjaxResult.success();
+    }
+
+    /*
+     * 直播间竞拍提速版,使用redis维护出价信息,拍卖结束后统一录入数据库,每秒更新redis数据同步到数据库的表里面
+     * 出价限制由拍卖的接口提供,这里只检查缓存中是否存在,不存在则不能参加:
+     * */
+    @PostMapping({"/v3/addPrice"})
+    public AjaxResult addPriceV4(@RequestBody BidVO bid) {
+        UserInfo userInfo = UserUtils.getSimpleUserInfo();
+        if(userInfo!=null){
+            String nickname = String.valueOf(userInfo.getSub());
+            bid.setCreateBy(nickname);
+            bid.setAccount(nickname);
+            String accountId = String.valueOf(userInfo.getId());
+            bid.setAccountId(accountId);
+            bid.setAvatar(userInfo.getAvatar());
+            bid.setAuctionId(2L);
+            bid.setBidNo(UUID.randomUUID().toString());
+            createBidRecordCache(bid);
+        }
+        else{return AjaxResult.error("出价失败");}
+
+        //从本场拍卖的缓存中拿限制信息,未能通过的用户不能出价参拍
+        Map<String,String> forbbidenUsermap = redisCache.getCacheMap(String.format(Constants.REDIS_MAP_AUC_LOT_BID_USER_TEMPLATE, bid.getLotId()));
+        if(forbbidenUsermap.containsKey(userInfo.getId().toString())){
+            return AjaxResult.error(forbbidenUsermap.get(userInfo.getId().toString()));
+        }
+
+        // 判断拍卖状态
+        String error = checkLotStatus(bid);
+        if (StringUtils.hasLength(error)) {
+            log.warn("出价失败:{}", error);
+            return AjaxResult.error("出价失败");
+        }
+
+        try {
+            bidService.addPriceV3(bid,() ->{ });
+        }catch (RuntimeException e) {
+            log.error("addPriceV3 error {}", bid, e);
+            return AjaxResult.error("当前价格已变动");
+        }
+        return AjaxResult.success();
+    }
+
+    @ApiOperation("查询并返回指定拍卖中已结束的拍卖物品列表,并以分页的形式展示")
+    @PostMapping("/view/{auctionId}/finished")
+    public AjaxResult viewFinished(@RequestBody BidVO bidVO, @PathVariable Long auctionId) {
+        log.info("= " + bidVO + "=");
+        bidVO.setOrderBy("real_end_time desc");
+        startPage(bidVO);
+        List<Lot> lots = lotService.selectLotList(Lot.builder()
+            .auctionId(auctionId)
+            .delFlag(Constants.DEL_FLAG_NO_DELETE)
+            .goodsType(bidVO.getGoodsType())
+            .status(Constants.LOT_STATUS_SOLD)
+            .privateDomain(0)
+            .build());
+        Page page = (Page) lots;
+        PaginationUtil.PageResult<Lot> lotlist =  new PaginationUtil.PageResult<>(((Page<Lot>) page).getResult(), Integer.parseInt(page.getTotal() + ""), page.getPageNum(), bidVO.getPageSize());
+        lotlist.getItems().forEach(lot->{
+            lot.setDealAccount(SensitiveDataUtils.maskString(lot.getDealAccount(), 4));
+            lot.setUpdateBy(SensitiveDataUtils.maskString(lot.getUpdateBy(), 4));
+        });
+        return AjaxResult.success(lotlist);
+    }
+
+    @ApiOperation("获取指定拍卖ID的所有拍卖品信息")
+    @PostMapping("/view/{auctionId}/starting")
+    public AjaxResult viewBidding(@RequestBody BidVO bidVO, @PathVariable Long auctionId) {
+        log.info("= " + bidVO + "=");
+        Map<String, Live> map = bidService.view(auctionId);
+        Set<String> statusSet = Sets.newHashSet(Constants.LOT_STATUS_STARTING, Constants.LOT_STATUS_BIDDING, Constants.LOT_STATUS_WAITING);
+        List<Live> list = map.values().stream()
+            .filter(live -> statusSet.contains(live.getLot().getStatus()) && live.getShow())
+            .filter(live -> StringUtils.isEmpty(bidVO.getGoodsType()) || Objects.equals(live.getLot().getGoodsType(), bidVO.getGoodsType()))
+            .map(SensitiveDataUtils::handleViewDataSafe)
+            .sorted(this::sort)
+            .collect(Collectors.toList());
+        return AjaxResult.success(PaginationUtil.page(bidVO, list));
+    }
+
+    @ApiOperation("从 Redis 缓存中获取id对应的 Live 对象,并将其返回")
+    @GetMapping("/viewEx/{id}")
+    public AjaxResult viewEx(@PathVariable Long id) {
+        Live live = redisCache.getCacheMapValue(String.format(Constants.REDIS_AUC_TEMPLATE, 2), id.toString());
+        return AjaxResult.success(JSON.toJSON(live));
+    }
+
+    @ApiOperation("获取并返回Live 对象,该对象包含了特定拍卖品的详细信息,包括出价记录、交易账户信息等")
+    @GetMapping("/view/{auctionId}/{id}")
+    public AjaxResult view(@PathVariable Long auctionId, @PathVariable Long id) {
+        Live live = bidService.viewLot(auctionId, id);
+        Lot lot = lotService.selectLotById(id);
+        UserInfo userInfo = null;
+        try {
+            userInfo = UserUtils.getUserInfo(Convert.toInt(lot.getDealAccountId()));
+        }
+        catch (Exception e) {
+            log.error("getUserInfo error", e);
+        }
+
+        try {
+            log.info("request param auction {} id {}",auctionId,id);
+            // region 获取用户禁止拍卖的检查信息并写入缓存
+            UserInfo curruserInfo = UserUtils.getUserInfo(getUserId());
+            boolean isForbiddenUser = false;
+            String forbiddenReason = "";
+            if (Objects.equals(2, curruserInfo.getAccountStatus())) {
+                isForbiddenUser = true;
+                forbiddenReason = "暂时无法参与当前竞价";
+            }
+            if (!Objects.equals(1, curruserInfo.getFaceVerify())) {
+                isForbiddenUser = true;
+                forbiddenReason = "请先进行实名认证";
+            }
+            if(!curruserInfo.isHasAddress()) {
+                isForbiddenUser = true;
+                forbiddenReason = "请先添加地址";
+            }
+
+            if (Objects.equals(lot.getMerchantId().intValue(), curruserInfo.getMerchantId())) {
+                isForbiddenUser = true;
+                forbiddenReason = "出价失败";
+            }
+
+            Long merchantId = lot.getMerchantId();
+            Long userId = Long.valueOf(getUserId());
+            List<Order> orderList = orderStatusService.selectOrderStatusList(Order.builder().flag(1).merchantId(merchantId).userId(userId).build());
+            if (!CollectionUtils.isEmpty(orderList)) {
+                isForbiddenUser = true;
+                forbiddenReason = "因您存在超时支付,暂时无法参与当前竞价";
+            }
+            orderList=orderStatusService.selectOrderStatusList(Order.builder().flag(0).merchantId(lot.getMerchantId()).userId(userId).build());
+            if (!CollectionUtils.isEmpty(orderList)) {
+                isForbiddenUser = true;
+                forbiddenReason = "支付当前待支付订单才可继续参与竞价";
+            }
+            if(isForbiddenUser){
+                redisCache.setCacheMapValue(String.format(Constants.REDIS_MAP_AUC_LOT_BID_USER_TEMPLATE, lot.getId()), curruserInfo.getId().toString(), forbiddenReason);
+            }
+            else{
+                redisCache.delCacheMapValue(String.format(Constants.REDIS_MAP_AUC_LOT_BID_USER_TEMPLATE, lot.getId()), curruserInfo.getId().toString());
+            }
+            // endregion
+
+        }
+        catch (Exception e) {
+            log.error("get curruserInfo error", e);
+        }
+
+        final Lot clone = CloneUtils.clone(lot);
+        List<Bid> bids = bidService.selectBidList(Bid.builder().lotId(lot.getId()).build());
+        List<Bid> bidsDealList = bids.stream()
+            .filter(bid -> bid.getStatus() == 1 && bid.getAccountId().equals(lot.getDealAccountId()))
+            .collect(Collectors.toList());
+        // 检查 bidsList 是否存在,并且如果存在,找到 anonymous 字段等于 1 的 Bid 对象
+        boolean isDealAccountAnonymous = false;
+        if (!bidsDealList.isEmpty()) {
+            for (Bid bid : bidsDealList) {
+                if (bid.getAnonymous() == 1) {
+                    isDealAccountAnonymous = true;
+                    break; // 如果只需要找到第一个符合条件的对象,可以在这里停止循环
+                }
+            }
+        }
+        if (Objects.isNull(live)) {
+            LiveContext liveContext = LiveContext.builder().dbLot(lot).bidList(bids).build();
+            ruleHandlerHolder.sync(liveContext);
+            live = liveContext.getLive();
+            if (Objects.equals(auctionId, 2L)) {
+                Long groupId = lot.getGroupId();
+                LotGroup lotGroup = lotService.selectLotGroupById(groupId);
+                if (lotGroup.getFinishNum() >= lotGroup.getNum()) {
+                    live.setFinish(1);
+                }else {
+                    live.setFinish(0);
+                }
+            }
+            if(Objects.nonNull(userInfo)){
+                live.getLot().setDealAccountavatar(userInfo.getAvatar());
+                if (clone != null) {
+                    clone.setDealAccountavatar(userInfo.getAvatar());
+                }
+            }
+            if(Objects.equals(getUserId().toString(), lot.getDealAccountId())){
+                live.getLot().setOrderId(lot.getOrderId());
+            }
+            else{
+                if (clone != null) {
+                    clone.setOrderId(null);
+                    if(isDealAccountAnonymous){
+                        clone.setDealAccountavatar(null);
+                        clone.setDealAccount(null);
+                    }
+                }
+            }
+            live.setLot(clone);
+            return AjaxResult.success(SensitiveDataUtils.handleViewDataSafe(live));
+        }
+        live.setLot(clone);
+        if (Objects.equals(auctionId, 2L)) {
+            Long groupId = lot.getGroupId();
+            LotGroup lotGroup = lotService.selectLotGroupById(groupId);
+            if (lotGroup.getFinishNum() >= lotGroup.getNum()) {
+                live.setFinish(1);
+            }else {
+                live.setFinish(0);
+            }
+        }
+        if(Objects.nonNull(userInfo)){
+            live.getLot().setDealAccountavatar(userInfo.getAvatar());
+        }
+        if(Objects.equals(getUserId().toString(), lot.getDealAccountId())){
+            live.getLot().setOrderId(lot.getOrderId());
+        }
+        else{
+            live.getLot().setOrderId(null);
+            if(isDealAccountAnonymous){
+                live.getLot().setDealAccountavatar(null);
+                live.getLot().setDealAccount(null);
+            }
+        }
+        log.info("/bid/bidding/view/ result {}", live);
+        log.info("/bid/bidding/view/ result {}", SensitiveDataUtils.handleViewDataSafe(live));
+        return AjaxResult.success(SensitiveDataUtils.handleViewDataSafe(live));
+    }
+
+    /*
+     * 配合v3的竞价使用,查询缓存中的竞价结果
+     * */
+    @GetMapping("/viewv3/{auctionId}/{id}")
+    public AjaxResult viewV4(@PathVariable Long auctionId, @PathVariable Long id) {
+        Live live = bidService.viewLot(auctionId, id);
+        Lot lot = lotService.selectLotById(id);
+        UserInfo userInfo = UserUtils.getUserInfo(Convert.toInt(lot.getDealAccountId()));
+        final Lot clone = CloneUtils.clone(lot);
+        List<Bid> bids = bidService.selectBidList(Bid.builder().lotId(lot.getId()).build());
+        List<Bid> bidsDealList = bids.stream()
+            .filter(bid -> bid.getStatus() == 1 && bid.getAccountId().equals(lot.getDealAccountId()))
+            .collect(Collectors.toList());
+        // 检查 bidsList 是否存在,并且如果存在,找到 anonymous 字段等于 1 的 Bid 对象
+        boolean isDealAccountAnonymous = false;
+        if (!bidsDealList.isEmpty()) {
+            for (Bid bid : bidsDealList) {
+                if (bid.getAnonymous() == 1) {
+                    isDealAccountAnonymous = true;
+                    break; // 如果只需要找到第一个符合条件的对象,可以在这里停止循环
+                }
+            }
+        }
+        if (Objects.isNull(live)) {
+            LiveContext liveContext = LiveContext.builder().dbLot(lot).bidList(bids).build();
+            ruleHandlerHolder.sync(liveContext);
+            live = liveContext.getLive();
+            if (Objects.equals(auctionId, 2L)) {
+                Long groupId = lot.getGroupId();
+                LotGroup lotGroup = lotService.selectLotGroupById(groupId);
+                if (lotGroup.getFinishNum() >= lotGroup.getNum()) {
+                    live.setFinish(1);
+                }else {
+                    live.setFinish(0);
+                }
+            }
+            if(Objects.nonNull(userInfo)){
+                live.getLot().setDealAccountavatar(userInfo.getAvatar());
+                if (clone != null) {
+                    clone.setDealAccountavatar(userInfo.getAvatar());
+                }
+            }
+            if(Objects.equals(getUserId().toString(), lot.getDealAccountId())){
+                live.getLot().setOrderId(lot.getOrderId());
+            }
+            else{
+                if (clone != null) {
+                    clone.setOrderId(null);
+                    if(isDealAccountAnonymous){
+                        clone.setDealAccountavatar(null);
+                        clone.setDealAccount(null);
+                    }
+                }
+            }
+            live.setLot(clone);
+            log.info("/bid/bidding/view/ result {}", live);
+            log.info("/bid/bidding/view/ result {}", SensitiveDataUtils.handleViewDataSafe(live));
+            return AjaxResult.success(SensitiveDataUtils.handleViewDataSafe(live));
+        }
+        if (Objects.equals(auctionId, 2L)) {
+            Long groupId = lot.getGroupId();
+            LotGroup lotGroup = lotService.selectLotGroupById(groupId);
+            if (lotGroup.getFinishNum() >= lotGroup.getNum()) {
+                live.setFinish(1);
+            }else {
+                live.setFinish(0);
+            }
+        }
+        if(Objects.nonNull(userInfo)){
+            live.getLot().setDealAccountavatar(userInfo.getAvatar());
+        }
+        if(Objects.equals(getUserId().toString(), lot.getDealAccountId())){
+            live.getLot().setOrderId(lot.getOrderId());
+        }
+        else{
+            live.getLot().setOrderId(null);
+            if(isDealAccountAnonymous){
+                live.getLot().setDealAccountavatar(null);
+                live.getLot().setDealAccount(null);
+            }
+        }
+        log.info("/bid/bidding/view/ result {}", live);
+        log.info("/bid/bidding/view/ result {}", SensitiveDataUtils.handleViewDataSafe(live));
+        return AjaxResult.success(SensitiveDataUtils.handleViewDataSafe(live));
+    }
+
+    @GetMapping("/viewcache/{auctionId}/{id}")
+    public AjaxResult viewV3cache(@PathVariable Long auctionId, @PathVariable Long id) {
+        List<BidVO> bidList = redisCache.getCacheList(String.format(Constants.REDIS_MAP_AUC_LOT_BID_LIST_PREFIX,id));
+        return AjaxResult.success(bidList);
+    }
+
+    @ApiOperation("查询拍卖品出价记录等信息")
+    @PostMapping("/list")
+    public AjaxResult listBid(@RequestBody BidVO bidVO) {
+        Lot dbLot = lotService.selectLotById(bidVO.getLotId());
+        if (Constants.LOT_STATUS_SOLD.equals(dbLot.getStatus()) ||
+            Constants.LOT_STATUS_CANCELLED.equals(dbLot.getStatus()) ||
+            Constants.LOT_STATUS_PASS.equals(dbLot.getStatus())) {
+            return findBidPageFromDb(bidVO);
+        }
+        if (bidVO.getPageSize() > 20 || (bidVO.getPageSize() * bidVO.getPageNum()) > 20) {
+            return findBidPageFromDb(bidVO);
+        }else {
+            Integer userId = UserUtils.getSimpleUserInfo().getId();
+            List<Bid> list = bidService.selectCacheBidList(bidVO).stream().map(bid -> {
+                boolean sensitive=false;
+                if (Objects.equals(1, bid.getAnonymous())) {
+                    bid.setAvatar("/MC143302022060920611/avatar/1666182630730.jpg");
+                    bid.setAccount("匿名用户");
+                    sensitive=true;
+                }
+                if (org.apache.commons.lang3.StringUtils.isBlank(bid.getAvatar())){
+                    bid.setAvatar("/MC143302022060920611/avatar/1666182630730.jpg");
+                }
+                if (org.apache.commons.lang3.StringUtils.isBlank(bid.getAccount())){
+                    bid.setAccount("匿名用户");
+                    sensitive=true;
+                }
+                bid.setCurrentAccount(Objects.equals(Integer.parseInt(bid.getAccountId()), userId));
+                if (!sensitive) {
+                    return SensitiveDataUtils.handleViewDataSafe(bid);
+                }
+                return bid;
+            }).collect(Collectors.toList());
+            if (!CollectionUtils.isEmpty(list)) {
+                log.info("CollectionUtils list {}", list);
+                // 使用 Comparator 接口进行排序
+                list.sort((bid1, bid2) -> bid2.getAmount().compareTo(bid1.getAmount()));
+                log.info("CollectionUtils list size after sort {}", list.size());
+                Bid bidLast = list.get(0);
+                Long total = bidLast.getRound();
+                log.info("CollectionUtils bidLast {}", bidLast);
+                List<List<Bid>> partition = Lists.partition(list, bidVO.getPageSize());
+                List<Bid> resultList = Lists.newArrayList();
+                log.info("CollectionUtils partition {}", partition.size());
+                if (partition.size() >= bidVO.getPageNum()) {
+                    log.info("CollectionUtils partitionget {}", partition.get(bidVO.getPageNum() - 1));
+                    log.info("CollectionUtils partitionget {}", resultList.size());
+                    resultList = partition.get(bidVO.getPageNum() - 1);
+                }
+                return AjaxResult.success(new PaginationUtil.PageResult<>(resultList, total.intValue(), bidVO.getPageNum(), bidVO.getPageSize()));
+            }else {
+                log.info("CollectionUtils list is empty");
+                return AjaxResult.success(PaginationUtil.page(bidVO, list));
+            }
+        }
+    }
+    // endregion
+
+
+    // region Methods
+
+    private void point(BidVO bid) {
+        List<Bid> bids = redisCache.getCacheList(String.format(Constants.REDIS_MAP_AUC_LOT_BID_TEMPLATE, bid.getLotId()));
+        Bid lastBid = null;
+        if (!CollectionUtils.isEmpty(bids)) {
+            lastBid = bids.get(0);
+        }
+        log.info("currBid {} lastBid {}",JSON.toJSONString(bid),JSON.toJSONString(lastBid));
+        if (!appClient.operatePoint(bid, lastBid)) {
+            throw new AddPriceException("积分校验失败");
+        }
+    }
+
+    private String checkLotStatus(BidVO bid) {
+        log.info("checkLotStatus cacheMap {}",cacheMap);
+        log.info("checkLotStatus bid {}",bid);
+        Live live = cacheMap.viewLot(bid.getAuctionId(), bid.getLotId());
+        log.info("checkLotStatus live {}",live);
+
+        if (Objects.isNull(live)) {
+            List<Bid> bids = bidService.selectBidList(Bid.builder().lotId(bid.getLotId()).build());
+            log.info("sync bids {}",bids);
+            Lot lot = lotService.selectLotById(bid.getLotId());
+            log.info("sync lot {}",lot);
+            LiveContext liveContext = LiveContext.builder().dbLot(lot).bidList(bids).build();
+            ruleHandlerHolder.sync(liveContext);
+            log.info("sync liveContext {}",liveContext);
+            live = liveContext.getLive();
+            live.setLot(lot);
+            if (Objects.equals(bid.getAuctionId(), 2L)) {
+                Long groupId = lot.getGroupId();
+                LotGroup lotGroup = lotService.selectLotGroupById(groupId);
+                if (lotGroup.getFinishNum() >= lotGroup.getNum()) {
+                    live.setFinish(1);
+                    log.info("cacheMap putLive {}","未找到正在进行中的拍卖");
+                    return "未找到正在进行中的拍卖";
+                }else {
+                    live.setFinish(0);
+                    cacheMap.putLive(live);
+                    log.info("cacheMap putLive {}",live);
+                    return null;
+                }
+            }
+            return "未找到正在进行中的拍卖";
+        }
+        if (Constants.LOT_STATUS_WAITING.equals(live.getLot().getStatus())) {
+            return "拍卖还未开始";
+        }
+        if (!Lists.newArrayList(Constants.LOT_STATUS_BIDDING, Constants.LOT_STATUS_STARTING).contains(live.getLot().getStatus())) {
+            return "拍卖已结束";
+        }
+        return null;
+    }
+
+    private Long createBidRecord(Bid bid){
+        BidRecord record = BidRecord.builder()
+            .lotId(bid.getLotId())
+            .bidNo(bid.getBidNo())
+            .userId(Long.parseLong(bid.getAccountId()))
+            .amount(bid.getAmount())
+            .createTime(new Date()).build();
+        bidRecordMapper.insertBidRecord(record);
+        return record.getId();
+    }
+
+    private void createBidRecordCache(Bid bid){
+        List<BidRecord> bidList = new ArrayList<>();
+        BidRecord record = BidRecord.builder()
+            .lotId(bid.getLotId())
+            .bidNo(bid.getBidNo())
+            .userId(Long.parseLong(bid.getAccountId()))
+            .amount(bid.getAmount())
+            .createTime(new Date()).build();
+        bidList.add(record);
+        redisCache.addToCacheList(String.format(Constants.REDIS_MAP_AUC_LOT_BIDRECORD_LIST_PREFIX, bid.getLotId()), bidList);
+    }
+
+    private int sort(Live live1, Live live2) {
+        int sort1 = Objects.nonNull(live1.getLot().getSort()) ? live1.getLot().getSort() : 0;
+        int sort2 = Objects.nonNull(live2.getLot().getSort()) ? live2.getLot().getSort() : 0;
+        return (sort2 - sort1)  == 0 ? (int)(live1.getCurrentEndTime() - live2.getCurrentEndTime()) : (sort2 - sort1);
+    }
+
+    private AjaxResult findBidPageFromDb(BidVO bidVO) {
+        bidVO.setDelFlag(Constants.DEL_FLAG_NO_DELETE);
+        startPage(bidVO);
+        List<Bid> data = bidService.selectBidList(bidVO);
+        Integer userId = UserUtils.getSimpleUserInfo().getId();
+        data.forEach(bid -> {
+            boolean sensitive=false;
+            if (Objects.equals(1, bid.getAnonymous())) {
+                bid.setAvatar("/MC143302022060920611/avatar/1666182630730.jpg");
+                bid.setAccount("匿名用户");
+                sensitive=true;
+            }
+            if (org.apache.commons.lang3.StringUtils.isBlank(bid.getAvatar())){
+                bid.setAvatar("/MC143302022060920611/avatar/1666182630730.jpg");
+            }
+            if (org.apache.commons.lang3.StringUtils.isBlank(bid.getAccount())){
+                bid.setAccount("匿名用户");
+                sensitive=true;
+            }
+            bid.setCurrentAccount(Objects.equals(Integer.parseInt(bid.getAccountId()), userId));
+            if (!sensitive) {
+                SensitiveDataUtils.handleViewDataSafe(bid);
+            }
+        });
+        Page page = (Page) data;
+        return AjaxResult.success(new PaginationUtil.PageResult<>(((Page<Bid>) data).getResult(), Integer.parseInt(page.getTotal() + ""), page.getPageNum(), bidVO.getPageSize()));
+    }
+
+
+}

+ 50 - 0
bid/src/main/java/cn/hobbystocks/auc/web/FansController.java

@@ -0,0 +1,50 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import cn.hobbystocks.auc.domain.LotFans;
+import cn.hobbystocks.auc.service.ILotFansService;
+import cn.hobbystocks.auc.vo.FansVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/bid/fans")
+@Slf4j
+@Api(tags = "关注拍品相关接口")
+public class FansController {
+
+    @Autowired
+    private ILotFansService lotFansService;
+
+    @ApiOperation("关注拍卖品")
+    @PostMapping
+    public AjaxResult fans(@RequestBody FansVO fansVO) {
+        lotFansService.fans(fansVO);
+        return AjaxResult.success();
+    }
+
+
+    @ApiOperation("检查当前用户是否关注了指定的拍卖品")
+    @PostMapping("/isFans")
+    public AjaxResult isFans(@RequestBody FansVO fansVO) {
+        List<LotFans> lotFansList = lotFansService.selectLotFansList(LotFans.builder()
+                .lotId(fansVO.getLotId())
+                .userId(UserUtils.getSimpleUserInfo().getId().longValue())
+                .type("user_like")
+                .build());
+        return AjaxResult.success(!CollectionUtils.isEmpty(lotFansList));
+    }
+
+
+
+}

+ 88 - 0
bid/src/main/java/cn/hobbystocks/auc/web/InController.java

@@ -0,0 +1,88 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.domain.Lot;
+import cn.hobbystocks.auc.domain.LotGroup;
+import cn.hobbystocks.auc.handle.context.tradition.TraditionRule;
+import cn.hobbystocks.auc.service.ILotService;
+import cn.hobbystocks.auc.vo.LotGroupDTO;
+import cn.hobbystocks.auc.vo.LotGroupVO;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.google.common.collect.Lists;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+@RestController
+@RequestMapping("/bid/bidding/core")
+@Slf4j
+@Api(tags = "拍卖控制相关接口")
+public class InController {
+    @Autowired
+    private ILotService lotService;
+
+    @ApiOperation(value = "检查拍品ID数组的拍卖是否正在进行",notes = "")
+    @PostMapping("/lot/status")
+    public AjaxResult living(@RequestBody LotGroupVO lotGroup) {
+        List<LotGroup> lotGroups = lotService.findStartingGroupByLotIds(lotGroup.getLotIds());
+        JSONObject jsonObject = new JSONObject();
+        if (!CollectionUtils.isEmpty(lotGroups)) {
+            jsonObject.put("status",true);
+            return AjaxResult.success("竞价正在进行",jsonObject);
+        }else {
+            //根据groupId数组查询拍品
+            List<Lot> lotList = lotService.selectLotByGroupIds(lotGroup.getLotIds());
+            for (Lot lot : lotList) {
+                Date realEndTime = lot.getRealEndTime();
+                if (Objects.nonNull(realEndTime) && (System.currentTimeMillis() - realEndTime.getTime() < 1000 * 60 * 3) ) {
+                    jsonObject.put("status",true);
+                    return AjaxResult.success("竞价结束不足3分钟",jsonObject);
+                }
+            }
+            jsonObject.put("status",false);
+            return AjaxResult.success(jsonObject);
+        }
+    }
+
+    @ApiOperation(value = "拍品组列表查询",notes = "根据请求体中的 LotGroupVO 对象,返回符合条件的拍卖组列表")
+    @PostMapping("/list")
+    public AjaxResult list(@RequestBody LotGroupVO lotGroup) {
+        if (Objects.isNull(lotGroup.getLotIds())) {
+            return AjaxResult.error("未查询到相关信息");
+        }
+        List<LotGroupDTO> list = Lists.newArrayList();
+        lotService.findPubbedLotGroupByIds(lotGroup.getLotIds(),lotGroup.getMerchantId()).forEach(group -> {
+            LotGroupDTO lotGroupDTO = new LotGroupDTO();
+            BeanUtils.copyProperties(group, lotGroupDTO);
+            TraditionRule traditionRule = JSON.parseObject(group.getRuleContent(), TraditionRule.class);
+            lotGroupDTO.setGoodsPrice(traditionRule.getStartPrice());
+            Long lotId = lotGroupDTO.getLotId();
+            if (Objects.nonNull(lotId)) {
+                Lot lot = lotService.selectLotById(lotId);
+                if (Objects.nonNull(lot)) {
+                    lotGroupDTO.setName(lot.getName());
+                }
+            }
+            list.add(lotGroupDTO);
+        });
+        return new AjaxResult(200, null, list);
+    }
+
+
+
+
+
+}

+ 139 - 0
bid/src/main/java/cn/hobbystocks/auc/web/LocalController.java

@@ -0,0 +1,139 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.annotation.RequireRoles;
+import cn.hobbystocks.auc.common.constant.Constants;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import cn.hobbystocks.auc.common.utils.UserType;
+import cn.hobbystocks.auc.domain.Bid;
+import cn.hobbystocks.auc.domain.Lot;
+import cn.hobbystocks.auc.domain.LotGroup;
+import cn.hobbystocks.auc.domain.Order;
+import cn.hobbystocks.auc.dto.LotDTO;
+import cn.hobbystocks.auc.handle.context.tradition.TraditionRule;
+import cn.hobbystocks.auc.service.IBidService;
+import cn.hobbystocks.auc.service.ILotService;
+import cn.hobbystocks.auc.service.IOrderStatusService;
+import cn.hobbystocks.auc.vo.LotGroupDTO;
+import cn.hobbystocks.auc.vo.LotGroupVO;
+import cn.hobbystocks.auc.vo.OrderVO;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.google.common.collect.Lists;
+import io.swagger.annotations.*;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+@RestController
+@RequestMapping("/api/local/")
+@Api(tags = "本地服务接口")
+public class LocalController {
+    @Autowired
+    IOrderStatusService orderStatusService;
+
+    @Autowired
+    private ILotService lotService;
+    @Autowired
+    private IBidService bidService;
+
+    @ApiOperation(value = "查询拍品是否正在竞拍",notes = "检查拍品ID数组的拍卖是否正在进行")
+    @PostMapping("/lot/status")
+    public AjaxResult living(@RequestBody LotGroupVO lotGroup) {
+        List<LotGroup> lotGroups = lotService.findStartingGroupByLotIds(lotGroup.getLotIds());
+        if (!CollectionUtils.isEmpty(lotGroups)) {
+            return AjaxResult.success("竞价正在进行",true);
+        }else {
+            //根据groupId数组查询拍品
+            List<Lot> lotList = lotService.selectLotByGroupIds(lotGroup.getLotIds());
+            for (Lot lot : lotList) {
+                Date realEndTime = lot.getRealEndTime();
+                if (Objects.nonNull(realEndTime) && (System.currentTimeMillis() - realEndTime.getTime() < 1000 * 60 * 3) ) {
+                    return AjaxResult.success("竞价结束不足3分钟",true);
+                }
+            }
+            return AjaxResult.success(false);
+        }
+    }
+
+    @ApiOperation(value = "拍品组列表查询",notes = "根据请求体中的 LotGroupVO 对象,返回符合条件的拍卖组列表")
+    @PostMapping("lot/list")
+    public AjaxResult list(@RequestBody LotGroupVO lotGroup) {
+        if (Objects.isNull(lotGroup.getLotIds())) {
+            return AjaxResult.error("未查询到相关信息");
+        }
+        List<LotGroupDTO> list = Lists.newArrayList();
+        lotService.findPubbedLotGroupByIds(lotGroup.getLotIds(),lotGroup.getMerchantId()).forEach(group -> {
+            LotGroupDTO lotGroupDTO = new LotGroupDTO();
+            BeanUtils.copyProperties(group, lotGroupDTO);
+            TraditionRule traditionRule = JSON.parseObject(group.getRuleContent(), TraditionRule.class);
+            lotGroupDTO.setGoodsPrice(traditionRule.getStartPrice());
+            Long lotId = lotGroupDTO.getLotId();
+            if (Objects.nonNull(lotId)) {
+                Lot lot = lotService.selectLotById(lotId);
+                if (Objects.nonNull(lot)) {
+                    lotGroupDTO.setName(lot.getName());
+                    String status = lot.getStatus();
+                    if (StringUtils.equals(status, Constants.LOT_STATUS_BIDDING)||StringUtils.equals(status, Constants.LOT_STATUS_STARTING)){
+                        LotDTO lotDTO = new LotDTO();
+                        BeanUtils.copyProperties(lot,lotDTO);
+                        lotGroupDTO.setLot(lotDTO);
+                    }
+                }
+            }
+            list.add(lotGroupDTO);
+        });
+        return new AjaxResult(200, null, list);
+    }
+
+    @GetMapping("order/callback/{orderNo}/{status}")
+    @ApiOperation(value = "订单状态回调",notes = "订单状态回调,竞价成功创建订单,用户订单支付后回调该接口")
+    @ApiImplicitParams({
+        @ApiImplicitParam(name = "orderNo",value = "订单号"),@ApiImplicitParam(name = "status" ,value = "订单状态")
+    })
+    public AjaxResult callback(@PathVariable("orderNo") String orderNo,@PathVariable("status") Long status){
+        //根据orderId更新order状态
+        Order order = Order.builder().orderNo(orderNo).status(status).build();
+        int num = orderStatusService.modifyOrder(order);
+        if (num>0){
+            return AjaxResult.success("请求成功");
+        }
+        return AjaxResult.error("更新状态失败");
+    }
+    @ApiOperation(value = "出价记录查询",notes = "根据出价id查询当前出价是否中标")
+    @GetMapping("bid/get/{id}")
+    public AjaxResult getBid(@PathVariable("id")@ApiParam("出价记录id")Long id){
+        Bid bid = bidService.selectBidById(id);
+        if (Objects.isNull(bid)){
+            return AjaxResult.error("未查询到相关信息");
+        }
+        Integer status = bid.getStatus();
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("status",status);
+        jsonObject.put("id",bid.getId());
+        return AjaxResult.success(jsonObject);
+    }
+    @ApiOperation(value = "开始竞拍",notes = "启动竞拍流程,并发送竞拍开始的通知")
+    @GetMapping("/lot/start/{lotGroupId}")
+    public AjaxResult start(@PathVariable("lotGroupId")Long lotGroupId) {
+        LotGroup lotGroup = lotService.selectLotGroupById(lotGroupId);
+        //校验拍品状态,如果当前拍品为正在竞价中,返回开始竞价失败
+        String status = lotGroup.getStatus();
+        if (StringUtils.equals(status,Constants.GROUP_STATUS_STARTING)||StringUtils.equals(status,Constants.GROUP_STATUS_BIDDING)){
+            return AjaxResult.error("当前有正在竞拍的拍品,无法开始下一轮竞拍");
+        }
+        Long party;
+        try {
+            party = lotService.party(lotGroup);
+        } catch (Exception e) {
+            return AjaxResult.error(e.getMessage());
+        }
+        return AjaxResult.success(party);
+    }
+}

+ 34 - 0
bid/src/main/java/cn/hobbystocks/auc/web/SelfController.java

@@ -0,0 +1,34 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.utils.PaginationUtil;
+import cn.hobbystocks.auc.service.ILotService;
+import cn.hobbystocks.auc.vo.SelfVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/bid/self")
+@Slf4j
+@Api(tags = "我参与的拍卖")
+public class SelfController {
+    @Autowired
+    private ILotService lotService;
+
+    @ApiOperation("分页获取正在进行的拍卖/已结束的拍卖/已获胜的拍卖")
+    @PostMapping
+    public AjaxResult self(@RequestBody SelfVO selfVO) {
+        if ("live".equals(selfVO.getType())) {
+            return AjaxResult.success(PaginationUtil.page(selfVO, lotService.selfLive(selfVO)));
+        }else if ("finish".equals(selfVO.getType())) {
+            return AjaxResult.success(PaginationUtil.page(selfVO, lotService.selfFinish(selfVO)));
+        }else if ("win".equals(selfVO.getType())){
+            return AjaxResult.success(PaginationUtil.page(selfVO, lotService.selfWin(selfVO)));
+        }
+        return AjaxResult.success();
+    }
+
+}

+ 69 - 0
bid/src/main/java/cn/hobbystocks/auc/web/ShippingBiddingController.java

@@ -0,0 +1,69 @@
+package cn.hobbystocks.auc.web;
+
+import cn.hobbystocks.auc.annotation.RequireRoles;
+import cn.hobbystocks.auc.app.AppClient;
+import cn.hobbystocks.auc.common.core.domain.AjaxResult;
+import cn.hobbystocks.auc.common.user.UserUtils;
+import cn.hobbystocks.auc.common.utils.PageUtils;
+import cn.hobbystocks.auc.common.utils.StringUtils;
+import cn.hobbystocks.auc.common.utils.UserType;
+import cn.hobbystocks.auc.domain.LotGroup;
+import cn.hobbystocks.auc.service.ILotService;
+import cn.hobbystocks.auc.vo.CardGroupLivesConfigVO;
+import cn.hobbystocks.auc.vo.LivingExplainDTO;
+import cn.hobbystocks.auc.vo.LotGroupVO;
+import com.alibaba.fastjson.JSON;
+import com.google.common.collect.Maps;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@RestController
+@RequestMapping("/bid/bidding/shipping")
+@Slf4j
+@Api(tags = "处理与物流竞价相关的请求")
+public class ShippingBiddingController {
+
+    @Autowired
+    private ILotService lotService;
+    @Autowired
+    private AppClient appClient;
+
+    /*@ApiOperation("获取并返回直播间的讲解信息")
+    @PostMapping("/getLivingExplain")
+    public AjaxResult getLivingExplain(@RequestBody LivingExplainDTO explainDTO) {
+        return AjaxResult.success(appClient.getLivingExplain(explainDTO.getLiveId()));
+    }*/
+
+    @ApiOperation("根据传入的 LotGroupVO 对象中的 LiveId,返回该直播间下已发布的竞拍商品组列表")
+    @PostMapping("/list")
+    @RequireRoles({UserType.USER_ROLE_SHIPPING})
+    public AjaxResult list(@RequestBody LotGroupVO lotGroup) {
+        PageUtils.startPage(lotGroup);
+        List<LotGroup> lotGroups = lotService.findPubbedLotGroupByIds(lotGroup.getLotIds(),lotGroup.getMerchantId());
+        return AjaxResult.successNewPage(lotGroups);
+    }
+
+    @ApiOperation("返回指定的竞拍商品组信息")
+    @PostMapping("/get")
+    public AjaxResult get(@RequestBody LotGroupVO lotGroup) {
+        return AjaxResult.success(lotService.selectLotGroupById(lotGroup.getId()));
+    }
+
+
+
+
+
+
+
+}

+ 38 - 0
bid/src/main/resources/application-dev.yml

@@ -0,0 +1,38 @@
+spring:
+  redis:
+    sentinel:
+      master: ${SENTINEL_MASTER:}
+    password: ${REDIS_PASSWORD:}
+    database: 9
+    timeout: 3000
+    lettuce:
+      pool:
+        max-active: 1000
+        max-wait: -1ms
+        max-idle: 10
+        min-idle: 5
+
+hobbystocks:
+  host:
+    pointUrl: http://app/api/local/v1/point/operate
+    orderUrl: http://order/api/local/v1/order/auction/submit
+    noticeUrl: http://app/api/local/v1/notify/mail #站内信通知
+    couponUrl: http://app/api/local/v1/coupon/send
+    imUrl: http://poyee-im/chat/handler
+    jPushUrl: http://app/api/local/v1/notify/jpush
+    notifyUrl: http://app/api/local/v1/auction/notify/LIVE/
+  hongkong:
+    host:
+      pointUrl: http://app//api/local/v1/point/operate
+      orderUrl: http://order/api/local/v1/order/auction/submit
+      couponUrl: http://app/api/local/v1/coupon/send
+      noticeUrl: http://poyee-im/chat/handler
+      jPushUrl: http://app/api/local/send/jPush
+  redis:
+    sentinel:
+      nodes: ${SENTINEL_NODES:}
+knife4j:
+  enable: true
+  setting:
+    language: zh-CN
+  production: false #是否生产环境

+ 33 - 0
bid/src/main/resources/application-local.yml

@@ -0,0 +1,33 @@
+redis:
+  database: 9
+  host: ${REDIS_HOST:192.168.50.10}
+  port: ${REDIS_PORT:26379}
+  password: Pass2010    # 密码(默认为空)
+  timeout: 60000  # 连接超时时长(毫秒)
+  pool:
+    max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
+    max-wait: -1ms    # 连接池最大阻塞等待时间(使用负值表示没有限制)
+    max-idle: 10      # 连接池中的最大空闲连接
+    min-idle: 5       # 连接池中的最小空闲连接
+
+hobbystocks:
+  host:
+    pointUrl: http://app/api/local/v1/point/operate
+    orderUrl: http://order/api/local/v1/order/auction/submit
+    noticeUrl: http://poyee-im/chat/handler #IM服务通知
+    couponUrl: http://app/api/local/v1/coupon/send
+    imUrl: http://poyee-im/chat/handler
+    jPushUrl: http://poyee-app/api/local/send/jPush
+    notifyUrl: https://m-dev.hobbystocks.net/app/api/local/v1/auction/notify/LIVE/ #app后端通知接口
+  hongkong:
+    host:
+      pointUrl: http://app//api/local/v1/point/operate
+      orderUrl: http://order/api/local/v1/order/auction/submit
+      couponUrl: http://app/api/local/v1/coupon/send
+      noticeUrl: http://poyee-im/chat/handler
+      jPushUrl: http://app/api/local/send/jPush
+knife4j:
+  enable: true
+  setting:
+    language: zh-CN
+  production: false #是否生产环境

+ 31 - 0
bid/src/main/resources/application-prod.yml

@@ -0,0 +1,31 @@
+spring:
+  redis:
+    sentinel:
+      master: ${SENTINEL_MASTER:}
+    password: ${REDIS_PASSWORD:}
+    database: 9
+    timeout: 3000
+    lettuce:
+      pool:
+        max-active: 1000
+        max-wait: -1ms
+        max-idle: 10
+        min-idle: 5
+
+hobbystocks:
+  host:
+    pointUrl: http://app/api/local/v1/point/operate
+    orderUrl: http://order/api/local/v1/order/auction/submit #中标创建订单
+    noticeUrl: http://app/api/local/v1/notify/mail
+    couponUrl: http://app/api/local/v1/coupon/send # 中标兑换优惠券
+    imUrl: http://poyee-im/chat/handler
+    jPushUrl: http://app/api/local/v1/notify/jpush
+    notifyUrl: http://app/api/local/v1/auction/notify/LIVE/
+  redis:
+    sentinel:
+      nodes: ${SENTINEL_NODES:}
+knife4j:
+  enable: true
+  setting:
+    language: zh-CN
+  production: true #

+ 79 - 0
bid/src/main/resources/application.yml

@@ -0,0 +1,79 @@
+spring:
+  profiles:
+    active: local
+  application:
+    name: bid-hk
+  datasource:
+    url: ${DB_URL:jdbc:postgresql://192.168.50.10:15432/hobby_auction}
+    username: ${DB_USERNAME:poyee_auction}
+    password: ${DB_PASSWORD:Pass2025}
+
+  mvc:
+    pathmatch:
+      matching-strategy: ant_path_matcher
+        # redis 配置
+# 日志配置
+logging:
+  console.enabled: ${CONSOLE_ENABLED:true}
+  file.enabled: ${FILE_ENABLED:false}
+  level:
+    cn.hobbystocks: info
+    org.springframework: warn
+  fluentd:
+    enabled: ${FLUENTD_ENABLED:false}
+    host: ${FLUENTD_HOST:127.0.0.1}
+    port: ${FLUENTD_PORT:24225}
+
+
+server:
+  # 服务器的HTTP端口,默认为8080
+  port: 8081
+
+swagger:
+  # 是否开启swagger
+  enabled: true
+  # 请求前缀
+  pathMapping: /
+
+#Forest
+forest:
+  #bean-id: config0 # 在spring上下文中bean的id(默认为 forestConfiguration)
+  #backend: okhttp3 # 后端HTTP框架(默认为 okhttp3)
+  #max-connections: 1000 # 连接池最大连接数(默认为 500)
+  #max-route-connections: 500 # 每个路由的最大连接数(默认为 500)你这
+  timeout: 30000 # 请求超时时间,单位为毫秒(默认为 3000)
+  connect-timeout: 30000 # 连接超时时间,单位为毫秒(默认为 timeout)
+  read-timeout: 300000 # 数据读取超时时间,单位为毫秒(默认为 timeout)
+  #max-retry-count: 0 # 请求失败后重试次数(默认为 0 次不重试)
+  #ssl-protocol: SSLv3 # 单向验证的HTTPS的默认SSL协议(默认为 SSLv3)
+  #logEnabled: true # 打开或关闭日志(默认为 true)
+  #log-request: true # 打开/关闭Forest请求日志(默认为 true)
+  #log-response-status: true # 打开/关闭Forest响应状态日志(默认为 true)
+  #log-response-content: true # 打开/关闭Forest响应内容日志(默认为 false)
+  variables:
+    appleUrl: https://appleid.apple.com/auth
+
+bee:
+  osql:
+    loggerType: slf4j
+
+hobbystocks:
+  app-version: ${spring.profiles.active}
+
+user:
+  info-url: http://app/api/local/v1/user/auction/check-msg/
+
+
+management:
+  endpoints:
+    web:
+      exposure:
+        include: health
+  endpoint:
+    health:
+      enabled: true
+      show-details: always
+mybatis-plus:
+  type-aliases-package: cn.hobbystocks.auc.domain
+  mapper-locations: classpath*:mapper/**/*Mapper.xml
+  config-location: classpath:mybatis/mybatis-config.xml

+ 1 - 0
bid/src/main/resources/bee.properties

@@ -0,0 +1 @@
+bee.osql.loggerType=slf4j

+ 21 - 0
bid/src/main/resources/logback-fluentd.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<included>
+    <appender name="FLUENT_SYNC" class="ch.qos.logback.more.appenders.DataFluentAppender">
+        <tag>logback</tag>
+        <label>${spring.application.name}.${hostname}</label>
+        <remoteHost>${logging.fluentd.host}</remoteHost>
+        <port>${logging.fluentd.port}</port>
+
+        <encoder charset="UTF-8">
+            <pattern>%logger{15}:%L - %msg</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="FLUENT" class="ch.qos.logback.classic.AsyncAppender">
+        <queueSize>1000</queueSize>
+        <neverBlock>true</neverBlock>
+        <maxFlushTime>5000</maxFlushTime>
+        <includeCallerData>false</includeCallerData>
+        <appender-ref ref="FLUENT_SYNC" />
+    </appender>
+</included>

+ 57 - 0
bid/src/main/resources/logback-spring.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+    <property name="log.path" value="../logs" />
+    <!-- 日志输出格式 -->
+    <!-- 时间戳属性 -->
+    <property name="log.timestamp" value="%d{yyyy-MM-dd HH:mm:ss.SSS}" />
+    <!-- 线程名称属性 -->
+    <property name="log.thread" value="[%thread]" />
+    <!-- 日志级别属性 -->
+    <property name="log.level" value="%-5level" />
+    <!-- 日志记录器名称属性 -->
+    <property name="log.logger" value="%logger{20}" />
+    <!-- 方法和行号属性 -->
+    <property name="log.methodLine" value="[%method,%line]" />
+    <!-- traceId -->
+    <property name="log.traceId" value="[%X{traceId}]" />
+    <!-- 日志消息属性 -->
+    <property name="log.message" value="%msg%n" />
+    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+    <springProperty scope="context" name="spring.application.name" source="spring.application.name"/>
+    <springProperty scope="context" name="logging.fluentd.enabled" source="logging.fluentd.enabled"/>
+    <springProperty scope="context" name="logging.console.enabled" source="logging.console.enabled"/>
+    <springProperty scope="context" name="logging.fluentd.host" source="logging.fluentd.host"/>
+    <springProperty scope="context" name="logging.fluentd.port" source="logging.fluentd.port"/>
+    <define name="hostname" class="cn.hobbystocks.auc.common.utils.CanonicalHostNamePropertyDefiner"/>
+
+    <if condition='p("logging.fluentd.enabled").equals("true")'>
+        <then>
+            <include resource="logback-fluentd.xml" />
+        </then>
+    </if>
+
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <!-- Spring日志级别控制  -->
+    <logger name="org.springframework" level="warn" />
+    <!--系统操作日志-->
+    <root level="info">
+        <if condition='p("logging.console.enabled").equals("true")'>
+            <then>
+                <appender-ref ref="console" />
+            </then>
+        </if>
+        <if condition='p("logging.fluentd.enabled").equals("true")'>
+            <then>
+                <appender-ref ref="FLUENT"/>
+            </then>
+        </if>
+    </root>
+</configuration>

+ 20 - 0
bid/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+    <!-- 全局参数 -->
+    <settings>
+        <!-- 使全局的映射器启用或禁用缓存 -->
+<!--        <setting name="cacheEnabled"             value="true"   />-->
+        <!-- 允许JDBC 支持自动生成主键 -->
+        <setting name="useGeneratedKeys"         value="true"   />
+        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
+        <setting name="defaultExecutorType"      value="SIMPLE" />
+		<!-- 指定 MyBatis 所用日志的具体实现 -->
+        <setting name="logImpl"                  value="SLF4J"  />
+        <!-- 使用驼峰命名法转换字段 -->
+		<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
+	</settings>
+	
+</configuration>

+ 208 - 0
lot/pom.xml

@@ -0,0 +1,208 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>auction</artifactId>
+		<groupId>cn.hobbystocks</groupId>
+		<version>0.1.0</version>
+	</parent>
+
+	<artifactId>lot</artifactId>
+
+	<dependencies>
+		<!-- web -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		<!-- actuator -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
+		</dependency>
+		<!-- 数据库连接驱动 -->
+
+		<dependency>
+			<groupId>org.postgresql</groupId>
+			<artifactId>postgresql</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.baomidou</groupId>
+			<artifactId>mybatis-plus-boot-starter</artifactId>
+		</dependency>
+		<!-- Druid 数据库连接池 -->
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>druid-spring-boot-starter</artifactId>
+		</dependency>
+		<!-- AOP(Druid监控Spring时需要依赖) -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-aop</artifactId>
+		</dependency>
+
+		<!-- swagger3 -->
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-boot-starter</artifactId>
+			<version>3.0.0</version>
+		</dependency>
+		<dependency>
+			<groupId>com.github.xiaoymin</groupId>
+			<artifactId>knife4j-spring-boot-starter</artifactId>
+			<version>3.0.3</version>
+		</dependency>
+		<!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
+		<dependency>
+			<groupId>io.swagger</groupId>
+			<artifactId>swagger-models</artifactId>
+			<version>1.6.2</version>
+		</dependency>
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+		</dependency>
+		<!--常用工具类 -->
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-lang3</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.github.pagehelper</groupId>
+			<artifactId>pagehelper-spring-boot-starter</artifactId>
+		</dependency>
+		<!-- spring security 安全认证 -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-security</artifactId>
+		</dependency>
+		<!-- redis 缓存操作 -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-redis</artifactId>
+		</dependency>
+		<!-- 阿里JSON解析器 -->
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+			<version>1.2.80</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.poi</groupId>
+			<artifactId>poi-ooxml</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+			<version>4.5.10</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.codehaus.janino</groupId>
+			<artifactId>janino</artifactId>
+			<version>3.0.12</version>
+		</dependency>
+		<dependency>
+			<groupId>com.sndyuk</groupId>
+			<artifactId>logback-more-appenders</artifactId>
+			<version>1.8.5</version>
+		</dependency>
+		<dependency>
+			<groupId>org.fluentd</groupId>
+			<artifactId>fluent-logger</artifactId>
+			<version>0.3.4</version>
+		</dependency>
+		<!-- 自定义验证注解 -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-validation</artifactId>
+		</dependency>
+
+
+		<!-- Spring框架基本的核心工具 -->
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-context-support</artifactId>
+		</dependency>
+
+		<!-- SpringWeb模块 -->
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-web</artifactId>
+		</dependency>
+
+		<!-- JSON工具类 -->
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+		</dependency>
+
+		<!-- io常用工具类 -->
+		<dependency>
+			<groupId>commons-io</groupId>
+			<artifactId>commons-io</artifactId>
+		</dependency>
+
+		<!-- 文件上传工具类 -->
+		<dependency>
+			<groupId>commons-fileupload</groupId>
+			<artifactId>commons-fileupload</artifactId>
+		</dependency>
+
+		<!-- yml解析器 -->
+		<dependency>
+			<groupId>org.yaml</groupId>
+			<artifactId>snakeyaml</artifactId>
+		</dependency>
+
+		<!-- Token生成与解析-->
+		<dependency>
+			<groupId>io.jsonwebtoken</groupId>
+			<artifactId>jjwt</artifactId>
+		</dependency>
+
+		<!-- Jaxb -->
+		<dependency>
+			<groupId>javax.xml.bind</groupId>
+			<artifactId>jaxb-api</artifactId>
+		</dependency>
+
+		<!-- pool 对象池 -->
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-pool2</artifactId>
+		</dependency>
+
+		<!-- servlet包 -->
+		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>javax.servlet-api</artifactId>
+		</dependency>
+
+		<!--foest-->
+		<dependency>
+			<groupId>com.dtflys.forest</groupId>
+			<artifactId>forest-spring-boot-starter</artifactId>
+			<version>1.7.3</version>
+			<exclusions>
+				<exclusion>
+					<artifactId>httpclient-cache</artifactId>
+					<groupId>org.apache.httpcomponents</groupId>
+				</exclusion>
+				<exclusion>
+					<artifactId>commons-logging</artifactId>
+					<groupId>commons-logging</groupId>
+				</exclusion>
+				<exclusion>
+					<artifactId>commons-io</artifactId>
+					<groupId>commons-io</groupId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+
+
+	</dependencies>
+</project>

+ 14 - 0
lot/src/main/java/cn/hobbystocks/auc/annotation/RequireRoles.java

@@ -0,0 +1,14 @@
+package cn.hobbystocks.auc.annotation;
+
+import java.lang.annotation.*;
+
+@Target({ ElementType.PARAMETER, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RequireRoles {
+
+    /**
+     * 角色
+     */
+    String[] value();
+}

+ 13 - 0
lot/src/main/java/cn/hobbystocks/auc/annotation/Sensitive.java

@@ -0,0 +1,13 @@
+package cn.hobbystocks.auc.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Sensitive {
+    /**1-仅手机号脱敏 2-取首字母脱敏*/
+    int type() default 1;
+}

+ 11 - 0
lot/src/main/java/cn/hobbystocks/auc/annotation/SensitiveData.java

@@ -0,0 +1,11 @@
+package cn.hobbystocks.auc.annotation;
+
+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)
+public @interface SensitiveData {
+}

+ 15 - 0
lot/src/main/java/cn/hobbystocks/auc/annotation/View.java

@@ -0,0 +1,15 @@
+package cn.hobbystocks.auc.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface View {
+
+    /**1-设置为NULL*/
+    int type();
+
+}

+ 18 - 0
lot/src/main/java/cn/hobbystocks/auc/cache/CacheMap.java

@@ -0,0 +1,18 @@
+package cn.hobbystocks.auc.cache;
+
+import cn.hobbystocks.auc.handle.context.Live;
+
+import java.util.Map;
+
+public interface CacheMap {
+
+    void putLive(Live live);
+
+    Map<String, Live> viewAuction(Long auctionId);
+
+    Live viewLot(Long auctionId, Long lotId);
+
+    void end(Long auctionId);
+
+    void sync();
+}

+ 71 - 0
lot/src/main/java/cn/hobbystocks/auc/common/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,71 @@
+package cn.hobbystocks.auc.common.config;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import com.alibaba.fastjson.parser.ParserConfig;
+import org.springframework.util.Assert;
+import java.nio.charset.Charset;
+
+/**
+ * Redis使用FastJson序列化
+ * 
+ * @author ruoyi
+ */
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
+{
+    @SuppressWarnings("unused")
+    private ObjectMapper objectMapper = new ObjectMapper();
+
+    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+    private Class<T> clazz;
+
+    static
+    {
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+    }
+
+    public FastJson2JsonRedisSerializer(Class<T> clazz)
+    {
+        super();
+        this.clazz = clazz;
+    }
+
+    @Override
+    public byte[] serialize(T t) throws SerializationException
+    {
+        if (t == null)
+        {
+            return new byte[0];
+        }
+        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+    }
+
+    @Override
+    public T deserialize(byte[] bytes) throws SerializationException
+    {
+        if (bytes == null || bytes.length <= 0)
+        {
+            return null;
+        }
+        String str = new String(bytes, DEFAULT_CHARSET);
+
+        return JSON.parseObject(str, clazz);
+    }
+
+    public void setObjectMapper(ObjectMapper objectMapper)
+    {
+        Assert.notNull(objectMapper, "'objectMapper' must not be null");
+        this.objectMapper = objectMapper;
+    }
+
+    protected JavaType getJavaType(Class<?> clazz)
+    {
+        return TypeFactory.defaultInstance().constructType(clazz);
+    }
+}

+ 13 - 0
lot/src/main/java/cn/hobbystocks/auc/common/config/HttpConfig.java

@@ -0,0 +1,13 @@
+package cn.hobbystocks.auc.common.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.ForwardedHeaderFilter;
+
+@Configuration
+public class HttpConfig {
+	@Bean
+	ForwardedHeaderFilter forwardedHeaderFilter() {
+		return new ForwardedHeaderFilter();
+	}
+}

+ 127 - 0
lot/src/main/java/cn/hobbystocks/auc/common/config/RedisConfig.java

@@ -0,0 +1,127 @@
+package cn.hobbystocks.auc.common.config;
+
+import io.lettuce.core.ClientOptions;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisSentinelConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+
+/**
+ * redis配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport {
+
+    @Bean
+    @ConditionalOnProperty(name = "hobbystocks.redis.sentinel.nodes")
+    public LettuceConnectionFactory redisConnectionFactory(RedisProperties redisProperties, @Value("${hobbystocks.redis.sentinel.nodes:}") String hosts) {
+        // 创建 Redis 哨兵配置
+        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
+                .master(redisProperties.getSentinel().getMaster()); // 设置主节点名称,确保与哨兵配置一致
+        for (String host : hosts.split(",")) {
+            sentinelConfig.sentinel(host.split(":")[0], Integer.parseInt(host.split(":")[1]));
+        }
+        // 如果 Redis 服务器设置了密码,则可以在此配置
+        sentinelConfig.setPassword(redisProperties.getPassword());
+
+        sentinelConfig.setDatabase(redisProperties.getDatabase());
+
+        // 配置连接池参数
+        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
+        poolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive()); // 最大连接数
+        poolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle()); // 最大空闲连接数
+        poolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle()); // 最小空闲连接数
+
+        // 配置客户端选项,启用自动重连
+        ClientOptions clientOptions = ClientOptions.builder()
+            .autoReconnect(true)
+            .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
+            .build();
+
+        // 创建 Lettuce 连接池配置
+        LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
+                .poolConfig(poolConfig)
+                .clientOptions(clientOptions)
+                .clientOptions(ClientOptions.builder()
+                        .pingBeforeActivateConnection(true) // 在激活连接前发送 ping
+                        .build())
+                .build();
+
+        // 创建并配置 Lettuce 连接工厂
+        return new LettuceConnectionFactory(sentinelConfig, clientConfig);
+
+    }
+
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<Object, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return tonumber(current);\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return tonumber(current);";
+    }
+}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff