Compare commits
55 Commits
93749ca65e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c825f6af70 | ||
|
|
45885bf509 | ||
|
|
d004dc75f5 | ||
|
|
14efee9c85 | ||
|
|
149ee13ec3 | ||
|
|
a51bca1867 | ||
|
|
a3020bccae | ||
|
|
daf40ab224 | ||
|
|
912d5efee7 | ||
|
|
0c1b74ddc0 | ||
|
|
e1d57f7b37 | ||
|
|
daf4c30a91 | ||
|
|
08f5a482eb | ||
|
|
1ff76957a7 | ||
|
|
d77ce701e1 | ||
|
|
4b309efbb5 | ||
|
|
73ac643771 | ||
|
|
0b9601278c | ||
|
|
0a1dc1c10d | ||
|
|
9e5ba17dc4 | ||
|
|
88c73c4462 | ||
|
|
749241f19b | ||
|
|
f69eca39ff | ||
|
|
38fb2ec4d5 | ||
|
|
b56ae5741b | ||
|
|
1d4c8ec629 | ||
|
|
4456722328 | ||
|
|
ad2254bc80 | ||
|
|
5569da47f7 | ||
|
|
fb892b6b21 | ||
|
|
dea2409cea | ||
|
|
9d4c675594 | ||
|
|
da72640783 | ||
|
|
48c4679820 | ||
|
|
86773339ec | ||
|
|
92906881fe | ||
|
|
520627a8fa | ||
|
|
cc839dce1d | ||
|
|
5ceda7991d | ||
|
|
6f4e71b9e9 | ||
|
|
38c12b9ba5 | ||
|
|
232953acb0 | ||
|
|
b862da5b50 | ||
|
|
0605839c87 | ||
|
|
774e6e0c6b | ||
|
|
1802b2b500 | ||
|
|
f0f772ae89 | ||
|
|
e433921abe | ||
|
|
259a7c8c2a | ||
|
|
0740aefa1a | ||
| f25029be87 | |||
|
|
1744480822 | ||
|
|
9cc302fa53 | ||
|
|
8c29d292d5 | ||
|
|
0019ae01ea |
@@ -4,7 +4,8 @@ on:
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: java21
|
||||
|
||||
outputs:
|
||||
build_status: ${{ job.status }}
|
||||
build_url: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }}
|
||||
@@ -26,52 +27,20 @@ jobs:
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: 2.Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
|
||||
- name: 3.缓存 Maven 依赖
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maven-
|
||||
|
||||
- name: 3.设置JAVA Maven 环境
|
||||
- name: 4.构建项目
|
||||
run: |
|
||||
# 适配root/普通用户
|
||||
SUDO=""
|
||||
[ "$(id -u)" != "0" ] && SUDO="sudo"
|
||||
|
||||
# 安装依赖
|
||||
$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
|
||||
java -version
|
||||
mvn -v
|
||||
mvn clean package -DskipTests
|
||||
|
||||
- name: 5.生成Dockerfile
|
||||
run: |
|
||||
@@ -81,7 +50,7 @@ jobs:
|
||||
VOLUME /tmp
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo 'Asia/Shanghai' > /etc/timezone
|
||||
ADD ./aida-seller-1.0.0.jar /app.jar
|
||||
ADD ./*.jar /app.jar
|
||||
ENTRYPOINT ["java","-jar","/app.jar"]
|
||||
EOF
|
||||
echo "Dockerfile内容:"
|
||||
@@ -93,7 +62,7 @@ jobs:
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
version: '3'
|
||||
services:
|
||||
aida_back:
|
||||
master-aida-seller:
|
||||
container_name: master-aida-seller
|
||||
build: .
|
||||
volumes:
|
||||
@@ -102,46 +71,41 @@ jobs:
|
||||
- ./temp:/temp
|
||||
- ./uploads:/temp/uploads
|
||||
ports:
|
||||
- '10093:5567'
|
||||
- '10093:10093'
|
||||
restart: always
|
||||
EOF
|
||||
# 验证docker-compose.yml生成
|
||||
echo "docker-compose.yml内容:"
|
||||
cat docker-compose.yml
|
||||
|
||||
- name: 7.安装SSH工具
|
||||
run: |
|
||||
$SUDO apt install -y sshpass openssh-client --no-install-recommends
|
||||
# 配置SSH免密
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
|
||||
- name: 7.上传jar到远程服务器
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
port: 22
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
source: "target/*.jar,Dockerfile,docker-compose.yml"
|
||||
target: ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
preserve_host_directory_structure: false
|
||||
|
||||
- name: 8.同步文件到远程服务器
|
||||
run: |
|
||||
echo "===== 同步文件到远程服务器 ====="
|
||||
# 使用scp同步文件
|
||||
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
./target/*.jar ./Dockerfile ./docker-compose.yml \
|
||||
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ env.REMOTE_DEPLOY_PATH }} 2>&1
|
||||
|
||||
- name: 9.部署和运行服务
|
||||
run: |
|
||||
echo "===== 开始部署服务 ====="
|
||||
# SSH执行部署命令
|
||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF_SSH'
|
||||
- name: 8. 重启 Docker 服务
|
||||
uses: appleboy/ssh-action@master # 👈 专门执行命令的 action
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
key_base64: true
|
||||
script: |
|
||||
echo "========= 进入部署目录 ========="
|
||||
cd ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
echo "停止旧容器..."
|
||||
docker compose down || true
|
||||
echo "清理Docker资源..."
|
||||
docker system prune -f
|
||||
echo "构建镜像..."
|
||||
docker compose build --no-cache
|
||||
echo "启动服务..."
|
||||
docker compose up -d
|
||||
echo "验证容器状态..."
|
||||
docker compose ps
|
||||
echo "部署完成!"
|
||||
EOF_SSH
|
||||
ls -l
|
||||
|
||||
echo "========= 停止旧服务 ========="
|
||||
docker compose down
|
||||
|
||||
echo "========= 启动新服务 ========="
|
||||
docker compose up -d --build
|
||||
|
||||
echo "========= 查看运行状态 ========="
|
||||
docker compose ps
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/target/
|
||||
/log/
|
||||
|
||||
27
pom.xml
27
pom.xml
@@ -24,15 +24,15 @@
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<mybatis-plus.version>3.5.6</mybatis-plus.version>
|
||||
<minio.version>8.5.7</minio.version>
|
||||
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||||
<minio.version>8.0.3</minio.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>
|
||||
<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.version>2023.0.0</spring-cloud.version>
|
||||
<spring-cloud-alibaba.version>2023.0.3.4</spring-cloud-alibaba.version>
|
||||
<spring-cloud.version>2023.0.4</spring-cloud.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -63,6 +63,12 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Logging(显式引入,确保 logback 正确初始化) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -86,7 +92,6 @@
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- MinIO -->
|
||||
@@ -149,12 +154,6 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 微服务 ==================== -->
|
||||
<dependency>
|
||||
@@ -185,6 +184,8 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- 强制工作目录为模块根目录,确保 ./log 指向项目目录而非 Maven 安装目录 -->
|
||||
<workingDirectory>${project.basedir}</workingDirectory>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@@ -5,11 +5,13 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.aida.seller.module.*.mapper")
|
||||
@EnableFeignClients
|
||||
@EnableDiscoveryClient
|
||||
@ComponentScan(basePackages = "com.aida.seller")
|
||||
public class AidaSellerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ public class CommonConstants {
|
||||
|
||||
public static final int TOKEN_EXPIRE_TIME = 7 * 24; // token 7 天过期(Hour)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 内部服务间调用的签名 Header,Feign 远程调用时携带,用于标识为内部可信调用
|
||||
*/
|
||||
public static final String INTERNAL_CALL_HEADER = "X-Internal-Call";
|
||||
public static final String INTERNAL_CALL_VALUE = "true";
|
||||
}
|
||||
@@ -1,24 +1,55 @@
|
||||
package com.aida.seller.common.context;
|
||||
|
||||
import com.aida.seller.common.exception.UnauthorizedException;
|
||||
import com.aida.seller.model.vo.AuthPrincipalVo;
|
||||
|
||||
public class UserContext {
|
||||
private static final ThreadLocal<AuthPrincipalVo> userHolder = new ThreadLocal<>();
|
||||
|
||||
public static AuthPrincipalVo getUserHolder() {
|
||||
return userHolder.get();
|
||||
}
|
||||
|
||||
public static void delete() {
|
||||
userHolder.remove();
|
||||
}
|
||||
private static final ThreadLocal<Boolean> optionalAuth = ThreadLocal.withInitial(() -> false);
|
||||
|
||||
public static void setUserHolder(AuthPrincipalVo 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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ import java.util.stream.Collectors;
|
||||
@RestControllerAdvice
|
||||
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)
|
||||
public Response<?> handleBusinessException(BusinessException e) {
|
||||
log.error("业务异常: code={}, msg={}", e.getCode(), e.getMsg());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,18 @@ package com.aida.seller.common.interceptor;
|
||||
import com.aida.seller.common.context.UserContext;
|
||||
import com.aida.seller.model.vo.AuthPrincipalVo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 从 Gateway 转发的请求头中读取已鉴权的用户身份,写入 UserContext。
|
||||
* <p>
|
||||
@@ -18,13 +23,40 @@ import org.springframework.web.servlet.HandlerInterceptor;
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserContextInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final String USER_ID_HEADER = "X-User-Id";
|
||||
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
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
@@ -36,10 +68,24 @@ public class UserContextInterceptor implements HandlerInterceptor {
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse X-User-Info header: {}", e.getMessage());
|
||||
}
|
||||
} else if (isOptionalAuthPath(request.getRequestURI())) {
|
||||
UserContext.setOptionalAuth(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
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler, Exception ex) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.List;
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "分页响应结果")
|
||||
public class PageResponse<T> extends Response<List<T>> {
|
||||
public class PageResponse<T> {
|
||||
@Schema(description = "页码")
|
||||
private long page;
|
||||
@Schema(description = "每页数量")
|
||||
@@ -26,21 +26,16 @@ public class PageResponse<T> extends Response<List<T>> {
|
||||
@Schema(description = "结果集")
|
||||
private List<T> content;
|
||||
|
||||
public PageResponse(Response<List<T>> response, long page, long size, long total, long pages) {
|
||||
if (response != null) {
|
||||
this.setData(response.getData());
|
||||
this.setErrCode(response.getErrCode());
|
||||
this.setErrMsg(response.getErrMsg());
|
||||
}
|
||||
|
||||
public PageResponse(List<T> list, long page, long size, long total, long pages) {
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
this.total = total;
|
||||
this.pages = pages;
|
||||
this.content = response.getData();
|
||||
this.content = list;
|
||||
}
|
||||
|
||||
public static <T> PageResponse<T> success(IPage<T> page) {
|
||||
Response<List<T>> response = success(page.getRecords());
|
||||
return new PageResponse<>(response, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
return new PageResponse<>(page.getRecords(), page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
public class MyBatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.aida.seller.config;
|
||||
|
||||
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.spring.MybatisSqlSessionFactoryBean;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
@@ -24,6 +25,9 @@ public class PrimaryDataSourceConfig {
|
||||
@Autowired
|
||||
private MybatisPlusInterceptor mybatisPlusInterceptor;
|
||||
|
||||
@Autowired
|
||||
private MetaObjectHandler myMetaObjectHandler;
|
||||
|
||||
@Primary
|
||||
@Bean(name = "primaryDataSource")
|
||||
@ConfigurationProperties(prefix = "spring.datasource.primary")
|
||||
@@ -42,6 +46,7 @@ public class PrimaryDataSourceConfig {
|
||||
|
||||
GlobalConfig globalConfig = new GlobalConfig();
|
||||
globalConfig.setBanner(false);
|
||||
globalConfig.setMetaObjectHandler(myMetaObjectHandler);
|
||||
bean.setGlobalConfig(globalConfig);
|
||||
|
||||
bean.setPlugins(mybatisPlusInterceptor);
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package com.aida.seller.module.designer.controller;
|
||||
|
||||
import com.aida.seller.common.context.UserContext;
|
||||
import com.aida.seller.common.result.PageResponse;
|
||||
import com.aida.seller.common.result.Response;
|
||||
import com.aida.seller.module.designer.dto.DesignerApplyDTO;
|
||||
import com.aida.seller.module.designer.dto.DesignerAuditDTO;
|
||||
import com.aida.seller.module.designer.dto.DesignerDTO;
|
||||
import com.aida.seller.module.designer.entity.DesignerEntity;
|
||||
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.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "设计师入驻管理")
|
||||
@RestController
|
||||
@RequestMapping("/designer")
|
||||
@@ -22,8 +28,8 @@ public class DesignerController {
|
||||
|
||||
@Operation(summary = "查询设计师是否有售卖资格")
|
||||
@GetMapping("/check")
|
||||
public Response<Boolean> check(
|
||||
@Parameter(description = "用户ID") @RequestParam Long userId) {
|
||||
public Response<Boolean> check() {
|
||||
Long userId = UserContext.getUserId();
|
||||
boolean hasQualification = designerService.checkQualification(userId);
|
||||
return Response.success(hasQualification);
|
||||
}
|
||||
@@ -61,4 +67,52 @@ public class DesignerController {
|
||||
designerService.audit(dto);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取设计师申请状态", description = "根据当前登录用户ID获取设计师申请状态0-待审核, 1-审核通过, 2-审核拒绝,null-未申请过")
|
||||
@GetMapping("/apply/status")
|
||||
public Response<Integer> getApplyStatus() {
|
||||
Long userId = UserContext.getUserId();
|
||||
Integer applyStatus = designerService.getApplyStatus(userId);
|
||||
return Response.success(applyStatus);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新设计师信息", description = "当前登录设计师更新自身信息")
|
||||
@PutMapping("/update")
|
||||
public Response<Void> update(
|
||||
@Parameter(description = "更新表单") @RequestBody DesignerDTO dto) {
|
||||
designerService.update(dto);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取设计师详细信息", description = "根据当前登录用户ID获取设计师详细信息")
|
||||
@GetMapping("/info")
|
||||
public Response<DesignerDTO> getInfo() {
|
||||
Long userId = UserContext.getUserId();
|
||||
DesignerDTO designerInfo = designerService.getDesignerInfo(userId);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,11 @@ public class DesignerApplyDTO implements Serializable {
|
||||
|
||||
@Schema(description = "手机号")
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String mobile;
|
||||
|
||||
@Schema(description = "作品集/社交媒体链接(JSON数组)")
|
||||
private String socialLinks;
|
||||
|
||||
@Schema(description = "设计师简介")
|
||||
private String description;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.aida.seller.module.designer.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 设计师信息DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "设计师信息表单")
|
||||
public class DesignerDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "店铺名称")
|
||||
private String shopName;
|
||||
|
||||
@Schema(description = "店铺头像URL")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "品牌Banner URL")
|
||||
private String brandBanner;
|
||||
|
||||
@Schema(description = "所有者全名")
|
||||
private String ownerName;
|
||||
|
||||
@Schema(description = "邮箱")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号")
|
||||
private String mobile;
|
||||
|
||||
@Schema(description = "作品集/社交媒体链接(JSON数组)")
|
||||
private String socialLinks;
|
||||
|
||||
@Schema(description = "设计师简介")
|
||||
private String description;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.aida.seller.module.designer.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -9,13 +11,14 @@ import java.time.LocalDateTime;
|
||||
* 设计师表实体类
|
||||
*/
|
||||
@Data
|
||||
@TableName("designer")
|
||||
@TableName("seller_designer")
|
||||
public class DesignerEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 设计师ID */
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 用户ID(关联用户表) */
|
||||
@@ -41,6 +44,9 @@ public class DesignerEntity implements Serializable {
|
||||
|
||||
/** 作品集/社交媒体链接(JSON数组) */
|
||||
private String socialLinks;
|
||||
|
||||
/** 设计师简介 */
|
||||
private String description;
|
||||
|
||||
/** 申请状态: 0-待审核, 1-审核通过, 2-审核拒绝 */
|
||||
private Integer applyStatus;
|
||||
|
||||
@@ -2,10 +2,15 @@ package com.aida.seller.module.designer.service;
|
||||
|
||||
import com.aida.seller.module.designer.dto.DesignerApplyDTO;
|
||||
import com.aida.seller.module.designer.dto.DesignerAuditDTO;
|
||||
import com.aida.seller.module.designer.dto.DesignerDTO;
|
||||
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.extension.service.IService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface DesignerService extends IService<DesignerEntity> {
|
||||
|
||||
/**
|
||||
@@ -33,4 +38,44 @@ public interface DesignerService extends IService<DesignerEntity> {
|
||||
* 审核设计师入驻申请
|
||||
*/
|
||||
void audit(DesignerAuditDTO dto);
|
||||
|
||||
/**
|
||||
* 获取设计师申请状态
|
||||
*/
|
||||
Integer getApplyStatus(Long userId);
|
||||
|
||||
/**
|
||||
* 更新设计师信息
|
||||
*/
|
||||
void update(DesignerDTO dto);
|
||||
|
||||
/**
|
||||
* 获取设计师详细信息
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
package com.aida.seller.module.designer.service;
|
||||
|
||||
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.module.designer.dto.DesignerApplyDTO;
|
||||
import com.aida.seller.module.designer.dto.DesignerAuditDTO;
|
||||
import com.aida.seller.module.designer.dto.DesignerDTO;
|
||||
import com.aida.seller.module.designer.entity.DesignerEntity;
|
||||
import com.aida.seller.module.designer.enums.DesignerApplyStatusEnum;
|
||||
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.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -13,6 +27,11 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
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;
|
||||
|
||||
@@ -20,6 +39,12 @@ import java.time.LocalDateTime;
|
||||
@RequiredArgsConstructor
|
||||
public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEntity> implements DesignerService {
|
||||
|
||||
private final MinioUtil minioUtil;
|
||||
private final OrderInfoMapper orderInfoMapper;
|
||||
private final OrderItemMapper orderItemMapper;
|
||||
private final ListingMapper listingMapper;
|
||||
private final ListingImageMapper listingImageMapper;
|
||||
|
||||
@Override
|
||||
public Boolean checkQualification(Long userId) {
|
||||
DesignerEntity entity = this.getOne(
|
||||
@@ -50,14 +75,23 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
|
||||
}
|
||||
|
||||
DesignerEntity entity = new DesignerEntity();
|
||||
entity.setUserId(dto.getUserId());
|
||||
entity.setUserId(UserContext.getUserId());
|
||||
entity.setShopName(dto.getShopName());
|
||||
entity.setAvatar(dto.getAvatar());
|
||||
entity.setBrandBanner(dto.getBrandBanner());
|
||||
entity.setAvatar(
|
||||
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.setEmail(dto.getEmail());
|
||||
entity.setMobile(dto.getMobile());
|
||||
entity.setSocialLinks(dto.getSocialLinks());
|
||||
entity.setDescription(dto.getDescription());
|
||||
entity.setApplyStatus(DesignerApplyStatusEnum.PENDING.getCode());
|
||||
entity.setStatus(0);
|
||||
|
||||
@@ -130,4 +164,235 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
|
||||
|
||||
this.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getApplyStatus(Long userId) {
|
||||
DesignerEntity entity = this.getOne(
|
||||
new LambdaQueryWrapper<DesignerEntity>()
|
||||
.eq(DesignerEntity::getUserId, userId)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
return entity != null ? entity.getApplyStatus() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(DesignerDTO dto) {
|
||||
Long userId = UserContext.getUserId();
|
||||
DesignerEntity entity = this.getOne(
|
||||
new LambdaQueryWrapper<DesignerEntity>()
|
||||
.eq(DesignerEntity::getUserId, userId)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (entity == null) {
|
||||
throw new BusinessException("设计师记录不存在");
|
||||
}
|
||||
|
||||
if (dto.getShopName() != null) {
|
||||
entity.setShopName(dto.getShopName());
|
||||
}
|
||||
entity.setAvatar(
|
||||
dto.getAvatar() != null && !dto.getAvatar().isBlank()
|
||||
? minioUtil.convertToLogicalPath(dto.getAvatar())
|
||||
: null
|
||||
);
|
||||
entity.setBrandBanner(
|
||||
dto.getBrandBanner() != null && !dto.getBrandBanner().isBlank()
|
||||
? minioUtil.convertToLogicalPath(dto.getBrandBanner())
|
||||
: null
|
||||
);
|
||||
if (dto.getOwnerName() != null) {
|
||||
entity.setOwnerName(dto.getOwnerName());
|
||||
}
|
||||
if (dto.getEmail() != null) {
|
||||
entity.setEmail(dto.getEmail());
|
||||
}
|
||||
if (dto.getMobile() != null) {
|
||||
entity.setMobile(dto.getMobile());
|
||||
}
|
||||
if (dto.getSocialLinks() != null) {
|
||||
entity.setSocialLinks(dto.getSocialLinks());
|
||||
}
|
||||
if (dto.getDescription() != null) {
|
||||
entity.setDescription(dto.getDescription());
|
||||
}
|
||||
|
||||
this.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DesignerDTO getDesignerInfo(Long userId) {
|
||||
DesignerEntity entity = this.getOne(
|
||||
new LambdaQueryWrapper<DesignerEntity>()
|
||||
.eq(DesignerEntity::getUserId, userId)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (entity == null) {
|
||||
throw new BusinessException("设计师记录不存在");
|
||||
}
|
||||
|
||||
DesignerDTO dto = new DesignerDTO();
|
||||
dto.setShopName(entity.getShopName());
|
||||
dto.setAvatar(minioUtil.processMinioResource(entity.getAvatar(), CommonConstants.MINIO_PATH_TIMEOUT));
|
||||
dto.setBrandBanner(minioUtil.processMinioResource(entity.getBrandBanner(), CommonConstants.MINIO_PATH_TIMEOUT));
|
||||
dto.setOwnerName(entity.getOwnerName());
|
||||
dto.setEmail(entity.getEmail());
|
||||
dto.setMobile(entity.getMobile());
|
||||
dto.setSocialLinks(entity.getSocialLinks());
|
||||
dto.setDescription(entity.getDescription());
|
||||
|
||||
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());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 lombok.Data;
|
||||
import java.io.Serializable;
|
||||
@@ -14,8 +16,6 @@ public class DesignerApplyDetailVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "设计师ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "店铺名称")
|
||||
private String shopName;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.aida.seller.module.file;
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
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.result.Response;
|
||||
import com.aida.seller.util.MinioUtil;
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 文件上传控制器
|
||||
*
|
||||
* @author Fida Team
|
||||
* @date 2026-02-03
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
@Tag(name = "文件管理", description = "文件上传、下载等功能接口")
|
||||
@RequiredArgsConstructor
|
||||
public class FileUploadController {
|
||||
|
||||
private final MinioUtil minioUtil;
|
||||
|
||||
@Value("${multipart.max-file-size}")
|
||||
private Long maxFileSize;
|
||||
|
||||
@PostMapping("/upload")
|
||||
@Operation(summary = "文件上传", description = "上传文件到Minio服务器")
|
||||
public Response<String> uploadFile(
|
||||
@Parameter(description = "文件", required = true) @RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "允许的文件类型") @RequestParam(value = "allowedTypes", required = false) String[] allowedTypes
|
||||
) {
|
||||
Long userId = UserContext.getUserId();
|
||||
|
||||
if (file.isEmpty()) {
|
||||
throw new BusinessException("文件不能为空");
|
||||
}
|
||||
// 验证文件类型
|
||||
String contentType = file.getContentType();
|
||||
if (allowedTypes != null && allowedTypes.length > 0) {
|
||||
boolean validType = false;
|
||||
for (String allowedType : allowedTypes) {
|
||||
if (contentType != null && contentType.startsWith(allowedType)) {
|
||||
validType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!validType) {
|
||||
throw new BusinessException("不支持的文件类型: " + contentType);
|
||||
}
|
||||
}
|
||||
// 验证文件大小
|
||||
if (file.getSize() > maxFileSize * 1024 * 1024) {
|
||||
throw new BusinessException("文件大小超出限制: " + maxFileSize + " MB");
|
||||
}
|
||||
try {
|
||||
// 计算文件MD5(可选,用于文件完整性校验)
|
||||
String md5 = DigestUtil.md5Hex(file.getInputStream());
|
||||
log.info("文件MD5: {}", md5);
|
||||
|
||||
// 调用MinioUtil上传文件(使用默认桶名,按userId划分文件夹)
|
||||
String filePath = minioUtil.uploadImage(file, String.valueOf(userId));
|
||||
|
||||
log.info("文件上传成功: {}, 文件路径: {}", file.getOriginalFilename(), filePath);
|
||||
|
||||
return Response.success(minioUtil.processMinioResource(filePath, CommonConstants.MINIO_PATH_TIMEOUT));
|
||||
} catch (IOException e) {
|
||||
log.error("文件上传失败: {}", e.getMessage(), e);
|
||||
throw new BusinessException("文件上传失败");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.aida.seller.module.listing.controller;
|
||||
|
||||
import com.aida.seller.common.context.UserContext;
|
||||
import com.aida.seller.common.result.PageResponse;
|
||||
import com.aida.seller.common.result.Response;
|
||||
import com.aida.seller.module.listing.dto.*;
|
||||
import com.aida.seller.module.listing.service.ListingService;
|
||||
import com.aida.seller.module.listing.vo.ListingPageVO;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 商品管理 Controller
|
||||
*/
|
||||
@Tag(name = "Listing - 商品管理")
|
||||
@RestController
|
||||
@RequestMapping("/listing")
|
||||
@RequiredArgsConstructor
|
||||
public class ListingController {
|
||||
|
||||
private final ListingService listingService;
|
||||
|
||||
@Operation(summary = "批量保存/更新商品", description = "根据 id 是否存在判断新增或更新,同时保存图片")
|
||||
@PostMapping("/batch")
|
||||
public Response<Void> saveOrUpdate(
|
||||
@Parameter(description = "商品保存/更新表单列表") @RequestBody List<ListingSaveDTO> dtoList) {
|
||||
Long sellerId = UserContext.getUserId();
|
||||
listingService.saveOrUpdate(dtoList, sellerId);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取商品详情", description = "返回商品信息及所有图片")
|
||||
@GetMapping("/detail")
|
||||
public Response<ListingSaveDTO> getById(
|
||||
@Parameter(description = "商品ID") @RequestParam Long id) {
|
||||
Long sellerId = UserContext.getUserId();
|
||||
ListingSaveDTO result = listingService.getById(id);
|
||||
return Response.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询商品列表", description = "按 status 过滤,返回 ListingPageVO 不含图片详情")
|
||||
@GetMapping("/page")
|
||||
public Response<PageResponse<ListingPageVO>> getPage(
|
||||
@Parameter(description = "商品状态,可选0-草稿, 1-已发布, 2-已删除") @RequestParam(required = false) Integer status,
|
||||
@Parameter(description = "页码,默认1") @RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@Parameter(description = "每页数量,默认10") @RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
Long sellerId = UserContext.getUserId();
|
||||
ListingQueryDTO dto = new ListingQueryDTO();
|
||||
dto.setStatus(status);
|
||||
dto.setPageNum(pageNum);
|
||||
dto.setPageSize(pageSize);
|
||||
IPage<ListingPageVO> page = listingService.getPage(dto, sellerId);
|
||||
return Response.success(PageResponse.success(page));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新商品状态", description = "支持设为已删除或恢复为草稿")
|
||||
@PutMapping("/status")
|
||||
public Response<Void> updateStatus(
|
||||
@Parameter(description = "状态更新表单") @RequestBody ListingStatusUpdateDTO dto) {
|
||||
Long sellerId = UserContext.getUserId();
|
||||
listingService.updateStatus(dto.getId(), dto.getStatus(), sellerId);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "设置弹窗提醒标志", description = "在Redis中设置7天过期的弹窗提醒标志")
|
||||
@PostMapping("/popup/set")
|
||||
public Response<Void> setPopupReminder() {
|
||||
Long sellerId = UserContext.getUserId();
|
||||
listingService.setPopupReminder(sellerId);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "检查是否需要弹窗提醒", description = "检查Redis中是否存在未过期的弹窗提醒标志")
|
||||
@GetMapping("/popup/check")
|
||||
public Response<Integer> checkPopupReminder() {
|
||||
Long sellerId = UserContext.getUserId();
|
||||
boolean needPopup = listingService.checkPopupReminder(sellerId);
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 商城首页商品 Controller(Feign 端)
|
||||
*/
|
||||
@Tag(name = "ListingMall - 商城首页商品")
|
||||
@RestController
|
||||
@RequestMapping("/listing")
|
||||
@RequiredArgsConstructor
|
||||
public class ListingMallController {
|
||||
|
||||
private final ListingMallService listingMallService;
|
||||
|
||||
@Operation(summary = "商城首页商品分页列表", description = "")
|
||||
@PostMapping("/mall")
|
||||
public PageResponse<ListingMallVO> getMallListings(
|
||||
@RequestBody ListingMallQueryDTO dto) {
|
||||
IPage<ListingMallVO> page = listingMallService.getMallListings(dto);
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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 lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 商品图片 DTO(入参/出参复用)
|
||||
*/
|
||||
@Data
|
||||
public class ListingImageDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 图片ID,有值则更新,无则新增 */
|
||||
@Schema(description = "图片ID,有值则更新,无则新增")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video */
|
||||
@Schema(description = "图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video")
|
||||
private String category;
|
||||
|
||||
/** 图片URL */
|
||||
@Schema(description = "图片URL")
|
||||
private String imageUrl;
|
||||
|
||||
/** 排序 */
|
||||
@Schema(description = "排序")
|
||||
private Integer sortOrder;
|
||||
|
||||
/** 是否选中: 0-未选中, 1-选中(仅 product 有效) */
|
||||
@Schema(description = "是否选中: 0-未选中, 1-选中(仅 product 有效)")
|
||||
private Boolean isSelected;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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 ListingQueryDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 按状态过滤(选填,不传则查所有非删除) */
|
||||
@Schema(description = "按状态过滤(选填,不传则查所有非删除)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "页码,默认1")
|
||||
private Integer pageNum = 1;
|
||||
|
||||
@Schema(description = "每页数量,默认10")
|
||||
private Integer pageSize = 10;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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 lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品保存/更新 DTO(入参/出参复用)
|
||||
*/
|
||||
@Data
|
||||
public class ListingSaveDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 商品ID,无则新建,有则更新 */
|
||||
@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 Integer viewCount;
|
||||
|
||||
/** 状态: 0-草稿, 1-已发布, 2-已删除 */
|
||||
@Schema(description = "状态: 0-草稿, 1-已发布, 2-已删除")
|
||||
private Integer status;
|
||||
|
||||
/** 图片列表(更新时全量覆盖) */
|
||||
@Schema(description = "图片列表(更新时全量覆盖)")
|
||||
private List<ListingImageDTO> images;
|
||||
|
||||
/** 适用性别: male/female */
|
||||
@Schema(description = "适用性别: male/female")
|
||||
private String designFor;
|
||||
|
||||
/** 商品分类列表: outwear/trousers/blouse/dress/skirt/others/tops/bottoms */
|
||||
@Schema(description = "商品分类列表: outwear/trousers/blouse/dress/skirt/others/tops/bottoms")
|
||||
private List<String> productCategory;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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 ListingStatusUpdateDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 商品ID */
|
||||
@Schema(description = "商品ID")
|
||||
private Long id;
|
||||
|
||||
/** 目标状态: 0-草稿, 1-已发布, 2-已删除 */
|
||||
@Schema(description = "目标状态: 0-草稿, 1-已发布, 2-已删除")
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.aida.seller.module.listing.entity;
|
||||
|
||||
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 java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品实体
|
||||
*/
|
||||
@Data
|
||||
@TableName(value = "seller_listing", autoResultMap = true)
|
||||
public class ListingEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 商品ID */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 卖家ID */
|
||||
private Long sellerId;
|
||||
|
||||
/** 商品标题 */
|
||||
private String title;
|
||||
|
||||
/** 商品描述 */
|
||||
private String description;
|
||||
|
||||
/** 价格 */
|
||||
private BigDecimal price;
|
||||
|
||||
/** 销量 */
|
||||
private Integer salesVolume;
|
||||
|
||||
/** 封面图URL(列表页展示用) */
|
||||
private String cover;
|
||||
|
||||
/** 浏览量 */
|
||||
private Integer viewCount;
|
||||
|
||||
/** 状态: 0-草稿, 1-已发布, 2-已删除 */
|
||||
private Integer status;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/** 是否删除:0-否,1-是 */
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
|
||||
/** 适用性别: male/female */
|
||||
private String designFor;
|
||||
|
||||
/** 商品分类列表 */
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> productCategory;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.aida.seller.module.listing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 商品图片实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("seller_listing_image")
|
||||
public class ListingImageEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 图片ID */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 商品ID */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long listingId;
|
||||
|
||||
/** 图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video */
|
||||
private String category;
|
||||
|
||||
/** 图片URL */
|
||||
private String imageUrl;
|
||||
|
||||
/** 排序 */
|
||||
private Integer sortOrder;
|
||||
|
||||
/** 是否选中: 0-未选中, 1-选中(仅 product 有效) */
|
||||
private Integer isSelected;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 是否删除:0-否,1-是 */
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.aida.seller.module.listing.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 适用性别枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DesignForEnum {
|
||||
|
||||
MALE("male", "男性"),
|
||||
FEMALE("female", "女性"),
|
||||
ALL("all", "全部");
|
||||
|
||||
private final String code;
|
||||
private final String desc;
|
||||
|
||||
public static DesignForEnum of(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (DesignForEnum value : values()) {
|
||||
if (value.code.equals(code)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.aida.seller.module.listing.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 商品图片类别枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ImageCategoryEnum {
|
||||
|
||||
COVER("cover", "封面图", false),
|
||||
MAIN_PRODUCT("main_product", "主产品图", false),
|
||||
PRODUCT("product", "产品图", true),
|
||||
SKETCH("sketch", "草图", 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 desc;
|
||||
private final boolean hasSelection;
|
||||
|
||||
public static ImageCategoryEnum of(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (ImageCategoryEnum value : values()) {
|
||||
if (value.code.equals(code)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.aida.seller.module.listing.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 商品状态枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ListingStatusEnum {
|
||||
|
||||
DRAFT(0, "草稿"),
|
||||
PUBLISHED(1, "已发布"),
|
||||
DELETED(2, "已删除");
|
||||
|
||||
private final Integer code;
|
||||
private final String desc;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.aida.seller.module.listing.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 商品分类枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProductCategoryEnum {
|
||||
|
||||
OUTWEAR("outwear", "外套"),
|
||||
TROUSERS("trousers", "裤装"),
|
||||
BLOUSE("blouse", "衬衫"),
|
||||
DRESS("dress", "连衣裙"),
|
||||
SKIRT("skirt", "半身裙"),
|
||||
OTHERS("others", "其他"),
|
||||
TOP("tops", "上装"),
|
||||
BOTTOMS("bottoms", "下装");
|
||||
|
||||
@JsonValue
|
||||
private final String code;
|
||||
private final String desc;
|
||||
|
||||
@JsonCreator
|
||||
public static ProductCategoryEnum of(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (ProductCategoryEnum value : values()) {
|
||||
if (value.code.equalsIgnoreCase(code)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.aida.seller.module.listing.mapper;
|
||||
|
||||
import com.aida.seller.module.listing.entity.ListingImageEntity;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 商品图片 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ListingImageMapper extends BaseMapper<ListingImageEntity> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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 ListingMapper extends BaseMapper<ListingEntity> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.aida.seller.module.listing.service;
|
||||
|
||||
import com.aida.seller.module.listing.dto.ListingQueryDTO;
|
||||
import com.aida.seller.module.listing.dto.ListingSaveDTO;
|
||||
import com.aida.seller.module.listing.entity.ListingEntity;
|
||||
import com.aida.seller.module.listing.vo.ListingPageVO;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品 Service 接口
|
||||
*/
|
||||
public interface ListingService extends IService<ListingEntity> {
|
||||
|
||||
/**
|
||||
* 保存或更新商品(含图片)
|
||||
*
|
||||
* @param dto 商品信息
|
||||
* @param sellerId 卖家ID
|
||||
*/
|
||||
void saveOrUpdate(ListingSaveDTO dto, Long sellerId);
|
||||
|
||||
/**
|
||||
* 批量保存或更新商品(含图片)
|
||||
*
|
||||
* @param dtoList 商品信息列表
|
||||
* @param sellerId 卖家ID
|
||||
*/
|
||||
void saveOrUpdate(List<ListingSaveDTO> dtoList, Long sellerId);
|
||||
|
||||
/**
|
||||
* 获取商品详情(含所有图片)
|
||||
*
|
||||
* @param id 商品ID
|
||||
* @return 商品详情
|
||||
*/
|
||||
ListingSaveDTO getById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询商品列表
|
||||
*
|
||||
* @param dto 查询条件
|
||||
* @param sellerId 卖家ID
|
||||
* @return 分页结果
|
||||
*/
|
||||
IPage<ListingPageVO> getPage(ListingQueryDTO dto, Long sellerId);
|
||||
|
||||
/**
|
||||
* 更新商品状态
|
||||
*
|
||||
* @param id 商品ID
|
||||
* @param status 目标状态
|
||||
* @param sellerId 卖家ID
|
||||
*/
|
||||
void updateStatus(Long id, Integer status, Long sellerId);
|
||||
|
||||
/**
|
||||
* 设置弹窗提醒标志(7天过期)
|
||||
*
|
||||
* @param sellerId 卖家ID
|
||||
*/
|
||||
void setPopupReminder(Long sellerId);
|
||||
|
||||
/**
|
||||
* 检查是否需要弹窗提醒
|
||||
*
|
||||
* @param sellerId 卖家ID
|
||||
* @return true需要弹窗,false不需要
|
||||
*/
|
||||
boolean checkPopupReminder(Long sellerId);
|
||||
|
||||
/**
|
||||
* 获取店铺已发布商品列表,供买家端店铺主页调用
|
||||
* <p>按 status=1、deleted=0、sellerId、designFor 筛选,按 updateTime 倒序</p>
|
||||
*
|
||||
* @param sellerId 设计师用户ID
|
||||
* @param designFor 适用性别 female/male,all 表示不限制性别
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 每页数量
|
||||
* @return 分页商品列表
|
||||
*/
|
||||
IPage<ListingPageVO> getShopListings(Long sellerId, String designFor, int pageNum, int pageSize);
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package com.aida.seller.module.listing.service;
|
||||
|
||||
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.module.listing.dto.*;
|
||||
import com.aida.seller.module.listing.entity.ListingEntity;
|
||||
import com.aida.seller.module.listing.entity.ListingImageEntity;
|
||||
import com.aida.seller.module.listing.enums.ImageCategoryEnum;
|
||||
import com.aida.seller.module.listing.enums.ListingStatusEnum;
|
||||
import com.aida.seller.module.listing.enums.DesignForEnum;
|
||||
import com.aida.seller.module.listing.enums.ProductCategoryEnum;
|
||||
import com.aida.seller.module.listing.mapper.ListingImageMapper;
|
||||
import com.aida.seller.module.listing.mapper.ListingMapper;
|
||||
import com.aida.seller.module.listing.vo.ListingPageVO;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 商品 Service 实现
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity> implements ListingService {
|
||||
|
||||
private final ListingImageMapper listingImageMapper;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final MinioUtil minioUtil;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveOrUpdate(ListingSaveDTO dto, Long sellerId) {
|
||||
// 当 status 为 1(已发布)时,检查必填字段
|
||||
validateListingFields(dto);
|
||||
|
||||
ListingEntity entity = new ListingEntity();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
entity.setSellerId(sellerId);
|
||||
|
||||
|
||||
if (dto.getDesignFor() != null && DesignForEnum.of(dto.getDesignFor()) == null) {
|
||||
throw new BusinessException("designFor 只能为 male/female");
|
||||
}
|
||||
|
||||
if (entity.getViewCount() == null) {
|
||||
entity.setViewCount(0);
|
||||
}
|
||||
|
||||
Long listingId;
|
||||
if (dto.getId() == null) {
|
||||
entity.setStatus(dto.getStatus());
|
||||
this.save(entity);
|
||||
listingId = entity.getId();
|
||||
} else {
|
||||
ListingEntity existing = this.getOne(
|
||||
new LambdaQueryWrapper<ListingEntity>()
|
||||
.eq(ListingEntity::getId, dto.getId())
|
||||
.eq(ListingEntity::getSellerId, sellerId)
|
||||
.eq(ListingEntity::getDeleted, 0));
|
||||
if (existing == null) {
|
||||
throw new BusinessException("商品不存在");
|
||||
}
|
||||
entity.setCreateTime(existing.getCreateTime());
|
||||
this.updateById(entity);
|
||||
listingImageMapper.delete(new LambdaQueryWrapper<ListingImageEntity>()
|
||||
.eq(ListingImageEntity::getListingId, dto.getId()));
|
||||
listingId = dto.getId();
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(dto.getImages())) {
|
||||
validateImages(dto.getImages());
|
||||
handleImages(listingId, dto.getImages());
|
||||
String cover = extractCover(dto.getImages());
|
||||
if (StringUtils.hasText(cover)) {
|
||||
ListingEntity update = new ListingEntity();
|
||||
update.setId(listingId);
|
||||
update.setCover(minioUtil.convertToLogicalPath(cover));
|
||||
this.updateById(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@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(
|
||||
new LambdaQueryWrapper<ListingEntity>()
|
||||
.eq(ListingEntity::getId, id)
|
||||
.eq(ListingEntity::getDeleted, 0));
|
||||
if (entity == null) {
|
||||
throw new BusinessException("商品不存在");
|
||||
}
|
||||
|
||||
ListingSaveDTO dto = new ListingSaveDTO();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
|
||||
List<ListingImageEntity> images = listingImageMapper.selectList(
|
||||
new LambdaQueryWrapper<ListingImageEntity>()
|
||||
.eq(ListingImageEntity::getListingId, id)
|
||||
.orderByAsc(ListingImageEntity::getSortOrder));
|
||||
if (!CollectionUtils.isEmpty(images)) {
|
||||
List<ListingImageDTO> imageDTOs = images.stream().map(img -> {
|
||||
ListingImageDTO imgDto = new ListingImageDTO();
|
||||
BeanUtils.copyProperties(img, imgDto);
|
||||
imgDto.setImageUrl(minioUtil.processMinioResource(imgDto.getImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
|
||||
imgDto.setIsSelected(img.getIsSelected() != null && img.getIsSelected() == 1);
|
||||
return imgDto;
|
||||
}).collect(Collectors.toList());
|
||||
dto.setImages(imageDTOs);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<ListingPageVO> getPage(ListingQueryDTO dto, Long sellerId) {
|
||||
Page<ListingEntity> pageParam = new Page<>(dto.getPageNum(), dto.getPageSize());
|
||||
LambdaQueryWrapper<ListingEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(ListingEntity::getSellerId, sellerId);
|
||||
queryWrapper.eq(ListingEntity::getDeleted, 0);
|
||||
if (dto.getStatus() != null) {
|
||||
queryWrapper.eq(ListingEntity::getStatus, dto.getStatus());
|
||||
} else {
|
||||
queryWrapper.ne(ListingEntity::getStatus, ListingStatusEnum.DELETED.getCode());
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(Long id, Integer status, Long sellerId) {
|
||||
ListingEntity existing = this.getOne(
|
||||
new LambdaQueryWrapper<ListingEntity>()
|
||||
.eq(ListingEntity::getId, id)
|
||||
.eq(ListingEntity::getSellerId, sellerId)
|
||||
.eq(ListingEntity::getDeleted, 0));
|
||||
if (existing == null) {
|
||||
throw new BusinessException("商品不存在");
|
||||
}
|
||||
ListingEntity update = new ListingEntity();
|
||||
update.setId(id);
|
||||
update.setStatus(status);
|
||||
this.updateById(update);
|
||||
}
|
||||
|
||||
private void handleImages(Long listingId, List<ListingImageDTO> images) {
|
||||
Map<String, List<ListingImageDTO>> byCategory = images.stream()
|
||||
.collect(Collectors.groupingBy(img -> img.getCategory() == null ? "" : img.getCategory()));
|
||||
|
||||
for (Map.Entry<String, List<ListingImageDTO>> entry : byCategory.entrySet()) {
|
||||
String category = entry.getKey();
|
||||
List<ListingImageDTO> categoryImages = entry.getValue();
|
||||
|
||||
if (!StringUtils.hasText(category)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ImageCategoryEnum categoryEnum = ImageCategoryEnum.of(category);
|
||||
boolean supportsSelection = categoryEnum != null && categoryEnum.isHasSelection();
|
||||
|
||||
for (int i = 0; i < categoryImages.size(); i++) {
|
||||
ListingImageDTO imgDto = categoryImages.get(i);
|
||||
if (imgDto.getImageUrl() != null && !imgDto.getImageUrl().isEmpty()) {
|
||||
ListingImageEntity imgEntity = new ListingImageEntity();
|
||||
imgEntity.setListingId(listingId);
|
||||
imgEntity.setCategory(category);
|
||||
|
||||
imgEntity.setImageUrl(minioUtil.convertToLogicalPath(imgDto.getImageUrl()));
|
||||
imgEntity.setSortOrder(imgDto.getSortOrder() != null ? imgDto.getSortOrder() : i);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String extractCover(List<ListingImageDTO> images) {
|
||||
if (CollectionUtils.isEmpty(images)) {
|
||||
return null;
|
||||
}
|
||||
for (ListingImageDTO img : images) {
|
||||
if (ImageCategoryEnum.COVER.getCode().equals(img.getCategory())) {
|
||||
return img.getImageUrl();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setPopupReminder(Long sellerId) {
|
||||
String key = "popup:reminder:" + sellerId;
|
||||
redisTemplate.opsForValue().set(key, true, 7, java.util.concurrent.TimeUnit.DAYS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkPopupReminder(Long sellerId) {
|
||||
String key = "popup:reminder:" + sellerId;
|
||||
Object value = redisTemplate.opsForValue().get(key);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
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);
|
||||
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<ListingMallVO> listingMallVOS = listingIds.stream()
|
||||
.filter(entityMap::containsKey)
|
||||
.map(id -> {
|
||||
ListingEntity entity = entityMap.get(id);
|
||||
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());
|
||||
return vo;
|
||||
})
|
||||
.toList();
|
||||
return listingMallVOS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.aida.seller.module.listing.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;
|
||||
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 为 category,value 为该分类下所有图片 URL")
|
||||
private Map<String, List<String>> images;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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 java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 商城首页商品 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;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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 java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 商品分页列表 VO(不含图片详情)
|
||||
*/
|
||||
@Data
|
||||
public class ListingPageVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 商品ID */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 封面图URL(列表页直返,无须关联图片表) */
|
||||
private String cover;
|
||||
|
||||
/** 商品标题 */
|
||||
private String title;
|
||||
|
||||
/** 价格 */
|
||||
private BigDecimal price;
|
||||
|
||||
/** 修改时间 */
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -1,20 +1,26 @@
|
||||
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.result.PageResponse;
|
||||
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.BuyerOrdersDTO;
|
||||
import com.aida.seller.module.order.dto.UpdateOrderStatusDTO;
|
||||
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.OrderVO;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* My Orders - 订单管理控制器
|
||||
@@ -61,4 +67,33 @@ public class OrderController {
|
||||
IPage<OrderVO> orderPage = orderService.getOrderPage(dto, sellerId);
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.aida.seller.module.order.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -11,18 +13,27 @@ import java.time.LocalDateTime;
|
||||
* 订单主表
|
||||
*/
|
||||
@Data
|
||||
@TableName("order_info")
|
||||
@TableName("seller_orders")
|
||||
public class OrderInfoEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 订单唯一标识(如 SP897772698) */
|
||||
@TableId(type = IdType.INPUT)
|
||||
private String orderId;
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 卖家ID */
|
||||
private Long sellerId;
|
||||
|
||||
/** 买家ID */
|
||||
private Long buyerId;
|
||||
|
||||
/** 订单状态:0-未支付,1-已支付,2-已取消 */
|
||||
private Integer status;
|
||||
|
||||
/** 店铺名称 */
|
||||
private String shopName;
|
||||
|
||||
/** 订单总金额(HK$) */
|
||||
private BigDecimal totalPrice;
|
||||
|
||||
@@ -35,7 +46,8 @@ public class OrderInfoEntity implements Serializable {
|
||||
/** 总浏览量 */
|
||||
private Long totalViews;
|
||||
|
||||
/** 下单时间 */
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
|
||||
@@ -1,33 +1,46 @@
|
||||
package com.aida.seller.module.order.entity;
|
||||
|
||||
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 java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 订单商品明细表
|
||||
*/
|
||||
@Data
|
||||
@TableName("order_item")
|
||||
@TableName(value = "seller_order_item", autoResultMap = true)
|
||||
public class OrderItemEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键ID */
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 订单ID(关联 order_info) */
|
||||
private String orderId;
|
||||
/** 订单ID(关联 seller_orders) */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long orderId;
|
||||
|
||||
/** 卖家ID */
|
||||
private Long sellerId;
|
||||
|
||||
/** 买家ID */
|
||||
private Long buyerId;
|
||||
|
||||
/** 商品ID */
|
||||
private Long productId;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long listingId;
|
||||
|
||||
/** 商品名称 */
|
||||
private String productName;
|
||||
private String listingName;
|
||||
|
||||
/** 商品缩略图URL */
|
||||
private String thumbnailUrl;
|
||||
@@ -35,9 +48,6 @@ public class OrderItemEntity implements Serializable {
|
||||
/** 成交单价(HK$) */
|
||||
private BigDecimal price;
|
||||
|
||||
/** 购买数量 */
|
||||
private Integer quantity;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
@@ -45,4 +55,8 @@ public class OrderItemEntity implements Serializable {
|
||||
/** 是否删除:0-否,1-是 */
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
|
||||
/** 商品分类列表 */
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> productCategory;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
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.vo.AssetsItemVO;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
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.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.OrderVO;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 订单服务接口
|
||||
*/
|
||||
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 分页后的订单列表,按下单时间降序排列
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
package com.aida.seller.module.order.service;
|
||||
|
||||
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.UpdateOrderStatusDTO;
|
||||
import com.aida.seller.module.order.dto.BuyerOrdersDTO;
|
||||
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.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.OrderVO;
|
||||
import com.aida.seller.util.MinioUtil;
|
||||
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.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -30,6 +48,9 @@ import java.util.stream.Collectors;
|
||||
public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEntity> implements OrderService {
|
||||
|
||||
private final OrderItemMapper orderItemMapper;
|
||||
private final MinioUtil minioUtil;
|
||||
private final ListingMapper listingMapper;
|
||||
private final DesignerMapper designerMapper;
|
||||
|
||||
/**
|
||||
* 查询指定卖家的订单汇总数据
|
||||
@@ -72,22 +93,23 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
|
||||
if (StringUtils.hasText(dto.getKeyword())) {
|
||||
String keyword = dto.getKeyword().trim();
|
||||
queryWrapper.and(w -> w
|
||||
.like(OrderInfoEntity::getOrderId, keyword)
|
||||
.like(OrderInfoEntity::getId, keyword)
|
||||
.or()
|
||||
.inSql(OrderInfoEntity::getOrderId,
|
||||
"SELECT order_id FROM order_item WHERE product_name LIKE '%" + keyword + "%'")
|
||||
.inSql(OrderInfoEntity::getId,
|
||||
"SELECT order_id FROM seller_order_item WHERE deleted = 0 AND product_name LIKE '%" + keyword + "%'")
|
||||
);
|
||||
}
|
||||
|
||||
queryWrapper.eq(OrderInfoEntity::getStatus, 1);
|
||||
queryWrapper.orderByDesc(OrderInfoEntity::getCreateTime);
|
||||
|
||||
Page<OrderInfoEntity> page = this.page(pageParam, queryWrapper);
|
||||
|
||||
List<String> orderIds = page.getRecords().stream()
|
||||
.map(OrderInfoEntity::getOrderId)
|
||||
List<Long> orderIds = page.getRecords().stream()
|
||||
.map(OrderInfoEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, List<OrderItemEntity>> itemsMap = orderIds.isEmpty()
|
||||
Map<Long, List<OrderItemEntity>> itemsMap = orderIds.isEmpty()
|
||||
? Collections.emptyMap()
|
||||
: orderItemMapper.selectList(
|
||||
new LambdaQueryWrapper<OrderItemEntity>()
|
||||
@@ -97,17 +119,17 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
|
||||
|
||||
List<OrderVO> voList = page.getRecords().stream().map(order -> {
|
||||
OrderVO vo = new OrderVO();
|
||||
vo.setOrderId(order.getOrderId());
|
||||
vo.setOrderId(order.getId());
|
||||
vo.setPrice(order.getTotalPrice());
|
||||
vo.setBuyerUsername("@" + (order.getBuyerUsername() != null ? order.getBuyerUsername() : ""));
|
||||
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 -> {
|
||||
OrderVO.ItemVO itemVO = new OrderVO.ItemVO();
|
||||
itemVO.setProductId(item.getProductId());
|
||||
itemVO.setProductName(item.getProductName());
|
||||
itemVO.setThumbnailUrl(item.getThumbnailUrl());
|
||||
itemVO.setProductId(item.getListingId());
|
||||
itemVO.setProductName(item.getListingName());
|
||||
itemVO.setThumbnailUrl(minioUtil.processMinioResource(item.getThumbnailUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
|
||||
return itemVO;
|
||||
}).collect(Collectors.toList());
|
||||
vo.setItems(itemVOs);
|
||||
@@ -119,4 +141,209 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
|
||||
resultPage.setRecords(voList);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
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;
|
||||
|
||||
@@ -14,8 +16,9 @@ public class OrderVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "订单唯一标识", example = "SP897772698")
|
||||
private String orderId;
|
||||
@Schema(description = "订单唯一标识")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long orderId;
|
||||
|
||||
@Schema(description = "商品明细列表")
|
||||
private List<ItemVO> items;
|
||||
@@ -36,6 +39,7 @@ public class OrderVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "商品ID")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "商品名")
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# ============================================================
|
||||
|
||||
server:
|
||||
port: 5568
|
||||
port: 10093
|
||||
|
||||
spring:
|
||||
application:
|
||||
@@ -17,7 +17,7 @@ mybatis-plus:
|
||||
type-aliases-package: com.aida.seller.module.*.entity
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto
|
||||
id-type: assign_id
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
@@ -30,5 +30,3 @@ minio:
|
||||
default-bucket: aida-user
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.aida: debug
|
||||
|
||||
@@ -4,21 +4,32 @@
|
||||
# 示例:docker run -e NACOS_NAMESPACE=prod ...
|
||||
# ============================================================
|
||||
|
||||
nacos:
|
||||
namespace: dev
|
||||
host: 18.167.251.121:28848
|
||||
username: nacos
|
||||
password: Aidlab123123!
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: aida-seller
|
||||
config:
|
||||
import: optional:nacos:aida-public-${NACOS_NAMESPACE:test}.yml
|
||||
import: optional:nacos:aida-public-${nacos.namespace}.yml
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:18.167.251.121:28848}
|
||||
namespace: ${NACOS_NAMESPACE:ltx}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:Aidlab123123!}
|
||||
server-addr: ${nacos.host}
|
||||
namespace: ${nacos.namespace}
|
||||
username: ${nacos.username}
|
||||
password: ${nacos.password}
|
||||
# ip: 18.167.251.121
|
||||
port: 10093
|
||||
# ip-type: ipv4
|
||||
# prefer-ip-address: true
|
||||
|
||||
config:
|
||||
server-addr: ${NACOS_HOST:18.167.251.121:28848}
|
||||
namespace: ${NACOS_NAMESPACE:ltx}
|
||||
server-addr: ${nacos.host}
|
||||
namespace: ${nacos.namespace}
|
||||
file-extension: yaml
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:Aidlab123123!}
|
||||
username: ${nacos.username}
|
||||
password: ${nacos.password}
|
||||
|
||||
@@ -1,87 +1,95 @@
|
||||
-- 商品表
|
||||
CREATE TABLE seller_listing (
|
||||
id BIGINT PRIMARY KEY COMMENT '商品ID',
|
||||
seller_id BIGINT NOT NULL COMMENT '卖家ID',
|
||||
title VARCHAR(255) NOT NULL COMMENT '商品标题',
|
||||
description TEXT COMMENT '商品描述',
|
||||
price DECIMAL(10,2) COMMENT '价格',
|
||||
cover VARCHAR(200) COMMENT '封面图URL',
|
||||
view_count INT DEFAULT 0 COMMENT '浏览量',
|
||||
status INT(1) DEFAULT 0 COMMENT '状态: 0-草稿, 1-已发布, 2-已删除',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted INT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是',
|
||||
design_for VARCHAR(50) COMMENT '适用性别: male/female',
|
||||
product_category JSON COMMENT '商品分类列表',
|
||||
INDEX idx_seller_id (seller_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_deleted (deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
|
||||
|
||||
-- 商品图片表
|
||||
CREATE TABLE seller_listing_image (
|
||||
id BIGINT PRIMARY KEY COMMENT '图片ID',
|
||||
listing_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
category VARCHAR(50) NOT NULL COMMENT '图片类别: cover/main_product/product/sketch/apparel',
|
||||
image_url VARCHAR(500) NOT NULL COMMENT '图片URL',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||
is_selected INT(1) DEFAULT 0 COMMENT '是否选中: 0-未选中, 1-选中(仅product有效)',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
deleted INT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是',
|
||||
INDEX idx_listing_id (listing_id),
|
||||
INDEX idx_deleted (deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品图片表';
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS aida_seller DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
USE aida_seller;
|
||||
|
||||
-- ==================== 1. 设计师表 ====================
|
||||
DROP TABLE IF EXISTS `designer`;
|
||||
CREATE TABLE `designer` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '设计师ID',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '用户ID(关联用户表)',
|
||||
`shop_name` VARCHAR(100) NOT NULL COMMENT '店铺名称',
|
||||
`owner_name` VARCHAR(50) NOT NULL COMMENT '所有者全名',
|
||||
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
|
||||
`mobile` VARCHAR(20) NOT NULL COMMENT '手机号',
|
||||
`social_links` TEXT DEFAULT NULL COMMENT '作品集/社交媒体链接(JSON数组)',
|
||||
`apply_status` TINYINT NOT NULL DEFAULT 0 COMMENT '申请状态: 0-待审核, 1-审核通过, 2-审核拒绝',
|
||||
`audit_remark` VARCHAR(500) DEFAULT NULL COMMENT '审核备注',
|
||||
`audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-禁用, 1-启用',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0-否, 1-是',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_mobile` (`mobile`),
|
||||
KEY `idx_email` (`email`),
|
||||
KEY `idx_apply_status` (`apply_status`),
|
||||
KEY `idx_status` (`status`)
|
||||
-- 设计师表
|
||||
CREATE TABLE seller_designer (
|
||||
id BIGINT PRIMARY KEY COMMENT '设计师ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
shop_name VARCHAR(100) NOT NULL COMMENT '店铺名称',
|
||||
avatar VARCHAR(200) COMMENT '店铺头像URL',
|
||||
brand_banner VARCHAR(200) COMMENT '品牌Banner URL',
|
||||
owner_name VARCHAR(100) COMMENT '所有者全名',
|
||||
email VARCHAR(100) COMMENT '邮箱',
|
||||
mobile VARCHAR(30) COMMENT '手机号',
|
||||
social_links JSON COMMENT '作品集/社交媒体链接',
|
||||
description TEXT COMMENT '设计师简介',
|
||||
apply_status INT(1) DEFAULT 0 COMMENT '申请状态: 0-待审核, 1-审核通过, 2-审核拒绝',
|
||||
audit_remark VARCHAR(500) COMMENT '审核备注',
|
||||
audit_time DATETIME COMMENT '审核时间',
|
||||
status INT(1) DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted INT(1) DEFAULT 0 COMMENT '是否删除: 0-否, 1-是',
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_apply_status (apply_status),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_deleted (deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设计师表';
|
||||
|
||||
-- ==================== 2. 订单表 ====================
|
||||
-- 注意: 代码中 OrderInfoEntity 使用 @TableName("order_info"),
|
||||
-- 若生产库表名为 "orders" 请改为 "order_info",列名 "order_no" 建议改为 "order_id"
|
||||
DROP TABLE IF EXISTS `orders`;
|
||||
CREATE TABLE `orders` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`order_id` VARCHAR(32) NOT NULL COMMENT '订单号',
|
||||
`seller_id` BIGINT NOT NULL COMMENT '商家ID',
|
||||
`buyer_id` BIGINT NOT NULL COMMENT '买家ID',
|
||||
`buyer_name` VARCHAR(100) DEFAULT NULL COMMENT '买家名称',
|
||||
`total_price` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '商品总金额',
|
||||
`order_status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态: 0-待支付, 1-已支付, 2-已发货, 3-已完成, 4-已取消, 5-退款中',
|
||||
`total_views` BIGINT NOT NULL DEFAULT 0 COMMENT '商品浏览量(订单关联商品的总浏览数)',
|
||||
`shipping_address` TEXT DEFAULT NULL COMMENT '收货地址',
|
||||
`receiver_name` VARCHAR(50) DEFAULT NULL COMMENT '收货人',
|
||||
`receiver_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
|
||||
`receiver_address` VARCHAR(500) DEFAULT NULL COMMENT '详细地址',
|
||||
`tracking_number` VARCHAR(100) DEFAULT NULL COMMENT '快递单号',
|
||||
`tracking_company` VARCHAR(100) DEFAULT NULL COMMENT '快递公司',
|
||||
`pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
|
||||
`ship_time` DATETIME DEFAULT NULL COMMENT '发货时间',
|
||||
`receive_time` DATETIME DEFAULT NULL COMMENT '收货时间',
|
||||
`cancel_time` DATETIME DEFAULT NULL COMMENT '取消时间',
|
||||
`cancel_reason` VARCHAR(500) DEFAULT NULL COMMENT '取消原因',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0-否, 1-是',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_order_id` (`order_id`),
|
||||
KEY `idx_seller_id` (`seller_id`),
|
||||
KEY `idx_buyer_id` (`buyer_id`),
|
||||
KEY `idx_order_status` (`order_status`),
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
|
||||
-- 订单主表
|
||||
CREATE TABLE seller_orders (
|
||||
id BIGINT PRIMARY KEY 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$)',
|
||||
buyer_username VARCHAR(100) COMMENT '买家账号',
|
||||
total_items INT COMMENT '商品总数量',
|
||||
total_views BIGINT DEFAULT 0 COMMENT '总浏览量',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted INT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是',
|
||||
INDEX idx_seller_id (seller_id),
|
||||
INDEX idx_deleted (deleted),
|
||||
INDEX idx_create_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';
|
||||
|
||||
-- ==================== 3. 订单项表 ====================
|
||||
DROP TABLE IF EXISTS `order_item`;
|
||||
CREATE TABLE `order_item` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单项ID',
|
||||
`order_id` BIGINT NOT NULL COMMENT '订单ID(关联orders.id)',
|
||||
`order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
|
||||
`product_id` BIGINT NOT NULL COMMENT '商品ID',
|
||||
`sku_id` BIGINT DEFAULT NULL COMMENT 'SKU ID',
|
||||
`product_name` VARCHAR(200) NOT NULL COMMENT '商品名称',
|
||||
`sku_name` VARCHAR(200) DEFAULT NULL COMMENT 'SKU名称',
|
||||
`product_image` VARCHAR(500) DEFAULT NULL COMMENT '商品图片',
|
||||
`price` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '商品单价',
|
||||
`quantity` INT NOT NULL DEFAULT 1 COMMENT '购买数量',
|
||||
`total_amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '小计金额',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0-否, 1-是',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_order_id` (`order_id`),
|
||||
KEY `idx_order_no` (`order_no`),
|
||||
KEY `idx_product_id` (`product_id`),
|
||||
KEY `idx_sku_id` (`sku_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项表';
|
||||
-- 订单商品明细表
|
||||
CREATE TABLE seller_order_item (
|
||||
id BIGINT PRIMARY KEY COMMENT '主键ID',
|
||||
order_id BIGINT NOT NULL COMMENT '订单ID',
|
||||
seller_id BIGINT NOT NULL COMMENT '卖家ID',
|
||||
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',
|
||||
price DECIMAL(10,2) COMMENT '成交单价(HK$)',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
deleted INT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是',
|
||||
product_category JSON COMMENT '商品分类列表',
|
||||
INDEX idx_order_id (order_id),
|
||||
INDEX idx_listing_id (listing_id),
|
||||
INDEX idx_deleted (deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单商品明细表';
|
||||
|
||||
66
src/main/resources/logback-spring.xml
Normal file
66
src/main/resources/logback-spring.xml
Normal 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>
|
||||
50
src/main/resources/mapper/OrderItemMapper.xml
Normal file
50
src/main/resources/mapper/OrderItemMapper.xml
Normal 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>
|
||||
Reference in New Issue
Block a user