Compare commits

...

57 Commits

Author SHA1 Message Date
litianxiang
ab552fc414 买家端联调bug 2026-05-22 13:02:11 +08:00
litianxiang
28f2d7678c fix商品接口报错 2026-05-21 16:41:17 +08:00
litianxiang
c11ed9bf96 买家端bugfix 2026-05-21 14:42:22 +08:00
litianxiang
18bc9fd5e4 买家端bugfix 2026-05-21 13:33:33 +08:00
litianxiang
c825f6af70 购物车相关代码 2026-05-20 16:53:58 +08:00
litianxiang
45885bf509 订单相关 2026-05-20 15:33:22 +08:00
litianxiang
d004dc75f5 订单相关接口 2026-05-20 11:42:31 +08:00
litianxiang
14efee9c85 订单相关接口 2026-05-20 11:10:01 +08:00
litianxiang
149ee13ec3 订单相关接口 2026-05-20 11:09:30 +08:00
litianxiang
a51bca1867 买家端订单fegin接口 2026-05-19 13:29:17 +08:00
litianxiang
a3020bccae 买家端接口fegin适配 2026-05-18 14:54:08 +08:00
litianxiang
daf40ab224 登录鉴权按照Source判断id来自于何处 2026-05-13 09:40:32 +08:00
litianxiang
912d5efee7 保存卖家信息解析为逻辑路径 2026-05-11 17:18:18 +08:00
litianxiang
0c1b74ddc0 买家端需要的获取商家主页和模糊搜索接口 2026-05-11 16:40:47 +08:00
litianxiang
e1d57f7b37 图片存在性校验 2026-05-07 15:59:04 +08:00
litianxiang
daf4c30a91 商品草稿状态也要校验 2026-05-07 14:11:18 +08:00
litianxiang
08f5a482eb log配置 2026-05-07 13:39:46 +08:00
litianxiang
1ff76957a7 选中状态fix 2026-05-07 11:38:19 +08:00
litianxiang
d77ce701e1 swagger bug 2026-05-07 10:31:28 +08:00
litianxiang
4b309efbb5 swagger bug 2026-05-07 10:31:01 +08:00
litianxiang
73ac643771 商品新增视频类型图片 2026-05-07 10:18:41 +08:00
litianxiang
0b9601278c 商品新增封面源头类型图片 2026-05-07 10:06:08 +08:00
litianxiang
0a1dc1c10d 工作流 2026-05-07 09:39:26 +08:00
litianxiang
9e5ba17dc4 工作流恢复 2026-05-06 17:23:45 +08:00
litianxiang
88c73c4462 工作流恢复 2026-05-06 17:13:36 +08:00
litianxiang
749241f19b 日志
订单表字段改名
视频返回新增字段
2026-05-06 16:58:48 +08:00
litianxiang
f69eca39ff 新工作流 2026-05-06 15:06:31 +08:00
litianxiang
38fb2ec4d5 服务端口号与宿主机统一,方便本地调试不需要修改bootstrap 2026-05-04 14:21:37 +08:00
litianxiang
b56ae5741b 服务端口号与宿主机统一,方便本地调试不需要修改bootstrap 2026-05-04 14:19:38 +08:00
litianxiang
1d4c8ec629 新增删除seller接口 2026-05-04 13:35:34 +08:00
litianxiang
4456722328 nacos注册测试 2026-05-04 10:19:18 +08:00
litianxiang
ad2254bc80 ProductCategory获取不到fix 2026-04-29 16:33:23 +08:00
litianxiang
5569da47f7 ProductCategory获取不到fix 2026-04-29 15:26:59 +08:00
litianxiang
fb892b6b21 商品排序规则按照修改时间 2026-04-29 15:18:23 +08:00
litianxiang
dea2409cea fix:时间自动创建 2026-04-29 14:19:10 +08:00
litianxiang
9d4c675594 fix:发布商品状态错误 2026-04-29 13:51:56 +08:00
litianxiang
da72640783 fix 2026-04-28 17:28:52 +08:00
litianxiang
48c4679820 工作流 2026-04-28 16:35:13 +08:00
litianxiang
86773339ec 工作流 2026-04-28 16:30:07 +08:00
litianxiang
92906881fe 工作流 2026-04-28 16:20:52 +08:00
litianxiang
520627a8fa 工作流 2026-04-28 16:13:30 +08:00
litianxiang
cc839dce1d bootstrap配置 2026-04-28 16:04:49 +08:00
litianxiang
5ceda7991d bootstrap配置 2026-04-28 15:53:56 +08:00
litianxiang
6f4e71b9e9 bootstrap配置 2026-04-28 15:46:47 +08:00
litianxiang
38c12b9ba5 bootstrap配置 2026-04-28 15:44:11 +08:00
litianxiang
232953acb0 bootstrap配置 2026-04-28 15:29:10 +08:00
litianxiang
b862da5b50 bootstrap配置 2026-04-28 14:38:02 +08:00
litianxiang
0605839c87 1 2026-04-28 13:30:57 +08:00
litianxiang
774e6e0c6b 1 2026-04-28 13:28:31 +08:00
litianxiang
1802b2b500 1 2026-04-28 13:22:14 +08:00
litianxiang
f0f772ae89 加入销量字段,解决数据库自动填充字段问题 2026-04-28 13:16:45 +08:00
litianxiang
e433921abe 商品bug 2026-04-28 09:39:28 +08:00
litianxiang
259a7c8c2a host配置 2026-04-27 16:43:20 +08:00
litianxiang
0740aefa1a host配置 2026-04-27 16:29:56 +08:00
f25029be87 更新 .gitea/workflows/master_sellrt_build_manual.yaml 2026-04-27 16:02:34 +08:00
litianxiang
1744480822 端口号错误 2026-04-27 15:09:53 +08:00
litianxiang
9cc302fa53 微服务改造 2026-04-27 13:54:05 +08:00
61 changed files with 2021 additions and 321 deletions

View File

@@ -4,7 +4,8 @@ on:
jobs: jobs:
build_and_deploy: build_and_deploy:
runs-on: ubuntu-latest runs-on: java21
outputs: outputs:
build_status: ${{ job.status }} build_status: ${{ job.status }}
build_url: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }} build_url: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }}
@@ -26,52 +27,20 @@ jobs:
with: with:
ref: master ref: master
- name: 2.Set up JDK 21
uses: actions/setup-java@v5 - name: 3.缓存 Maven 依赖
uses: actions/cache@v5
with: with:
java-version: '21' path: ~/.m2/repository
distribution: 'temurin' key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: 3.设置JAVA Maven 环境 - name: 4.构建项目
run: | run: |
# 适配root/普通用户 java -version
SUDO="" mvn -v
[ "$(id -u)" != "0" ] && SUDO="sudo" mvn clean package -DskipTests
# 安装依赖
$SUDO apt update && $SUDO apt install -y wget tar --no-install-recommends
# 下载Maven
MAVEN_VERSION="3.9.11"
MAVEN_TAR="apache-maven-${MAVEN_VERSION}-bin.tar.gz"
MAVEN_URL="https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/${MAVEN_TAR}"
wget --no-verbose -O /tmp/${MAVEN_TAR} ${MAVEN_URL}
# 解压+软链接
$SUDO tar -xzf /tmp/${MAVEN_TAR} -C /usr/local/
$SUDO ln -sf /usr/local/apache-maven-${MAVEN_VERSION} /usr/local/maven
# 配置PATH
echo "/usr/local/maven/bin" >> $GITHUB_PATH
export PATH="/usr/local/maven/bin:$PATH"
# 验证
mvn -v
- name: 4.构建jar包
run: |
echo "===== 开始构建JAR包 ====="
# 新增:打印当前构建分支(两种方式双重确认)
echo "当前工作目录分支:$(git branch --show-current)"
echo "Gitea检出分支:${{ github.ref_name }}"
echo "预期构建分支: master"
echo "========================"
mvn -B clean install -DskipTests -Pdev 2>&1
# 检查构建是否成功
if [ $? -ne 0 ]; then
echo "JAR包构建失败!"
exit 1
fi
- name: 5.生成Dockerfile - name: 5.生成Dockerfile
run: | run: |
@@ -81,7 +50,7 @@ jobs:
VOLUME /tmp VOLUME /tmp
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' > /etc/timezone RUN echo 'Asia/Shanghai' > /etc/timezone
ADD ./aida-seller-1.0.0.jar /app.jar ADD ./*.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"] ENTRYPOINT ["java","-jar","/app.jar"]
EOF EOF
echo "Dockerfile内容:" echo "Dockerfile内容:"
@@ -93,7 +62,7 @@ jobs:
cat > docker-compose.yml << 'EOF' cat > docker-compose.yml << 'EOF'
version: '3' version: '3'
services: services:
aida_back: master-aida-seller:
container_name: master-aida-seller container_name: master-aida-seller
build: . build: .
volumes: volumes:
@@ -102,46 +71,41 @@ jobs:
- ./temp:/temp - ./temp:/temp
- ./uploads:/temp/uploads - ./uploads:/temp/uploads
ports: ports:
- '10093:5567' - '10093:10093'
restart: always restart: always
EOF EOF
# 验证docker-compose.yml生成 # 验证docker-compose.yml生成
echo "docker-compose.yml内容:" echo "docker-compose.yml内容:"
cat docker-compose.yml cat docker-compose.yml
- name: 7.安装SSH工具 - name: 7.上传jar到远程服务器
run: | uses: appleboy/scp-action@master
$SUDO apt install -y sshpass openssh-client --no-install-recommends with:
# 配置SSH免密 host: ${{ secrets.SERVER_HOST }}
mkdir -p ~/.ssh port: 22
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa username: ${{ secrets.SERVER_USER }}
chmod 600 ~/.ssh/id_rsa key: ${{ secrets.SSH_KEY }}
ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts source: "target/*.jar,Dockerfile,docker-compose.yml"
target: ${{ env.REMOTE_DEPLOY_PATH }}
preserve_host_directory_structure: false
- name: 8.同步文件到远程服务 - name: 8. 重启 Docker 服务
run: | uses: appleboy/ssh-action@master # 👈 专门执行命令的 action
echo "===== 同步文件到远程服务器 =====" with:
# 使用scp同步文件 host: ${{ secrets.SERVER_HOST }}
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ username: ${{ secrets.SERVER_USER }}
./target/*.jar ./Dockerfile ./docker-compose.yml \ key: ${{ secrets.SSH_KEY }}
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ env.REMOTE_DEPLOY_PATH }} 2>&1 key_base64: true
script: |
- name: 9.部署和运行服务 echo "========= 进入部署目录 ========="
run: |
echo "===== 开始部署服务 ====="
# SSH执行部署命令
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF_SSH'
cd ${{ env.REMOTE_DEPLOY_PATH }} cd ${{ env.REMOTE_DEPLOY_PATH }}
echo "停止旧容器..." ls -l
docker compose down || true
echo "清理Docker资源..." echo "========= 停止旧服务 ========="
docker system prune -f docker compose down
echo "构建镜像..."
docker compose build --no-cache echo "========= 启动新服务 ========="
echo "启动服务..." docker compose up -d --build
docker compose up -d
echo "验证容器状态..." echo "========= 查看运行状态 ========="
docker compose ps docker compose ps
echo "部署完成!"
EOF_SSH

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target/ /target/
/log/

27
pom.xml
View File

@@ -24,15 +24,15 @@
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.5.6</mybatis-plus.version> <mybatis-plus.version>3.5.7</mybatis-plus.version>
<minio.version>8.5.7</minio.version> <minio.version>8.0.3</minio.version>
<jwt.version>0.12.3</jwt.version> <jwt.version>0.12.3</jwt.version>
<hutool.version>5.8.26</hutool.version> <hutool.version>5.8.23</hutool.version>
<commons-lang3.version>3.13.0</commons-lang3.version> <commons-lang3.version>3.13.0</commons-lang3.version>
<knife4j.version>4.5.0</knife4j.version> <knife4j.version>4.4.0</knife4j.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version> <spring-cloud-alibaba.version>2023.0.3.4</spring-cloud-alibaba.version>
<spring-cloud.version>2023.0.0</spring-cloud.version> <spring-cloud.version>2023.0.4</spring-cloud.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@@ -63,6 +63,12 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- Spring Boot Logging显式引入确保 logback 正确初始化) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Validation --> <!-- Spring Boot Validation -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -86,7 +92,6 @@
<dependency> <dependency>
<groupId>com.mysql</groupId> <groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId> <artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency> </dependency>
<!-- MinIO --> <!-- MinIO -->
@@ -149,12 +154,6 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- ==================== 微服务 ==================== --> <!-- ==================== 微服务 ==================== -->
<dependency> <dependency>
@@ -185,6 +184,8 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<configuration> <configuration>
<!-- 强制工作目录为模块根目录,确保 ./log 指向项目目录而非 Maven 安装目录 -->
<workingDirectory>${project.basedir}</workingDirectory>
<excludes> <excludes>
<exclude> <exclude>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>

View File

@@ -5,11 +5,13 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication @SpringBootApplication
@MapperScan("com.aida.seller.module.*.mapper") @MapperScan("com.aida.seller.module.*.mapper")
@EnableFeignClients @EnableFeignClients
@EnableDiscoveryClient @EnableDiscoveryClient
@ComponentScan(basePackages = "com.aida.seller")
public class AidaSellerApplication { public class AidaSellerApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -0,0 +1,17 @@
package com.aida.seller.common.annotation;
import java.lang.annotation.*;
/**
* 标记接口仅允许内部服务调用Feign 远程调用)。
* <p>
* 被此注解标记的 Controller 方法会通过 AOP 拦截,
* 仅放行携带了正确内部调用 Header 的请求,外部 HTTP 请求将被拒绝。
*
* @see com.aida.seller.common.aop.InternalOnlyAspect
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InternalOnly {
}

View File

@@ -0,0 +1,46 @@
package com.aida.seller.common.aop;
import com.aida.seller.common.annotation.InternalOnly;
import com.aida.seller.common.constants.CommonConstants;
import com.aida.seller.common.exception.BusinessException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* AOP 切面:校验 {@link InternalOnly} 标记的方法是否来自内部服务调用。
* <p>
* 内部调用Feign会携带 {@link CommonConstants#INTERNAL_CALL_HEADER} Header
* 外部直接 HTTP 请求则不携带,视为非法访问。
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class InternalOnlyAspect {
@Around("@annotation(com.aida.seller.common.annotation.InternalOnly)")
public Object validateInternalCall(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
throw new BusinessException("禁止外部直接访问此接口");
}
HttpServletRequest request = attributes.getRequest();
String internalCall = request.getHeader(CommonConstants.INTERNAL_CALL_HEADER);
if (!CommonConstants.INTERNAL_CALL_VALUE.equals(internalCall)) {
log.warn("Unauthorized external access attempt to internal-only endpoint: {}",
((MethodSignature) joinPoint.getSignature()).getMethod().getName());
throw new BusinessException("禁止外部直接访问此接口");
}
return joinPoint.proceed();
}
}

View File

@@ -6,9 +6,9 @@ public class CommonConstants {
public static final int TOKEN_EXPIRE_TIME = 7 * 24; // token 7 天过期Hour public static final int TOKEN_EXPIRE_TIME = 7 * 24; // token 7 天过期Hour
/**
* 内部服务间调用的签名 HeaderFeign 远程调用时携带,用于标识为内部可信调用
*/
public static final String INTERNAL_CALL_HEADER = "X-Internal-Call";
public static final String INTERNAL_CALL_VALUE = "true";
} }

View File

@@ -1,24 +1,55 @@
package com.aida.seller.common.context; package com.aida.seller.common.context;
import com.aida.seller.common.exception.UnauthorizedException;
import com.aida.seller.model.vo.AuthPrincipalVo; import com.aida.seller.model.vo.AuthPrincipalVo;
public class UserContext { public class UserContext {
private static final ThreadLocal<AuthPrincipalVo> userHolder = new ThreadLocal<>(); private static final ThreadLocal<AuthPrincipalVo> userHolder = new ThreadLocal<>();
private static final ThreadLocal<Boolean> optionalAuth = ThreadLocal.withInitial(() -> false);
public static AuthPrincipalVo getUserHolder() {
return userHolder.get();
}
public static void delete() {
userHolder.remove();
}
public static void setUserHolder(AuthPrincipalVo authPrincipalVo) { public static void setUserHolder(AuthPrincipalVo authPrincipalVo) {
userHolder.set(authPrincipalVo); userHolder.set(authPrincipalVo);
} }
public static Long getUserId() { public static void setOptionalAuth(boolean value) {
optionalAuth.set(value);
}
public static AuthPrincipalVo getUserHolder() {
AuthPrincipalVo holder = userHolder.get(); AuthPrincipalVo holder = userHolder.get();
return holder != null ? holder.getId() : null; if (holder == null) {
if (optionalAuth.get()) {
return null;
}
throw new UnauthorizedException("Gateway token verification failed");
}
if (!"AIDA".equals(holder.getSource())) {
throw new UnauthorizedException("Gateway token verification failed");
}
return holder;
}
public static void delete() {
userHolder.remove();
optionalAuth.remove();
}
public static Long getUserId() {
return getUserHolder() == null ? null : getUserHolder().getId();
}
//买家端请求需要调用此方法获取买家id
public static Long getBuyerId() {
AuthPrincipalVo holder = userHolder.get();
if (holder == null) {
if (optionalAuth.get()) {
return null;
}
throw new UnauthorizedException("Gateway token verification failed");
}
if (!"BUYER".equals(holder.getSource())) {
throw new UnauthorizedException("Gateway token verification failed");
}
return holder.getId();
} }
} }

View File

@@ -18,6 +18,15 @@ import java.util.stream.Collectors;
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<Object> handleUnauthorizedException(UnauthorizedException e) {
log.error("Unauthorized: {}", e.getMessage());
return new ResponseEntity<>(
Response.fail(401, e.getMessage()),
HttpStatus.UNAUTHORIZED
);
}
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
public Response<?> handleBusinessException(BusinessException e) { public Response<?> handleBusinessException(BusinessException e) {
log.error("业务异常: code={}, msg={}", e.getCode(), e.getMsg()); log.error("业务异常: code={}, msg={}", e.getCode(), e.getMsg());

View File

@@ -0,0 +1,15 @@
package com.aida.seller.common.exception;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String message) {
super(message);
}
public UnauthorizedException() {
super("Gateway token verification failed");
}
}

View File

@@ -3,13 +3,18 @@ package com.aida.seller.common.interceptor;
import com.aida.seller.common.context.UserContext; import com.aida.seller.common.context.UserContext;
import com.aida.seller.model.vo.AuthPrincipalVo; import com.aida.seller.model.vo.AuthPrincipalVo;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
import java.util.stream.Collectors;
/** /**
* 从 Gateway 转发的请求头中读取已鉴权的用户身份,写入 UserContext。 * 从 Gateway 转发的请求头中读取已鉴权的用户身份,写入 UserContext。
* <p> * <p>
@@ -18,13 +23,40 @@ import org.springframework.web.servlet.HandlerInterceptor;
*/ */
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor
public class UserContextInterceptor implements HandlerInterceptor { public class UserContextInterceptor implements HandlerInterceptor {
private static final String USER_ID_HEADER = "X-User-Id"; private static final String USER_ID_HEADER = "X-User-Id";
private static final String USER_INFO_HEADER = "X-User-Info"; private static final String USER_INFO_HEADER = "X-User-Info";
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper = new ObjectMapper();
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Value("${gateway.auth.optional-auth-paths:}")
private List<String> optionalAuthPaths;
private List<String> localOptionalAuthPaths;
@PostConstruct
public void init() {
localOptionalAuthPaths = optionalAuthPaths.stream()
.map(this::toLocalPath)
.collect(Collectors.toList());
log.info("Local optional auth paths: {}", localOptionalAuthPaths);
}
/**
* 将 Gateway 前端路径(如 /seller/listing/shop转换为服务本地路径如 /listing/shop
*/
private String toLocalPath(String gatewayPath) {
if (gatewayPath == null) {
return gatewayPath;
}
if (gatewayPath.startsWith("/seller")) {
String local = gatewayPath.substring("/seller".length());
return local.isEmpty() ? "/" : local;
}
return gatewayPath;
}
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
@@ -36,10 +68,24 @@ public class UserContextInterceptor implements HandlerInterceptor {
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to parse X-User-Info header: {}", e.getMessage()); log.warn("Failed to parse X-User-Info header: {}", e.getMessage());
} }
} else if (isOptionalAuthPath(request.getRequestURI())) {
UserContext.setOptionalAuth(true);
} }
return true; return true;
} }
private boolean isOptionalAuthPath(String requestUri) {
if (localOptionalAuthPaths == null || localOptionalAuthPaths.isEmpty()) {
return false;
}
for (String pattern : localOptionalAuthPaths) {
if (pathMatcher.match(pattern, requestUri)) {
return true;
}
}
return false;
}
@Override @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) { Object handler, Exception ex) {

View File

@@ -1,16 +1,14 @@
package com.aida.seller.config; package com.aida.seller.config;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.time.LocalDateTime;
@Configuration @Configuration
@EnableTransactionManagement
public class MyBatisPlusConfig { public class MyBatisPlusConfig {
@Bean @Bean
@@ -19,20 +17,4 @@ public class MyBatisPlusConfig {
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor; return interceptor;
} }
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
};
}
} }

View File

@@ -0,0 +1,23 @@
package com.aida.seller.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
}

View File

@@ -1,6 +1,7 @@
package com.aida.seller.config; package com.aida.seller.config;
import com.baomidou.mybatisplus.core.config.GlobalConfig; import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactory;
@@ -24,6 +25,9 @@ public class PrimaryDataSourceConfig {
@Autowired @Autowired
private MybatisPlusInterceptor mybatisPlusInterceptor; private MybatisPlusInterceptor mybatisPlusInterceptor;
@Autowired
private MetaObjectHandler myMetaObjectHandler;
@Primary @Primary
@Bean(name = "primaryDataSource") @Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary") @ConfigurationProperties(prefix = "spring.datasource.primary")
@@ -42,6 +46,7 @@ public class PrimaryDataSourceConfig {
GlobalConfig globalConfig = new GlobalConfig(); GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setBanner(false); globalConfig.setBanner(false);
globalConfig.setMetaObjectHandler(myMetaObjectHandler);
bean.setGlobalConfig(globalConfig); bean.setGlobalConfig(globalConfig);
bean.setPlugins(mybatisPlusInterceptor); bean.setPlugins(mybatisPlusInterceptor);

View File

@@ -8,12 +8,16 @@ import com.aida.seller.module.designer.dto.DesignerAuditDTO;
import com.aida.seller.module.designer.dto.DesignerDTO; import com.aida.seller.module.designer.dto.DesignerDTO;
import com.aida.seller.module.designer.entity.DesignerEntity; import com.aida.seller.module.designer.entity.DesignerEntity;
import com.aida.seller.module.designer.service.DesignerService; import com.aida.seller.module.designer.service.DesignerService;
import com.aida.seller.module.designer.vo.DesignerSearchVO;
import com.aida.seller.module.designer.vo.DesignerShopVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "设计师入驻管理") @Tag(name = "设计师入驻管理")
@RestController @RestController
@RequestMapping("/designer") @RequestMapping("/designer")
@@ -87,4 +91,28 @@ public class DesignerController {
DesignerDTO designerInfo = designerService.getDesignerInfo(userId); DesignerDTO designerInfo = designerService.getDesignerInfo(userId);
return Response.success(designerInfo); return Response.success(designerInfo);
} }
@Operation(summary = "删除设计师", description = "根据当前登录用户ID逻辑删除设计师及其所有关联数据订单、订单明细、商品、商品图片")
@DeleteMapping("/delete")
public Response<Void> delete() {
Long userId = UserContext.getUserId();
designerService.deleteByUserId(userId);
return Response.success();
}
@Operation(summary = "搜索设计师", description = "根据关键词不区分大小写同时匹配店铺名称和所有者姓名返回设计师信息、最近5张商品封面图按updateTime倒序、商品总数")
@GetMapping("/search")
public Response<List<DesignerSearchVO>> search(
@Parameter(description = "关键词(同时匹配店铺名称和所有者姓名,不区分大小写)") @RequestParam String keyword) {
List<DesignerSearchVO> result = designerService.searchDesigners(keyword);
return Response.success(result);
}
@Operation(summary = "获取商城店铺详情", description = "根据 sellerId即 卖家userId获取店铺公开信息供买家端店铺主页调用")
@GetMapping("/shop/{sellerId}")
public Response<DesignerShopVO> getShopDetail(
@Parameter(description = "设计师用户ID") @PathVariable Long sellerId) {
DesignerShopVO vo = designerService.getShopDetailBySellerId(sellerId);
return Response.success(vo);
}
} }

View File

@@ -1,5 +1,7 @@
package com.aida.seller.module.designer.dto; package com.aida.seller.module.designer.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;

View File

@@ -1,6 +1,8 @@
package com.aida.seller.module.designer.entity; package com.aida.seller.module.designer.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -16,6 +18,7 @@ public class DesignerEntity implements Serializable {
/** 设计师ID */ /** 设计师ID */
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id; private Long id;
/** 用户ID(关联用户表) */ /** 用户ID(关联用户表) */

View File

@@ -4,9 +4,13 @@ import com.aida.seller.module.designer.dto.DesignerApplyDTO;
import com.aida.seller.module.designer.dto.DesignerAuditDTO; import com.aida.seller.module.designer.dto.DesignerAuditDTO;
import com.aida.seller.module.designer.dto.DesignerDTO; import com.aida.seller.module.designer.dto.DesignerDTO;
import com.aida.seller.module.designer.entity.DesignerEntity; import com.aida.seller.module.designer.entity.DesignerEntity;
import com.aida.seller.module.designer.vo.DesignerSearchVO;
import com.aida.seller.module.designer.vo.DesignerShopVO;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface DesignerService extends IService<DesignerEntity> { public interface DesignerService extends IService<DesignerEntity> {
/** /**
@@ -49,4 +53,29 @@ public interface DesignerService extends IService<DesignerEntity> {
* 获取设计师详细信息 * 获取设计师详细信息
*/ */
DesignerDTO getDesignerInfo(Long userId); DesignerDTO getDesignerInfo(Long userId);
/**
* 删除设计师(逻辑删除)及其所有关联数据
* <p>级联删除seller_designer按userId→ seller_orders → seller_order_item、
* seller_listing → seller_listing_image</p>
*
* @param userId 用户ID
*/
void deleteByUserId(Long userId);
/**
* 模糊搜索设计师(不区分大小写),返回设计师信息及关联商品封面列表
*
* @param keyword 关键词(同时匹配店铺名称和所有者姓名,不区分大小写)
* @return 搜索结果列表每条包含设计师基础信息、最近5张商品封面图按updateTime倒序、商品总数
*/
List<DesignerSearchVO> searchDesigners(String keyword);
/**
* 根据 sellerId即 userId获取设计师店铺详情供买家端店铺主页调用
*
* @param sellerId 设计师用户ID
* @return 店铺详情
*/
DesignerShopVO getShopDetailBySellerId(Long sellerId);
} }

View File

@@ -9,6 +9,16 @@ import com.aida.seller.module.designer.dto.DesignerDTO;
import com.aida.seller.module.designer.entity.DesignerEntity; import com.aida.seller.module.designer.entity.DesignerEntity;
import com.aida.seller.module.designer.enums.DesignerApplyStatusEnum; import com.aida.seller.module.designer.enums.DesignerApplyStatusEnum;
import com.aida.seller.module.designer.mapper.DesignerMapper; import com.aida.seller.module.designer.mapper.DesignerMapper;
import com.aida.seller.module.designer.vo.DesignerSearchVO;
import com.aida.seller.module.designer.vo.DesignerShopVO;
import com.aida.seller.module.listing.entity.ListingEntity;
import com.aida.seller.module.listing.entity.ListingImageEntity;
import com.aida.seller.module.listing.mapper.ListingImageMapper;
import com.aida.seller.module.listing.mapper.ListingMapper;
import com.aida.seller.module.order.entity.OrderInfoEntity;
import com.aida.seller.module.order.entity.OrderItemEntity;
import com.aida.seller.module.order.mapper.OrderInfoMapper;
import com.aida.seller.module.order.mapper.OrderItemMapper;
import com.aida.seller.util.MinioUtil; import com.aida.seller.util.MinioUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -17,6 +27,11 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -25,6 +40,11 @@ import java.time.LocalDateTime;
public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEntity> implements DesignerService { public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEntity> implements DesignerService {
private final MinioUtil minioUtil; private final MinioUtil minioUtil;
private final OrderInfoMapper orderInfoMapper;
private final OrderItemMapper orderItemMapper;
private final ListingMapper listingMapper;
private final ListingImageMapper listingImageMapper;
@Override @Override
public Boolean checkQualification(Long userId) { public Boolean checkQualification(Long userId) {
DesignerEntity entity = this.getOne( DesignerEntity entity = this.getOne(
@@ -57,8 +77,16 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
DesignerEntity entity = new DesignerEntity(); DesignerEntity entity = new DesignerEntity();
entity.setUserId(UserContext.getUserId()); entity.setUserId(UserContext.getUserId());
entity.setShopName(dto.getShopName()); entity.setShopName(dto.getShopName());
entity.setAvatar(minioUtil.convertToLogicalPath(dto.getAvatar())); entity.setAvatar(
entity.setBrandBanner(minioUtil.convertToLogicalPath(dto.getBrandBanner())); dto.getAvatar() != null && !dto.getAvatar().isBlank()
? minioUtil.convertToLogicalPath(dto.getAvatar())
: null
);
entity.setBrandBanner(
dto.getBrandBanner() != null && !dto.getBrandBanner().isBlank()
? minioUtil.convertToLogicalPath(dto.getBrandBanner())
: null
);
entity.setOwnerName(dto.getOwnerName()); entity.setOwnerName(dto.getOwnerName());
entity.setEmail(dto.getEmail()); entity.setEmail(dto.getEmail());
entity.setMobile(dto.getMobile()); entity.setMobile(dto.getMobile());
@@ -162,12 +190,16 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
if (dto.getShopName() != null) { if (dto.getShopName() != null) {
entity.setShopName(dto.getShopName()); entity.setShopName(dto.getShopName());
} }
if (dto.getAvatar() != null) { entity.setAvatar(
entity.setAvatar(dto.getAvatar()); dto.getAvatar() != null && !dto.getAvatar().isBlank()
} ? minioUtil.convertToLogicalPath(dto.getAvatar())
if (dto.getBrandBanner() != null) { : null
entity.setBrandBanner(dto.getBrandBanner()); );
} entity.setBrandBanner(
dto.getBrandBanner() != null && !dto.getBrandBanner().isBlank()
? minioUtil.convertToLogicalPath(dto.getBrandBanner())
: null
);
if (dto.getOwnerName() != null) { if (dto.getOwnerName() != null) {
entity.setOwnerName(dto.getOwnerName()); entity.setOwnerName(dto.getOwnerName());
} }
@@ -210,4 +242,159 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
return dto; return dto;
} }
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteByUserId(Long userId) {
DesignerEntity designer = this.getOne(
new LambdaQueryWrapper<DesignerEntity>()
.eq(DesignerEntity::getUserId, userId)
.last("LIMIT 1")
);
if (designer == null) {
throw new BusinessException("设计师记录不存在");
}
Long sellerId = designer.getId();
// 1. 查询所有关联的 listing_id再删除 listing 及其图片
List<ListingEntity> listings = listingMapper.selectList(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getSellerId, sellerId)
);
if (!listings.isEmpty()) {
List<Long> listingIds = listings.stream()
.map(ListingEntity::getId)
.collect(Collectors.toList());
// 逻辑删除关联的图片
listingImageMapper.delete(
new LambdaQueryWrapper<ListingImageEntity>()
.in(ListingImageEntity::getListingId, listingIds)
);
// 逻辑删除 listing
listingMapper.delete(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getSellerId, sellerId)
);
}
// 2. 删除 seller_orders 及关联的 order_item
List<OrderInfoEntity> orders = orderInfoMapper.selectList(
new LambdaQueryWrapper<OrderInfoEntity>()
.eq(OrderInfoEntity::getSellerId, sellerId)
);
if (!orders.isEmpty()) {
List<Long> orderIds = orders.stream()
.map(OrderInfoEntity::getId)
.collect(Collectors.toList());
// 逻辑删除关联的订单明细
orderItemMapper.delete(
new LambdaQueryWrapper<OrderItemEntity>()
.in(OrderItemEntity::getOrderId, orderIds)
);
// 逻辑删除订单
orderInfoMapper.delete(
new LambdaQueryWrapper<OrderInfoEntity>()
.eq(OrderInfoEntity::getSellerId, sellerId)
);
}
// 3. 逻辑删除设计师本人
this.removeById(sellerId);
}
@Override
public List<DesignerSearchVO> searchDesigners(String keyword) {
// Step 1: 构造设计师模糊查询条件,同时匹配店铺名称和所有者姓名,不区分大小写
LambdaQueryWrapper<DesignerEntity> designerQuery = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
designerQuery.and(wrapper -> wrapper
.apply("LOWER(shop_name) LIKE LOWER({0})", "%" + keyword + "%")
.or()
.apply("LOWER(owner_name) LIKE LOWER({0})", "%" + keyword + "%")
);
}
List<DesignerEntity> designers = this.list(designerQuery);
if (designers.isEmpty()) {
return List.of();
}
// Step 2: 提取设计师的 userId 集合,用于后续按 userId 查询其关联商品
List<Long> userIds = designers.stream()
.map(DesignerEntity::getUserId)
.collect(Collectors.toList());
// Step 3: 查询所有匹配设计师关联的商品,按 updateTime 倒序
LambdaQueryWrapper<ListingEntity> listingQuery = new LambdaQueryWrapper<ListingEntity>()
.in(ListingEntity::getSellerId, userIds)
.orderByDesc(ListingEntity::getUpdateTime);
List<ListingEntity> listings = listingMapper.selectList(listingQuery);
if (listings.isEmpty()) {
return designers.stream().map(d -> buildSearchVO(d, List.of(), 0L))
.collect(Collectors.toList());
}
// Step 4: 按 sellerId 分组,统计每个设计师的商品总数
Map<Long, Long> listingCountMap = listings.stream()
.collect(Collectors.groupingBy(
ListingEntity::getSellerId,
Collectors.counting()
));
// Step 5: 按 sellerId 分组,便于后续取每个设计师的商品列表
Map<Long, List<ListingEntity>> listingsByDesigner = listings.stream()
.collect(Collectors.groupingBy(ListingEntity::getSellerId));
// Step 6: 组装每个设计师的搜索结果,最多取 5 个商品封面图
return designers.stream().map(d -> {
List<String> covers = listingsByDesigner
.getOrDefault(d.getUserId(), List.of())
.stream()
.filter(l -> l.getCover() != null && !l.getCover().isBlank())
.limit(5)
.map(l -> minioUtil.processMinioResource(l.getCover(), CommonConstants.MINIO_PATH_TIMEOUT))
.collect(Collectors.toList());
long listingTotal = listingCountMap.getOrDefault(d.getUserId(), 0L);
return buildSearchVO(d, covers, listingTotal);
}).collect(Collectors.toList());
}
private DesignerSearchVO buildSearchVO(DesignerEntity entity, List<String> covers, Long listingTotal) {
DesignerSearchVO vo = new DesignerSearchVO();
vo.setSellerId(entity.getUserId());
vo.setShopName(entity.getShopName());
vo.setOwnerName(entity.getOwnerName());
vo.setAvatar(minioUtil.processMinioResource(entity.getAvatar(), CommonConstants.MINIO_PATH_TIMEOUT));
vo.setCovers(covers);
vo.setListingTotal(listingTotal);
return vo;
}
@Override
public DesignerShopVO getShopDetailBySellerId(Long sellerId) {
DesignerEntity entity = this.getOne(
new LambdaQueryWrapper<DesignerEntity>()
.eq(DesignerEntity::getUserId, sellerId)
.last("LIMIT 1")
);
if (entity == null) {
throw new BusinessException("设计师不存在");
}
DesignerShopVO vo = new DesignerShopVO();
vo.setShopName(entity.getShopName());
vo.setAvatar(minioUtil.processMinioResource(entity.getAvatar(), CommonConstants.MINIO_PATH_TIMEOUT));
vo.setBrandBanner(minioUtil.processMinioResource(entity.getBrandBanner(), CommonConstants.MINIO_PATH_TIMEOUT));
vo.setOwnerName(entity.getOwnerName());
vo.setDescription(entity.getDescription());
vo.setSocialLinks(entity.getSocialLinks());
vo.setEmail(entity.getEmail());
vo.setMobile(entity.getMobile());
return vo;
}
} }

View File

@@ -1,5 +1,7 @@
package com.aida.seller.module.designer.vo; package com.aida.seller.module.designer.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -14,8 +16,6 @@ public class DesignerApplyDetailVo implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Schema(description = "设计师ID")
private Long id;
@Schema(description = "店铺名称") @Schema(description = "店铺名称")
private String shopName; private String shopName;

View File

@@ -0,0 +1,36 @@
package com.aida.seller.module.designer.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 设计师搜索结果VO
*/
@Data
public class DesignerSearchVO implements Serializable {
private static final long serialVersionUID = 1L;
/** 用户ID */
@JsonSerialize(using = ToStringSerializer.class)
private Long sellerId;
/** 店铺名称 */
private String shopName;
/** 所有者全名 */
private String ownerName;
/** 店铺头像URL */
private String avatar;
/** 商品封面图列表最多5张按更新时间倒序 */
private List<String> covers;
/** 该设计师的商品总数 */
private Long listingTotal;
}

View File

@@ -0,0 +1,38 @@
package com.aida.seller.module.designer.vo;
import lombok.Data;
import java.io.Serializable;
/**
* 设计师店铺详情VO供买家端店铺主页调用
*/
@Data
public class DesignerShopVO implements Serializable {
private static final long serialVersionUID = 1L;
/** 店铺名称 */
private String shopName;
/** 店铺头像URL */
private String avatar;
/** 品牌 Banner URL */
private String brandBanner;
/** 所有者全名 */
private String ownerName;
/** 店铺简介 */
private String description;
/** 社交媒体链接JSON 字符串) */
private String socialLinks;
/** 邮箱 */
private String email;
/** 手机号 */
private String mobile;
}

View File

@@ -29,9 +29,9 @@ public class ListingController {
@Operation(summary = "批量保存/更新商品", description = "根据 id 是否存在判断新增或更新,同时保存图片") @Operation(summary = "批量保存/更新商品", description = "根据 id 是否存在判断新增或更新,同时保存图片")
@PostMapping("/batch") @PostMapping("/batch")
public Response<Void> saveOrUpdate( public Response<Void> saveOrUpdate(
@Parameter(description = "商品保存/更新表单") @RequestBody ListingSaveDTO dto) { @Parameter(description = "商品保存/更新表单列表") @RequestBody List<ListingSaveDTO> dtoList) {
Long sellerId = UserContext.getUserId(); Long sellerId = UserContext.getUserId();
listingService.saveOrUpdate(dto, sellerId); listingService.saveOrUpdate(dtoList, sellerId);
return Response.success(); return Response.success();
} }
@@ -40,7 +40,7 @@ public class ListingController {
public Response<ListingSaveDTO> getById( public Response<ListingSaveDTO> getById(
@Parameter(description = "商品ID") @RequestParam Long id) { @Parameter(description = "商品ID") @RequestParam Long id) {
Long sellerId = UserContext.getUserId(); Long sellerId = UserContext.getUserId();
ListingSaveDTO result = listingService.getById(id, sellerId); ListingSaveDTO result = listingService.getById(id);
return Response.success(result); return Response.success(result);
} }
@@ -68,15 +68,6 @@ public class ListingController {
return Response.success(); return Response.success();
} }
@Operation(summary = "批量设置库存", description = "入参为商品ID和库存值列表")
@PutMapping("/stock/batch")
public Response<Void> batchUpdateStock(
@Parameter(description = "库存更新列表") @RequestBody List<ListingStockDTO> list) {
Long sellerId = UserContext.getUserId();
listingService.batchUpdateStock(list, sellerId);
return Response.success();
}
@Operation(summary = "设置弹窗提醒标志", description = "在Redis中设置7天过期的弹窗提醒标志") @Operation(summary = "设置弹窗提醒标志", description = "在Redis中设置7天过期的弹窗提醒标志")
@PostMapping("/popup/set") @PostMapping("/popup/set")
public Response<Void> setPopupReminder() { public Response<Void> setPopupReminder() {
@@ -92,4 +83,15 @@ public class ListingController {
boolean needPopup = listingService.checkPopupReminder(sellerId); boolean needPopup = listingService.checkPopupReminder(sellerId);
return Response.success(needPopup ? 1 : 0); return Response.success(needPopup ? 1 : 0);
} }
@Operation(summary = "获取店铺商品列表", description = "按返回店铺已发布商品分页列表")
@GetMapping("/shop/seller")
public Response<PageResponse<ListingPageVO>> getShopListings(
@Parameter(description = "设计师用户ID") @RequestParam Long sellerId,
@Parameter(description = "适用性别 female/male/all") @RequestParam String designFor,
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") int pageSize) {
IPage<ListingPageVO> page = listingService.getShopListings(sellerId, designFor, pageNum, pageSize);
return Response.success(PageResponse.success(page));
}
} }

View File

@@ -0,0 +1,52 @@
package com.aida.seller.module.listing.controller;
import com.aida.seller.common.result.PageResponse;
import com.aida.seller.common.result.Response;
import com.aida.seller.module.listing.dto.ListingMallQueryDTO;
import com.aida.seller.module.listing.service.ListingMallService;
import com.aida.seller.module.listing.vo.ListingDetailVO;
import com.aida.seller.module.listing.vo.ListingMallVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 商城首页商品 ControllerFeign 端)
*/
@Tag(name = "ListingMall - 商城首页商品")
@RestController
@RequestMapping("/listing")
@RequiredArgsConstructor
public class ListingMallController {
private final ListingMallService listingMallService;
@Operation(summary = "商城首页商品分页列表", description = "")
@PostMapping("/mall")
public Response<PageResponse<ListingMallVO>> getMallListings(
@RequestBody ListingMallQueryDTO dto) {
IPage<ListingMallVO> page = listingMallService.getMallListings(dto);
return Response.success(PageResponse.success(page));
}
@Operation(summary = "商品详情(商城详情页)", description = "关联图片与店铺信息")
@GetMapping("/mall/detail")
public Response<ListingDetailVO> getListingDetail(
@Parameter(description = "商品ID") @RequestParam Long id) {
ListingDetailVO detail = listingMallService.getListingDetail(id);
return Response.success(detail);
}
@Operation(summary = "批量获取商品简要信息(购物车用)", description = "返回指定商品ID列表的基本信息")
@PostMapping("/mall/batch")
public Response<List<ListingMallVO>> getListingSummaries(
@RequestBody List<Long> listingIds) {
List<ListingMallVO> result = listingMallService.getListingSummaries(listingIds);
return Response.success(result);
}
}

View File

@@ -1,5 +1,7 @@
package com.aida.seller.module.listing.dto; package com.aida.seller.module.listing.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -15,10 +17,11 @@ public class ListingImageDTO implements Serializable {
/** 图片ID有值则更新无则新增 */ /** 图片ID有值则更新无则新增 */
@Schema(description = "图片ID有值则更新无则新增") @Schema(description = "图片ID有值则更新无则新增")
@JsonSerialize(using = ToStringSerializer.class)
private Long id; private Long id;
/** 图片类别: cover/main_product/product/sketch/apparel */ /** 图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video */
@Schema(description = "图片类别: cover/main_product/product/sketch/apparel") @Schema(description = "图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video")
private String category; private String category;
/** 图片URL */ /** 图片URL */

View File

@@ -0,0 +1,34 @@
package com.aida.seller.module.listing.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 商城首页商品查询 DTO
*/
@Data
public class ListingMallQueryDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "适用性别 female/male/all不传表示全部")
private String designFor;
@Schema(description = "商品分类列表,支持多选,如 outwear&categories=dress")
private List<String> categories;
@Schema(description = "排序字段price/salesVolume/updateTime/viewCount/createTime默认 updateTime")
private String sortField = "updateTime";
@Schema(description = "排序方向asc/desc默认 desc")
private String sortOrder = "desc";
@Schema(description = "页码默认1")
private Integer pageNum = 1;
@Schema(description = "每页数量默认10")
private Integer pageSize = 10;
}

View File

@@ -1,5 +1,7 @@
package com.aida.seller.module.listing.dto; package com.aida.seller.module.listing.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -17,6 +19,7 @@ public class ListingSaveDTO implements Serializable {
/** 商品ID无则新建有则更新 */ /** 商品ID无则新建有则更新 */
@Schema(description = "商品ID无则新建有则更新") @Schema(description = "商品ID无则新建有则更新")
@JsonSerialize(using = ToStringSerializer.class)
private Long id; private Long id;
/** 商品标题 */ /** 商品标题 */
@@ -31,9 +34,6 @@ public class ListingSaveDTO implements Serializable {
@Schema(description = "价格") @Schema(description = "价格")
private BigDecimal price; private BigDecimal price;
/** 库存 */
@Schema(description = "库存")
private Integer stock;
/** 浏览量(更新时传入) */ /** 浏览量(更新时传入) */
@Schema(description = "浏览量(更新时传入)") @Schema(description = "浏览量(更新时传入)")
@@ -51,7 +51,7 @@ public class ListingSaveDTO implements Serializable {
@Schema(description = "适用性别: male/female") @Schema(description = "适用性别: male/female")
private String designFor; private String designFor;
/** 商品分类列表: outwear/trousers/blouse/dress/skirt/accessories */ /** 商品分类列表: outwear/trousers/blouse/dress/skirt/others/tops/bottoms */
@Schema(description = "商品分类列表: outwear/trousers/blouse/dress/skirt/accessories") @Schema(description = "商品分类列表: outwear/trousers/blouse/dress/skirt/others/tops/bottoms")
private List<String> productCategory; private List<String> productCategory;
} }

View File

@@ -1,23 +0,0 @@
package com.aida.seller.module.listing.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 批量库存更新 DTO
*/
@Data
public class ListingStockDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** 商品ID */
@Schema(description = "商品ID")
private Long id;
/** 库存值 */
@Schema(description = "库存值")
private Integer stock;
}

View File

@@ -1,8 +1,9 @@
package com.aida.seller.module.listing.entity; package com.aida.seller.module.listing.entity;
import com.aida.seller.module.listing.enums.ProductCategoryEnum;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -14,13 +15,14 @@ import java.util.List;
* 商品实体 * 商品实体
*/ */
@Data @Data
@TableName("seller_listing") @TableName(value = "seller_listing", autoResultMap = true)
public class ListingEntity implements Serializable { public class ListingEntity implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 商品ID */ /** 商品ID */
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id; private Long id;
/** 卖家ID */ /** 卖家ID */
@@ -35,8 +37,8 @@ public class ListingEntity implements Serializable {
/** 价格 */ /** 价格 */
private BigDecimal price; private BigDecimal price;
/** 库存数量 */ /** 量 */
private Integer stock; private Integer salesVolume;
/** 封面图URL列表页展示用 */ /** 封面图URL列表页展示用 */
private String cover; private String cover;
@@ -64,5 +66,5 @@ public class ListingEntity implements Serializable {
/** 商品分类列表 */ /** 商品分类列表 */
@TableField(typeHandler = JacksonTypeHandler.class) @TableField(typeHandler = JacksonTypeHandler.class)
private List<ProductCategoryEnum> productCategory; private List<String> productCategory;
} }

View File

@@ -1,6 +1,8 @@
package com.aida.seller.module.listing.entity; package com.aida.seller.module.listing.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -17,12 +19,14 @@ public class ListingImageEntity implements Serializable {
/** 图片ID */ /** 图片ID */
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id; private Long id;
/** 商品ID */ /** 商品ID */
@JsonSerialize(using = ToStringSerializer.class)
private Long listingId; private Long listingId;
/** 图片类别: cover/main_product/product/sketch/apparel */ /** 图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video */
private String category; private String category;
/** 图片URL */ /** 图片URL */
@@ -37,4 +41,8 @@ public class ListingImageEntity implements Serializable {
/** 创建时间 */ /** 创建时间 */
@TableField(fill = FieldFill.INSERT) @TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime; private LocalDateTime createTime;
/** 是否删除0-否1-是 */
@TableLogic
private Integer deleted;
} }

View File

@@ -11,7 +11,8 @@ import lombok.Getter;
public enum DesignForEnum { public enum DesignForEnum {
MALE("male", "男性"), MALE("male", "男性"),
FEMALE("female", "女性"); FEMALE("female", "女性"),
ALL("all", "全部");
private final String code; private final String code;
private final String desc; private final String desc;

View File

@@ -14,7 +14,11 @@ public enum ImageCategoryEnum {
MAIN_PRODUCT("main_product", "主产品图", false), MAIN_PRODUCT("main_product", "主产品图", false),
PRODUCT("product", "产品图", true), PRODUCT("product", "产品图", true),
SKETCH("sketch", "草图", false), SKETCH("sketch", "草图", false),
APPAREL("apparel", "成衣图", false); COVERFROM("cover_from", "封面源", false),
APPAREL("apparel", "成衣图", false),
FIRST_FRAME("firstFrame", "首帧图", true),
GIF("gif", "GIF图", true),
VIDEO("video", "视频", true);
private final String code; private final String code;
private final String desc; private final String desc;

View File

@@ -1,5 +1,7 @@
package com.aida.seller.module.listing.enums; package com.aida.seller.module.listing.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@@ -15,17 +17,21 @@ public enum ProductCategoryEnum {
BLOUSE("blouse", "衬衫"), BLOUSE("blouse", "衬衫"),
DRESS("dress", "连衣裙"), DRESS("dress", "连衣裙"),
SKIRT("skirt", "半身裙"), SKIRT("skirt", "半身裙"),
ACCESSORIES("accessories", "配饰"); OTHERS("others", "其他"),
TOP("tops", "上装"),
BOTTOMS("bottoms", "下装");
@JsonValue
private final String code; private final String code;
private final String desc; private final String desc;
@JsonCreator
public static ProductCategoryEnum of(String code) { public static ProductCategoryEnum of(String code) {
if (code == null) { if (code == null) {
return null; return null;
} }
for (ProductCategoryEnum value : values()) { for (ProductCategoryEnum value : values()) {
if (value.code.equals(code)) { if (value.code.equalsIgnoreCase(code)) {
return value; return value;
} }
} }

View File

@@ -0,0 +1,12 @@
package com.aida.seller.module.listing.mapper;
import com.aida.seller.module.listing.entity.ListingEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* 商城首页商品 Mapper
*/
@Mapper
public interface ListingMallMapper extends BaseMapper<ListingEntity> {
}

View File

@@ -0,0 +1,38 @@
package com.aida.seller.module.listing.service;
import com.aida.seller.module.listing.dto.ListingMallQueryDTO;
import com.aida.seller.module.listing.vo.ListingDetailVO;
import com.aida.seller.module.listing.vo.ListingMallVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import java.util.List;
/**
* 商城首页商品 Service 接口
*/
public interface ListingMallService {
/**
* 商城首页商品分页列表
*
* @param dto 查询条件
* @return 分页结果
*/
IPage<ListingMallVO> getMallListings(ListingMallQueryDTO dto);
/**
* 获取商品详情(商城详情页)
*
* @param id 商品ID
* @return 商品详情
*/
ListingDetailVO getListingDetail(Long id);
/**
* 批量获取商品简要信息(购物车用)
*
* @param listingIds 商品ID列表
* @return 商品简要信息列表
*/
List<ListingMallVO> getListingSummaries(List<Long> listingIds);
}

View File

@@ -2,12 +2,13 @@ package com.aida.seller.module.listing.service;
import com.aida.seller.module.listing.dto.ListingQueryDTO; import com.aida.seller.module.listing.dto.ListingQueryDTO;
import com.aida.seller.module.listing.dto.ListingSaveDTO; import com.aida.seller.module.listing.dto.ListingSaveDTO;
import com.aida.seller.module.listing.dto.ListingStockDTO;
import com.aida.seller.module.listing.entity.ListingEntity; import com.aida.seller.module.listing.entity.ListingEntity;
import com.aida.seller.module.listing.vo.ListingPageVO; import com.aida.seller.module.listing.vo.ListingPageVO;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/** /**
* 商品 Service 接口 * 商品 Service 接口
*/ */
@@ -21,14 +22,21 @@ public interface ListingService extends IService<ListingEntity> {
*/ */
void saveOrUpdate(ListingSaveDTO dto, Long sellerId); void saveOrUpdate(ListingSaveDTO dto, Long sellerId);
/**
* 批量保存或更新商品(含图片)
*
* @param dtoList 商品信息列表
* @param sellerId 卖家ID
*/
void saveOrUpdate(List<ListingSaveDTO> dtoList, Long sellerId);
/** /**
* 获取商品详情(含所有图片) * 获取商品详情(含所有图片)
* *
* @param id 商品ID * @param id 商品ID
* @param sellerId 卖家ID
* @return 商品详情 * @return 商品详情
*/ */
ListingSaveDTO getById(Long id, Long sellerId); ListingSaveDTO getById(Long id);
/** /**
* 分页查询商品列表 * 分页查询商品列表
@@ -48,14 +56,6 @@ public interface ListingService extends IService<ListingEntity> {
*/ */
void updateStatus(Long id, Integer status, Long sellerId); void updateStatus(Long id, Integer status, Long sellerId);
/**
* 批量更新库存
*
* @param list 库存列表
* @param sellerId 卖家ID
*/
void batchUpdateStock(java.util.List<ListingStockDTO> list, Long sellerId);
/** /**
* 设置弹窗提醒标志7天过期 * 设置弹窗提醒标志7天过期
* *
@@ -70,4 +70,16 @@ public interface ListingService extends IService<ListingEntity> {
* @return true需要弹窗false不需要 * @return true需要弹窗false不需要
*/ */
boolean checkPopupReminder(Long sellerId); boolean checkPopupReminder(Long sellerId);
/**
* 获取店铺已发布商品列表,供买家端店铺主页调用
* <p>按 status=1、deleted=0、sellerId、designFor 筛选,按 updateTime 倒序</p>
*
* @param sellerId 设计师用户ID
* @param designFor 适用性别 female/maleall 表示不限制性别
* @param pageNum 页码
* @param pageSize 每页数量
* @return 分页商品列表
*/
IPage<ListingPageVO> getShopListings(Long sellerId, String designFor, int pageNum, int pageSize);
} }

View File

@@ -1,6 +1,7 @@
package com.aida.seller.module.listing.service; package com.aida.seller.module.listing.service;
import com.aida.seller.common.constants.CommonConstants; import com.aida.seller.common.constants.CommonConstants;
import com.aida.seller.common.context.UserContext;
import com.aida.seller.common.exception.BusinessException; import com.aida.seller.common.exception.BusinessException;
import com.aida.seller.module.listing.dto.*; import com.aida.seller.module.listing.dto.*;
import com.aida.seller.module.listing.entity.ListingEntity; import com.aida.seller.module.listing.entity.ListingEntity;
@@ -29,6 +30,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -45,17 +47,13 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(ListingSaveDTO dto, Long sellerId) { public void saveOrUpdate(ListingSaveDTO dto, Long sellerId) {
// 当 status 为 1已发布检查必填字段
validateListingFields(dto);
ListingEntity entity = new ListingEntity(); ListingEntity entity = new ListingEntity();
BeanUtils.copyProperties(dto, entity); BeanUtils.copyProperties(dto, entity);
entity.setSellerId(sellerId); entity.setSellerId(sellerId);
if (!CollectionUtils.isEmpty(dto.getProductCategory())) {
List<ProductCategoryEnum> categories = dto.getProductCategory().stream()
.map(ProductCategoryEnum::of)
.filter(Objects::nonNull)
.collect(Collectors.toList());
entity.setProductCategory(categories.isEmpty() ? null : categories);
}
if (dto.getDesignFor() != null && DesignForEnum.of(dto.getDesignFor()) == null) { if (dto.getDesignFor() != null && DesignForEnum.of(dto.getDesignFor()) == null) {
throw new BusinessException("designFor 只能为 male/female"); throw new BusinessException("designFor 只能为 male/female");
@@ -67,14 +65,15 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
Long listingId; Long listingId;
if (dto.getId() == null) { if (dto.getId() == null) {
entity.setStatus(ListingStatusEnum.DRAFT.getCode()); entity.setStatus(dto.getStatus());
this.save(entity); this.save(entity);
listingId = entity.getId(); listingId = entity.getId();
} else { } else {
ListingEntity existing = this.getOne( ListingEntity existing = this.getOne(
new LambdaQueryWrapper<ListingEntity>() new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getId, dto.getId()) .eq(ListingEntity::getId, dto.getId())
.eq(ListingEntity::getSellerId, sellerId)); .eq(ListingEntity::getSellerId, sellerId)
.eq(ListingEntity::getDeleted, 0));
if (existing == null) { if (existing == null) {
throw new BusinessException("商品不存在"); throw new BusinessException("商品不存在");
} }
@@ -86,6 +85,7 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
} }
if (!CollectionUtils.isEmpty(dto.getImages())) { if (!CollectionUtils.isEmpty(dto.getImages())) {
validateImages(dto.getImages());
handleImages(listingId, dto.getImages()); handleImages(listingId, dto.getImages());
String cover = extractCover(dto.getImages()); String cover = extractCover(dto.getImages());
if (StringUtils.hasText(cover)) { if (StringUtils.hasText(cover)) {
@@ -98,11 +98,23 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
} }
@Override @Override
public ListingSaveDTO getById(Long id, Long sellerId) { @Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(List<ListingSaveDTO> dtoList, Long sellerId) {
if (CollectionUtils.isEmpty(dtoList)) {
return;
}
for (ListingSaveDTO dto : dtoList) {
saveOrUpdate(dto, sellerId);
}
}
@Override
public ListingSaveDTO getById(Long id) {
ListingEntity entity = this.getOne( ListingEntity entity = this.getOne(
new LambdaQueryWrapper<ListingEntity>() new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getId, id) .eq(ListingEntity::getId, id)
.eq(ListingEntity::getSellerId, sellerId)); .eq(ListingEntity::getDeleted, 0));
if (entity == null) { if (entity == null) {
throw new BusinessException("商品不存在"); throw new BusinessException("商品不存在");
} }
@@ -110,13 +122,6 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
ListingSaveDTO dto = new ListingSaveDTO(); ListingSaveDTO dto = new ListingSaveDTO();
BeanUtils.copyProperties(entity, dto); BeanUtils.copyProperties(entity, dto);
if (!CollectionUtils.isEmpty(entity.getProductCategory())) {
dto.setProductCategory(
entity.getProductCategory().stream()
.map(ProductCategoryEnum::getCode)
.collect(Collectors.toList()));
}
List<ListingImageEntity> images = listingImageMapper.selectList( List<ListingImageEntity> images = listingImageMapper.selectList(
new LambdaQueryWrapper<ListingImageEntity>() new LambdaQueryWrapper<ListingImageEntity>()
.eq(ListingImageEntity::getListingId, id) .eq(ListingImageEntity::getListingId, id)
@@ -125,7 +130,7 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
List<ListingImageDTO> imageDTOs = images.stream().map(img -> { List<ListingImageDTO> imageDTOs = images.stream().map(img -> {
ListingImageDTO imgDto = new ListingImageDTO(); ListingImageDTO imgDto = new ListingImageDTO();
BeanUtils.copyProperties(img, imgDto); BeanUtils.copyProperties(img, imgDto);
imgDto.setImageUrl(minioUtil.processMinioResource(imgDto.getImageUrl(),CommonConstants.MINIO_PATH_TIMEOUT)); imgDto.setImageUrl(minioUtil.processMinioResource(imgDto.getImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
imgDto.setIsSelected(img.getIsSelected() != null && img.getIsSelected() == 1); imgDto.setIsSelected(img.getIsSelected() != null && img.getIsSelected() == 1);
return imgDto; return imgDto;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
@@ -139,12 +144,13 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
Page<ListingEntity> pageParam = new Page<>(dto.getPageNum(), dto.getPageSize()); Page<ListingEntity> pageParam = new Page<>(dto.getPageNum(), dto.getPageSize());
LambdaQueryWrapper<ListingEntity> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<ListingEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ListingEntity::getSellerId, sellerId); queryWrapper.eq(ListingEntity::getSellerId, sellerId);
queryWrapper.eq(ListingEntity::getDeleted, 0);
if (dto.getStatus() != null) { if (dto.getStatus() != null) {
queryWrapper.eq(ListingEntity::getStatus, dto.getStatus()); queryWrapper.eq(ListingEntity::getStatus, dto.getStatus());
} else { } else {
queryWrapper.ne(ListingEntity::getStatus, ListingStatusEnum.DELETED.getCode()); queryWrapper.ne(ListingEntity::getStatus, ListingStatusEnum.DELETED.getCode());
} }
queryWrapper.orderByDesc(ListingEntity::getCreateTime); queryWrapper.orderByDesc(ListingEntity::getUpdateTime);
IPage<ListingEntity> page = this.page(pageParam, queryWrapper); IPage<ListingEntity> page = this.page(pageParam, queryWrapper);
Page<ListingPageVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); Page<ListingPageVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
@@ -162,7 +168,8 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
ListingEntity existing = this.getOne( ListingEntity existing = this.getOne(
new LambdaQueryWrapper<ListingEntity>() new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getId, id) .eq(ListingEntity::getId, id)
.eq(ListingEntity::getSellerId, sellerId)); .eq(ListingEntity::getSellerId, sellerId)
.eq(ListingEntity::getDeleted, 0));
if (existing == null) { if (existing == null) {
throw new BusinessException("商品不存在"); throw new BusinessException("商品不存在");
} }
@@ -172,26 +179,6 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
this.updateById(update); this.updateById(update);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public void batchUpdateStock(List<ListingStockDTO> list, Long sellerId) {
if (CollectionUtils.isEmpty(list)) {
return;
}
List<Long> ids = list.stream().map(ListingStockDTO::getId).collect(Collectors.toList());
List<ListingEntity> existing = this.list(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getSellerId, sellerId)
.in(ListingEntity::getId, ids));
if (existing.size() != ids.size()) {
throw new BusinessException("部分商品不存在或无权操作");
}
Map<Long, Integer> stockMap = list.stream()
.collect(Collectors.toMap(ListingStockDTO::getId, ListingStockDTO::getStock));
existing.forEach(e -> e.setStock(stockMap.get(e.getId())));
this.updateBatchById(existing);
}
private void handleImages(Long listingId, List<ListingImageDTO> images) { private void handleImages(Long listingId, List<ListingImageDTO> images) {
Map<String, List<ListingImageDTO>> byCategory = images.stream() Map<String, List<ListingImageDTO>> byCategory = images.stream()
.collect(Collectors.groupingBy(img -> img.getCategory() == null ? "" : img.getCategory())); .collect(Collectors.groupingBy(img -> img.getCategory() == null ? "" : img.getCategory()));
@@ -209,18 +196,49 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
for (int i = 0; i < categoryImages.size(); i++) { for (int i = 0; i < categoryImages.size(); i++) {
ListingImageDTO imgDto = categoryImages.get(i); ListingImageDTO imgDto = categoryImages.get(i);
ListingImageEntity imgEntity = new ListingImageEntity(); if (imgDto.getImageUrl() != null && !imgDto.getImageUrl().isEmpty()) {
imgEntity.setListingId(listingId); ListingImageEntity imgEntity = new ListingImageEntity();
imgEntity.setCategory(category); imgEntity.setListingId(listingId);
imgEntity.setImageUrl(minioUtil.convertToLogicalPath(imgDto.getImageUrl())); imgEntity.setCategory(category);
imgEntity.setSortOrder(imgDto.getSortOrder() != null ? imgDto.getSortOrder() : i);
if (supportsSelection) { imgEntity.setImageUrl(minioUtil.convertToLogicalPath(imgDto.getImageUrl()));
imgEntity.setIsSelected(Boolean.TRUE.equals(imgDto.getIsSelected()) ? 1 : 0); imgEntity.setSortOrder(imgDto.getSortOrder() != null ? imgDto.getSortOrder() : i);
} else {
imgEntity.setIsSelected(0); if (supportsSelection) {
imgEntity.setIsSelected(Boolean.TRUE.equals(imgDto.getIsSelected()) ? 1 : 0);
} else {
imgEntity.setIsSelected(0);
}
listingImageMapper.insert(imgEntity);
}
}
}
}
private void validateImages(List<ListingImageDTO> images) {
Set<String> requiredCategories = Set.of(
ImageCategoryEnum.COVER.getCode(),
ImageCategoryEnum.COVERFROM.getCode(),
ImageCategoryEnum.SKETCH.getCode(),
ImageCategoryEnum.APPAREL.getCode());
Set<String> presentCategories = images.stream()
.map(ListingImageDTO::getCategory)
.filter(StringUtils::hasText)
.collect(Collectors.toSet());
for (String category : requiredCategories) {
if (!presentCategories.contains(category)) {
throw new BusinessException("category [" + category + "] is required");
}
}
for (ListingImageDTO img : images) {
String category = img.getCategory();
if (requiredCategories.contains(category)) {
if (!StringUtils.hasText(img.getImageUrl())) {
throw new BusinessException(category + " category imageUrl cannot be null or empty");
} }
listingImageMapper.insert(imgEntity);
} }
} }
} }
@@ -234,12 +252,10 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
return img.getImageUrl(); return img.getImageUrl();
} }
} }
if (!CollectionUtils.isEmpty(images)) {
return images.get(0).getImageUrl();
}
return null; return null;
} }
@Override @Override
public void setPopupReminder(Long sellerId) { public void setPopupReminder(Long sellerId) {
String key = "popup:reminder:" + sellerId; String key = "popup:reminder:" + sellerId;
@@ -252,4 +268,61 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
Object value = redisTemplate.opsForValue().get(key); Object value = redisTemplate.opsForValue().get(key);
return value == null; return value == null;
} }
/**
* 校验商品字段
* 当 status 为 1已发布检查必填字段
*/
private void validateListingFields(ListingSaveDTO dto) {
if (!StringUtils.hasText(dto.getTitle())) {
throw new BusinessException("商品标题不能为空");
}
if (!StringUtils.hasText(dto.getDescription())) {
throw new BusinessException("商品描述不能为空");
}
if (dto.getPrice() == null) {
throw new BusinessException("商品价格不能为空");
}
if (!StringUtils.hasText(dto.getDesignFor())) {
throw new BusinessException("适用性别不能为空");
}
if (DesignForEnum.of(dto.getDesignFor()) == null) {
throw new BusinessException("适用性别只能为 male/female");
}
if (CollectionUtils.isEmpty(dto.getProductCategory())) {
throw new BusinessException("商品分类不能为空");
}
for (String category : dto.getProductCategory()) {
if (ProductCategoryEnum.of(category) == null) {
throw new BusinessException("商品分类只能为 outwear/trousers/blouse/dress/skirt/others/tops/bottoms");
}
}
}
@Override
public IPage<ListingPageVO> getShopListings(Long sellerId, String designFor, int pageNum, int pageSize) {
Page<ListingEntity> pageParam = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<ListingEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ListingEntity::getSellerId, sellerId);
DesignForEnum designForEnum = DesignForEnum.of(designFor);
if (designForEnum == null) {
throw new BusinessException("designFor 只能为 female/male/all");
}
if (designForEnum != DesignForEnum.ALL) {
queryWrapper.eq(ListingEntity::getDesignFor, designForEnum.getCode());
}
queryWrapper.eq(ListingEntity::getStatus, 1);
queryWrapper.eq(ListingEntity::getDeleted, 0);
queryWrapper.orderByDesc(ListingEntity::getUpdateTime);
IPage<ListingEntity> page = this.page(pageParam, queryWrapper);
Page<ListingPageVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
result.setRecords(page.getRecords().stream().map(entity -> {
ListingPageVO vo = new ListingPageVO();
BeanUtils.copyProperties(entity, vo);
vo.setCover(minioUtil.processMinioResource(vo.getCover(), CommonConstants.MINIO_PATH_TIMEOUT));
return vo;
}).collect(Collectors.toList()));
return result;
}
} }

View File

@@ -0,0 +1,222 @@
package com.aida.seller.module.listing.service.impl;
import com.aida.seller.common.constants.CommonConstants;
import com.aida.seller.common.exception.BusinessException;
import com.aida.seller.module.designer.entity.DesignerEntity;
import com.aida.seller.module.designer.mapper.DesignerMapper;
import com.aida.seller.module.listing.dto.ListingMallQueryDTO;
import com.aida.seller.module.listing.entity.ListingEntity;
import com.aida.seller.module.listing.entity.ListingImageEntity;
import com.aida.seller.module.listing.enums.DesignForEnum;
import com.aida.seller.module.listing.mapper.ListingImageMapper;
import com.aida.seller.module.listing.mapper.ListingMallMapper;
import com.aida.seller.module.listing.vo.ListingDetailVO;
import com.aida.seller.module.listing.vo.ListingMallVO;
import com.aida.seller.util.MinioUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.aida.seller.module.listing.vo.ListingMallVO;
/**
* 商城首页商品 Service 实现
*/
@Service
public class ListingMallServiceImpl extends ServiceImpl<ListingMallMapper, ListingEntity> implements com.aida.seller.module.listing.service.ListingMallService {
private final MinioUtil minioUtil;
private final ListingImageMapper listingImageMapper;
private final DesignerMapper designerMapper;
public ListingMallServiceImpl(MinioUtil minioUtil, ListingImageMapper listingImageMapper, DesignerMapper designerMapper) {
this.minioUtil = minioUtil;
this.listingImageMapper = listingImageMapper;
this.designerMapper = designerMapper;
}
@Override
public IPage<ListingMallVO> getMallListings(ListingMallQueryDTO dto) {
Page<ListingEntity> pageParam = new Page<>(dto.getPageNum(), dto.getPageSize());
LambdaQueryWrapper<ListingEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ListingEntity::getStatus, 1);
queryWrapper.eq(ListingEntity::getDeleted, 0);
if (dto.getDesignFor() != null && !dto.getDesignFor().isEmpty()) {
DesignForEnum designForEnum = DesignForEnum.of(dto.getDesignFor());
if (designForEnum != null && designForEnum != DesignForEnum.ALL) {
queryWrapper.eq(ListingEntity::getDesignFor, designForEnum.getCode());
}
}
if (!CollectionUtils.isEmpty(dto.getCategories())) {
for (String cat : dto.getCategories()) {
queryWrapper.apply(
"JSON_CONTAINS(product_category, {0}, '$')",
"\"" + cat + "\""
);
}
}
applySorting(queryWrapper, dto.getSortField(), dto.getSortOrder());
IPage<ListingEntity> page = this.page(pageParam, queryWrapper);
Page<ListingMallVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
result.setRecords(page.getRecords().stream().map(entity -> {
ListingMallVO vo = new ListingMallVO();
vo.setId(entity.getId());
vo.setCover(minioUtil.processMinioResource(entity.getCover(), CommonConstants.MINIO_PATH_TIMEOUT));
vo.setTitle(entity.getTitle());
vo.setPrice(entity.getPrice());
return vo;
}).toList());
return result;
}
private void applySorting(LambdaQueryWrapper<ListingEntity> queryWrapper, String sortField, String sortOrder) {
if (!StringUtils.hasText(sortField)) {
queryWrapper.orderByDesc(ListingEntity::getUpdateTime);
return;
}
boolean isAsc = "asc".equalsIgnoreCase(sortOrder);
switch (sortField.toLowerCase()) {
case "price" -> {
if (isAsc) queryWrapper.orderByAsc(ListingEntity::getPrice);
else queryWrapper.orderByDesc(ListingEntity::getPrice);
}
case "salesvolume" -> {
if (isAsc) queryWrapper.orderByAsc(ListingEntity::getSalesVolume);
else queryWrapper.orderByDesc(ListingEntity::getSalesVolume);
}
case "viewcount" -> {
if (isAsc) queryWrapper.orderByAsc(ListingEntity::getViewCount);
else queryWrapper.orderByDesc(ListingEntity::getViewCount);
}
case "createtime" -> {
if (isAsc) queryWrapper.orderByAsc(ListingEntity::getCreateTime);
else queryWrapper.orderByDesc(ListingEntity::getCreateTime);
}
default -> queryWrapper.orderByDesc(ListingEntity::getUpdateTime);
}
}
@Override
public ListingDetailVO getListingDetail(Long id) {
ListingEntity entity = this.getOne(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getId, id)
.eq(ListingEntity::getStatus, 1)
.eq(ListingEntity::getDeleted, 0));
if (entity == null) {
throw new BusinessException("商品不存在");
}
List<ListingImageEntity> images = listingImageMapper.selectList(
new LambdaQueryWrapper<ListingImageEntity>()
.eq(ListingImageEntity::getListingId, id)
.orderByAsc(ListingImageEntity::getSortOrder));
Map<String, List<String>> imageMap = images.stream()
.filter(img -> StringUtils.hasText(img.getCategory()))
// 先按 category 分组,再按 sortOrder 组内排序,确保同组图片按 sortOrder 升序排列
.sorted(Comparator.comparing(ListingImageEntity::getCategory)
.thenComparing(ListingImageEntity::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder())))
.collect(Collectors.groupingBy(
ListingImageEntity::getCategory,
Collectors.mapping(
img -> minioUtil.processMinioResource(img.getImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT),
Collectors.toList()
)
));
List<String> mainProductUrls = imageMap.get("main_product");
if (mainProductUrls != null && !mainProductUrls.isEmpty()) {
List<String> productUrls = imageMap.get("product");
if (productUrls != null) {
imageMap.put("product", productUrls.stream()
.filter(url -> !mainProductUrls.contains(url))
.toList());
}
}
DesignerEntity designer = designerMapper.selectOne(
new LambdaQueryWrapper<DesignerEntity>()
.eq(DesignerEntity::getUserId, entity.getSellerId())
.eq(DesignerEntity::getDeleted, 0));
ListingDetailVO vo = new ListingDetailVO();
vo.setId(entity.getId());
vo.setTitle(entity.getTitle());
vo.setDescription(entity.getDescription());
vo.setPrice(entity.getPrice());
vo.setUpdateTime(entity.getUpdateTime());
vo.setShopName(designer != null ? designer.getShopName() : null);
vo.setImages(imageMap);
vo.setDesignFor(entity.getDesignFor());
vo.setProductCategory(entity.getProductCategory());
vo.setAvatar(minioUtil.processMinioResource(designer != null ? designer.getAvatar() : null, CommonConstants.MINIO_PATH_TIMEOUT));
return vo;
}
@Override
public List<ListingMallVO> getListingSummaries(List<Long> listingIds) {
if (listingIds == null || listingIds.isEmpty()) {
return List.of();
}
List<ListingEntity> entities = this.list(
new LambdaQueryWrapper<ListingEntity>()
.in(ListingEntity::getId, listingIds)
.eq(ListingEntity::getDeleted, 0));
Map<Long, ListingEntity> entityMap = entities.stream()
.collect(Collectors.toMap(ListingEntity::getId, e -> e));
List<Long> sellerIds = entities.stream()
.map(ListingEntity::getSellerId)
.distinct()
.toList();
Map<Long, DesignerEntity> designerMap = designerMapper.selectList(
new LambdaQueryWrapper<DesignerEntity>()
.select(DesignerEntity::getUserId, DesignerEntity::getShopName)
.in(DesignerEntity::getUserId, sellerIds)
.eq(DesignerEntity::getDeleted, 0))
.stream()
.collect(Collectors.toMap(
DesignerEntity::getUserId,
designer -> designer,
// 如果 sellerIds 中有重复的 userId保留第一个
(existing, replacement) -> existing
));
List<ListingMallVO> listingMallVOS = listingIds.stream()
.filter(entityMap::containsKey)
.map(id -> {
ListingEntity entity = entityMap.get(id);
DesignerEntity designer = designerMap.get(entity.getSellerId());
ListingMallVO vo = new ListingMallVO();
vo.setId(entity.getId());
vo.setCover(minioUtil.processMinioResource(entity.getCover(), CommonConstants.MINIO_PATH_TIMEOUT));
vo.setTitle(entity.getTitle());
vo.setPrice(entity.getPrice());
vo.setStatus(entity.getStatus());
vo.setProductCategory(entity.getProductCategory());
vo.setShopName(designer != null ? designer.getShopName() : null);
return vo;
})
.toList();
return listingMallVOS;
}
}

View File

@@ -0,0 +1,55 @@
package com.aida.seller.module.listing.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 商品详情 VO商城详情页
*/
@Data
@Schema(description = "商品详情")
public class ListingDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "商品ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
@Schema(description = "商品标题")
private String title;
@Schema(description = "商品描述")
private String description;
@Schema(description = "价格")
private BigDecimal price;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "店铺名称")
private String shopName;
@Schema(description = "图片列表key 为 categoryvalue 为该分类下所有图片 URL")
private Map<String, List<String>> images;
/** 适用性别: male/female */
private String designFor;
/** 商品分类列表 */
private List<String> productCategory;
/** 店铺头像URL */
private String avatar;
}

View File

@@ -0,0 +1,36 @@
package com.aida.seller.module.listing.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 商城首页商品 VO
*/
@Data
public class ListingMallVO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String cover;
private String title;
private BigDecimal price;
private Integer status;
/** 商品分类列表 */
private List<String> productCategory;
/** 店铺名称 */
private String shopName;
}

View File

@@ -1,5 +1,7 @@
package com.aida.seller.module.listing.vo; package com.aida.seller.module.listing.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -15,6 +17,7 @@ public class ListingPageVO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 商品ID */ /** 商品ID */
@JsonSerialize(using = ToStringSerializer.class)
private Long id; private Long id;
/** 封面图URL列表页直返无须关联图片表 */ /** 封面图URL列表页直返无须关联图片表 */
@@ -26,15 +29,6 @@ public class ListingPageVO implements Serializable {
/** 价格 */ /** 价格 */
private BigDecimal price; private BigDecimal price;
/** 库存 */ /** 修改时间 */
private Integer stock; private LocalDateTime updateTime;
/** 浏览量 */
private Integer viewCount;
/** 状态 */
private Integer status;
/** 创建时间(用于排序) */
private LocalDateTime createTime;
} }

View File

@@ -1,20 +1,26 @@
package com.aida.seller.module.order.controller; package com.aida.seller.module.order.controller;
import com.aida.seller.common.annotation.InternalOnly;
import com.aida.seller.common.context.UserContext; import com.aida.seller.common.context.UserContext;
import com.aida.seller.common.result.PageResponse; import com.aida.seller.common.result.PageResponse;
import com.aida.seller.common.result.Response; import com.aida.seller.common.result.Response;
import com.aida.seller.module.order.dto.AssetsDTO;
import com.aida.seller.module.order.dto.CreateOrderDTO;
import com.aida.seller.module.order.dto.OrderListDTO; import com.aida.seller.module.order.dto.OrderListDTO;
import com.aida.seller.module.order.dto.BuyerOrdersDTO;
import com.aida.seller.module.order.dto.UpdateOrderStatusDTO;
import com.aida.seller.module.order.service.OrderService; import com.aida.seller.module.order.service.OrderService;
import com.aida.seller.module.order.vo.AssetsItemVO;
import com.aida.seller.module.order.vo.BuyerOrderVO;
import com.aida.seller.module.order.vo.CreateOrderResultVO;
import com.aida.seller.module.order.vo.OrderSummaryVO; import com.aida.seller.module.order.vo.OrderSummaryVO;
import com.aida.seller.module.order.vo.OrderVO; import com.aida.seller.module.order.vo.OrderVO;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** /**
* My Orders - 订单管理控制器 * My Orders - 订单管理控制器
@@ -61,4 +67,33 @@ public class OrderController {
IPage<OrderVO> orderPage = orderService.getOrderPage(dto, sellerId); IPage<OrderVO> orderPage = orderService.getOrderPage(dto, sellerId);
return Response.success(PageResponse.success(orderPage)); return Response.success(PageResponse.success(orderPage));
} }
@InternalOnly
@PostMapping("/buyer/orders")
@Operation(summary = "根据买家ID查询订单列表供远程调用")
public Response<PageResponse<BuyerOrderVO>> getOrdersByBuyerId(@RequestBody BuyerOrdersDTO dto) {
return Response.success(PageResponse.success(orderService.getOrdersByBuyerId(dto)));
}
@InternalOnly
@PostMapping("/create")
@Operation(summary = "创建订单(按卖家分组合并)")
public Response<CreateOrderResultVO> createOrder(@RequestBody CreateOrderDTO dto) {
return Response.success(orderService.createOrder(dto));
}
@InternalOnly
@PutMapping("/status/batch")
@Operation(summary = "批量修改订单状态(仅内部服务调用)")
public Response<Void> updateOrderStatus(@RequestBody UpdateOrderStatusDTO dto) {
orderService.updateOrderStatus(dto);
return Response.success();
}
@InternalOnly
@PostMapping("/assets/page")
@Operation(summary = "我的资产分页查询")
public Response<PageResponse<AssetsItemVO>> getAssetsPage(@RequestBody AssetsDTO dto) {
return Response.success(PageResponse.success(orderService.getAssetsPage(dto)));
}
} }

View File

@@ -0,0 +1,29 @@
package com.aida.seller.module.order.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
@Schema(description = "我的资产分页查询请求参数")
public class AssetsDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "买家ID")
private Long buyerId;
@Schema(description = "商品分类列表,支持多选,如 outwear&categories=dress")
private List<String> categories;
@Schema(description = "适用性别 female/male/all不传表示全部")
private String designFor;
@Schema(description = "页码默认1")
private long page = 1;
@Schema(description = "每页数量默认10")
private long size = 10;
}

View File

@@ -0,0 +1,25 @@
package com.aida.seller.module.order.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "根据买家ID查询订单列表请求参数")
public class BuyerOrdersDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "买家ID")
private Long buyerId;
@Schema(description = "页码默认1")
private long page = 1;
@Schema(description = "每页数量默认10")
private long size = 10;
@Schema(description = "订单状态0-未支付1-已支付2-已取消")
private Integer status;
}

View File

@@ -0,0 +1,26 @@
package com.aida.seller.module.order.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 创建订单请求参数
*/
@Data
@Schema(description = "创建订单请求参数")
public class CreateOrderDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "商品ID列表")
private List<Long> listingIds;
@Schema(description = "买家ID")
private Long buyerId;
@Schema(description = "买家账号")
private String buyerUsername;
}

View File

@@ -0,0 +1,23 @@
package com.aida.seller.module.order.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 批量修改订单状态请求参数
*/
@Data
@Schema(description = "批量修改订单状态请求参数")
public class UpdateOrderStatusDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "订单ID列表")
private List<Long> orderIds;
@Schema(description = "目标状态0-未支付1-已支付2-已取消")
private Integer status;
}

View File

@@ -1,6 +1,8 @@
package com.aida.seller.module.order.entity; package com.aida.seller.module.order.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -17,14 +19,21 @@ public class OrderInfoEntity implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id; private Long id;
/** 订单唯一标识(如 SP897772698 */
private String orderId;
/** 卖家ID */ /** 卖家ID */
private Long sellerId; private Long sellerId;
/** 买家ID */
private Long buyerId;
/** 订单状态0-未支付1-已支付2-已取消 */
private Integer status;
/** 店铺名称 */
private String shopName;
/** 订单总金额HK$ */ /** 订单总金额HK$ */
private BigDecimal totalPrice; private BigDecimal totalPrice;
@@ -37,7 +46,8 @@ public class OrderInfoEntity implements Serializable {
/** 总浏览量 */ /** 总浏览量 */
private Long totalViews; private Long totalViews;
/** 下单时间 */ /** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime; private LocalDateTime createTime;
/** 更新时间 */ /** 更新时间 */

View File

@@ -1,33 +1,46 @@
package com.aida.seller.module.order.entity; package com.aida.seller.module.order.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
/** /**
* 订单商品明细表 * 订单商品明细表
*/ */
@Data @Data
@TableName("seller_order_item") @TableName(value = "seller_order_item", autoResultMap = true)
public class OrderItemEntity implements Serializable { public class OrderItemEntity implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 主键ID */ /** 主键ID */
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id; private Long id;
/** 订单ID关联 order_info */ /** 订单ID关联 seller_orders */
private String orderId; @JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
/** 卖家ID */
private Long sellerId;
/** 买家ID */
private Long buyerId;
/** 商品ID */ /** 商品ID */
private Long productId; @JsonSerialize(using = ToStringSerializer.class)
private Long listingId;
/** 商品名称 */ /** 商品名称 */
private String productName; private String listingName;
/** 商品缩略图URL */ /** 商品缩略图URL */
private String thumbnailUrl; private String thumbnailUrl;
@@ -35,9 +48,6 @@ public class OrderItemEntity implements Serializable {
/** 成交单价HK$ */ /** 成交单价HK$ */
private BigDecimal price; private BigDecimal price;
/** 购买数量 */
private Integer quantity;
/** 创建时间 */ /** 创建时间 */
@TableField(fill = FieldFill.INSERT) @TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime; private LocalDateTime createTime;
@@ -45,4 +55,8 @@ public class OrderItemEntity implements Serializable {
/** 是否删除0-否1-是 */ /** 是否删除0-否1-是 */
@TableLogic @TableLogic
private Integer deleted; private Integer deleted;
/** 商品分类列表 */
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> productCategory;
} }

View File

@@ -1,9 +1,18 @@
package com.aida.seller.module.order.mapper; package com.aida.seller.module.order.mapper;
import com.aida.seller.module.order.dto.AssetsDTO;
import com.aida.seller.module.order.entity.OrderItemEntity; import com.aida.seller.module.order.entity.OrderItemEntity;
import com.aida.seller.module.order.vo.AssetsItemVO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper @Mapper
public interface OrderItemMapper extends BaseMapper<OrderItemEntity> { public interface OrderItemMapper extends BaseMapper<OrderItemEntity> {
List<AssetsItemVO> selectAssetsPage(@Param("offset") long offset, @Param("size") long size, @Param("dto") AssetsDTO dto);
Long selectAssetsCount(@Param("dto") AssetsDTO dto);
} }

View File

@@ -1,15 +1,39 @@
package com.aida.seller.module.order.service; package com.aida.seller.module.order.service;
import com.aida.seller.module.order.dto.AssetsDTO;
import com.aida.seller.module.order.dto.CreateOrderDTO;
import com.aida.seller.module.order.dto.OrderListDTO; import com.aida.seller.module.order.dto.OrderListDTO;
import com.aida.seller.module.order.dto.UpdateOrderStatusDTO;
import com.aida.seller.module.order.dto.BuyerOrdersDTO;
import com.aida.seller.module.order.vo.AssetsItemVO;
import com.aida.seller.module.order.vo.BuyerOrderVO;
import com.aida.seller.module.order.vo.CreateOrderResultVO;
import com.aida.seller.module.order.vo.OrderSummaryVO; import com.aida.seller.module.order.vo.OrderSummaryVO;
import com.aida.seller.module.order.vo.OrderVO; import com.aida.seller.module.order.vo.OrderVO;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import java.util.List;
/** /**
* 订单服务接口 * 订单服务接口
*/ */
public interface OrderService { public interface OrderService {
/**
* 创建订单(按卖家分组合并)
*
* @param dto 创建订单参数包含商品ID列表、买家ID、买家账号
* @return 包含订单ID列表和总金额的结果对象
*/
CreateOrderResultVO createOrder(CreateOrderDTO dto);
/**
* 批量修改订单状态
*
* @param dto 包含订单ID列表和目标状态
*/
void updateOrderStatus(UpdateOrderStatusDTO dto);
/** /**
* 获取卖家订单数据总览 * 获取卖家订单数据总览
* *
@@ -26,4 +50,29 @@ public interface OrderService {
* @return 分页后的订单列表,按下单时间降序排列 * @return 分页后的订单列表,按下单时间降序排列
*/ */
IPage<OrderVO> getOrderPage(OrderListDTO dto, Long sellerId); IPage<OrderVO> getOrderPage(OrderListDTO dto, Long sellerId);
/**
* 根据买家ID分页查询订单列表供远程调用
*
* @param dto 查询参数买家ID、页码、每页数量、订单状态
* @return 买家订单分页列表,按更新时间降序排列
*/
IPage<BuyerOrderVO> getOrdersByBuyerId(BuyerOrdersDTO dto);
/**
* 我的资产分页查询
*
* @param dto 查询参数买家ID、分类列表、适用性别
* @return 已购商品分页列表,按购买时间降序排列
*/
IPage<AssetsItemVO> getAssetsPage(AssetsDTO dto);
/**
* 批量查询指定商品是否已被买家购买
*
* @param listingIds 商品ID列表
* @param buyerId 买家ID
* @return 已购买的商品ID列表
*/
List<Long> getPurchasedListingIds(List<Long> listingIds, Long buyerId);
} }

View File

@@ -1,20 +1,36 @@
package com.aida.seller.module.order.service; package com.aida.seller.module.order.service;
import com.aida.seller.common.constants.CommonConstants; import com.aida.seller.common.constants.CommonConstants;
import com.aida.seller.common.exception.BusinessException;
import com.aida.seller.module.designer.entity.DesignerEntity;
import com.aida.seller.module.designer.mapper.DesignerMapper;
import com.aida.seller.module.listing.entity.ListingEntity;
import com.aida.seller.module.listing.mapper.ListingMapper;
import com.aida.seller.module.order.dto.AssetsDTO;
import com.aida.seller.module.order.dto.CreateOrderDTO;
import com.aida.seller.module.order.dto.OrderListDTO; import com.aida.seller.module.order.dto.OrderListDTO;
import com.aida.seller.module.order.dto.UpdateOrderStatusDTO;
import com.aida.seller.module.order.dto.BuyerOrdersDTO;
import com.aida.seller.module.order.entity.OrderInfoEntity; import com.aida.seller.module.order.entity.OrderInfoEntity;
import com.aida.seller.module.order.entity.OrderItemEntity; import com.aida.seller.module.order.entity.OrderItemEntity;
import com.aida.seller.module.order.mapper.OrderInfoMapper; import com.aida.seller.module.order.mapper.OrderInfoMapper;
import com.aida.seller.module.order.mapper.OrderItemMapper; import com.aida.seller.module.order.mapper.OrderItemMapper;
import com.aida.seller.module.order.vo.AssetsItemVO;
import com.aida.seller.module.order.vo.BuyerOrderItemVO;
import com.aida.seller.module.order.vo.BuyerOrderVO;
import com.aida.seller.module.order.vo.CreateOrderResultVO;
import com.aida.seller.module.order.vo.OrderSummaryVO; import com.aida.seller.module.order.vo.OrderSummaryVO;
import com.aida.seller.module.order.vo.OrderVO; import com.aida.seller.module.order.vo.OrderVO;
import com.aida.seller.util.MinioUtil; import com.aida.seller.util.MinioUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -33,6 +49,8 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
private final OrderItemMapper orderItemMapper; private final OrderItemMapper orderItemMapper;
private final MinioUtil minioUtil; private final MinioUtil minioUtil;
private final ListingMapper listingMapper;
private final DesignerMapper designerMapper;
/** /**
* 查询指定卖家的订单汇总数据 * 查询指定卖家的订单汇总数据
@@ -75,22 +93,23 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
if (StringUtils.hasText(dto.getKeyword())) { if (StringUtils.hasText(dto.getKeyword())) {
String keyword = dto.getKeyword().trim(); String keyword = dto.getKeyword().trim();
queryWrapper.and(w -> w queryWrapper.and(w -> w
.like(OrderInfoEntity::getOrderId, keyword) .like(OrderInfoEntity::getId, keyword)
.or() .or()
.inSql(OrderInfoEntity::getOrderId, .inSql(OrderInfoEntity::getId,
"SELECT order_id FROM order_item WHERE product_name LIKE '%" + keyword + "%'") "SELECT order_id FROM seller_order_item WHERE deleted = 0 AND product_name LIKE '%" + keyword + "%'")
); );
} }
queryWrapper.eq(OrderInfoEntity::getStatus, 1);
queryWrapper.orderByDesc(OrderInfoEntity::getCreateTime); queryWrapper.orderByDesc(OrderInfoEntity::getCreateTime);
Page<OrderInfoEntity> page = this.page(pageParam, queryWrapper); Page<OrderInfoEntity> page = this.page(pageParam, queryWrapper);
List<String> orderIds = page.getRecords().stream() List<Long> orderIds = page.getRecords().stream()
.map(OrderInfoEntity::getOrderId) .map(OrderInfoEntity::getId)
.collect(Collectors.toList()); .collect(Collectors.toList());
Map<String, List<OrderItemEntity>> itemsMap = orderIds.isEmpty() Map<Long, List<OrderItemEntity>> itemsMap = orderIds.isEmpty()
? Collections.emptyMap() ? Collections.emptyMap()
: orderItemMapper.selectList( : orderItemMapper.selectList(
new LambdaQueryWrapper<OrderItemEntity>() new LambdaQueryWrapper<OrderItemEntity>()
@@ -100,16 +119,16 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
List<OrderVO> voList = page.getRecords().stream().map(order -> { List<OrderVO> voList = page.getRecords().stream().map(order -> {
OrderVO vo = new OrderVO(); OrderVO vo = new OrderVO();
vo.setOrderId(order.getOrderId()); vo.setOrderId(order.getId());
vo.setPrice(order.getTotalPrice()); vo.setPrice(order.getTotalPrice());
vo.setBuyerUsername("@" + (order.getBuyerUsername() != null ? order.getBuyerUsername() : "")); vo.setBuyerUsername("@" + (order.getBuyerUsername() != null ? order.getBuyerUsername() : ""));
vo.setDate(order.getCreateTime()); vo.setDate(order.getCreateTime());
List<OrderItemEntity> items = itemsMap.getOrDefault(order.getOrderId(), new ArrayList<>()); List<OrderItemEntity> items = itemsMap.getOrDefault(order.getId(), new ArrayList<>());
List<OrderVO.ItemVO> itemVOs = items.stream().map(item -> { List<OrderVO.ItemVO> itemVOs = items.stream().map(item -> {
OrderVO.ItemVO itemVO = new OrderVO.ItemVO(); OrderVO.ItemVO itemVO = new OrderVO.ItemVO();
itemVO.setProductId(item.getProductId()); itemVO.setProductId(item.getListingId());
itemVO.setProductName(item.getProductName()); itemVO.setProductName(item.getListingName());
itemVO.setThumbnailUrl(minioUtil.processMinioResource(item.getThumbnailUrl(), CommonConstants.MINIO_PATH_TIMEOUT)); itemVO.setThumbnailUrl(minioUtil.processMinioResource(item.getThumbnailUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
return itemVO; return itemVO;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
@@ -122,4 +141,209 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
resultPage.setRecords(voList); resultPage.setRecords(voList);
return resultPage; return resultPage;
} }
@Override
public IPage<BuyerOrderVO> getOrdersByBuyerId(BuyerOrdersDTO dto) {
Page<OrderInfoEntity> pageParam = new Page<>(dto.getPage(), dto.getSize());
LambdaQueryWrapper<OrderInfoEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderInfoEntity::getBuyerId, dto.getBuyerId());
if (dto.getStatus() != null) {
wrapper.eq(OrderInfoEntity::getStatus, dto.getStatus());
}
wrapper.orderByDesc(OrderInfoEntity::getUpdateTime);
Page<OrderInfoEntity> orderPage = this.page(pageParam, wrapper);
if (orderPage.getRecords().isEmpty()) {
return new Page<>(dto.getPage(), dto.getSize(), 0);
}
List<Long> orderIds = orderPage.getRecords().stream()
.map(OrderInfoEntity::getId)
.collect(Collectors.toList());
Map<Long, List<OrderItemEntity>> itemsMap = orderItemMapper.selectList(
new LambdaQueryWrapper<OrderItemEntity>()
.in(OrderItemEntity::getOrderId, orderIds)
).stream()
.collect(Collectors.groupingBy(OrderItemEntity::getOrderId));
List<BuyerOrderVO> voList = orderPage.getRecords().stream().map(order -> {
BuyerOrderVO vo = new BuyerOrderVO();
vo.setOrderId(order.getId());
vo.setUpdateTime(order.getUpdateTime());
vo.setTotalPrice(order.getTotalPrice());
vo.setStatus(order.getStatus());
vo.setShopName(order.getShopName());
List<OrderItemEntity> items = itemsMap.getOrDefault(order.getId(), Collections.emptyList());
List<BuyerOrderItemVO> itemVOs = items.stream().map(item -> {
BuyerOrderItemVO itemVO = new BuyerOrderItemVO();
itemVO.setId(item.getId());
itemVO.setThumbnailUrl(minioUtil.processMinioResource(item.getThumbnailUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
itemVO.setListingName(item.getListingName());
itemVO.setProductCategory(item.getProductCategory());
itemVO.setPrice(item.getPrice());
return itemVO;
}).collect(Collectors.toList());
vo.setItems(itemVOs);
return vo;
}).collect(Collectors.toList());
Page<BuyerOrderVO> resultPage = new Page<>(orderPage.getCurrent(), orderPage.getSize(), orderPage.getTotal());
resultPage.setRecords(voList);
return resultPage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public CreateOrderResultVO createOrder(CreateOrderDTO dto) {
if (CollectionUtils.isEmpty(dto.getListingIds())) {
throw new BusinessException("商品ID列表不能为空");
}
if (dto.getBuyerId() == null) {
throw new BusinessException("买家ID不能为空");
}
if (!StringUtils.hasText(dto.getBuyerUsername())) {
throw new BusinessException("买家账号不能为空");
}
List<Long> purchasedListingIds = getPurchasedListingIds(dto.getListingIds(), dto.getBuyerId());
List<Long> unpurchasedListingIds = dto.getListingIds().stream()
.filter(id -> !purchasedListingIds.contains(id))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(unpurchasedListingIds)) {
throw new BusinessException("所有商品均已购买,无需重复下单");
}
dto.setListingIds(unpurchasedListingIds);
List<ListingEntity> listings = listingMapper.selectBatchIds(dto.getListingIds());
if (CollectionUtils.isEmpty(listings)) {
throw new BusinessException("未找到对应的商品");
}
Map<Long, List<ListingEntity>> listingsBySeller = listings.stream()
.filter(l -> l.getStatus() == 1)
.collect(Collectors.groupingBy(ListingEntity::getSellerId));
List<Long> orderIds = new ArrayList<>();
List<BigDecimal> totalPrices = new ArrayList<>();
for (Map.Entry<Long, List<ListingEntity>> entry : listingsBySeller.entrySet()) {
Long sellerId = entry.getKey();
List<ListingEntity> sellerListings = entry.getValue();
DesignerEntity designer = designerMapper.selectOne(
new LambdaQueryWrapper<DesignerEntity>()
.eq(DesignerEntity::getUserId, sellerId)
.eq(DesignerEntity::getDeleted, 0));
String shopName = (designer != null) ? designer.getShopName() : null;
BigDecimal totalPrice = sellerListings.stream()
.map(ListingEntity::getPrice)
.filter(p -> p != null)
.reduce(BigDecimal.ZERO, BigDecimal::add);
totalPrices.add(totalPrice);
Long totalViews = sellerListings.stream()
.map(ListingEntity::getViewCount)
.filter(v -> v != null)
.reduce(0, Integer::sum).longValue();
for (ListingEntity listing : sellerListings) {
listing.setSalesVolume(listing.getSalesVolume() == null ? 1 : listing.getSalesVolume() + 1);
listingMapper.updateById(listing);
}
OrderInfoEntity order = new OrderInfoEntity();
order.setSellerId(sellerId);
order.setBuyerId(dto.getBuyerId());
order.setBuyerUsername(dto.getBuyerUsername());
order.setStatus(0);
order.setShopName(shopName);
order.setTotalPrice(totalPrice);
order.setTotalItems(sellerListings.size());
order.setTotalViews(totalViews);
this.save(order);
for (ListingEntity listing : sellerListings) {
OrderItemEntity item = new OrderItemEntity();
item.setOrderId(order.getId());
item.setSellerId(sellerId);
item.setBuyerId(dto.getBuyerId());
item.setListingId(listing.getId());
item.setListingName(listing.getTitle());
item.setThumbnailUrl(listing.getCover());
item.setPrice(listing.getPrice());
item.setProductCategory(listing.getProductCategory());
orderItemMapper.insert(item);
}
orderIds.add(order.getId());
}
CreateOrderResultVO result = new CreateOrderResultVO();
result.setOrderIds(orderIds);
BigDecimal grandTotal = totalPrices.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
result.setTotalAmount(grandTotal);
result.setUnpurchasedListingIds(dto.getListingIds());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus(UpdateOrderStatusDTO dto) {
if (dto == null || dto.getOrderIds() == null || dto.getOrderIds().isEmpty()) {
throw new BusinessException("订单ID列表不能为空");
}
if (dto.getStatus() == null) {
throw new BusinessException("订单状态不能为空");
}
LambdaUpdateWrapper<OrderInfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.in(OrderInfoEntity::getId, dto.getOrderIds())
.set(OrderInfoEntity::getStatus, dto.getStatus());
boolean updated = this.update(updateWrapper);
if (!updated) {
throw new BusinessException("订单不存在或无权修改");
}
}
@Override
public IPage<AssetsItemVO> getAssetsPage(AssetsDTO dto) {
if (dto.getBuyerId() == null) {
throw new BusinessException("买家ID不能为空");
}
long page = dto.getPage();
long size = dto.getSize();
long offset = (page - 1) * size;
List<AssetsItemVO> records = orderItemMapper.selectAssetsPage(offset, size, dto);
Long total = orderItemMapper.selectAssetsCount(dto);
records.forEach(item -> {
item.setThumbnailUrl(minioUtil.processMinioResource(item.getThumbnailUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
});
Page<AssetsItemVO> resultPage = new Page<>(page, size, total);
resultPage.setRecords(records);
return resultPage;
}
@Override
public List<Long> getPurchasedListingIds(List<Long> listingIds, Long buyerId) {
if (CollectionUtils.isEmpty(listingIds) || buyerId == null) {
return Collections.emptyList();
}
LambdaQueryWrapper<OrderItemEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderItemEntity::getBuyerId, buyerId)
.in(OrderItemEntity::getListingId, listingIds);
return orderItemMapper.selectList(wrapper).stream()
.map(OrderItemEntity::getListingId)
.distinct()
.collect(Collectors.toList());
}
} }

View File

@@ -0,0 +1,33 @@
package com.aida.seller.module.order.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@Schema(description = "我的资产商品明细")
public class AssetsItemVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "商品ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long listingId;
@Schema(description = "商品名称")
private String listingName;
@Schema(description = "商品缩略图URL")
private String thumbnailUrl;
@Schema(description = "成交单价HK$")
private BigDecimal price;
@Schema(description = "购买时间")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,33 @@
package com.aida.seller.module.order.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
@Data
@Schema(description = "买家订单商品明细")
public class BuyerOrderItemVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "订单商品ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
@Schema(description = "商品缩略图URL")
private String thumbnailUrl;
@Schema(description = "商品名称")
private String listingName;
@Schema(description = "商品分类列表")
private List<String> productCategory;
@Schema(description = "成交单价HK$")
private BigDecimal price;
}

View File

@@ -0,0 +1,37 @@
package com.aida.seller.module.order.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Schema(description = "买家订单信息")
public class BuyerOrderVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "订货号")
@JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
@Schema(description = "订单更新时间")
private LocalDateTime updateTime;
@Schema(description = "订单总金额HK$")
private BigDecimal totalPrice;
@Schema(description = "订单状态0-未支付1-已支付2-已取消")
private Integer status;
@Schema(description = "店铺名称")
private String shopName;
@Schema(description = "商品明细列表")
private List<BuyerOrderItemVO> items;
}

View File

@@ -0,0 +1,24 @@
package com.aida.seller.module.order.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
@Data
@Schema(description = "创建订单结果")
public class CreateOrderResultVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "订单ID列表按卖家分组")
private List<Long> orderIds;
@Schema(description = "所有订单总金额HK$")
private BigDecimal totalAmount;
@Schema(description = "本次成功下单的未购买商品ID列表")
private List<Long> unpurchasedListingIds;
}

View File

@@ -1,5 +1,7 @@
package com.aida.seller.module.order.vo; package com.aida.seller.module.order.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -14,8 +16,9 @@ public class OrderVO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Schema(description = "订单唯一标识", example = "SP897772698") @Schema(description = "订单唯一标识")
private String orderId; @JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
@Schema(description = "商品明细列表") @Schema(description = "商品明细列表")
private List<ItemVO> items; private List<ItemVO> items;
@@ -36,6 +39,7 @@ public class OrderVO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Schema(description = "商品ID") @Schema(description = "商品ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long productId; private Long productId;
@Schema(description = "商品名") @Schema(description = "商品名")

View File

@@ -5,7 +5,7 @@
# ============================================================ # ============================================================
server: server:
port: 5568 port: 10093
spring: spring:
application: application:
@@ -17,7 +17,7 @@ mybatis-plus:
type-aliases-package: com.aida.seller.module.*.entity type-aliases-package: com.aida.seller.module.*.entity
global-config: global-config:
db-config: db-config:
id-type: auto id-type: assign_id
logic-delete-field: deleted logic-delete-field: deleted
logic-delete-value: 1 logic-delete-value: 1
logic-not-delete-value: 0 logic-not-delete-value: 0
@@ -30,5 +30,3 @@ minio:
default-bucket: aida-user default-bucket: aida-user
logging: logging:
level:
com.aida: debug

View File

@@ -4,21 +4,32 @@
# 示例docker run -e NACOS_NAMESPACE=prod ... # 示例docker run -e NACOS_NAMESPACE=prod ...
# ============================================================ # ============================================================
nacos:
namespace: dev
host: 18.167.251.121:28848
username: nacos
password: Aidlab123123!
spring: spring:
application: application:
name: aida-seller name: aida-seller
config: config:
import: optional:nacos:aida-public-${NACOS_NAMESPACE:test}.yml import: optional:nacos:aida-public-${nacos.namespace}.yml
cloud: cloud:
nacos: nacos:
discovery: discovery:
server-addr: ${NACOS_HOST:18.167.251.121:28848} server-addr: ${nacos.host}
namespace: ${NACOS_NAMESPACE:ltx} namespace: ${nacos.namespace}
username: ${NACOS_USERNAME:nacos} username: ${nacos.username}
password: ${NACOS_PASSWORD:Aidlab123123!} password: ${nacos.password}
# ip: 18.167.251.121
port: 10093
# ip-type: ipv4
# prefer-ip-address: true
config: config:
server-addr: ${NACOS_HOST:18.167.251.121:28848} server-addr: ${nacos.host}
namespace: ${NACOS_NAMESPACE:ltx} namespace: ${nacos.namespace}
file-extension: yaml file-extension: yaml
username: ${NACOS_USERNAME:nacos} username: ${nacos.username}
password: ${NACOS_PASSWORD:Aidlab123123!} password: ${nacos.password}

View File

@@ -5,7 +5,6 @@ CREATE TABLE seller_listing (
title VARCHAR(255) NOT NULL COMMENT '商品标题', title VARCHAR(255) NOT NULL COMMENT '商品标题',
description TEXT COMMENT '商品描述', description TEXT COMMENT '商品描述',
price DECIMAL(10,2) COMMENT '价格', price DECIMAL(10,2) COMMENT '价格',
stock INT COMMENT '库存数量',
cover VARCHAR(200) COMMENT '封面图URL', cover VARCHAR(200) COMMENT '封面图URL',
view_count INT DEFAULT 0 COMMENT '浏览量', view_count INT DEFAULT 0 COMMENT '浏览量',
status INT(1) DEFAULT 0 COMMENT '状态: 0-草稿, 1-已发布, 2-已删除', status INT(1) DEFAULT 0 COMMENT '状态: 0-草稿, 1-已发布, 2-已删除',
@@ -28,7 +27,9 @@ CREATE TABLE seller_listing_image (
sort_order INT DEFAULT 0 COMMENT '排序', sort_order INT DEFAULT 0 COMMENT '排序',
is_selected INT(1) DEFAULT 0 COMMENT '是否选中: 0-未选中, 1-选中(仅product有效)', is_selected INT(1) DEFAULT 0 COMMENT '是否选中: 0-未选中, 1-选中(仅product有效)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_listing_id (listing_id) deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
INDEX idx_listing_id (listing_id),
INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品图片表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品图片表';
-- 设计师表 -- 设计师表
@@ -36,7 +37,7 @@ CREATE TABLE seller_designer (
id BIGINT PRIMARY KEY COMMENT '设计师ID', id BIGINT PRIMARY KEY COMMENT '设计师ID',
user_id BIGINT NOT NULL COMMENT '用户ID', user_id BIGINT NOT NULL COMMENT '用户ID',
shop_name VARCHAR(100) NOT NULL COMMENT '店铺名称', shop_name VARCHAR(100) NOT NULL COMMENT '店铺名称',
avatar VARCHAR(200) COMMENT '店铺头像URL', avatar VARCHAR(200) NOT NULL DEFAULT 'aida-users/87/avatar/default.png' COMMENT '店铺头像URL',
brand_banner VARCHAR(200) COMMENT '品牌Banner URL', brand_banner VARCHAR(200) COMMENT '品牌Banner URL',
owner_name VARCHAR(100) COMMENT '所有者全名', owner_name VARCHAR(100) COMMENT '所有者全名',
email VARCHAR(100) COMMENT '邮箱', email VARCHAR(100) COMMENT '邮箱',
@@ -59,8 +60,10 @@ CREATE TABLE seller_designer (
-- 订单主表 -- 订单主表
CREATE TABLE seller_orders ( CREATE TABLE seller_orders (
id BIGINT PRIMARY KEY COMMENT '主键ID', id BIGINT PRIMARY KEY COMMENT '主键ID',
order_id VARCHAR(50) NOT NULL COMMENT '订单唯一标识(如 SP897772698',
seller_id BIGINT NOT NULL COMMENT '卖家ID', seller_id BIGINT NOT NULL COMMENT '卖家ID',
buyer_id BIGINT NOT NULL COMMENT '买家ID',
status INT DEFAULT 0 COMMENT '订单状态: 0-未支付, 1-已支付, 2-已取消',
shop_name VARCHAR(100) COMMENT '店铺名称',
total_price DECIMAL(10,2) COMMENT '订单总金额(HK$)', total_price DECIMAL(10,2) COMMENT '订单总金额(HK$)',
buyer_username VARCHAR(100) COMMENT '买家账号', buyer_username VARCHAR(100) COMMENT '买家账号',
total_items INT COMMENT '商品总数量', total_items INT COMMENT '商品总数量',
@@ -68,7 +71,6 @@ CREATE TABLE seller_orders (
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是', deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
INDEX idx_order_id (order_id),
INDEX idx_seller_id (seller_id), INDEX idx_seller_id (seller_id),
INDEX idx_deleted (deleted), INDEX idx_deleted (deleted),
INDEX idx_create_time (create_time) INDEX idx_create_time (create_time)
@@ -77,15 +79,17 @@ CREATE TABLE seller_orders (
-- 订单商品明细表 -- 订单商品明细表
CREATE TABLE seller_order_item ( CREATE TABLE seller_order_item (
id BIGINT PRIMARY KEY COMMENT '主键ID', id BIGINT PRIMARY KEY COMMENT '主键ID',
order_id VARCHAR(50) NOT NULL COMMENT '订单ID', order_id BIGINT NOT NULL COMMENT '订单ID',
product_id BIGINT NOT NULL COMMENT '商品ID', seller_id BIGINT NOT NULL COMMENT '卖家ID',
product_name VARCHAR(255) COMMENT '商品名称', buyer_id BIGINT NOT NULL COMMENT '买家ID',
listing_id BIGINT NOT NULL COMMENT '商品ID',
listing_name VARCHAR(255) COMMENT '商品名称',
thumbnail_url VARCHAR(200) COMMENT '商品缩略图URL', thumbnail_url VARCHAR(200) COMMENT '商品缩略图URL',
price DECIMAL(10,2) COMMENT '成交单价(HK$)', price DECIMAL(10,2) COMMENT '成交单价(HK$)',
quantity INT NOT NULL COMMENT '购买数量',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是', deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
product_category JSON COMMENT '商品分类列表',
INDEX idx_order_id (order_id), INDEX idx_order_id (order_id),
INDEX idx_product_id (product_id), INDEX idx_listing_id (listing_id),
INDEX idx_deleted (deleted) INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单商品明细表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单商品明细表';

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- 日志存放路径 -->
<property name="log.path" value="./log" />
<!-- 日志输出格式 -->
<property name="log.pattern.console" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<property name="log.pattern.file" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%15t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern.console}</pattern>
</encoder>
</appender>
<!-- Info 日志文件 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/aida-seller-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/aida-seller-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern.file}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- Error 日志文件 -->
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/aida-seller-error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/aida-seller-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern.file}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 服务模块日志级别控制 -->
<logger name="com.aida" level="debug" />
<logger name="com.aida.seller.mapper" level="info" />
<!-- Spring 日志级别控制 -->
<logger name="org.springframework" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
</configuration>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.aida.seller.module.order.mapper.OrderItemMapper">
<select id="selectAssetsPage" resultType="com.aida.seller.module.order.vo.AssetsItemVO">
SELECT
soi.listing_id,
soi.listing_name,
soi.thumbnail_url,
soi.price,
soi.create_time
FROM seller_order_item soi
INNER JOIN seller_orders so ON soi.order_id = so.id
INNER JOIN seller_listing l ON soi.listing_id = l.id
WHERE soi.deleted = 0
AND so.deleted = 0
AND so.buyer_id = #{dto.buyerId}
AND so.status = 1
<if test="dto.categories != null and dto.categories.size() > 0">
<foreach collection="dto.categories" item="cat">
AND JSON_CONTAINS(soi.product_category, #{cat})
</foreach>
</if>
<if test="dto.designFor != null and dto.designFor != '' and dto.designFor != 'all'">
AND l.design_for = #{dto.designFor}
</if>
ORDER BY soi.create_time DESC
LIMIT #{offset}, #{size}
</select>
<select id="selectAssetsCount" resultType="java.lang.Long">
SELECT COUNT(*)
FROM seller_order_item soi
INNER JOIN seller_orders so ON soi.order_id = so.id
INNER JOIN seller_listing l ON soi.listing_id = l.id
WHERE soi.deleted = 0
AND so.deleted = 0
AND so.buyer_id = #{dto.buyerId}
AND so.status = 1
<if test="dto.categories != null and dto.categories.size() > 0">
<foreach collection="dto.categories" item="cat">
AND JSON_CONTAINS(soi.product_category, #{cat})
</foreach>
</if>
<if test="dto.designFor != null and dto.designFor != '' and dto.designFor != 'all'">
AND l.design_for = #{dto.designFor}
</if>
</select>
</mapper>