Selaa lähdekoodia

项目初始化
后台商品模块

hr~ 2 päivää sitten
commit
19cb263279
100 muutettua tiedostoa jossa 5103 lisäystä ja 0 poistoa
  1. 7 0
      .claude/settings.local.json
  2. 44 0
      .gitignore
  3. 179 0
      pom.xml
  4. 38 0
      product-client/pom.xml
  5. 22 0
      product-client/src/main/java/com/poyee/api/checklist/CheckListFeignClient.java
  6. 17 0
      product-client/src/main/java/com/poyee/api/dict/SysDictFeignClient.java
  7. 8 0
      product-client/src/main/java/com/poyee/api/order/OrderFeignClient.java
  8. 109 0
      product-common/pom.xml
  9. 13 0
      product-common/src/main/java/com/poyee/annotation/NoLogin.java
  10. 32 0
      product-common/src/main/java/com/poyee/annotation/UserLoginToken.java
  11. 168 0
      product-common/src/main/java/com/poyee/aspect/UserLoginTokenAspect.java
  12. 33 0
      product-common/src/main/java/com/poyee/aspect/weblog/RequestNoContext.java
  13. 67 0
      product-common/src/main/java/com/poyee/aspect/weblog/WebLog.java
  14. 113 0
      product-common/src/main/java/com/poyee/aspect/weblog/WebLogAspect.java
  15. 28 0
      product-common/src/main/java/com/poyee/config/FeignConfig.java
  16. 39 0
      product-common/src/main/java/com/poyee/config/FeignDateConfig.java
  17. 45 0
      product-common/src/main/java/com/poyee/config/I18nConfig.java
  18. 25 0
      product-common/src/main/java/com/poyee/config/MybatisPlusConfig.java
  19. 138 0
      product-common/src/main/java/com/poyee/config/SwaggerConfig.java
  20. 57 0
      product-common/src/main/java/com/poyee/config/TencentCosConfig.java
  21. 25 0
      product-common/src/main/java/com/poyee/constant/ProductConstant.java
  22. 81 0
      product-common/src/main/java/com/poyee/domain/CardAttribute.java
  23. 94 0
      product-common/src/main/java/com/poyee/domain/ProductFastSale.java
  24. 148 0
      product-common/src/main/java/com/poyee/domain/ProductInfo.java
  25. 131 0
      product-common/src/main/java/com/poyee/domain/ProductItem.java
  26. 86 0
      product-common/src/main/java/com/poyee/domain/ProductMedia.java
  27. 76 0
      product-common/src/main/java/com/poyee/domain/ProductPermission.java
  28. 68 0
      product-common/src/main/java/com/poyee/enums/ExceptionEnum.java
  29. 37 0
      product-common/src/main/java/com/poyee/enums/ProductTypeEnum.java
  30. 22 0
      product-common/src/main/java/com/poyee/enums/ReviewStatusEnum.java
  31. 28 0
      product-common/src/main/java/com/poyee/enums/SaleTypeEnum.java
  32. 17 0
      product-common/src/main/java/com/poyee/enums/StatusEnum.java
  33. 34 0
      product-common/src/main/java/com/poyee/exception/BusinessException.java
  34. 61 0
      product-common/src/main/java/com/poyee/exception/GlobalExceptionHandler.java
  35. 14 0
      product-common/src/main/java/com/poyee/mapstruct/ProductInfoMapstruct.java
  36. 16 0
      product-common/src/main/java/com/poyee/mapstruct/ProductItemMapstruct.java
  37. 27 0
      product-common/src/main/java/com/poyee/param/ProductListBO.java
  38. 11 0
      product-common/src/main/java/com/poyee/param/UserInfo.java
  39. 16 0
      product-common/src/main/java/com/poyee/req/AccessTokenRequest.java
  40. 39 0
      product-common/src/main/java/com/poyee/req/FastSaleReq.java
  41. 30 0
      product-common/src/main/java/com/poyee/req/IdleAttributeReq.java
  42. 22 0
      product-common/src/main/java/com/poyee/req/OrderServiceProductItemSearchReq.java
  43. 65 0
      product-common/src/main/java/com/poyee/req/ProductInfoAddReq.java
  44. 38 0
      product-common/src/main/java/com/poyee/req/ProductInfoEditReq.java
  45. 35 0
      product-common/src/main/java/com/poyee/req/ProductItemAddReq.java
  46. 35 0
      product-common/src/main/java/com/poyee/req/ProductItemEditReq.java
  47. 33 0
      product-common/src/main/java/com/poyee/req/ProductItemSearchReq.java
  48. 24 0
      product-common/src/main/java/com/poyee/req/ProductReviewReq.java
  49. 22 0
      product-common/src/main/java/com/poyee/req/ProductSearchReq.java
  50. 50 0
      product-common/src/main/java/com/poyee/req/client/checklist/BaseInfoReq.java
  51. 18 0
      product-common/src/main/java/com/poyee/req/page/Page.java
  52. 20 0
      product-common/src/main/java/com/poyee/res/IdleAttributeRes.java
  53. 24 0
      product-common/src/main/java/com/poyee/res/OrderServiceProductItemSearchRes.java
  54. 40 0
      product-common/src/main/java/com/poyee/res/ProductDetailRes.java
  55. 26 0
      product-common/src/main/java/com/poyee/res/ProductFastSaleRes.java
  56. 28 0
      product-common/src/main/java/com/poyee/res/ProductItemDetail.java
  57. 47 0
      product-common/src/main/java/com/poyee/res/ProductItemListRes.java
  58. 40 0
      product-common/src/main/java/com/poyee/res/ProductListRes.java
  59. 20 0
      product-common/src/main/java/com/poyee/res/ProductTypeRes.java
  60. 57 0
      product-common/src/main/java/com/poyee/res/Result.java
  61. 34 0
      product-common/src/main/java/com/poyee/res/UserInfo.java
  62. 85 0
      product-common/src/main/java/com/poyee/res/client/checklist/BaseInfoRes.java
  63. 87 0
      product-common/src/main/java/com/poyee/res/client/dict/SysDictDataRes.java
  64. 32 0
      product-common/src/main/java/com/poyee/threads/ThreadPoolExecutor.java
  65. 44 0
      product-common/src/main/java/com/poyee/utils/ApiUtils.java
  66. 99 0
      product-common/src/main/java/com/poyee/utils/I18nUtil.java
  67. 25 0
      product-common/src/main/java/com/poyee/utils/JwtUtils.java
  68. 62 0
      product-common/src/main/java/com/poyee/utils/ServletUtils.java
  69. 366 0
      product-common/src/main/java/com/poyee/utils/TencentCosUtil.java
  70. 146 0
      product-web/pom.xml
  71. 17 0
      product-web/src/main/java/com/poyee/ProductServiceApplication.java
  72. 34 0
      product-web/src/main/java/com/poyee/controller/ImageUploadController.java
  73. 104 0
      product-web/src/main/java/com/poyee/controller/ProductInfoController.java
  74. 59 0
      product-web/src/main/java/com/poyee/controller/ProductItemController.java
  75. 12 0
      product-web/src/main/java/com/poyee/facade/IOssUploadFacade.java
  76. 85 0
      product-web/src/main/java/com/poyee/facade/IProductInfoFacade.java
  77. 56 0
      product-web/src/main/java/com/poyee/facade/IProductItemFacade.java
  78. 18 0
      product-web/src/main/java/com/poyee/facade/impl/OssUploadFacade.java
  79. 353 0
      product-web/src/main/java/com/poyee/facade/impl/ProductInfoFacade.java
  80. 145 0
      product-web/src/main/java/com/poyee/facade/impl/ProductItemFacade.java
  81. 7 0
      product-web/src/main/java/com/poyee/mapper/CardAttributeMapper.java
  82. 16 0
      product-web/src/main/java/com/poyee/mapper/ProductFastSaleMapper.java
  83. 16 0
      product-web/src/main/java/com/poyee/mapper/ProductInfoMapper.java
  84. 16 0
      product-web/src/main/java/com/poyee/mapper/ProductItemMapper.java
  85. 16 0
      product-web/src/main/java/com/poyee/mapper/ProductMediaMapper.java
  86. 16 0
      product-web/src/main/java/com/poyee/mapper/ProductPermissionMapper.java
  87. 15 0
      product-web/src/main/java/com/poyee/service/CardAttributeService.java
  88. 16 0
      product-web/src/main/java/com/poyee/service/ProductFastSaleService.java
  89. 16 0
      product-web/src/main/java/com/poyee/service/ProductInfoService.java
  90. 16 0
      product-web/src/main/java/com/poyee/service/ProductItemService.java
  91. 16 0
      product-web/src/main/java/com/poyee/service/ProductMediaService.java
  92. 16 0
      product-web/src/main/java/com/poyee/service/ProductPermissionService.java
  93. 27 0
      product-web/src/main/java/com/poyee/service/SysDictDataService.java
  94. 12 0
      product-web/src/main/java/com/poyee/service/impl/CardAttributeServiceImpl.java
  95. 20 0
      product-web/src/main/java/com/poyee/service/impl/ProductFastSaleServiceImpl.java
  96. 20 0
      product-web/src/main/java/com/poyee/service/impl/ProductInfoServiceImpl.java
  97. 20 0
      product-web/src/main/java/com/poyee/service/impl/ProductItemServiceImpl.java
  98. 20 0
      product-web/src/main/java/com/poyee/service/impl/ProductMediaServiceImpl.java
  99. 20 0
      product-web/src/main/java/com/poyee/service/impl/ProductPermissionServiceImpl.java
  100. 32 0
      product-web/src/main/java/com/poyee/service/impl/SysDictDataServiceImpl.java

+ 7 - 0
.claude/settings.local.json

@@ -0,0 +1,7 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(taskkill /PID 25992 /F)"
+    ]
+  }
+}

+ 44 - 0
.gitignore

@@ -0,0 +1,44 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+.kotlin
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
+/.idea
+/logs
+/.claude
+/.qoder
+/.idea

+ 179 - 0
pom.xml

@@ -0,0 +1,179 @@
+<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.poyee</groupId>
+    <artifactId>b2b-product-service</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>pom</packaging>
+
+    <name>product-service-parent</name>
+    <description>商品服务父模块</description>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.1.17.RELEASE</version>
+    </parent>
+
+    <modules>
+        <module>product-common</module>
+        <module>product-client</module>
+        <module>product-web</module>
+    </modules>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <spring-cloud.version>Greenwich.SR6</spring-cloud.version>
+        <mybatis-plus.version>3.4.3.4</mybatis-plus.version>
+        <knife4j.version>2.0.4</knife4j.version>
+        <swagger.version>2.9.2</swagger.version>
+        <pagehelper.version>1.4.1</pagehelper.version>
+        <redisson.version>3.17.6</redisson.version>
+        <lombok.version>1.18.30</lombok.version>
+        <fastjson.version>1.2.83</fastjson.version>
+        <mapstruct.version>1.4.2.Final</mapstruct.version>
+        <commons-lang3.version>3.12.0</commons-lang3.version>
+        <jwt-version>3.3.0</jwt-version>
+        <mapstruct.version>1.4.2.Final</mapstruct.version>
+        <hutoll-version>5.8.11</hutoll-version>
+        <qcloud.version>5.6.257</qcloud.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>com.qcloud</groupId>
+                <artifactId>cos_api</artifactId>
+                <version>${qcloud.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct-processor</artifactId>
+                <version>${mapstruct.version}</version>
+                <scope>provided</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.pagehelper</groupId>
+                <artifactId>pagehelper-spring-boot-starter</artifactId>
+                <version>${pagehelper.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.mybatis</groupId>
+                        <artifactId>mybatis</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.mybatis</groupId>
+                        <artifactId>mybatis-spring</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>org.hibernate.validator</groupId>
+                <artifactId>hibernate-validator</artifactId>
+            </dependency>
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+            <!-- Spring Cloud -->
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring-cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutoll-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.auth0</groupId>
+                <artifactId>java-jwt</artifactId>
+                <version>${jwt-version}</version>
+            </dependency>
+
+            <!-- 内部模块 -->
+            <dependency>
+                <groupId>com.poyee</groupId>
+                <artifactId>product-common</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.poyee</groupId>
+                <artifactId>product-client</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+
+            <!-- MyBatis-Plus -->
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-boot-starter</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+
+            <!-- Knife4j -->
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId>
+                <artifactId>knife4j-spring-boot-starter</artifactId>
+                <version>${knife4j.version}</version>
+            </dependency>
+
+            <!-- Redisson -->
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson-spring-boot-starter</artifactId>
+                <version>${redisson.version}</version>
+            </dependency>
+
+            <!-- Lombok -->
+            <dependency>
+                <groupId>org.projectlombok</groupId>
+                <artifactId>lombok</artifactId>
+                <version>${lombok.version}</version>
+                <scope>provided</scope>
+            </dependency>
+
+            <!-- FastJSON -->
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>fastjson</artifactId>
+                <version>${fastjson.version}</version>
+            </dependency>
+
+            <!-- Commons Lang3 -->
+            <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-lang3</artifactId>
+                <version>${commons-lang3.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-maven-plugin</artifactId>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+
+</project>

+ 38 - 0
product-client/pom.xml

@@ -0,0 +1,38 @@
+<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.poyee</groupId>
+        <artifactId>b2b-product-service</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>product-client</artifactId>
+    <name>product-client</name>
+    <description>商品服务客户端模块(调用外部服务)</description>
+
+    <dependencies>
+        <!-- 公共模块 -->
+        <dependency>
+            <groupId>com.poyee</groupId>
+            <artifactId>product-common</artifactId>
+        </dependency>
+
+        <!-- Spring Cloud OpenFeign -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 22 - 0
product-client/src/main/java/com/poyee/api/checklist/CheckListFeignClient.java

@@ -0,0 +1,22 @@
+package com.poyee.api.checklist;
+
+
+import com.poyee.config.FeignConfig;
+import com.poyee.config.FeignDateConfig;
+import com.poyee.req.client.checklist.BaseInfoReq;
+import com.poyee.res.Result;
+import com.poyee.res.client.checklist.BaseInfoRes;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.util.List;
+
+@FeignClient(name = "checklist-service", url = "http://localhost:8084", decode404 = true, configuration = {FeignConfig.class, FeignDateConfig.class})
+public interface CheckListFeignClient {
+
+    @PostMapping("/api/i18n/sysBaseInfo/searchBaseInfoBySelect")
+    Result<List<BaseInfoRes>> getBaseInfo(@RequestBody @Validated BaseInfoReq req);
+
+}

+ 17 - 0
product-client/src/main/java/com/poyee/api/dict/SysDictFeignClient.java

@@ -0,0 +1,17 @@
+package com.poyee.api.dict;
+
+import com.poyee.config.FeignConfig;
+import com.poyee.res.Result;
+import com.poyee.res.client.dict.SysDictDataRes;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import java.util.List;
+
+@FeignClient(name = "sys-dict-service", url = "http://localhost:8083", decode404 = true, configuration = FeignConfig.class)
+public interface SysDictFeignClient {
+
+    @GetMapping("/api/dict/data/type/{dictType}")
+    Result<List<SysDictDataRes>> getByDictType(@PathVariable("dictType") String dictType);
+}

+ 8 - 0
product-client/src/main/java/com/poyee/api/order/OrderFeignClient.java

@@ -0,0 +1,8 @@
+package com.poyee.api.order;
+
+import org.springframework.cloud.openfeign.FeignClient;
+
+@FeignClient(name = "order-service", url = "http://localhost:8080", decode404 = true)
+public interface OrderFeignClient {
+
+}

+ 109 - 0
product-common/pom.xml

@@ -0,0 +1,109 @@
+<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.poyee</groupId>
+        <artifactId>b2b-product-service</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>product-common</artifactId>
+    <name>product-common</name>
+    <description>商品服务公共模块</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.qcloud</groupId>
+            <artifactId>cos_api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+        </dependency>
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>32.1.2-jre</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+        <!-- MapStruct -->
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-processor</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.mapstruct</groupId>
+                            <artifactId>mapstruct-processor</artifactId>
+                            <version>${mapstruct.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>${lombok.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok-mapstruct-binding</artifactId>
+                            <version>0.2.0</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 13 - 0
product-common/src/main/java/com/poyee/annotation/NoLogin.java

@@ -0,0 +1,13 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 无需登录注解
+ * 标注在类或方法上表示不需要登录即可访问
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface NoLogin {
+}

+ 32 - 0
product-common/src/main/java/com/poyee/annotation/UserLoginToken.java

@@ -0,0 +1,32 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 用户登录Token注解
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface UserLoginToken {
+
+    /**
+     * 是否需要登录,默认true
+     */
+    boolean required() default true;
+
+    /**
+     * 是否需要实名认证
+     */
+    boolean faceVerify() default false;
+
+    /**
+     * 放行的角色
+     */
+    String[] roles() default {};
+
+    /**
+     * 是否通过审计
+     */
+    boolean pass_aud() default false;
+}

+ 168 - 0
product-common/src/main/java/com/poyee/aspect/UserLoginTokenAspect.java

@@ -0,0 +1,168 @@
+package com.poyee.aspect;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.poyee.annotation.NoLogin;
+import com.poyee.annotation.UserLoginToken;
+import com.poyee.exception.BusinessException;
+import com.poyee.utils.JwtUtils;
+import com.poyee.utils.ServletUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+
+/**
+ * 用户登录Token切面
+ */
+@Slf4j
+@Aspect
+@Component
+public class UserLoginTokenAspect {
+
+    /**
+     * 配置织入点 - 拦截所有 Controller 方法
+     */
+    @Pointcut("execution(public * com.poyee.controller.*.*(..))")
+    public void controllerPointCut() {
+    }
+
+    /**
+     * 前置通知
+     */
+    @Before("controllerPointCut()")
+    public void doBefore(JoinPoint joinPoint) {
+        HttpServletRequest request = ServletUtils.getRequest();
+        if (request == null) {
+            return;
+        }
+
+        // 获取方法上的注解
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+        Class<?> declaringClass = method.getDeclaringClass();
+
+        // 1. 检查是否有 @NoLogin 注解(方法级别优先)
+        NoLogin noLogin = method.getAnnotation(NoLogin.class);
+        if (noLogin == null) {
+            noLogin = declaringClass.getAnnotation(NoLogin.class);
+        }
+        // 如果有 @NoLogin 注解,直接放行
+        if (noLogin != null) {
+            return;
+        }
+
+        // 2. 没有 @NoLogin 注解,一律进行 Token 检查
+        String userInfoStr = request.getHeader("X-USER-BASE64");
+        if (StrUtil.isBlank(userInfoStr)) {
+            userInfoStr = request.getHeader("x-user-base64");
+        }
+
+        log.info("X-USER-BASE64 >>> {}", userInfoStr);
+        String authorization = request.getHeader("Authorization");
+        log.info("Authorization >>> {}", authorization);
+
+        // 3. 检查是否有 @UserLoginToken 注解(用于角色控制)
+        UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
+        if (userLoginToken == null) {
+            userLoginToken = declaringClass.getAnnotation(UserLoginToken.class);
+        }
+
+        // 执行登录检查
+        checkUserLoginToken(request, userInfoStr, authorization, userLoginToken);
+    }
+
+    /**
+     * 检查用户登录Token
+     */
+    private void checkUserLoginToken(HttpServletRequest request, String userInfoStr, 
+                                     String authorization, UserLoginToken userLoginToken) {
+        boolean isAuthorization = false;
+        String token = userInfoStr;
+
+        // 1. 尝试从 X-USER-BASE64 获取
+        if (StrUtil.isBlank(token)) {
+            // 2. 尝试从 Authorization 获取
+            if (StrUtil.isNotBlank(authorization)) {
+                if (authorization.startsWith("Bearer ")) {
+                    isAuthorization = true;
+                    token = authorization.substring(7);
+                } else {
+                    token = authorization;
+                }
+            }
+        }
+
+        if (StrUtil.isBlank(token)) {
+            throw new BusinessException(401, "未登录或登录已过期");
+        }
+
+        // 保存原始token到session
+        ServletUtils.getSession().setAttribute("x-user-base64", token);
+
+        // 解析用户信息
+        JSONObject userInfo = parseUserInfo(token, isAuthorization);
+        if (userInfo == null) {
+            throw new BusinessException(402, "登录信息无效,请重新登录");
+        }
+
+        // 保存用户信息到request
+        request.setAttribute("currentUser", userInfo);
+        ServletUtils.getSession().setAttribute("userInfo", JSON.toJSONString(userInfo));
+
+        // 检查角色权限(如果有 @UserLoginToken 注解且配置了角色)
+        if (userLoginToken != null && userLoginToken.roles().length > 0) {
+            boolean hasRole = checkRole(userInfo, userLoginToken.roles());
+            if (!hasRole) {
+                throw new BusinessException(403, "无权访问");
+            }
+        }
+
+        log.info("用户登录验证通过: {}", userInfo.getString("username"));
+    }
+
+    /**
+     * 解析用户信息
+     */
+    private JSONObject parseUserInfo(String token, boolean isAuthorization) {
+        try {
+            if (isAuthorization) {
+                // JWT Token 解析
+                return JwtUtils.getTokenUserInfo(token);
+            } else {
+                // Base64 编码的用户信息
+                String jsonStr = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
+                return JSON.parseObject(jsonStr);
+            }
+        } catch (Exception e) {
+            log.error("解析用户信息失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 检查用户角色
+     */
+    private boolean checkRole(JSONObject userInfo, String[] requiredRoles) {
+        if (userInfo == null) {
+            return false;
+        }
+        String role = userInfo.getString("role");
+        if (role == null) {
+            return false;
+        }
+        return Arrays.asList(requiredRoles).contains(role);
+    }
+}

+ 33 - 0
product-common/src/main/java/com/poyee/aspect/weblog/RequestNoContext.java

@@ -0,0 +1,33 @@
+package com.poyee.aspect.weblog;
+
+public class RequestNoContext {
+    /**
+     * 接口调用进程代码
+     */
+    public static ThreadLocal<Long> currentThreadId = new ThreadLocal<>();
+
+    /**
+     * 调用接口路径
+     */
+    public static ThreadLocal<String> apiUrl = new ThreadLocal<>();
+
+    /**
+     * 请求开始时间戳
+     */
+    public static ThreadLocal<Long> callTime = new ThreadLocal<>();
+
+    /**
+     * 请求参数
+     */
+    public static ThreadLocal<String> requestParam = new ThreadLocal<>();
+
+    /**
+     * 清除ThreadLocal变量
+     */
+    public static void remove() {
+        currentThreadId.remove();
+        apiUrl.remove();
+        callTime.remove();
+        requestParam.remove();
+    }
+}

+ 67 - 0
product-common/src/main/java/com/poyee/aspect/weblog/WebLog.java

@@ -0,0 +1,67 @@
+package com.poyee.aspect.weblog;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * Controller层的日志封装类
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class WebLog {
+    /**
+     * 操作描述
+     */
+    private String description;
+
+    /**
+     * 操作用户
+     */
+    private String username;
+
+    /**
+     * 操作时间
+     */
+    private Long startTime;
+
+    /**
+     * 消耗时间
+     */
+    private Integer spendTime;
+
+    /**
+     * 根路径
+     */
+    private String basePath;
+
+    /**
+     * URI
+     */
+    private String uri;
+
+    /**
+     * URL
+     */
+    private String url;
+
+    /**
+     * 请求类型
+     */
+    private String method;
+
+    /**
+     * IP地址
+     */
+    private String ip;
+
+    /**
+     * 请求参数
+     */
+    private Object parameter;
+
+    /**
+     * 返回结果
+     */
+    private Object result;
+
+}

+ 113 - 0
product-common/src/main/java/com/poyee/aspect/weblog/WebLogAspect.java

@@ -0,0 +1,113 @@
+package com.poyee.aspect.weblog;
+
+import cn.hutool.json.JSONUtil;
+import com.poyee.utils.ServletUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.*;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * 统一日志处理切面
+ */
+@Aspect
+@Component
+@Order(1)
+public class WebLogAspect {
+    private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
+
+    @Pointcut("execution(public * com.poyee.controller..*.*(..))")
+    public void webLog() {
+    }
+
+    @Before("webLog()")
+    public void doBefore(JoinPoint joinPoint) {
+        HttpServletRequest request = ServletUtils.getRequest();
+        long currentTimeMillis = System.currentTimeMillis();
+        String requestURI = Objects.requireNonNull(request).getRequestURI();
+        RequestNoContext.callTime.set(currentTimeMillis);
+        RequestNoContext.apiUrl.set(requestURI);
+        RequestNoContext.currentThreadId.set(currentTimeMillis);
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+        String argsJson = JSONUtil.toJsonStr(getParameter(method, joinPoint.getArgs()));
+        RequestNoContext.requestParam.set(argsJson);
+        String log = "\n===============[" + RequestNoContext.currentThreadId.get() + "]请求内容开始===============" +
+                "\n    请求地址:" + requestURI +
+                "\n    请求方式:" + request.getMethod() +
+                "\n    客户端 IP:" + request.getRemoteAddr() +
+                "\n    接口参数:" + argsJson +
+                "\n===============[" + RequestNoContext.currentThreadId.get() + "]请求内容结束===============";
+        logger.info(log);
+    }
+
+    @AfterReturning(value = "webLog()", returning = "returns")
+    public void afterReturning(Object returns) {
+        long costTime = System.currentTimeMillis() - RequestNoContext.callTime.get();
+        logger.info("\n--------------[{}]返回内容开始----------------\n    接口地址:{}\n    返回内容:{}\n    请求耗时:{}ms\n--------------[{}]返回内容结束----------------", RequestNoContext.currentThreadId.get(), RequestNoContext.apiUrl.get(), JSONUtil.toJsonStr(returns), costTime, RequestNoContext.currentThreadId.get());
+        RequestNoContext.remove();
+    }
+
+
+    @AfterThrowing(value = "webLog()", throwing = "e")
+    public void throwing(Throwable e) {
+        HttpServletRequest request = ServletUtils.getRequest();
+        long costTime = System.currentTimeMillis() - RequestNoContext.callTime.get();
+        logger.error("\n--------------[{}]返回内容开始----------------\n    接口地址:{}\n    请求令牌:{}\n    接口参数:{}\n    请求失败:{}\n    请求耗时:{}ms\n--------------[{}]返回内容结束----------------", RequestNoContext.currentThreadId.get(), RequestNoContext.apiUrl.get(), Objects.requireNonNull(request).getHeader("Authorization"), RequestNoContext.requestParam.get(), e.getMessage(), costTime, RequestNoContext.currentThreadId.get());
+        RequestNoContext.remove();
+    }
+
+    /**
+     * 根据方法和传入的参数获取请求参数
+     */
+    private Object getParameter(Method method, Object[] args) {
+        Parameter[] parameters = method.getParameters();
+        List<Object> argList = IntStream.range(0, parameters.length)
+                .mapToObj(i -> {
+                    Parameter parameter = parameters[i];
+                    RequestBody requestBody = parameter.getAnnotation(RequestBody.class);
+                    if (requestBody != null) {
+                        return args[i];
+                    }
+
+                    RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
+                    if (requestParam != null) {
+                        Map<String, Object> map = new HashMap<>();
+                        String key = StringUtils.hasText(requestParam.value())
+                                ? requestParam.value()
+                                : parameter.getName();
+                        map.put(key, args[i]);
+                        return map;
+                    }
+                    return null;
+                })
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        if (argList.isEmpty()) {
+            return null;
+        } else if (argList.size() == 1) {
+            return argList.get(0);
+        } else {
+            return argList;
+        }
+    }
+}

+ 28 - 0
product-common/src/main/java/com/poyee/config/FeignConfig.java

@@ -0,0 +1,28 @@
+package com.poyee.config;
+
+import cn.hutool.core.util.StrUtil;
+import feign.RequestInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Configuration
+public class FeignConfig {
+
+    @Bean
+    public RequestInterceptor requestInterceptor() {
+        return template -> {
+            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+            if (attributes != null) {
+                HttpServletRequest request = attributes.getRequest();
+                String userBase64 = request.getHeader("X-USER-BASE64");
+                if (StrUtil.isNotBlank(userBase64)) {
+                    template.header("X-USER-BASE64", userBase64);
+                }
+            }
+        };
+    }
+}

+ 39 - 0
product-common/src/main/java/com/poyee/config/FeignDateConfig.java

@@ -0,0 +1,39 @@
+package com.poyee.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import feign.codec.Decoder;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
+import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
+import org.springframework.cloud.openfeign.support.SpringDecoder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+public class FeignDateConfig {
+    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
+
+    @Bean
+    public Decoder feignDecoder() {
+        ObjectMapper objectMapper = new ObjectMapper();
+        JavaTimeModule javaTimeModule = new JavaTimeModule();
+        // 配置 LocalDateTime 的反序列化
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
+        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
+        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
+        objectMapper.registerModule(javaTimeModule);
+        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        ObjectFactory<HttpMessageConverters> messageConverters = () -> {
+            MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper);
+            return new HttpMessageConverters(converter);
+        };
+
+        return new ResponseEntityDecoder(new SpringDecoder(messageConverters));
+    }
+}

+ 45 - 0
product-common/src/main/java/com/poyee/config/I18nConfig.java

@@ -0,0 +1,45 @@
+package com.poyee.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
+import org.springframework.web.servlet.i18n.SessionLocaleResolver;
+
+import java.util.Locale;
+
+/**
+ * 国际化配置
+ */
+@Configuration
+public class I18nConfig implements WebMvcConfigurer {
+
+    /**
+     * 默认解析器,默认语言为中文
+     */
+    @Bean
+    public LocaleResolver localeResolver() {
+        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
+        // 默认语言:中文
+        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
+        return localeResolver;
+    }
+
+    /**
+     * 语言切换拦截器
+     * 通过 lang 参数切换语言,如 ?lang=en
+     */
+    @Bean
+    public LocaleChangeInterceptor localeChangeInterceptor() {
+        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
+        interceptor.setParamName("lang");
+        return interceptor;
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(localeChangeInterceptor());
+    }
+}

+ 25 - 0
product-common/src/main/java/com/poyee/config/MybatisPlusConfig.java

@@ -0,0 +1,25 @@
+package com.poyee.config;
+
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * MyBatis-Plus 配置
+ */
+@Configuration
+public class MybatisPlusConfig {
+
+    /**
+     * 配置 MyBatis-Plus 拦截器
+     * 包含乐观锁插件
+     */
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        // 乐观锁插件
+        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
+        return interceptor;
+    }
+}

+ 138 - 0
product-common/src/main/java/com/poyee/config/SwaggerConfig.java

@@ -0,0 +1,138 @@
+package com.poyee.config;
+
+import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
+import springfox.documentation.RequestHandler;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.Contact;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+/**
+ * Knife4j 接口文档配置
+ */
+@Configuration
+@EnableSwagger2
+@EnableKnife4j
+@Import(BeanValidatorPluginsConfiguration.class)
+public class SwaggerConfig {
+
+    private static String splitor = ";";
+
+    /** 是否开启swagger */
+    @Value("${swagger.enabled:false}")
+    private boolean enabled;
+
+    /**
+     * 创建API
+     */
+    @Bean
+    public Docket createRestApi() {
+        return new Docket(DocumentationType.SWAGGER_2)
+                // 是否启用Swagger
+                .enable(enabled)
+                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+                .apiInfo(apiInfo())
+                // 设置哪些接口暴露给Swagger展示
+                .select()
+                // 扫描所有有注解的api,用这种方式更灵活
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                // 扫描指定包中的swagger注解
+                .apis(RequestHandlerSelectors.basePackage("com.poyee"))
+                // 扫描所有
+                .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build();
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    private ApiInfo apiInfo() {
+        // 用ApiInfoBuilder进行定制
+        return new ApiInfoBuilder()
+                // 设置标题
+                .title("商品服务接口文档")
+                // 描述
+                .description("商品服务 API 接口文档")
+                // 作者信息
+                .contact(new Contact("poyee", null, null))
+                // 版本
+                .version("1.0")
+                .build();
+    }
+
+    /**
+     * 创建Partner API(分组示例)
+     */
+    @Bean
+    public Docket createPartnerRestApi() {
+        return new Docket(DocumentationType.SWAGGER_2)
+                .groupName("1.0")
+                // 是否启用Swagger
+                .enable(enabled)
+                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+                .apiInfo(partnerApiInfo())
+                // 设置哪些接口暴露给Swagger展示
+                .select()
+                // 扫描所有有注解的api,用这种方式更灵活
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                // 扫描指定包中的swagger注解
+                .apis(basePackage("com.poyee.controller"))
+                // 扫描所有
+                .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build();
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    private ApiInfo partnerApiInfo() {
+        // 用ApiInfoBuilder进行定制
+        return new ApiInfoBuilder()
+                // 设置标题
+                .title("商品服务接口文档")
+                // 描述
+                .description("商品服务 Partner API 接口文档")
+                // 作者信息
+                .contact(new Contact("poyee", null, null))
+                // 版本
+                .version("partner")
+                .build();
+    }
+
+    public static Predicate<RequestHandler> basePackage(final String basePackage) {
+        return input -> declaringClass(input).transform(handlerPackage(basePackage))
+                .or(true);
+    }
+
+    private static Function<Class<?>, Boolean> handlerPackage(final String basePackage) {
+        return input -> {
+            // 循环判断匹配
+            for (String strPackage : basePackage.split(splitor)) {
+                boolean isMatch = input.getPackage().getName().startsWith(strPackage);
+                if (isMatch) {
+                    return true;
+                }
+            }
+            return false;
+        };
+    }
+
+    private static Optional<? extends Class<?>> declaringClass(RequestHandler input) {
+        return Optional.fromNullable(input.declaringClass());
+    }
+}

+ 57 - 0
product-common/src/main/java/com/poyee/config/TencentCosConfig.java

@@ -0,0 +1,57 @@
+package com.poyee.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 腾讯云OSS配置
+ *
+ * @author: zheng
+ * @date: 2026/01/26
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "tencent.cos")
+public class TencentCosConfig {
+
+    /**
+     * 腾讯云SecretId
+     */
+    private String secretId;
+
+    /**
+     * 腾讯云SecretKey
+     */
+    private String secretKey;
+
+    /**
+     * 所属地域
+     */
+    private String region;
+
+    /**
+     * 存储桶名称
+     */
+    private String bucketName;
+
+    /**
+     * 访问域名
+     */
+    private String domain;
+
+    /**
+     * 文件路径前缀
+     */
+    private String prefix;
+
+    /**
+     * 文件大小限制(默认10MB)
+     */
+    private Long maxFileSize = 10 * 1024 * 1024L;
+
+    /**
+     * 允许上传的文件类型
+     */
+    private String[] allowFileTypes = {"jpg", "jpeg", "png", "gif", "bmp", "webp"};
+}

+ 25 - 0
product-common/src/main/java/com/poyee/constant/ProductConstant.java

@@ -0,0 +1,25 @@
+package com.poyee.constant;
+
+public class ProductConstant {
+
+
+    public static final String DICT_KEY_LABEL = "LABEL";
+
+    public static final String DICT_KEY_PRODUCT_STATUS = "PRODUCT_STATUS";
+
+    public static final String DICT_KEY_PRODUCT_TYPE = "PRODUCT_TYPE";
+
+    public static final String DICT_KEY_WHETHER = "WHETHER";
+
+    public static final String DICT_KEY_PERMISSION = "PERMISSION";
+
+    public static final String DICT_KEY_ITEM_TYPE = "ITEM_TYPE";
+
+    public static final String DICT_KEY_FROM_SOURCE = "FROM_SOURCE";
+
+    public static final String DICT_KEY_ITEM_SPEC = "ITEM_SPEC";
+
+    public static final String DICT_KEY_ITEM_STATUS = "ITEM_STATUS";
+
+    public static final String DICT_KEY_SALES_TYPE = "SALE_TYPE";
+}

+ 81 - 0
product-common/src/main/java/com/poyee/domain/CardAttribute.java

@@ -0,0 +1,81 @@
+package com.poyee.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@TableName("card_attribute")
+@NoArgsConstructor
+@AllArgsConstructor
+public class CardAttribute {
+    /**
+     * 主键
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    /**
+     * 赛季年份
+     */
+    private String year;
+    /**
+     * 运动类型
+     */
+    private String sport;
+    /**
+     * 发行厂商
+     */
+    private String manufacturer;
+    /**
+     * 产品系列
+     */
+    private String sets;
+    /**
+     * 系列版本
+     */
+    private String setsVersion;
+    /**
+     * 关联商品
+     */
+    private String relationSku;
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+    /**
+     * 修改时间
+     */
+    private LocalDateTime updateTime;
+    /**
+     * 逻辑删除 (0 有效 1无效)
+     */
+    private Integer deleteFlag;
+    /**
+     * 乐观锁版本
+     */
+    private Integer version;
+
+    @Override
+    public String toString() {
+        return "CardAttribute{" +
+                "id=" + id +
+                ", year='" + year + '\'' +
+                ", sport='" + sport + '\'' +
+                ", manufacturer='" + manufacturer + '\'' +
+                ", sets='" + sets + '\'' +
+                ", setsVersion='" + setsVersion + '\'' +
+                ", relationSku='" + relationSku + '\'' +
+                ", createTime=" + createTime +
+                ", updateTime=" + updateTime +
+                ", deleteFlag=" + deleteFlag +
+                ", version=" + version +
+                '}';
+    }
+}

+ 94 - 0
product-common/src/main/java/com/poyee/domain/ProductFastSale.java

@@ -0,0 +1,94 @@
+package com.poyee.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ *
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+@Data
+@Builder
+@TableName("product_fast_sale")
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductFastSale implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(type = IdType.INPUT)
+    private Long id;
+
+    /**
+     * sku
+     */
+    private String sku;
+
+    /**
+     * 是否允许回收(0不允许 1允许)
+     */
+    private Integer recycleFlag;
+    private Integer recycleInventory;
+
+    /**
+     * 销售类型(1 现货 2预售 )
+     */
+    private Integer saleType;
+
+    /**
+     * 官方发售时间
+     */
+    private LocalDateTime saleTime;
+
+    /**
+     * 创建人
+     */
+    private Long createBy;
+    private LocalDateTime createTime;
+    private Long updateBy;
+    private LocalDateTime updateTime;
+
+    /**
+     * 逻辑删除
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 乐观锁
+     */
+    private Integer version;
+
+
+    @Override
+    public String toString() {
+        return "ProductFastSale{" +
+                ", id = " + id +
+                ", sku = " + sku +
+                ", recycleFlag = " + recycleFlag +
+                ", recycleInventory = " + recycleInventory +
+                ", saleType = " + saleType +
+                ", saleTime = " + saleTime +
+                ", createBy = " + createBy +
+                ", createTime = " + createTime +
+                ", updateBy = " + updateBy +
+                ", updateTime = " + updateTime +
+                ", deleteFlag = " + deleteFlag +
+                ", version = " + version +
+                "}";
+    }
+}

+ 148 - 0
product-common/src/main/java/com/poyee/domain/ProductInfo.java

@@ -0,0 +1,148 @@
+package com.poyee.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+@Data
+@Builder
+@TableName("product_info")
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductInfo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 商品名称
+     */
+    private String productName;
+
+    /**
+     * sku
+     */
+    private String sku;
+
+    /**
+     * 状态
+     */
+    private Integer status;
+
+    /**
+     * 可售库存(冗余)
+     */
+    private Integer inventory;
+
+    /**
+     * 是否闪购产品(0不是 1是)
+     */
+    private Integer fastSaleFlag;
+
+    /**
+     * 是否允许闲置出售(0不允许 1允许)
+     */
+    private Integer idleFlag;
+
+    /**
+     * 商品种类 (1球星卡 2.其他)
+     */
+    private Integer productType;
+
+    /**
+     * 站点
+     */
+    private Long siteId;
+
+    /**
+     * 标签
+     */
+    private String label;
+
+    private String productDesc;
+
+    /**
+     * 基础库商品编码
+     */
+    private String relationCode;
+
+    /**
+     * 逻辑删除
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 乐观锁
+     */
+    private Integer version;
+
+    /**
+     * 创建人
+     */
+    private Long createBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 修改人
+     */
+    private Long updateBy;
+
+    /**
+     * 修改时间
+     */
+    private LocalDateTime updateTime;
+
+
+    private String productProperties;
+
+    private Integer sort;
+
+
+    @Override
+    public String toString() {
+        return "ProductInfo{" +
+        ", id = " + id +
+        ", productName = " + productName +
+        ", sku = " + sku +
+        ", status = " + status +
+        ", inventory = " + inventory +
+        ", fastSaleFlag = " + fastSaleFlag +
+        ", idleFlag = " + idleFlag +
+        ", productType = " + productType +
+        ", siteId = " + siteId +
+        ", label = " + label +
+        ", productDesc = " + productDesc +
+        ", relationCode = " + relationCode +
+        ", deleteFlag = " + deleteFlag +
+        ", version = " + version +
+        ", createBy = " + createBy +
+        ", createTime = " + createTime +
+        ", updateBy = " + updateBy +
+        ", updateTime = " + updateTime +
+        "}";
+    }
+}

+ 131 - 0
product-common/src/main/java/com/poyee/domain/ProductItem.java

@@ -0,0 +1,131 @@
+package com.poyee.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-18
+ */
+@Data
+@Builder
+@TableName("product_item")
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductItem implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    private String relationSku;
+    private String productName;
+
+    /**
+     * 商品类型(1.闪购 2.闲置)
+     */
+    private Integer itemType;
+
+    /**
+     * 来源 1.生态购 2.回收 3.闲置
+     */
+    private Integer fromSource;
+
+    /**
+     * 规格
+     */
+    private Integer itemSpec;
+
+    /**
+     * 上架状态
+     */
+    private Integer status;
+
+    /**
+     * 销售价格
+     */
+    private BigDecimal itemPrice;
+
+    /**
+     * 在售库存=在售库存-冻结库存
+     */
+    private Integer inventory;
+
+    /**
+     * 冻结库存
+     */
+    private Integer frozenInventory;
+
+    /**
+     * 销量
+     */
+    private Integer sales;
+
+    /**
+     * 发布者
+     */
+    private String publisher;
+
+    /**
+     * 创建人
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 修改人
+     */
+    private String updateBy;
+
+    /**
+     * 修改时间
+     */
+    private LocalDateTime updateTime;
+
+    private Integer deleteFlag;
+
+    private Integer version;
+
+
+    @Override
+    public String toString() {
+        return "ProductItem{" +
+        ", id = " + id +
+        ", relationSku = " + relationSku +
+        ", productName = " + productName +
+        ", itemType = " + itemType +
+        ", fromSource = " + fromSource +
+        ", itemSpec = " + itemSpec +
+        ", status = " + status +
+        ", itemPrice = " + itemPrice +
+        ", inventory = " + inventory +
+        ", frozenInventory = " + frozenInventory +
+        ", sales = " + sales +
+        ", publisher = " + publisher +
+        ", createBy = " + createBy +
+        ", createTime = " + createTime +
+        ", updateBy = " + updateBy +
+        ", updateTime = " + updateTime +
+        "}";
+    }
+}

+ 86 - 0
product-common/src/main/java/com/poyee/domain/ProductMedia.java

@@ -0,0 +1,86 @@
+package com.poyee.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ *
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+@Data
+@TableName("product_media")
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductMedia implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * sku
+     */
+    private String relationSku;
+
+    /**
+     * 主图
+     */
+    private String mainImage;
+
+    /**
+     * 轮播图
+     */
+    private String otherImages;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 修改时间
+     */
+    private LocalDateTime updateTime;
+
+
+    /**
+     * 逻辑删除
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 乐观锁
+     */
+    private Integer version;
+
+    @Override
+    public String toString() {
+        return "ProductMedia{" +
+                ", id = " + id +
+                ", relationSku = " + relationSku +
+                ", mainImage = " + mainImage +
+                ", otherImages = " + otherImages +
+                ", createTime = " + createTime +
+                ", updateTime = " + updateTime +
+                ", deleteFlag = " + deleteFlag +
+                ", version = " + version +
+                "}";
+    }
+}

+ 76 - 0
product-common/src/main/java/com/poyee/domain/ProductPermission.java

@@ -0,0 +1,76 @@
+package com.poyee.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ *
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+@Data
+@TableName("product_permission")
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductPermission implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+    @TableId(type = IdType.INPUT)
+    private Long id;
+
+    /**
+     * 关联商品
+     */
+    private String relationSku;
+
+    /**
+     * 可售权限(1全部 2商家 3用户)
+     */
+    private Integer permission;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTme;
+
+    /**
+     * 修改时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 逻辑删除
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 乐观锁
+     */
+    private Integer version;
+
+
+    @Override
+    public String toString() {
+        return "IdlePermission{" +
+                ", id = " + id +
+                ", relationSku = " + relationSku +
+                ", permission = " + permission +
+                ", createTme = " + createTme +
+                ", updateTime = " + updateTime +
+                ", deleteFlag = " + deleteFlag +
+                ", version = " + version +
+                "}";
+    }
+}

+ 68 - 0
product-common/src/main/java/com/poyee/enums/ExceptionEnum.java

@@ -0,0 +1,68 @@
+package com.poyee.enums;
+
+import lombok.Getter;
+
+/**
+ * 异常枚举
+ */
+@Getter
+public enum ExceptionEnum {
+
+    // ========== 通用异常 ==========
+    SUCCESS(200, "操作成功"),
+    ERROR(500, "系统繁忙,请稍后重试"),
+
+    // ========== 业务异常 ==========
+    PARAM_ERROR(400, "参数错误"),
+    DATA_NOT_FOUND(404, "数据不存在"),
+    DATA_ALREADY_EXISTS(409, "数据已存在"),
+    BUSINESS_ERROR(400, "业务异常"),
+
+    // ========== 权限相关 ==========
+    UNAUTHORIZED(401, "未授权,请先登录"),
+    FORBIDDEN(403, "禁止访问"),
+    TOKEN_INVALID(401, "Token 无效"),
+
+    // ========== 商品相关 ==========
+    PRODUCT_NOT_FOUND(1001, "商品不存在"),
+    PRODUCT_IMAGE_NOT_FOUND(1002, "商品图片不存在"),
+    PRODUCT_ATTRIBUTE_NOT_FOUND(1003, "商品属性不存在"),
+    REVIEW_STATUS_INVALID(1004, "审核状态不合法"),
+    REVIEW_STATUS_CANNOT_BE_NULL(1005, "审核状态不能为空"),
+    CAN_ONLY_LIST_DOWN_OR_REJECTED_PRODUCT(1006, "只能上架已下架或被驳回的商品"),
+    CAN_ONLY_DOWN_LISTED_PRODUCT(1007, "只能下架已上架的商品"),
+    CAN_ONLY_REJECT_PENDING_PRODUCT(1008, "只能驳回待审核的商品"),
+    PRODUCT_ALREADY_REJECTED(1009, "商品已被驳回"),
+
+    // ========== 用户相关 ==========
+    USER_NOT_LOGIN(2001, "用户未登录"),
+    USER_NOT_FOUND(2002, "用户不存在"),
+    USER_PASSWORD_ERROR(2003, "用户名或密码错误"),
+    USER_LOCKED(2004, "用户已被锁定"),
+
+    // ========== 订单相关 ==========
+    ORDER_NOT_FOUND(3001, "订单不存在"),
+    ORDER_STATUS_ERROR(3002, "订单状态错误"),
+    ORDER_INVENTORY_NOT_ENOUGH(3003, "库存不足"),
+
+    // ========== 文件相关 ==========
+    FILE_UPLOAD_FAILED(4001, "文件上传失败"),
+    FILE_DOWNLOAD_FAILED(4002, "文件下载失败"),
+    FILE_NOT_FOUND(4003, "文件不存在"),
+    FILE_TYPE_NOT_ALLOWED(4004, "不支持的文件类型"),
+    FILE_SIZE_EXCEED(4005, "文件大小超出限制"),
+
+    // ========== 第三方服务 ==========
+    REMOTE_SERVICE_ERROR(5001, "远程服务调用失败"),
+    FEIGN_CLIENT_ERROR(5002, "Feign 客户端调用失败"),
+
+    ;
+
+    private final int code;
+    private final String message;
+
+    ExceptionEnum(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+}

+ 37 - 0
product-common/src/main/java/com/poyee/enums/ProductTypeEnum.java

@@ -0,0 +1,37 @@
+package com.poyee.enums;
+
+import com.poyee.res.ProductTypeRes;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.List;
+
+public enum ProductTypeEnum {
+
+    TRADING_CARD(1, "球星卡"),
+    OTHER(2, "其他"),
+    ;
+    @Getter
+    final int code;
+    @Getter
+    final String name;
+
+    ProductTypeEnum(int code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    public static List<ProductTypeRes> getAll() {
+        return Arrays.stream(ProductTypeEnum.values())
+                .map(value -> ProductTypeRes.builder().code(value.code).name(value.name).build())
+                .collect(java.util.stream.Collectors.toList());
+    }
+
+
+    public static ProductTypeEnum getByCode(int code) {
+        return Arrays.stream(ProductTypeEnum.values())
+                .filter(value -> value.code == code)
+                .findFirst()
+                .orElse(null);
+    }
+}

+ 22 - 0
product-common/src/main/java/com/poyee/enums/ReviewStatusEnum.java

@@ -0,0 +1,22 @@
+package com.poyee.enums;
+
+import lombok.Getter;
+
+public enum ReviewStatusEnum {
+    PENDING(10, "待审核"),
+    LISTED(20, "已上架"),
+    DOWN(30, "已下架"),
+    REJECTED(40, "审核驳回"),
+    ;
+    @Getter
+    final int code;
+    @Getter
+    final String status;
+
+    ReviewStatusEnum(int code, String status) {
+        this.code = code;
+        this.status = status;
+    }
+
+
+}

+ 28 - 0
product-common/src/main/java/com/poyee/enums/SaleTypeEnum.java

@@ -0,0 +1,28 @@
+package com.poyee.enums;
+
+import lombok.Getter;
+
+import java.util.Arrays;
+
+public enum SaleTypeEnum {
+    SPOT_TRADING(1, "现货"),
+    PRESALE(2, "预售");
+
+    @Getter
+    final int code;
+    @Getter
+    final String name;
+
+    SaleTypeEnum(int code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    public static String getName(int code) {
+        return Arrays.stream(values())
+                .filter(v -> v.code == code)
+                .map(v -> v.name)
+                .findFirst()
+                .orElse(null);
+    }
+}

+ 17 - 0
product-common/src/main/java/com/poyee/enums/StatusEnum.java

@@ -0,0 +1,17 @@
+package com.poyee.enums;
+
+import lombok.Getter;
+
+public enum StatusEnum {
+    TRUE(1, "正常"),
+    FALSE(0, "禁用"),;
+    @Getter
+    final int code;
+    @Getter
+    final String message;
+
+    StatusEnum(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+}

+ 34 - 0
product-common/src/main/java/com/poyee/exception/BusinessException.java

@@ -0,0 +1,34 @@
+package com.poyee.exception;
+
+import lombok.Getter;
+
+/**
+ * 业务异常
+ */
+@Getter
+public class BusinessException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 错误码
+     */
+    private final Integer code;
+
+    /**
+     * 错误消息
+     */
+    private final String message;
+
+    public BusinessException(String message) {
+        super(message);
+        this.code = 500;
+        this.message = message;
+    }
+
+    public BusinessException(Integer code, String message) {
+        super(message);
+        this.code = code;
+        this.message = message;
+    }
+}

+ 61 - 0
product-common/src/main/java/com/poyee/exception/GlobalExceptionHandler.java

@@ -0,0 +1,61 @@
+package com.poyee.exception;
+
+import com.poyee.res.Result;
+import com.poyee.utils.I18nUtil;
+import com.poyee.utils.ServletUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 全局异常处理器
+ */
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    /**
+     * 处理业务异常
+     */
+    @ExceptionHandler(BusinessException.class)
+    public Result<Void> handleBusinessException(BusinessException e) {
+        log.warn("业务异常:{}", I18nUtil.getMessage(e.getMessage()));
+        ServletUtils.getHttpResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        return Result.error(e.getCode(), I18nUtil.getMessage(e.getMessage()));
+    }
+
+    /**
+     * 处理其他异常
+     */
+    @ExceptionHandler(Exception.class)
+    public Result<Void> handleException(Exception e) {
+        log.error("系统异常", e);
+        ServletUtils.getHttpResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        return Result.error(500, "系统繁忙,请稍后重试");
+    }
+
+    /**
+     * 参数错误
+     */
+    @ExceptionHandler(value = MethodArgumentNotValidException.class)
+    public Result methodArgumentNotValidExceptionHandle(MethodArgumentNotValidException ex, HttpServletRequest request) {
+        log.error("Unhandled RequestParamException from URL [" + request.getRequestURI() + "]", ex);
+        List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
+        List<String> collect = allErrors.stream()
+                .map(error -> {
+                    String defaultMessage = error.getDefaultMessage();
+                    String i18nMessage = I18nUtil.getMessage(defaultMessage);
+                    return i18nMessage != null ? i18nMessage : defaultMessage;
+                })
+                .collect(Collectors.toList());
+        ServletUtils.getHttpResponse().setStatus(HttpServletResponse.SC_BAD_REQUEST);
+        return Result.error(HttpServletResponse.SC_BAD_REQUEST, collect.toString());
+    }
+}

+ 14 - 0
product-common/src/main/java/com/poyee/mapstruct/ProductInfoMapstruct.java

@@ -0,0 +1,14 @@
+package com.poyee.mapstruct;
+
+import com.poyee.domain.ProductInfo;
+import com.poyee.param.ProductListBO;
+import com.poyee.res.ProductListRes;
+import org.mapstruct.Mapper;
+
+@Mapper(componentModel = "spring")
+public interface ProductInfoMapstruct {
+
+    ProductListBO productDo2Bo(ProductInfo productInfos);
+
+    ProductListRes productBO2Res(ProductListBO productListBO);
+}

+ 16 - 0
product-common/src/main/java/com/poyee/mapstruct/ProductItemMapstruct.java

@@ -0,0 +1,16 @@
+package com.poyee.mapstruct;
+
+import com.poyee.domain.ProductItem;
+import com.poyee.res.OrderServiceProductItemSearchRes;
+import com.poyee.res.ProductItemListRes;
+import org.mapstruct.Mapper;
+
+import java.util.List;
+
+@Mapper(componentModel = "spring")
+public interface ProductItemMapstruct {
+    ProductItemListRes productItem2ProductItemListRes(ProductItem productItem);
+
+
+    List<OrderServiceProductItemSearchRes> convertRes(List<ProductItem> productItems);
+}

+ 27 - 0
product-common/src/main/java/com/poyee/param/ProductListBO.java

@@ -0,0 +1,27 @@
+package com.poyee.param;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductListBO {
+    private Long id;
+    private String sku;
+    private Integer productType;
+    private String productName;
+    private Integer status;
+    private Integer fastSaleFlag;
+    private Integer recycleInventory;
+    private Integer idleFlag;
+    private String saleTime;
+    private String saleType;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 11 - 0
product-common/src/main/java/com/poyee/param/UserInfo.java

@@ -0,0 +1,11 @@
+package com.poyee.param;
+
+import lombok.Builder;
+import lombok.Data;
+
+
+@Data
+@Builder
+public class UserInfo {
+
+}

+ 16 - 0
product-common/src/main/java/com/poyee/req/AccessTokenRequest.java

@@ -0,0 +1,16 @@
+package com.poyee.req;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@ApiModel("获取token Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class AccessTokenRequest {
+    @ApiModelProperty("文件名")
+    private String filename;
+}

+ 39 - 0
product-common/src/main/java/com/poyee/req/FastSaleReq.java

@@ -0,0 +1,39 @@
+package com.poyee.req;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+@Data
+@Builder
+@ApiModel("编辑闪购Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class FastSaleReq {
+    @ApiModelProperty("是否允许回收")
+    @NotNull(message = "recycle_flag_cannot_be_empty")
+    private Boolean recycleFlag;
+    @ApiModelProperty("可回收库存")
+    private Integer recycleInventory;
+    @ApiModelProperty("官方发售时间")
+    private LocalDateTime saleTime;
+    @ApiModelProperty("用户权限")
+    @NotNull(message = "user_permission_cannot_be_empty")
+    private Integer permission;
+    @NotBlank(message = "sku_can_not_be_empty")
+    private String sku;
+
+    public static void checkParam(FastSaleReq fastSaleReq) {
+        if (fastSaleReq.getRecycleFlag() && Objects.isNull(fastSaleReq.getRecycleInventory())) {
+            throw new RuntimeException("product.recycleInventory.notNull");
+        }
+    }
+}

+ 30 - 0
product-common/src/main/java/com/poyee/req/IdleAttributeReq.java

@@ -0,0 +1,30 @@
+package com.poyee.req;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+@Builder
+@ApiModel("编辑闲置Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class IdleAttributeReq {
+
+    @ApiModelProperty("sku")
+    @NotBlank(message = "sku_can_not_be_empty")
+    private String sku;
+
+    @ApiModelProperty("是否允许闲置")
+    @NotBlank(message = "idle_flag_can_not_be_empty")
+    private Boolean idleFlag;
+
+    @ApiModelProperty("闲置权限 (忽略)")
+    private Integer permission;
+
+}

+ 22 - 0
product-common/src/main/java/com/poyee/req/OrderServiceProductItemSearchReq.java

@@ -0,0 +1,22 @@
+package com.poyee.req;
+
+import com.poyee.req.page.Page;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.*;
+
+import java.util.List;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Builder
+@ApiModel("订单服务商品子列表Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class OrderServiceProductItemSearchReq extends Page {
+    @ApiModelProperty("商品名称")
+    private String productName;
+
+    @ApiModelProperty("商品子列表")
+    private List<Long> itemList;
+}

+ 65 - 0
product-common/src/main/java/com/poyee/req/ProductInfoAddReq.java

@@ -0,0 +1,65 @@
+package com.poyee.req;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Data
+@Builder
+@ApiModel("新增基础库产品")
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductInfoAddReq {
+
+    @ApiModelProperty("商品种类")
+    @NotNull(message = "the_product_category_cannot_be_empty")
+    private Integer productType;
+    @ApiModelProperty("赛季年份")
+    @NotBlank(message = "season_year_cannot_be_empty")
+    private String year;
+    @ApiModelProperty("运动类型")
+    @NotBlank(message = "sports_type_cannot_be_empty")
+    private String sport;
+    @ApiModelProperty("发行厂商")
+    @NotBlank(message = "publisher_cannot_be_empty")
+    private String manufacturer;
+    @ApiModelProperty("产品系列")
+    @NotBlank(message = "product_series_cannot_be_empty")
+    private String sets;
+    @NotBlank(message = "series_version_cannot_be_empty")
+    @ApiModelProperty("系列版本")
+    private String setsVersion;
+    @NotBlank(message = "product_name_cannot_be_empty")
+    @ApiModelProperty("产品名称")
+    private String productName;
+    @NotBlank(message = "default_image_cannot_be_empty")
+    @ApiModelProperty("主图")
+    private String defaultImage;
+    @ApiModelProperty("轮播图")
+    @NotEmpty(message = "other_image_cannot_be_empty")
+    private List<String> otherImages;
+    @ApiModelProperty("产品亮点")
+    @NotBlank(message = "product_description_cannot_be_empty")
+    private String productDesc;
+    @ApiModelProperty("标签")
+    private String labelKey;
+
+    @ApiModelProperty("排序")
+    private Integer sort;
+
+    @ApiModelProperty("商品属性")
+    private String productProperties;
+
+    @ApiModelProperty("基础库商品编码")
+    @NotBlank(message = "relation_code_cannot_be_empty")
+    private String relationCode;
+
+}

+ 38 - 0
product-common/src/main/java/com/poyee/req/ProductInfoEditReq.java

@@ -0,0 +1,38 @@
+package com.poyee.req;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+@Data
+@Builder
+@ApiModel("编辑商品Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductInfoEditReq {
+
+    @ApiModelProperty("产品名称")
+    private String productName;
+    @NotBlank(message = "default_image_cannot_be_empty")
+    @ApiModelProperty("主图")
+    private String defaultImage;
+    @ApiModelProperty("轮播图")
+    @NotEmpty(message = "other_image_cannot_be_empty")
+    private List<String> otherImages;
+    @ApiModelProperty("产品亮点")
+    @NotBlank(message = "product_description_cannot_be_empty")
+    private String productDesc;
+    @ApiModelProperty("标签")
+    private String labelKey;
+
+    @NotBlank(message = "relation_code_cannot_be_empty")
+    @ApiModelProperty("sku")
+    private String sku;
+}

+ 35 - 0
product-common/src/main/java/com/poyee/req/ProductItemAddReq.java

@@ -0,0 +1,35 @@
+package com.poyee.req;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+@Data
+@Builder
+@ApiModel("添加生态购Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductItemAddReq {
+    @ApiModelProperty("关联商品SKU")
+    @NotBlank(message = "relation_code_cannot_be_empty")
+    private String relationSku;
+
+    @NotNull(message = "product_spec_can_not_be_empty")
+    @ApiModelProperty("商品规格")
+    private Integer spec;
+
+    @NotNull(message = "price_can_not_be_empty")
+    @ApiModelProperty("价格")
+    private BigDecimal price;
+
+    @NotNull(message = "inventory_can_not_be_empty")
+    @ApiModelProperty("库存")
+    private Integer inventory;
+}

+ 35 - 0
product-common/src/main/java/com/poyee/req/ProductItemEditReq.java

@@ -0,0 +1,35 @@
+package com.poyee.req;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("编辑子商品数据")
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductItemEditReq {
+
+    @ApiModelProperty("id")
+    @NotNull(message = "id_cannot_be_empty")
+    private Long id;
+
+    @NotNull(message = "product_spec_can_not_be_empty")
+    @ApiModelProperty("商品规格")
+    private Integer spec;
+
+    @NotNull(message = "price_can_not_be_empty")
+    @ApiModelProperty("价格")
+    private BigDecimal price;
+
+    @NotNull(message = "inventory_can_not_be_empty")
+    @ApiModelProperty("库存")
+    private Integer inventory;
+}

+ 33 - 0
product-common/src/main/java/com/poyee/req/ProductItemSearchReq.java

@@ -0,0 +1,33 @@
+package com.poyee.req;
+
+
+import com.poyee.req.page.Page;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.*;
+
+import javax.validation.constraints.NotBlank;
+
+
+@Data
+@Builder
+@ApiModel("子商品列表Request")
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductItemSearchReq  extends Page {
+
+    @ApiModelProperty("商品名称")
+    private String productName;
+
+    @ApiModelProperty("sku")
+    @NotBlank(message = "sku_can_not_be_empty")
+    private String relationSku;
+
+    @ApiModelProperty("状态")
+    private Integer status;
+
+    @ApiModelProperty("商品类型")
+    private Integer itemType;
+
+}

+ 24 - 0
product-common/src/main/java/com/poyee/req/ProductReviewReq.java

@@ -0,0 +1,24 @@
+package com.poyee.req;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+@Builder
+@ApiModel("商品审核Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductReviewReq {
+    @ApiModelProperty("审核sku")
+    @NotBlank(message = "sku_can_not_be_empty")
+    private String sku;
+
+    @ApiModelProperty("审核状态")
+    private Integer status;
+}

+ 22 - 0
product-common/src/main/java/com/poyee/req/ProductSearchReq.java

@@ -0,0 +1,22 @@
+package com.poyee.req;
+
+import com.poyee.req.page.Page;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.*;
+
+import java.io.Serializable;
+
+@Data
+@Builder
+@EqualsAndHashCode(callSuper = true)
+@ApiModel("商品基础库列表搜索Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductSearchReq extends Page implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @ApiModelProperty("商品名称")
+    private String productName;
+    @ApiModelProperty("状态")
+    private Integer status;
+}

+ 50 - 0
product-common/src/main/java/com/poyee/req/client/checklist/BaseInfoReq.java

@@ -0,0 +1,50 @@
+package com.poyee.req.client.checklist;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+@Builder
+@ApiModel("级联查询基础商品Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class BaseInfoReq {
+    @ApiModelProperty("年份(赛季)")
+    private String year;
+
+    @ApiModelProperty("品类")
+    private String firstSport;
+
+    @ApiModelProperty("运动类型")
+    private String sport;
+
+    @ApiModelProperty("厂商(发行商)")
+    private String manufacturer;
+
+    @ApiModelProperty("基础库id")
+    private Long baseInfoId;
+
+    @ApiModelProperty("系列")
+    @TableField(value = "sets", exist = false)
+    private String sets;
+
+    @ApiModelProperty(value = "查询字段:默认year:sport:manufacturer:sets:setVersion[查版本时baseInfoId必填]", example = "year", required = true)
+    @NotBlank(message = "查询字段不能为空")
+    private String gb;
+
+    @ApiModelProperty(value = "其他条件", hidden = true)
+    private String otherWhere;
+
+    @ApiModelProperty("用户ID,国际版必填")
+    private Integer userId;
+
+    @ApiModelProperty("商家id,国际版选填")
+    private Integer merchantId;
+}

+ 18 - 0
product-common/src/main/java/com/poyee/req/page/Page.java

@@ -0,0 +1,18 @@
+package com.poyee.req.page;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@ApiModel("分页数据")
+@NoArgsConstructor
+@AllArgsConstructor
+public class Page {
+    @ApiModelProperty("页数")
+    private int page = 1;
+    @ApiModelProperty("条数")
+    private Integer pageSize = 20;
+}

+ 20 - 0
product-common/src/main/java/com/poyee/res/IdleAttributeRes.java

@@ -0,0 +1,20 @@
+package com.poyee.res;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@ApiModel("闲置详情Response")
+@NoArgsConstructor
+@AllArgsConstructor
+public class IdleAttributeRes {
+    @ApiModelProperty("是否允许闲置出售")
+    private Integer idleFlag;
+    @ApiModelProperty("用户权限")
+    private Integer permission;
+}

+ 24 - 0
product-common/src/main/java/com/poyee/res/OrderServiceProductItemSearchRes.java

@@ -0,0 +1,24 @@
+package com.poyee.res;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("订单服务商品子列表Response")
+public class OrderServiceProductItemSearchRes {
+    @ApiModelProperty("商品类型")
+    private Integer itemType;
+    @ApiModelProperty("商品名称")
+    private String itemName;
+    @ApiModelProperty("商品规格")
+    private Integer spec;
+    @ApiModelProperty("商品id")
+    private Long id;
+}

+ 40 - 0
product-common/src/main/java/com/poyee/res/ProductDetailRes.java

@@ -0,0 +1,40 @@
+package com.poyee.res;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("商品详情Response")
+public class ProductDetailRes {
+    @ApiModelProperty("商品种类")
+    private Integer productType;
+    @ApiModelProperty("赛季年份")
+    private String year;
+    @ApiModelProperty("运动类型")
+    private String sport;
+    @ApiModelProperty("发行厂商")
+    private String manufacturer;
+    @ApiModelProperty("产品系列")
+    private String sets;
+    @ApiModelProperty("系列版本")
+    private String setsVersion;
+    @ApiModelProperty("产品名称")
+    private String productName;
+    @ApiModelProperty("主图")
+    private String defaultImage;
+    @ApiModelProperty("轮播图")
+    private List<String> otherImages;
+    @ApiModelProperty("产品亮点")
+    private String productDesc;
+    @ApiModelProperty("标签")
+    private String labelKey;
+}

+ 26 - 0
product-common/src/main/java/com/poyee/res/ProductFastSaleRes.java

@@ -0,0 +1,26 @@
+package com.poyee.res;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("商品闪购Response")
+public class ProductFastSaleRes {
+    @ApiModelProperty("是否允许回收")
+    private Boolean recycleFlag;
+    @ApiModelProperty("可回收库存")
+    private Integer recycleInventory;
+    @ApiModelProperty("官方发售时间")
+    private LocalDateTime saleTime;
+    @ApiModelProperty("用户权限")
+    private Integer permission;
+}

+ 28 - 0
product-common/src/main/java/com/poyee/res/ProductItemDetail.java

@@ -0,0 +1,28 @@
+package com.poyee.res;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("商品子列表详情")
+public class ProductItemDetail {
+
+    @ApiModelProperty("规格")
+    private Integer spec;
+    @ApiModelProperty("价格")
+    private BigDecimal price;
+    @ApiModelProperty("库存")
+    private Integer inventory;
+
+    @ApiModelProperty("子商品列表Id")
+    private Long id;
+}

+ 47 - 0
product-common/src/main/java/com/poyee/res/ProductItemListRes.java

@@ -0,0 +1,47 @@
+package com.poyee.res;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("子商品列表Response")
+public class ProductItemListRes {
+    @ApiModelProperty("子商品id")
+    private Long id;
+    @ApiModelProperty("商品类型")
+    private Integer itemType;
+    @ApiModelProperty("商品来源")
+    private Integer fromSource;
+    @ApiModelProperty("商品名称")
+    private String productName;
+    @ApiModelProperty("规格")
+    private Integer spec;
+    @ApiModelProperty("状态")
+    private Integer status;
+    @ApiModelProperty("销售价格")
+    private BigDecimal itemPrice;
+    @ApiModelProperty("可售库存")
+    private Integer inventory;
+    @ApiModelProperty("销量")
+    private Integer sales;
+    @ApiModelProperty("发布者")
+    private String publisher;
+    @ApiModelProperty("创建人")
+    private String createBy;
+    @ApiModelProperty("销售时间")
+    private LocalDateTime createTime;
+    @ApiModelProperty("修改人")
+    private String updateBy;
+    @ApiModelProperty("修改时间")
+    private LocalDateTime updateTime;
+}

+ 40 - 0
product-common/src/main/java/com/poyee/res/ProductListRes.java

@@ -0,0 +1,40 @@
+package com.poyee.res;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("基础库商品列表Response")
+public class ProductListRes {
+    @ApiModelProperty("产品id")
+    private Long id;
+    @ApiModelProperty("产品种类")
+    private Integer productType;
+    @ApiModelProperty("产品名称")
+    private String productName;
+    @ApiModelProperty("审核状态")
+    private Integer status;
+    @ApiModelProperty("闪购权限")
+    private Integer fastSaleFlag;
+    @ApiModelProperty("可回收库存")
+    private Integer recycleInventory;
+    @ApiModelProperty("闲置权限")
+    private Integer idleFlag;
+    @ApiModelProperty("官方发售时间")
+    private LocalDateTime saleTime;
+    @ApiModelProperty("发售类型")
+    private Integer saleType;
+    @ApiModelProperty("创建时间")
+    private LocalDateTime createTime;
+    @ApiModelProperty("更新时间")
+    private LocalDateTime updateTime;
+}

+ 20 - 0
product-common/src/main/java/com/poyee/res/ProductTypeRes.java

@@ -0,0 +1,20 @@
+package com.poyee.res;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("商品种类Response")
+public class ProductTypeRes {
+    @ApiModelProperty("商品种类code")
+    private Integer code;
+    @ApiModelProperty("商品种类名称")
+    private String name;
+}

+ 57 - 0
product-common/src/main/java/com/poyee/res/Result.java

@@ -0,0 +1,57 @@
+package com.poyee.res;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class Result<T> implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("状态码")
+    private Integer code;
+    @ApiModelProperty("消息")
+    private String message;
+    @ApiModelProperty("数据")
+    private T data;
+
+    public static <T> Result<T> success(T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage("success");
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> success(T data, String message) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage(message);
+        result.setData(data);
+        return result;
+    }
+
+    public static <T> Result<T> success() {
+        return success(null);
+    }
+
+    public static <T> Result<T> error(String message) {
+        Result<T> result = new Result<>();
+        result.setCode(500);
+        result.setMessage(message);
+        return result;
+    }
+
+    public static <T> Result<T> error(Integer code, String message) {
+        Result<T> result = new Result<>();
+        result.setCode(code);
+        result.setMessage(message);
+        return result;
+    }
+
+    public boolean successFlag() {
+        return code == 0;
+    }
+}

+ 34 - 0
product-common/src/main/java/com/poyee/res/UserInfo.java

@@ -0,0 +1,34 @@
+package com.poyee.res;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 用户信息
+ */
+@Data
+public class UserInfo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    private Long id;
+
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 昵称
+     */
+    private String nickname;
+
+    /**
+     * 角色
+     */
+    private String role;
+}

+ 85 - 0
product-common/src/main/java/com/poyee/res/client/checklist/BaseInfoRes.java

@@ -0,0 +1,85 @@
+package com.poyee.res.client.checklist;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class BaseInfoRes {
+    @ApiModelProperty(value = "id")
+    private Long id;
+
+    @ApiModelProperty("基础库id")
+    private Long baseInfoId;
+
+    @ApiModelProperty("code")
+    private String code;
+
+    @ApiModelProperty("年份(赛季)")
+    private String year;
+
+    @ApiModelProperty("品类")
+    private String firstSport;
+
+    @ApiModelProperty("运动类型")
+    private String sport;
+
+    @ApiModelProperty("厂商(发行商)")
+    private String manufacturer;
+
+    @ApiModelProperty("系列")
+    private String sets;
+
+    @ApiModelProperty("联赛(子分类)")
+    private String league;
+
+    @ApiModelProperty("版本")
+    private String setVersion;
+
+    @ApiModelProperty("显示名称")
+    private String displayName;
+
+    @ApiModelProperty(value = "显示名称[国际版]", hidden = true)
+    private String displayNameTranslations;
+
+    @ApiModelProperty("显示名称[国际版]")
+    private String i18nDisplayName;
+
+    @ApiModelProperty("产品图片")
+    private String productImages;
+
+    @ApiModelProperty("发行时间")
+    private LocalDateTime issuingTime;
+
+    @ApiModelProperty("配置")
+    private String baseConfig;
+
+    @ApiModelProperty("预售时间")
+    private LocalDateTime presaleTime;
+
+    @ApiModelProperty("轮播图")
+    private String carouseMap;
+
+    @ApiModelProperty(value = "卡密数量", hidden = true)
+    private Long num;
+
+    @ApiModelProperty(value = "最新模板数据下载地址")
+    private String tmpDownloadUrl;
+
+    @ApiModelProperty(value = "是否下载最新:默认0 未下载或未更新", hidden = true)
+    private Integer tmpIsNew;
+
+    @ApiModelProperty(value = "是否下载最新-国际版:默认0 未下载或未更新", hidden = true)
+    private String i18nTmpDownloadUrl;
+
+    @ApiModelProperty(value = "最新模板数据下载地址-国际版", hidden = true)
+    private Integer i18nTmpIsNew;
+
+}

+ 87 - 0
product-common/src/main/java/com/poyee/res/client/dict/SysDictDataRes.java

@@ -0,0 +1,87 @@
+package com.poyee.res.client.dict;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("字典数据Response")
+public class SysDictDataRes implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @ApiModelProperty("主键")
+    private Long id;
+
+    /**
+     * 排序
+     */
+    @ApiModelProperty("排序")
+    private Integer sort;
+
+    /**
+     * 字典标签
+     */
+    @ApiModelProperty("字典标签")
+    private String label;
+
+    /**
+     * 字典键值
+     */
+    @ApiModelProperty("字典键值")
+    private String value;
+
+    /**
+     * 字典类型
+     */
+    @ApiModelProperty("字典类型")
+    private String dictType;
+
+    /**
+     * 样式属性
+     */
+    @ApiModelProperty("样式属性")
+    private String cssClass;
+
+    /**
+     * 表格回显样式
+     */
+    @ApiModelProperty("表格回显样式")
+    private String listClass;
+
+    /**
+     * 是否默认(0否 1是)
+     */
+    @ApiModelProperty("是否默认(0否 1是)")
+    private Integer defaultFlag;
+
+    /**
+     * 状态(0正常 1停用)
+     */
+    @ApiModelProperty("状态(0正常 1停用)")
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    @ApiModelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    /**
+     * 修改时间
+     */
+    @ApiModelProperty("修改时间")
+    private LocalDateTime updateTime;
+}

+ 32 - 0
product-common/src/main/java/com/poyee/threads/ThreadPoolExecutor.java

@@ -0,0 +1,32 @@
+package com.poyee.threads;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+/**
+ * @author Hr
+ */
+@Configuration
+public class ThreadPoolExecutor {
+    //并发查询专用
+    @Bean("queryExecutor")
+    public ThreadPoolTaskExecutor queryExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        // 设置核心线程数
+        executor.setCorePoolSize(5);
+        // 设置最大线程数,此数量必须大于http处理线程最大数量
+        executor.setMaxPoolSize(20);
+        // 设置队列容量
+        executor.setQueueCapacity(1000);
+        // 设置线程活跃时间(秒)
+        executor.setKeepAliveSeconds(60);
+        // 设置默认线程名称
+        executor.setThreadNamePrefix("query_executor-");
+        // 设置拒绝策略
+        executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
+        // 等待所有任务结束后再关闭线程池
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        return executor;
+    }
+}

+ 44 - 0
product-common/src/main/java/com/poyee/utils/ApiUtils.java

@@ -0,0 +1,44 @@
+package com.poyee.utils;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.poyee.res.Result;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * @author Hr
+ */
+@Slf4j
+public class ApiUtils {
+
+    public static <T, R> R httpSuccess(Function<T, Result<R>> function, T request, R defaultValue, String msg) {
+        Assert.notNull(request, msg);
+        Result<R> response = function.apply(request);
+        if (response != null && response.successFlag()) {
+            return ObjectUtil.defaultIfNull(response.getData(), defaultValue);
+        } else {
+            log.error("{} error,request={}, response={}", msg, JSONUtil.toJsonStr(request), JSONUtil.toJsonStr(response));
+            assert response != null;
+            throw new RuntimeException(response.getMessage());
+        }
+    }
+
+    public static <T, R> R httpSuccess(Function<T, Result<R>> function, T request, String msg) {
+        return httpSuccess(function, request, null, msg);
+    }
+
+    public static <R> R httpSuccessNoParams(Supplier<Result<R>> function, R defaultValue, String msg) {
+        Assert.notNull(function, "Function must not be null");
+        Result<R> response = function.get();
+        if (response != null && response.successFlag()) {
+            return ObjectUtil.defaultIfNull(response.getData(), defaultValue);
+        } else {
+            log.error("{} error, response={}", msg, JSONUtil.toJsonStr(response));
+            throw new RuntimeException(msg);
+        }
+    }
+}

+ 99 - 0
product-common/src/main/java/com/poyee/utils/I18nUtil.java

@@ -0,0 +1,99 @@
+package com.poyee.utils;
+
+import cn.hutool.core.util.StrUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.MessageSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletRequest;
+import java.util.Locale;
+
+/**
+ * 国际化工具类
+ */
+@Component
+public class I18nUtil {
+
+    private static MessageSource staticMessageSource;
+
+    private static final Locale DEFAULT_LOCALE = Locale.CHINA;
+
+    @Autowired
+    private MessageSource messageSource;
+
+    @PostConstruct
+    public void init() {
+        staticMessageSource = messageSource;
+    }
+
+    /**
+     * 获取国际化消息
+     * @param code 消息编码
+     * @return 国际化消息
+     */
+    public static String getMessage(String code) {
+        return getMessage(code, null);
+    }
+
+    /**
+     * 获取国际化消息
+     * @param code 消息编码
+     * @param args 参数
+     * @return 国际化消息
+     */
+    public static String getMessage(String code, Object[] args) {
+        return getMessage(code, args, getCurrentLocale());
+    }
+
+    /**
+     * 获取国际化消息
+     * @param code 消息编码
+     * @param args 参数
+     * @param locale 语言环境
+     * @return 国际化消息
+     */
+    public static String getMessage(String code, Object[] args, Locale locale) {
+        if (staticMessageSource == null) {
+            return code;
+        }
+        try {
+            return staticMessageSource.getMessage(code, args, locale);
+        } catch (Exception e) {
+            // 如果获取不到国际化消息,返回原始 code
+            return code;
+        }
+    }
+
+    /**
+     * 获取当前语言环境
+     * 优先从请求头 Accept-Language 获取,如果没有则默认中文
+     */
+    public static Locale getCurrentLocale() {
+        ServletRequestAttributes attributes =
+                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes != null) {
+            HttpServletRequest request = attributes.getRequest();
+            String acceptLanguage = request.getHeader("Accept-Language");
+
+            if (StrUtil.isNotBlank(acceptLanguage)) {
+                // 解析 Accept-Language 头部,取第一个语言
+                String[] languages = acceptLanguage.split(",");
+                if (languages.length > 0) {
+                    String languageTag = languages[0].trim();
+                    // 处理类似 "zh-CN"、"en-US" 这样的格式
+                    if (languageTag.contains("-")) {
+                        String[] parts = languageTag.split("-");
+                        return new Locale(parts[0], parts[1]);
+                    } else {
+                        return new Locale(languageTag);
+                    }
+                }
+            }
+        }
+        // 默认返回中文
+        return DEFAULT_LOCALE;
+    }
+}

+ 25 - 0
product-common/src/main/java/com/poyee/utils/JwtUtils.java

@@ -0,0 +1,25 @@
+package com.poyee.utils;
+
+import cn.hutool.jwt.JWT;
+import cn.hutool.jwt.JWTUtil;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * JWT工具类(简化版)
+ */
+@Slf4j
+public class JwtUtils {
+
+    /**
+     * 从Token中获取用户信息
+     * 支持两种格式:
+     * 1. JWT Token (Bearer token)
+     * 2. Base64编码的用户信息
+     */
+    public static JSONObject getTokenUserInfo(String token) {
+        JWT jwt = JWTUtil.parseToken(token);
+        String payloads = jwt.getPayloads().toString();
+        return JSONObject.parseObject(payloads);
+    }
+}

+ 62 - 0
product-common/src/main/java/com/poyee/utils/ServletUtils.java

@@ -0,0 +1,62 @@
+package com.poyee.utils;
+
+import com.poyee.res.UserInfo;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ * Servlet 工具类
+ */
+public class ServletUtils {
+
+    /**
+     * 获取当前请求
+     */
+    public static HttpServletRequest getRequest() {
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes == null) {
+            return null;
+        }
+        return attributes.getRequest();
+    }
+
+    public static HttpServletResponse getHttpResponse() {
+        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
+    }
+
+    /**
+     * 获取当前 Session
+     */
+    public static HttpSession getSession() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        return request.getSession();
+    }
+
+    /**
+     * 获取当前登录用户
+     */
+    public static UserInfo getCurrentUser() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        return (UserInfo) request.getAttribute("currentUser");
+    }
+
+    /**
+     * 设置当前登录用户
+     */
+    public static void setCurrentUser(UserInfo userInfo) {
+        HttpServletRequest request = getRequest();
+        if (request != null) {
+            request.setAttribute("currentUser", userInfo);
+        }
+    }
+}

+ 366 - 0
product-common/src/main/java/com/poyee/utils/TencentCosUtil.java

@@ -0,0 +1,366 @@
+package com.poyee.utils;
+
+import com.poyee.config.TencentCosConfig;
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.ClientConfig;
+import com.qcloud.cos.auth.BasicCOSCredentials;
+import com.qcloud.cos.auth.COSCredentials;
+import com.qcloud.cos.auth.COSSigner;
+import com.qcloud.cos.model.ObjectMetadata;
+import com.qcloud.cos.model.PutObjectRequest;
+import com.qcloud.cos.model.PutObjectResult;
+import com.qcloud.cos.region.Region;
+import com.qcloud.cos.utils.Jackson;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 腾讯云COS工具类
+ *
+ * @author: zheng
+ * @date: 2026/01/26
+ */
+@Slf4j
+@Component
+public class TencentCosUtil {
+
+    @Resource
+    private TencentCosConfig tencentCosConfig;
+
+    /**
+     * 获取COS客户端
+     */
+    private COSClient getCosClient() {
+        // 1 初始化用户身份信息(secretId, secretKey)
+        COSCredentials cred = new BasicCOSCredentials(tencentCosConfig.getSecretId(), tencentCosConfig.getSecretKey());
+        // 2 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
+        ClientConfig clientConfig = new ClientConfig(new Region(tencentCosConfig.getRegion()));
+        // 3 生成cos客户端
+        return new COSClient(cred, clientConfig);
+    }
+
+    /**
+     * 上传文件
+     *
+     * @param file 文件
+     * @return 文件访问路径
+     */
+    public String uploadFile(MultipartFile file) throws IOException {
+        return uploadFile(file, null);
+    }
+
+    /**
+     * 上传文件
+     *
+     * @param file   文件
+     * @param folder 文件夹名称,可为空
+     * @return 文件访问路径
+     */
+    public String uploadFile(MultipartFile file, String folder) throws IOException {
+        // 检查文件大小
+        if (file.getSize() > tencentCosConfig.getMaxFileSize()) {
+            throw new IOException("文件大小超过限制,最大允许" + (tencentCosConfig.getMaxFileSize() / 1024 / 1024) + "MB");
+        }
+
+        // 检查文件类型
+        String originalFilename = file.getOriginalFilename();
+        if (originalFilename == null) {
+            throw new IOException("文件名不能为空");
+        }
+
+        String fileExtension = getFileExtension(originalFilename);
+        if (!isAllowedFileType(fileExtension)) {
+            throw new IOException("不支持的文件类型:" + fileExtension);
+        }
+
+        // 生成文件路径
+        String fileName = generateFileName(fileExtension);
+        String filePath = tencentCosConfig.getPrefix();
+        if (folder != null && !folder.isEmpty()) {
+            filePath += folder + "/";
+        }
+        filePath += fileName;
+
+        COSClient cosClient = null;
+        try {
+            cosClient = getCosClient();
+            // 设置文件元数据
+            ObjectMetadata metadata = new ObjectMetadata();
+            metadata.setContentLength(file.getSize());
+            metadata.setContentType(file.getContentType());
+
+            // 上传文件
+            PutObjectRequest putObjectRequest = new PutObjectRequest(
+                    tencentCosConfig.getBucketName(), filePath, file.getInputStream(), metadata);
+            PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
+
+            if (putObjectResult.getETag() != null) {
+                // 返回文件访问路径
+                return getFileUrl(filePath);
+            } else {
+                throw new IOException("文件上传失败");
+            }
+        } finally {
+            if (cosClient != null) {
+                cosClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 上传文件流
+     *
+     * @param inputStream 文件流
+     * @param fileName    文件名
+     * @return 文件访问路径
+     */
+    public String uploadFile(InputStream inputStream, String fileName) throws IOException {
+        return uploadFile(inputStream, fileName, null);
+    }
+
+    /**
+     * 上传文件流
+     *
+     * @param inputStream 文件流
+     * @param fileName    文件名
+     * @param folder      文件夹名称,可为空
+     * @return 文件访问路径
+     */
+    public String uploadFile(InputStream inputStream, String fileName, String folder) throws IOException {
+        // 检查文件类型
+        String fileExtension = getFileExtension(fileName);
+        if (!isAllowedFileType(fileExtension)) {
+            throw new IOException("不支持的文件类型:" + fileExtension);
+        }
+
+        // 生成文件路径
+        String newFileName = generateFileName(fileExtension);
+        String filePath = tencentCosConfig.getPrefix();
+        if (folder != null && !folder.isEmpty()) {
+            filePath += folder + "/";
+        }
+        filePath += newFileName;
+
+        COSClient cosClient = null;
+        try {
+            cosClient = getCosClient();
+            // 设置文件元数据
+            ObjectMetadata metadata = new ObjectMetadata();
+            metadata.setContentType(getContentType(fileExtension));
+
+            // 上传文件
+            PutObjectRequest putObjectRequest = new PutObjectRequest(
+                    tencentCosConfig.getBucketName(), filePath, inputStream, metadata);
+            PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
+
+            if (putObjectResult.getETag() != null) {
+                // 返回文件访问路径
+                return getFileUrl(filePath);
+            } else {
+                throw new IOException("文件上传失败");
+            }
+        } finally {
+            if (cosClient != null) {
+                cosClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 删除文件
+     *
+     * @param fileUrl 文件访问路径
+     */
+    public void deleteFile(String fileUrl) {
+        if (fileUrl == null || fileUrl.isEmpty()) {
+            return;
+        }
+
+        // 从URL中提取文件路径
+        String filePath = getFilePathFromUrl(fileUrl);
+        if (filePath == null) {
+            return;
+        }
+
+        COSClient cosClient = null;
+        try {
+            cosClient = getCosClient();
+            cosClient.deleteObject(tencentCosConfig.getBucketName(), filePath);
+            log.info("删除文件成功: {}", fileUrl);
+        } catch (Exception e) {
+            log.error("删除文件失败: {}", fileUrl, e);
+        } finally {
+            if (cosClient != null) {
+                cosClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 生成文件名
+     *
+     * @param fileExtension 文件扩展名
+     * @return 文件名
+     */
+    private String generateFileName(String fileExtension) {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        String dateStr = sdf.format(new Date());
+        String uuid = UUID.randomUUID().toString().replace("-", "");
+        return dateStr + "/" + uuid + "." + fileExtension;
+    }
+
+    /**
+     * 获取文件扩展名
+     *
+     * @param fileName 文件名
+     * @return 文件扩展名
+     */
+    private String getFileExtension(String fileName) {
+        if (fileName == null || fileName.isEmpty()) {
+            return "";
+        }
+        int dotIndex = fileName.lastIndexOf('.');
+        if (dotIndex == -1 || dotIndex == fileName.length() - 1) {
+            return "";
+        }
+        return fileName.substring(dotIndex + 1).toLowerCase();
+    }
+
+    /**
+     * 检查文件类型是否允许
+     *
+     * @param fileExtension 文件扩展名
+     * @return 是否允许
+     */
+    private boolean isAllowedFileType(String fileExtension) {
+        if (fileExtension == null || fileExtension.isEmpty()) {
+            return false;
+        }
+        for (String type : tencentCosConfig.getAllowFileTypes()) {
+            if (type.equalsIgnoreCase(fileExtension)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 根据文件扩展名获取Content-Type
+     *
+     * @param fileExtension 文件扩展名
+     * @return Content-Type
+     */
+    private String getContentType(String fileExtension) {
+        switch (fileExtension.toLowerCase()) {
+            case "jpg":
+            case "jpeg":
+                return "image/jpeg";
+            case "png":
+                return "image/png";
+            case "gif":
+                return "image/gif";
+            case "bmp":
+                return "image/bmp";
+            case "webp":
+                return "image/webp";
+            default:
+                return "application/octet-stream";
+        }
+    }
+
+    /**
+     * 获取文件访问路径
+     *
+     * @param filePath 文件路径
+     * @return 文件访问路径
+     */
+    private String getFileUrl(String filePath) {
+        // ranking-1309648802.cos.ap-shanghai.myqcloud.com
+        return "https://" + tencentCosConfig.getBucketName() + ".cos." + tencentCosConfig.getRegion() + ".myqcloud.com" + filePath;
+    }
+
+    /**
+     * 从URL中提取文件路径
+     *
+     * @param fileUrl 文件URL
+     * @return 文件路径
+     */
+    private String getFilePathFromUrl(String fileUrl) {
+        if (fileUrl == null || fileUrl.isEmpty()) {
+            return null;
+        }
+
+        try {
+            // 如果是自定义域名
+            if (tencentCosConfig.getDomain() != null && !tencentCosConfig.getDomain().isEmpty() && fileUrl.startsWith(tencentCosConfig.getDomain())) {
+                return fileUrl.substring(tencentCosConfig.getDomain().length() + 1);
+            }
+
+            // 如果是默认域名
+            String defaultDomain = "https://" + tencentCosConfig.getBucketName() + ".cos." + tencentCosConfig.getRegion() + ".myqcloud.com/";
+            if (fileUrl.startsWith(defaultDomain)) {
+                return fileUrl.substring(defaultDomain.length());
+            }
+
+            return null;
+        } catch (Exception e) {
+            log.error("从URL中提取文件路径失败: {}", fileUrl, e);
+            return null;
+        }
+    }
+
+
+    public String postObjectUpload(String filename) {
+        String bucketName = tencentCosConfig.getBucketName();
+//        String endpoint = "cos.ap-shanghai.myqcloud.com";
+        String key = filename;
+//        String contentType = "image/jpeg";
+        String secretId = tencentCosConfig.getSecretId();
+        String secretKey = tencentCosConfig.getSecretKey();
+        long startTimestamp = System.currentTimeMillis() / 1000;
+        long endTimestamp = startTimestamp + 10 * 60;
+        String endTimestampStr = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").
+                format(endTimestamp * 1000);
+        String keyTime = startTimestamp + ";" + endTimestamp;
+        // 设置表单的body字段值
+        Map<String, String> formFields = new HashMap<>();
+        formFields.put("q-sign-algorithm", "sha1");
+        formFields.put("key", key);
+        formFields.put("q-ak", secretId);
+        formFields.put("q-key-time", keyTime);
+        // 构造policy,参考文档: https://cloud.tencent.com/document/product/436/14690
+        String policy = "{\n" +
+                "    \"expiration\": \"" + endTimestampStr + "\",\n" +
+                "    \"conditions\": [\n" +
+                "        { \"bucket\": \"" + bucketName + "\" },\n" +
+                "        { \"q-sign-algorithm\": \"sha1\" },\n" +
+                "        { \"q-ak\": \"" + secretId + "\" },\n" +
+                "        { \"q-sign-time\":\"" + keyTime + "\" }\n" +
+                "    ]\n" +
+                "}";
+        // policy需要base64后算放入表单中
+        String encodedPolicy = new String(Base64.encodeBase64(policy.getBytes()));
+        // 设置policy
+        formFields.put("policy", encodedPolicy);
+        // 根据编码后的policy和secretKey计算签名
+        COSSigner cosSigner = new COSSigner();
+        String signature = cosSigner.buildPostObjectSignature(secretKey,keyTime, policy);
+        // 设置签名
+        formFields.put("q-signature", signature);
+        log.info("表单参数:{}",Jackson.toJsonPrettyString(formFields));
+        // 根据以上表单参数,构造最开始的body部分
+        return Jackson.toJsonPrettyString(formFields);
+    }
+
+}

+ 146 - 0
product-web/pom.xml

@@ -0,0 +1,146 @@
+<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.poyee</groupId>
+        <artifactId>b2b-product-service</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>product-web</artifactId>
+    <name>product-web</name>
+    <description>商品服务 Web 启动模块</description>
+    <packaging>jar</packaging>
+
+    <properties>
+        <mysql-version>8.0.33</mysql-version>
+        <driud-version>1.2.20</driud-version>
+        <guava-version>31.1-jre</guava-version>
+    </properties>
+    <dependencies>
+        <!-- 内部模块 -->
+        <dependency>
+            <groupId>com.poyee</groupId>
+            <artifactId>product-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.poyee</groupId>
+            <artifactId>product-client</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 Boot Web -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- Spring Cloud OpenFeign -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+
+        <!-- MyBatis-Plus -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+
+        <!-- MySQL -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>${mysql-version}</version>
+        </dependency>
+
+        <!-- Druid 连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+            <version>${driud-version}</version>
+        </dependency>
+
+        <!-- Guava -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>${guava-version}</version>
+        </dependency>
+
+        <!-- Knife4j -->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- Redisson -->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- FastJSON -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+        </dependency>
+
+        <!-- Commons Lang3 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
+        <!-- AOP -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- Validation -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- Actuator -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <!-- Test -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 17 - 0
product-web/src/main/java/com/poyee/ProductServiceApplication.java

@@ -0,0 +1,17 @@
+package com.poyee;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+@SpringBootApplication
+@EnableFeignClients(basePackages = "com.poyee")
+@MapperScan("com.poyee.mapper")
+@EnableAspectJAutoProxy(proxyTargetClass = true)
+public class ProductServiceApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(ProductServiceApplication.class, args);
+    }
+}

+ 34 - 0
product-web/src/main/java/com/poyee/controller/ImageUploadController.java

@@ -0,0 +1,34 @@
+package com.poyee.controller;
+
+import com.poyee.facade.impl.OssUploadFacade;
+import com.poyee.req.AccessTokenRequest;
+import com.poyee.res.Result;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+
+/**
+ * 图片上传管理
+ *
+ * @author: gengjintao
+ * @date: 2026/01/07
+ */
+@Slf4j
+@Api(tags = "图片上传管理")
+@RestController
+@AllArgsConstructor
+@RequestMapping("/api/image")
+public class ImageUploadController {
+
+    private final OssUploadFacade ossUploadFacade;
+
+    @ApiOperation("生成cos签名")
+    @CrossOrigin(origins = "*")
+    @PostMapping("/oss")
+    public Result<String> getAccessToken(@RequestBody AccessTokenRequest request) {
+        return Result.success(ossUploadFacade.getAccessToken(request.getFilename()));
+    }
+}

+ 104 - 0
product-web/src/main/java/com/poyee/controller/ProductInfoController.java

@@ -0,0 +1,104 @@
+package com.poyee.controller;
+
+import com.github.pagehelper.PageInfo;
+import com.poyee.api.checklist.CheckListFeignClient;
+import com.poyee.facade.IProductInfoFacade;
+import com.poyee.req.*;
+import com.poyee.req.client.checklist.BaseInfoReq;
+import com.poyee.res.*;
+import com.poyee.res.client.checklist.BaseInfoRes;
+import com.poyee.utils.ApiUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.AllArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/product")
+@Api(tags = "商品管理")
+@AllArgsConstructor
+public class ProductInfoController {
+
+    private final IProductInfoFacade productInfoFacade;
+    private final CheckListFeignClient checkListFeignClient;
+
+//    @GetMapping("/type/dropdown")
+//    @ApiOperation("商品种类下拉框")
+//    @Deprecated
+//    //使用字典表
+//    public Result<List<ProductTypeRes>> dropdown() {
+//        return Result.success(ProductTypeEnum.getAll());
+//    }
+
+
+    @GetMapping("/detail/{sku}")
+    @ApiOperation("查看商品")
+    public Result<ProductDetailRes> detail(@ApiParam("sku") @PathVariable("sku") String sku) {
+        return Result.success(productInfoFacade.detail(sku));
+    }
+
+    @PostMapping("/list")
+    @ApiOperation("查询基础商品库列表")
+    public Result<PageInfo<ProductListRes>> list(@RequestBody ProductSearchReq productSearchReq) {
+        return Result.success(productInfoFacade.list(productSearchReq));
+    }
+
+    @PostMapping("/review")
+    @ApiOperation("审核商品")
+    public Result<Boolean> review(@RequestBody @Validated ProductReviewReq productReviewReq) {
+        return Result.success(productInfoFacade.review(productReviewReq));
+    }
+
+    @GetMapping("/idea/attribute/{sku}")
+    @ApiOperation("查看闲置信息")
+    public Result<IdleAttributeRes> idleAttribute(@ApiParam("sku") @PathVariable("sku") String sku) {
+        return Result.success(productInfoFacade.idleAttribute(sku));
+    }
+
+    @PostMapping("/idea/attribute")
+    @ApiOperation("编辑闲置信息")
+    public Result<Boolean> idleAttribute(@RequestBody @Validated IdleAttributeReq idleAttributeReq) {
+        return Result.success(productInfoFacade.idleAttribute(idleAttributeReq));
+    }
+
+    @GetMapping("/fast/sale/{sku}")
+    @ApiOperation("查看商品闪购详情")
+    public Result<ProductFastSaleRes> fastSale(@ApiParam("sku") @PathVariable("sku") String sku) {
+        return Result.success(productInfoFacade.fastSale(sku));
+    }
+
+    @GetMapping("/fast/sale")
+    @ApiOperation("编辑商品闪购")
+    public Result<Boolean> fastSale(@RequestBody @Validated FastSaleReq fastSaleReq) {
+        FastSaleReq.checkParam(fastSaleReq);
+        return Result.success(productInfoFacade.fastSale(fastSaleReq));
+    }
+
+
+    @PostMapping("/base")
+    @ApiOperation(("级联获取基础库数据"))
+    public Result<List<BaseInfoRes>> baseInfo(@RequestBody @Validated BaseInfoReq baseInfoReq) {
+        return Result.success(ApiUtils.httpSuccess(checkListFeignClient::getBaseInfo, baseInfoReq, "failed_to_retrieve_checkList_basic_database_data"));
+    }
+
+    @PostMapping("/add")
+    @ApiOperation(("新增基础库商品"))
+    public Result<Boolean> add(@RequestBody @Validated ProductInfoAddReq productInfoAddReq) {
+        return Result.success(productInfoFacade.add(productInfoAddReq));
+    }
+
+    @PostMapping("/edit")
+    @ApiOperation(("编辑基础库商品"))
+    public Result<Boolean> edit(@RequestBody @Validated ProductInfoEditReq productInfoEditReq) {
+        return Result.success(productInfoFacade.edit(productInfoEditReq));
+    }
+    //TODO  等ossKey ossSecret
+    //TODO 1.搭建CI\CD环境
+    //TODO 3.后台订单
+
+
+}

+ 59 - 0
product-web/src/main/java/com/poyee/controller/ProductItemController.java

@@ -0,0 +1,59 @@
+package com.poyee.controller;
+
+import com.github.pagehelper.PageInfo;
+import com.poyee.facade.IProductItemFacade;
+import com.poyee.req.OrderServiceProductItemSearchReq;
+import com.poyee.req.ProductItemAddReq;
+import com.poyee.req.ProductItemEditReq;
+import com.poyee.req.ProductItemSearchReq;
+import com.poyee.res.OrderServiceProductItemSearchRes;
+import com.poyee.res.ProductItemDetail;
+import com.poyee.res.ProductItemListRes;
+import com.poyee.res.Result;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.AllArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/product/item")
+@Api(tags = "子商品管理")
+@AllArgsConstructor
+public class ProductItemController {
+    private final IProductItemFacade iProductItemFacade;
+
+    @PostMapping("/list")
+    @ApiOperation("子商品订单列表")
+    public Result<PageInfo<ProductItemListRes>> list(@RequestBody @Validated ProductItemSearchReq productItemSearchReq) {
+        return Result.success(iProductItemFacade.list(productItemSearchReq));
+    }
+
+    @PostMapping("/add")
+    @ApiOperation("添加生态购商品")
+    public Result<Boolean> add(@RequestBody @Validated ProductItemAddReq productItemAddReq) {
+        return Result.success(iProductItemFacade.add(productItemAddReq));
+    }
+
+    @PostMapping("/edit")
+    @ApiOperation("编辑生态购商品")
+    public Result<Boolean> edit(@RequestBody @Validated ProductItemEditReq productItemEditReq) {
+        return Result.success(iProductItemFacade.edit(productItemEditReq));
+    }
+
+    @GetMapping("/detail/{id}")
+    @ApiOperation("生态购详情")
+    public Result<ProductItemDetail> detail(@ApiParam("商品子列表id") @PathVariable("id") Long id) {
+        return Result.success(iProductItemFacade.detail(id));
+    }
+
+
+    @PostMapping("/order/service/item/list")
+    @ApiOperation("订单服务子商品列表")
+    public Result<PageInfo<OrderServiceProductItemSearchRes>> list(@RequestBody @Validated OrderServiceProductItemSearchReq orderServiceProductItemSearchReq) {
+        return Result.success(iProductItemFacade.list(orderServiceProductItemSearchReq));
+    }
+
+
+}

+ 12 - 0
product-web/src/main/java/com/poyee/facade/IOssUploadFacade.java

@@ -0,0 +1,12 @@
+package com.poyee.facade;
+
+public interface IOssUploadFacade {
+
+    /**
+     * 获取上传文件token
+     *
+     * @param filename filename
+     * @return String
+     */
+    String getAccessToken(String filename);
+}

+ 85 - 0
product-web/src/main/java/com/poyee/facade/IProductInfoFacade.java

@@ -0,0 +1,85 @@
+package com.poyee.facade;
+
+import com.github.pagehelper.PageInfo;
+import com.poyee.req.*;
+import com.poyee.res.IdleAttributeRes;
+import com.poyee.res.ProductDetailRes;
+import com.poyee.res.ProductFastSaleRes;
+import com.poyee.res.ProductListRes;
+
+public interface IProductInfoFacade {
+
+
+    /**
+     * 查询商品基础库列表
+     *
+     * @param productSearchReq productSearchReq
+     * @return ProductListRes
+     */
+    PageInfo<ProductListRes> list(ProductSearchReq productSearchReq);
+
+    /**
+     * 商品详情
+     *
+     * @param sku sku
+     * @return ProductDetailRes
+     */
+    ProductDetailRes detail(String sku);
+
+    /**
+     * 商品审核
+     *
+     * @param productReviewReq productReviewReq
+     * @return Boolean
+     */
+    Boolean review(ProductReviewReq productReviewReq);
+
+    /**
+     * 查看当前sku详情信息
+     *
+     * @param sku sku
+     * @return
+     */
+    IdleAttributeRes idleAttribute(String sku);
+
+    /**
+     * 编辑闲置
+     *
+     * @param idleAttributeReq idleAttributeReq
+     * @return IdleAttributeRes
+     */
+    Boolean idleAttribute(IdleAttributeReq idleAttributeReq);
+
+    /**
+     * 获取商品闪购详情
+     *
+     * @param sku sku
+     * @return ProductFastSaleRes
+     */
+    ProductFastSaleRes fastSale(String sku);
+
+
+    /**
+     * 编辑闪购商品
+     *
+     * @param fastSaleReq fastSaleReq
+     * @return Boolean
+     */
+    Boolean fastSale(FastSaleReq fastSaleReq);
+
+    /**
+     * 新增商品
+     *
+     * @param productInfoAddReq productInfoAddReq
+     * @return Boolean
+     */
+    Boolean add(ProductInfoAddReq productInfoAddReq);
+
+    /**
+     *
+     * @param productInfoEditReq productInfoEditReq
+     * @return Boolean
+     */
+    Boolean edit(ProductInfoEditReq productInfoEditReq);
+
+}

+ 56 - 0
product-web/src/main/java/com/poyee/facade/IProductItemFacade.java

@@ -0,0 +1,56 @@
+package com.poyee.facade;
+
+import com.github.pagehelper.PageInfo;
+import com.poyee.req.OrderServiceProductItemSearchReq;
+import com.poyee.req.ProductItemAddReq;
+import com.poyee.req.ProductItemEditReq;
+import com.poyee.req.ProductItemSearchReq;
+import com.poyee.res.OrderServiceProductItemSearchRes;
+import com.poyee.res.ProductItemDetail;
+import com.poyee.res.ProductItemListRes;
+import com.poyee.res.Result;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RequestBody;
+
+public interface IProductItemFacade {
+
+    /**
+     * 子商品列表
+     *
+     * @param productItemSearchReq productItemSearchReq
+     * @return ProductItemListRes
+     */
+    PageInfo<ProductItemListRes> list(ProductItemSearchReq productItemSearchReq);
+
+    /**
+     * 添加子商品
+     *
+     * @param productItemSearchReq productItemSearchReq
+     * @return Boolean
+     */
+    Boolean add(ProductItemAddReq productItemSearchReq);
+
+    /**
+     * 生态购详情
+     *
+     * @param id id
+     * @return ProductItemDetail
+     */
+    ProductItemDetail detail(Long id);
+
+    /**
+     * 修改子商品数据
+     *
+     * @param productItemEditReq productItemEditReq
+     * @return Boolean
+     */
+    Boolean edit(ProductItemEditReq productItemEditReq);
+
+
+    /**
+     * 订单服务子商品列表
+     * @param orderServiceProductItemSearchReq orderServiceProductItemSearchReq
+     * @return OrderServiceProductItemSearchResOrderServiceProductItemSearchRes
+     */
+    PageInfo<OrderServiceProductItemSearchRes> list(OrderServiceProductItemSearchReq orderServiceProductItemSearchReq);
+}

+ 18 - 0
product-web/src/main/java/com/poyee/facade/impl/OssUploadFacade.java

@@ -0,0 +1,18 @@
+package com.poyee.facade.impl;
+
+import com.poyee.facade.IOssUploadFacade;
+import com.poyee.utils.TencentCosUtil;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@AllArgsConstructor
+public class OssUploadFacade implements IOssUploadFacade {
+
+    private final TencentCosUtil tencentCosUtil;
+
+
+    public String getAccessToken(String filename) {
+        return tencentCosUtil.postObjectUpload(filename);
+    }
+}

+ 353 - 0
product-web/src/main/java/com/poyee/facade/impl/ProductInfoFacade.java

@@ -0,0 +1,353 @@
+package com.poyee.facade.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.poyee.constant.ProductConstant;
+import com.poyee.domain.*;
+import com.poyee.enums.ProductTypeEnum;
+import com.poyee.enums.ReviewStatusEnum;
+import com.poyee.enums.SaleTypeEnum;
+import com.poyee.enums.StatusEnum;
+import com.poyee.exception.BusinessException;
+import com.poyee.facade.IProductInfoFacade;
+import com.poyee.mapstruct.ProductInfoMapstruct;
+import com.poyee.param.ProductListBO;
+import com.poyee.req.*;
+import com.poyee.res.IdleAttributeRes;
+import com.poyee.res.ProductDetailRes;
+import com.poyee.res.ProductFastSaleRes;
+import com.poyee.res.ProductListRes;
+import com.poyee.service.*;
+import com.poyee.utils.I18nUtil;
+import com.poyee.utils.ServletUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static com.poyee.enums.ProductTypeEnum.getByCode;
+
+@Component
+@AllArgsConstructor
+public class ProductInfoFacade implements IProductInfoFacade {
+
+    private final ProductInfoService productInfoService;
+    private final ProductInfoMapstruct productInfoMapstruct;
+    private final ProductFastSaleService productFastSaleService;
+    private final CardAttributeService cardAttributeService;
+    private final ProductMediaService productMediaService;
+    public final ProductPermissionService productPermissionService;
+    private final SysDictDataService sysDictDataService;
+    private final static String SKU_PREFIX = "BC";
+
+    @Override
+
+    public PageInfo<ProductListRes> list(ProductSearchReq productSearchReq) {
+        PageHelper.startPage(productSearchReq.getPage(), productSearchReq.getPageSize());
+        LambdaQueryWrapper<ProductInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
+        if (StrUtil.isNotBlank(productSearchReq.getProductName())) {
+            lambdaQueryWrapper.likeRight(ProductInfo::getProductName, productSearchReq.getProductName());
+        }
+        if (Objects.nonNull(productSearchReq.getStatus())) {
+            lambdaQueryWrapper.eq(ProductInfo::getStatus, productSearchReq.getStatus());
+        }
+        List<ProductInfo> productInfos = this.productInfoService.list(lambdaQueryWrapper);
+        if (CollUtil.isEmpty(productInfos)) {
+            return PageInfo.emptyPageInfo();
+        }
+        List<String> fastSaleSkus = productInfos.stream()
+                .filter(product -> Objects.nonNull(product.getFastSaleFlag()) && product.getFastSaleFlag() == StatusEnum.TRUE.getCode())
+                .map(ProductInfo::getSku)
+                .collect(Collectors.toList());
+        Map<String, ProductFastSale> fastSaleMap = CollUtil.isEmpty(fastSaleSkus)
+                ? Maps.newHashMap()
+                : productFastSaleService.lambdaQuery()
+                .in(ProductFastSale::getSku, fastSaleSkus)
+                .list()
+                .stream()
+                .collect(Collectors.toMap(ProductFastSale::getSku, Function.identity()));
+        List<ProductListRes> productListResList = productInfos.stream()
+                .map(productInfo -> {
+                    ProductListBO productBO = productInfoMapstruct.productDo2Bo(productInfo);
+                    ProductListRes productListRes = productInfoMapstruct.productBO2Res(productBO);
+                    if (Objects.nonNull(productInfo.getFastSaleFlag()) && productInfo.getFastSaleFlag() == StatusEnum.TRUE.getCode() && Objects.nonNull(fastSaleMap.get(productInfo.getSku()))) {
+                        productListRes.setSaleTime(fastSaleMap.get(productInfo.getSku()).getSaleTime());
+                        productListRes.setRecycleInventory(fastSaleMap.get(productInfo.getSku()).getRecycleInventory());
+                    }
+                    return productListRes;
+                })
+                .collect(Collectors.toList());
+        return PageInfo.of(productListResList);
+    }
+
+    @Override
+    public ProductDetailRes detail(String sku) {
+        ProductInfo productInfo = productInfo(sku);
+        ProductTypeEnum productTypeEnum = getByCode(productInfo.getProductType());
+        ProductMedia productMedia = productMediaService.lambdaQuery().eq(ProductMedia::getRelationSku, productInfo.getSku()).one();
+        Assert.notNull(productMedia, "the_product_image_does_not_exist");
+        List<String> otherImages = StrUtil.isNotBlank(productMedia.getOtherImages()) ?
+                Arrays.asList(productMedia.getOtherImages().split(",")) : Lists.newArrayList();
+        if (Objects.equals(productTypeEnum, ProductTypeEnum.TRADING_CARD)) {
+            CardAttribute cardAttribute = cardAttributeService.lambdaQuery().eq(CardAttribute::getRelationSku, productInfo.getSku()).one();
+            Assert.notNull(cardAttribute, "The_product_attribute_does_not_exist");
+            return ProductDetailRes.builder()
+                    .productType(productInfo.getProductType())
+                    .year(cardAttribute.getYear())
+                    .sport(cardAttribute.getSport())
+                    .manufacturer(cardAttribute.getManufacturer())
+                    .sets(cardAttribute.getSets())
+                    .setsVersion(cardAttribute.getSetsVersion())
+                    .productName(productInfo.getProductName())
+                    .defaultImage(productMedia.getMainImage())
+                    .productDesc(productInfo.getProductDesc())
+                    .otherImages(otherImages).build();
+        } else {
+            return ProductDetailRes.builder()
+                    .productType(productInfo.getProductType())
+                    .productName(productInfo.getProductName())
+                    .defaultImage(productMedia.getMainImage())
+                    .labelKey(productInfo.getLabel())
+                    .productDesc(productInfo.getProductDesc())
+                    .otherImages(otherImages).build();
+        }
+    }
+
+    @Override
+    public Boolean review(ProductReviewReq productReviewReq) {
+        ProductInfo productInfo = productInfo(productReviewReq.getSku());
+        Assert.notNull(productInfo, I18nUtil.getMessage("the_product_does_not_exist"));
+        validateReviewStatus(productInfo, productReviewReq.getStatus());
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_PRODUCT_STATUS, StrUtil.toString(productReviewReq.getStatus()));
+        productInfo.setStatus(productReviewReq.getStatus());
+        return productInfoService.updateById(productInfo);
+    }
+
+    @Override
+    public IdleAttributeRes idleAttribute(String sku) {
+        ProductInfo productInfo = productInfo(sku);
+        return IdleAttributeRes.builder()
+                .idleFlag(productInfo.getIdleFlag())
+                .build();
+    }
+
+    @Transactional
+    @Override
+    public Boolean idleAttribute(IdleAttributeReq idleAttributeReq) {
+        ProductInfo productInfo = productInfo(idleAttributeReq.getSku());
+
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_WHETHER, idleAttributeReq.getIdleFlag() ? "1" : "0");
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_PERMISSION, StrUtil.toString(idleAttributeReq.getPermission()));
+
+        productInfo.setIdleFlag(BooleanUtil.isTrue(idleAttributeReq.getIdleFlag()) ? StatusEnum.TRUE.getCode() : StatusEnum.FALSE.getCode());
+        ProductPermission productPermission = productPermissionService.lambdaQuery().eq(ProductPermission::getRelationSku, productInfo.getSku()).one();
+        LocalDateTime utcNow = LocalDateTime.now(java.time.ZoneOffset.UTC);
+        if (Objects.isNull(productPermission)) {
+            productPermission = ProductPermission.builder()
+                    .permission(idleAttributeReq.getPermission())
+                    .createTme(utcNow)
+                    .relationSku(productInfo.getSku()).build();
+            productPermissionService.save(productPermission);
+        } else {
+            productPermission.setPermission(idleAttributeReq.getPermission());
+            productPermission.setUpdateTime(utcNow);
+            productPermissionService.updateById(productPermission);
+        }
+        return this.productInfoService.updateById(productInfo);
+    }
+
+    @Override
+    public ProductFastSaleRes fastSale(String sku) {
+        ProductInfo productInfo = productInfo(sku);
+        ProductFastSale productFastSale = productFastSaleService.lambdaQuery().eq(ProductFastSale::getSku, sku).one();
+        if (Objects.nonNull(productFastSale)) {
+            return ProductFastSaleRes.builder()
+                    .recycleFlag(productInfo.getFastSaleFlag() == StatusEnum.TRUE.getCode() ? Boolean.TRUE : Boolean.FALSE)
+                    .recycleInventory(productFastSale.getRecycleInventory())
+                    .saleTime(productFastSale.getSaleTime())
+                    .permission(productPermissionService.lambdaQuery().eq(ProductPermission::getRelationSku, sku).one().getPermission())
+                    .build();
+        }
+        return ProductFastSaleRes.builder().build();
+    }
+
+    @Transactional
+    @Override
+    public Boolean fastSale(FastSaleReq fastSaleReq) {
+        ProductInfo productInfo = productInfo(fastSaleReq.getSku());
+        ProductFastSale existingFastSale = productFastSaleService.lambdaQuery()
+                .eq(ProductFastSale::getSku, fastSaleReq.getSku())
+                .one();
+
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_WHETHER, fastSaleReq.getRecycleFlag() ? "1" : "0");
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_PERMISSION, StrUtil.toString(fastSaleReq.getPermission()));
+
+        if (Objects.isNull(existingFastSale)) {
+            ProductFastSale newFastSale = ProductFastSale.builder()
+                    .saleType(Objects.nonNull(fastSaleReq.getSaleTime()) ? SaleTypeEnum.PRESALE.getCode() : SaleTypeEnum.SPOT_TRADING.getCode())
+                    .sku(fastSaleReq.getSku())
+                    .recycleFlag(BooleanUtil.isTrue(fastSaleReq.getRecycleFlag()) ? StatusEnum.TRUE.getCode() : StatusEnum.FALSE.getCode())
+                    .recycleInventory(fastSaleReq.getRecycleInventory())
+                    .saleTime(fastSaleReq.getSaleTime())
+                    .createBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getId() : null)
+                    .createTime(LocalDateTime.now())
+                    .build();
+            this.productFastSaleService.save(newFastSale);
+            productInfo.setFastSaleFlag(StatusEnum.TRUE.getCode());
+            return this.productInfoService.updateById(productInfo);
+        } else {
+            existingFastSale.setSaleType(Objects.nonNull(fastSaleReq.getSaleTime()) ? SaleTypeEnum.PRESALE.getCode() : SaleTypeEnum.SPOT_TRADING.getCode());
+            existingFastSale.setRecycleFlag(BooleanUtil.isTrue(fastSaleReq.getRecycleFlag()) ? StatusEnum.TRUE.getCode() : StatusEnum.FALSE.getCode());
+            existingFastSale.setRecycleInventory(fastSaleReq.getRecycleInventory());
+            existingFastSale.setSaleTime(fastSaleReq.getSaleTime());
+            existingFastSale.setUpdateBy(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getId() : null);
+            existingFastSale.setUpdateTime(LocalDateTime.now());
+            return this.productFastSaleService.updateById(existingFastSale);
+        }
+    }
+
+    @Transactional
+    @Override
+    public Boolean add(ProductInfoAddReq productInfoAddReq) {
+        ProductTypeEnum productTypeEnum = getByCode(productInfoAddReq.getProductType());
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_PRODUCT_TYPE, StrUtil.toString(productInfoAddReq.getProductType()));
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_LABEL, StrUtil.toString(productInfoAddReq.getLabelKey()));
+
+        String sku = SKU_PREFIX + IdUtil.getSnowflakeNextId() % 1000000000000000000L;
+        ProductInfo productInfo;
+        switch (productTypeEnum) {
+            case TRADING_CARD:
+                CardAttribute cardAttribute = CardAttribute.builder()
+                        .year(productInfoAddReq.getYear())
+                        .sport(productInfoAddReq.getSport())
+                        .manufacturer(productInfoAddReq.getManufacturer())
+                        .sets(productInfoAddReq.getSets())
+                        .setsVersion(productInfoAddReq.getSetsVersion())
+                        .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                        .relationSku(sku)
+                        .build();
+                ProductMedia productMedia = ProductMedia.builder()
+                        .mainImage(productInfoAddReq.getDefaultImage())
+                        .relationSku(sku)
+                        .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                        .otherImages(StrUtil.join(",", productInfoAddReq.getOtherImages()))
+                        .build();
+                productInfo = ProductInfo.builder()
+                        .productName(productInfoAddReq.getProductName())
+                        .sku(sku)
+                        .inventory(0)
+                        .productType(productInfoAddReq.getProductType())
+                        .productDesc(productInfoAddReq.getProductDesc())
+                        .relationCode(productInfoAddReq.getRelationCode())
+                        .productProperties(productInfoAddReq.getProductProperties())
+                        .sort(productInfoAddReq.getSort())
+                        .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                        .build();
+                this.productInfoService.save(productInfo);
+                this.cardAttributeService.save(cardAttribute);
+                this.productMediaService.save(productMedia);
+                break;
+            case OTHER:
+                productInfo = ProductInfo.builder()
+                        .productName(productInfoAddReq.getProductName())
+                        .sku(sku)
+                        .inventory(0)
+                        .productType(productInfoAddReq.getProductType())
+                        .label(productInfoAddReq.getLabelKey())
+                        .sort(productInfoAddReq.getSort())
+                        .productProperties(productInfoAddReq.getProductProperties())
+                        .productDesc(productInfoAddReq.getProductDesc())
+                        .relationCode(productInfoAddReq.getRelationCode())
+                        .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                        .build();
+                this.productInfoService.save(productInfo);
+                break;
+            default:
+                throw new BusinessException("product_type_error");
+        }
+        return Boolean.TRUE;
+    }
+
+    @Transactional
+    @Override
+    public Boolean edit(ProductInfoEditReq productInfoEditReq) {
+        ProductInfo productInfo = productInfo(productInfoEditReq.getSku());
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_LABEL, StrUtil.toString(productInfoEditReq.getLabelKey()));
+        if (StrUtil.isNotBlank(productInfoEditReq.getProductName())) {
+            productInfo.setProductName(productInfoEditReq.getProductName());
+        }
+        if (StrUtil.isNotBlank(productInfoEditReq.getProductDesc())) {
+            productInfo.setProductDesc(productInfoEditReq.getProductDesc());
+        }
+        if (StrUtil.isNotBlank(productInfoEditReq.getLabelKey())) {
+            productInfo.setLabel(productInfoEditReq.getLabelKey());
+        }
+        this.productInfoService.updateById(productInfo);
+        if (StrUtil.isNotBlank(productInfoEditReq.getDefaultImage()) ||
+                CollUtil.isNotEmpty(productInfoEditReq.getOtherImages())) {
+            ProductMedia productMedia = this.productMediaService.lambdaQuery()
+                    .eq(ProductMedia::getRelationSku, productInfoEditReq.getSku())
+                    .one();
+            if (Objects.isNull(productMedia)) {
+                productMedia = ProductMedia.builder()
+                        .relationSku(productInfoEditReq.getSku())
+                        .mainImage(productInfoEditReq.getDefaultImage())
+                        .otherImages(CollUtil.join(productInfoEditReq.getOtherImages(), ","))
+                        .build();
+                this.productMediaService.save(productMedia);
+            } else {
+                if (StrUtil.isNotBlank(productInfoEditReq.getDefaultImage())) {
+                    productMedia.setMainImage(productInfoEditReq.getDefaultImage());
+                }
+                if (CollUtil.isNotEmpty(productInfoEditReq.getOtherImages())) {
+                    productMedia.setOtherImages(CollUtil.join(productInfoEditReq.getOtherImages(), ","));
+                }
+                this.productMediaService.updateById(productMedia);
+            }
+        }
+        return Boolean.TRUE;
+    }
+
+    private void validateReviewStatus(ProductInfo productInfo, Integer targetStatus) {
+        Integer currentStatus = productInfo.getStatus();
+        if (Objects.isNull(targetStatus)) {
+            throw new BusinessException("review_status_cannot_be_null");
+        }
+        if (targetStatus.equals(ReviewStatusEnum.LISTED.getCode())) {
+            if (!currentStatus.equals(ReviewStatusEnum.DOWN.getCode()) &&
+                    !currentStatus.equals(ReviewStatusEnum.REJECTED.getCode())) {
+                throw new BusinessException("can_only_list_down_or_rejected_product");
+            }
+        } else if (targetStatus.equals(ReviewStatusEnum.DOWN.getCode())) {
+            if (!currentStatus.equals(ReviewStatusEnum.LISTED.getCode())) {
+                throw new BusinessException("can_only_down_listed_product");
+            }
+        } else if (targetStatus.equals(ReviewStatusEnum.REJECTED.getCode())) {
+            throw new BusinessException("the_product_already_rejected");
+        }
+    }
+
+    /*----------------------------------------------------------------------------------------------utils-------------------------------------------------------------------------------*/
+
+    public ProductInfo productInfo(String sku) {
+        ProductInfo productInfo = productInfoService.lambdaQuery()
+                .eq(ProductInfo::getSku, sku).one();
+        Assert.notNull(productInfo, I18nUtil.getMessage("the_product_does_not_exist"));
+        return productInfo;
+    }
+}

+ 145 - 0
product-web/src/main/java/com/poyee/facade/impl/ProductItemFacade.java

@@ -0,0 +1,145 @@
+package com.poyee.facade.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import com.poyee.constant.ProductConstant;
+import com.poyee.domain.ProductInfo;
+import com.poyee.domain.ProductItem;
+import com.poyee.facade.IProductItemFacade;
+import com.poyee.mapstruct.ProductItemMapstruct;
+import com.poyee.req.OrderServiceProductItemSearchReq;
+import com.poyee.req.ProductItemAddReq;
+import com.poyee.req.ProductItemEditReq;
+import com.poyee.req.ProductItemSearchReq;
+import com.poyee.res.OrderServiceProductItemSearchRes;
+import com.poyee.res.ProductItemDetail;
+import com.poyee.res.ProductItemListRes;
+import com.poyee.service.ProductInfoService;
+import com.poyee.service.ProductItemService;
+import com.poyee.service.SysDictDataService;
+import com.poyee.utils.ServletUtils;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+
+@Slf4j
+@Component
+@AllArgsConstructor
+public class ProductItemFacade implements IProductItemFacade {
+
+    private final ProductItemService productItemService;
+    private final ProductItemMapstruct productItemMapstruct;
+    private final ProductInfoService productInfoService;
+    private final SysDictDataService sysDictDataService;
+
+    @Override
+    public PageInfo<ProductItemListRes> list(ProductItemSearchReq productItemSearchReq) {
+        PageHelper.startPage(productItemSearchReq.getPage(), productItemSearchReq.getPageSize());
+        LambdaQueryWrapper<ProductItem> lambdaQueryWrapper = new LambdaQueryWrapper<>();
+        lambdaQueryWrapper.eq(ProductItem::getRelationSku, productItemSearchReq.getRelationSku());
+        if (Objects.nonNull(productItemSearchReq.getItemType())) {
+            lambdaQueryWrapper.eq(ProductItem::getItemType, productItemSearchReq.getItemType());
+        }
+        if (Objects.nonNull(productItemSearchReq.getStatus())) {
+            lambdaQueryWrapper.eq(ProductItem::getStatus, productItemSearchReq.getStatus());
+        }
+        if (StrUtil.isNotBlank(productItemSearchReq.getProductName())) {
+            lambdaQueryWrapper.likeRight(ProductItem::getProductName, productItemSearchReq.getProductName());
+        }
+        PageInfo<ProductItem> productItemPageInfo = new PageInfo<>(this.productItemService.list(lambdaQueryWrapper));
+        List<ProductItemListRes> productListResList = productItemPageInfo.getList()
+                .stream()
+                .map(productItem -> {
+                    //在售库存=库存-冻结库存
+                    ProductItemListRes res = productItemMapstruct.productItem2ProductItemListRes(productItem);
+                    res.setInventory(productItem.getInventory() - productItem.getFrozenInventory());
+                    return res;
+                })
+                .collect(Collectors.toList());
+        return PageInfo.of(productListResList);
+    }
+
+
+    @Override
+    public Boolean add(ProductItemAddReq productItemSearchReq) {
+        ProductInfo productInfo = productInfoService.lambdaQuery().eq(ProductInfo::getSku, productItemSearchReq.getRelationSku()).one();
+        Assert.notNull(productInfo, "the_product_does_not_exist");
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_ITEM_SPEC, StrUtil.toString(productItemSearchReq.getSpec()));
+        ProductItem productItem = ProductItem.builder()
+                .relationSku(productInfo.getSku())
+                .productName(productInfo.getProductName())
+                .itemType(1)//闪购
+                .fromSource(1) //生态购
+                .itemSpec(productItemSearchReq.getSpec())
+                .status(2)//下架
+                .itemPrice(productItemSearchReq.getPrice())
+                .inventory(productItemSearchReq.getInventory())
+                .frozenInventory(0)
+                .sales(0)
+                .publisher(Objects.nonNull(ServletUtils.getCurrentUser()) ? ServletUtils.getCurrentUser().getUsername() : null)
+                .createTime(LocalDateTime.now(java.time.ZoneOffset.UTC))
+                .build();
+        return this.productItemService.save(productItem);
+    }
+
+    @Override
+    public ProductItemDetail detail(Long id) {
+        ProductItem productItem = this.productItemService.getById(id);
+        Assert.notNull(productItem, "the_product_does_not_exist");
+        return ProductItemDetail.builder().price(productItem.getItemPrice()).spec(productItem.getItemSpec())
+                .inventory((productItem.getInventory() - productItem.getFrozenInventory())).id(productItem.getId()).build();
+    }
+
+    @Override
+    public Boolean edit(ProductItemEditReq productItemEditReq) {
+        sysDictDataService.validateOptions(ProductConstant.DICT_KEY_ITEM_SPEC, StrUtil.toString(productItemEditReq.getSpec()));
+        ProductItem productItem = this.productItemService.lambdaQuery().eq(ProductItem::getId, productItemEditReq.getId()).one();
+        if (Objects.nonNull(productItemEditReq.getSpec())) {
+            productItem.setItemSpec(productItemEditReq.getSpec());
+        }
+        if (Objects.nonNull(productItemEditReq.getPrice())) {
+            productItem.setItemPrice(productItemEditReq.getPrice());
+        }
+        if (Objects.nonNull(productItemEditReq.getInventory())) {
+            productItem.setInventory(productItemEditReq.getInventory());
+        }
+        return this.productItemService.updateById(productItem);
+    }
+
+    @Override
+    public PageInfo<OrderServiceProductItemSearchRes> list(OrderServiceProductItemSearchReq orderServiceProductItemSearchReq) {
+        PageHelper.startPage(orderServiceProductItemSearchReq.getPage(), orderServiceProductItemSearchReq.getPageSize());
+        LambdaQueryWrapper<ProductItem> lambdaQueryWrapper = new LambdaQueryWrapper<>();
+        if (StrUtil.isBlank(orderServiceProductItemSearchReq.getProductName())) {
+            lambdaQueryWrapper.likeRight(ProductItem::getProductName, orderServiceProductItemSearchReq.getProductName());
+        }
+        if (CollUtil.isNotEmpty(orderServiceProductItemSearchReq.getItemList())) {
+            lambdaQueryWrapper.in(ProductItem::getId, orderServiceProductItemSearchReq.getItemList());
+        }
+        PageInfo<ProductItem> productItemPageInfo = new PageInfo<>(this.productItemService.list(lambdaQueryWrapper));
+        return PageInfo.of(productItemMapstruct.convertRes(productItemPageInfo.getList()));
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+/*--------------------------------------------------------------------------util---------------------------------------------------------------------------*/

+ 7 - 0
product-web/src/main/java/com/poyee/mapper/CardAttributeMapper.java

@@ -0,0 +1,7 @@
+package com.poyee.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.poyee.domain.CardAttribute;
+
+public interface CardAttributeMapper extends BaseMapper<CardAttribute> {
+}

+ 16 - 0
product-web/src/main/java/com/poyee/mapper/ProductFastSaleMapper.java

@@ -0,0 +1,16 @@
+package com.poyee.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.poyee.domain.ProductFastSale;
+
+/**
+ * <p>
+ *  Mapper 接口
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+public interface ProductFastSaleMapper extends BaseMapper<ProductFastSale> {
+
+}

+ 16 - 0
product-web/src/main/java/com/poyee/mapper/ProductInfoMapper.java

@@ -0,0 +1,16 @@
+package com.poyee.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.poyee.domain.ProductInfo;
+
+/**
+ * <p>
+ *  Mapper 接口
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+public interface ProductInfoMapper extends BaseMapper<ProductInfo> {
+
+}

+ 16 - 0
product-web/src/main/java/com/poyee/mapper/ProductItemMapper.java

@@ -0,0 +1,16 @@
+package com.poyee.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.poyee.domain.ProductItem;
+
+/**
+ * <p>
+ *  Mapper 接口
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-18
+ */
+public interface ProductItemMapper extends BaseMapper<ProductItem> {
+
+}

+ 16 - 0
product-web/src/main/java/com/poyee/mapper/ProductMediaMapper.java

@@ -0,0 +1,16 @@
+package com.poyee.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.poyee.domain.ProductMedia;
+
+/**
+ * <p>
+ * Mapper 接口
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+public interface ProductMediaMapper extends BaseMapper<ProductMedia> {
+
+}

+ 16 - 0
product-web/src/main/java/com/poyee/mapper/ProductPermissionMapper.java

@@ -0,0 +1,16 @@
+package com.poyee.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.poyee.domain.ProductPermission;
+
+/**
+ * <p>
+ *  Mapper 接口
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+public interface ProductPermissionMapper extends BaseMapper<ProductPermission> {
+
+}

+ 15 - 0
product-web/src/main/java/com/poyee/service/CardAttributeService.java

@@ -0,0 +1,15 @@
+package com.poyee.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.poyee.domain.CardAttribute;
+
+/**
+ * <p>
+ * 服务类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+public interface CardAttributeService extends IService<CardAttribute> {
+}

+ 16 - 0
product-web/src/main/java/com/poyee/service/ProductFastSaleService.java

@@ -0,0 +1,16 @@
+package com.poyee.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.poyee.domain.ProductFastSale;
+
+/**
+ * <p>
+ *  服务类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+public interface ProductFastSaleService extends IService<ProductFastSale> {
+
+}

+ 16 - 0
product-web/src/main/java/com/poyee/service/ProductInfoService.java

@@ -0,0 +1,16 @@
+package com.poyee.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.poyee.domain.ProductInfo;
+
+/**
+ * <p>
+ *  服务类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+public interface ProductInfoService extends IService<ProductInfo> {
+
+}

+ 16 - 0
product-web/src/main/java/com/poyee/service/ProductItemService.java

@@ -0,0 +1,16 @@
+package com.poyee.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.poyee.domain.ProductItem;
+
+/**
+ * <p>
+ *  服务类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-18
+ */
+public interface ProductItemService extends IService<ProductItem> {
+
+}

+ 16 - 0
product-web/src/main/java/com/poyee/service/ProductMediaService.java

@@ -0,0 +1,16 @@
+package com.poyee.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.poyee.domain.ProductMedia;
+
+/**
+ * <p>
+ *  服务类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+public interface ProductMediaService extends IService<ProductMedia> {
+
+}

+ 16 - 0
product-web/src/main/java/com/poyee/service/ProductPermissionService.java

@@ -0,0 +1,16 @@
+package com.poyee.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.poyee.domain.ProductPermission;
+
+/**
+ * <p>
+ *  服务类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+public interface ProductPermissionService extends IService<ProductPermission> {
+
+}

+ 27 - 0
product-web/src/main/java/com/poyee/service/SysDictDataService.java

@@ -0,0 +1,27 @@
+package com.poyee.service;
+
+import com.poyee.res.client.dict.SysDictDataRes;
+
+import java.util.List;
+
+public interface SysDictDataService {
+
+
+    /**
+     * 判断当前key在字典表是否存在
+     *
+     * @param dictType dictType
+     * @param key      key
+     */
+    void validateOptions(String dictType, String key);
+
+
+    /**
+     * 返回当前字典表项
+     *
+     * @param dictType dictType
+     * @return List<SysDictDataRes>
+     */
+    List<SysDictDataRes> returnValue(String dictType);
+
+}

+ 12 - 0
product-web/src/main/java/com/poyee/service/impl/CardAttributeServiceImpl.java

@@ -0,0 +1,12 @@
+package com.poyee.service.impl;
+
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.poyee.domain.CardAttribute;
+import com.poyee.mapper.CardAttributeMapper;
+import com.poyee.service.CardAttributeService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CardAttributeServiceImpl extends ServiceImpl<CardAttributeMapper, CardAttribute> implements CardAttributeService {
+}

+ 20 - 0
product-web/src/main/java/com/poyee/service/impl/ProductFastSaleServiceImpl.java

@@ -0,0 +1,20 @@
+package com.poyee.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.poyee.domain.ProductFastSale;
+import com.poyee.mapper.ProductFastSaleMapper;
+import com.poyee.service.ProductFastSaleService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ *  服务实现类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+@Service
+public class ProductFastSaleServiceImpl extends ServiceImpl<ProductFastSaleMapper, ProductFastSale> implements ProductFastSaleService {
+
+}

+ 20 - 0
product-web/src/main/java/com/poyee/service/impl/ProductInfoServiceImpl.java

@@ -0,0 +1,20 @@
+package com.poyee.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.poyee.domain.ProductInfo;
+import com.poyee.mapper.ProductInfoMapper;
+import com.poyee.service.ProductInfoService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ *  服务实现类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+@Service
+public class ProductInfoServiceImpl extends ServiceImpl<ProductInfoMapper, ProductInfo> implements ProductInfoService {
+
+}

+ 20 - 0
product-web/src/main/java/com/poyee/service/impl/ProductItemServiceImpl.java

@@ -0,0 +1,20 @@
+package com.poyee.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.poyee.domain.ProductItem;
+import com.poyee.mapper.ProductItemMapper;
+import com.poyee.service.ProductItemService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ *  服务实现类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-18
+ */
+@Service
+public class ProductItemServiceImpl extends ServiceImpl<ProductItemMapper, ProductItem> implements ProductItemService {
+
+}

+ 20 - 0
product-web/src/main/java/com/poyee/service/impl/ProductMediaServiceImpl.java

@@ -0,0 +1,20 @@
+package com.poyee.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.poyee.domain.ProductMedia;
+import com.poyee.mapper.ProductMediaMapper;
+import com.poyee.service.ProductMediaService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ *  服务实现类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+@Service
+public class ProductMediaServiceImpl extends ServiceImpl<ProductMediaMapper, ProductMedia> implements ProductMediaService {
+
+}

+ 20 - 0
product-web/src/main/java/com/poyee/service/impl/ProductPermissionServiceImpl.java

@@ -0,0 +1,20 @@
+package com.poyee.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.poyee.domain.ProductPermission;
+import com.poyee.mapper.ProductPermissionMapper;
+import com.poyee.service.ProductPermissionService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ *  服务实现类
+ * </p>
+ *
+ * @author Hr
+ * @since 2026-03-16
+ */
+@Service
+public class ProductPermissionServiceImpl extends ServiceImpl<ProductPermissionMapper, ProductPermission> implements ProductPermissionService {
+
+}

+ 32 - 0
product-web/src/main/java/com/poyee/service/impl/SysDictDataServiceImpl.java

@@ -0,0 +1,32 @@
+package com.poyee.service.impl;
+
+import com.poyee.api.dict.SysDictFeignClient;
+import com.poyee.exception.BusinessException;
+import com.poyee.res.client.dict.SysDictDataRes;
+import com.poyee.service.SysDictDataService;
+import com.poyee.utils.ApiUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+
+@Service
+@AllArgsConstructor
+public class SysDictDataServiceImpl implements SysDictDataService {
+
+
+    private final SysDictFeignClient sysDictFeignClient;
+
+
+    public void validateOptions(String dictType, String key) {
+        List<SysDictDataRes> sysDictDataRes = ApiUtils.httpSuccess(this.sysDictFeignClient::getByDictType, dictType, "failed_to_call_dictionary_table_api");
+        if (sysDictDataRes.stream().noneMatch(x -> x.getLabel().equals(key))) {
+            throw new BusinessException("not_found_value_for_current_dict");
+        }
+    }
+
+    public List<SysDictDataRes> returnValue(String dictType) {
+        return ApiUtils.httpSuccess(this.sysDictFeignClient::getByDictType, dictType, "failed_to_call_dictionary_table_api");
+    }
+}

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä