Compare commits

..

103 Commits

Author SHA1 Message Date
litianxiang
adde7b8ed0 卖家端总金额 2026-06-04 17:03:04 +08:00
litianxiang
920386f1fe 自动审核事务问题 2026-06-04 10:43:44 +08:00
litianxiang
9aaaa66e49 报错语言适配 2026-06-04 09:42:34 +08:00
bfa571424f TASK:异常提示语言更换 2026-06-03 19:04:04 +08:00
litianxiang
b095ca3dfb Merge remote-tracking branch 'origin/master' 2026-06-03 18:05:37 +08:00
litianxiang
5dd13caf50 报错语言适配 2026-06-03 18:05:04 +08:00
7568f6e3dd TASK:返回继续支付链接、发票链接 2026-06-03 17:56:49 +08:00
3f9d32ee80 Merge remote-tracking branch 'origin/master' 2026-06-03 17:55:58 +08:00
b130c52181 TASK:卖家审核通知 2026-06-03 17:55:14 +08:00
litianxiang
4beff1d870 已购查询 2026-06-03 17:50:38 +08:00
litianxiang
8002df8ffa 卖家红点 2026-06-03 17:25:36 +08:00
litianxiang
552be0036b 卖家红点 2026-06-03 17:23:30 +08:00
litianxiang
6d9ac6f393 报错返回语言适配 2026-06-03 16:25:53 +08:00
litianxiang
aa824f6e32 自动审核 2026-06-02 16:10:33 +08:00
litianxiang
bf8bd06f59 商品总浏览量FIX 2026-06-02 15:41:52 +08:00
litianxiang
37aadc54e2 商品总浏览量FIX 2026-06-02 15:32:48 +08:00
litianxiang
42f2ed06c3 迁移水印代码 2026-06-02 15:11:28 +08:00
litianxiang
a5ab27dcbb 优化水印 2026-06-02 15:01:56 +08:00
litianxiang
5f13ced8cd 销量设置默认值 2026-06-02 13:28:25 +08:00
litianxiang
9a3cd742c1 水印添加失败问题 2026-06-02 11:43:07 +08:00
litianxiang
a631b80598 水印图黑边问题 2026-06-02 11:29:03 +08:00
litianxiang
f838ce4d55 test 2026-06-02 11:11:43 +08:00
litianxiang
49a47ce269 时间问题 2026-06-02 10:57:27 +08:00
litianxiang
f89e3946f4 时间问题 2026-06-02 10:45:49 +08:00
litianxiang
6bc1e19e5b fegin 2026-06-02 10:29:05 +08:00
litianxiang
b01acc5f7b json转换fix 2026-06-01 16:54:06 +08:00
litianxiang
7994f4f2cd Dockerfile 2026-06-01 16:45:28 +08:00
litianxiang
e7b99735e8 恢复Dockerfile 2026-06-01 16:33:58 +08:00
litianxiang
241e0d7716 test字体库 2026-06-01 16:31:25 +08:00
litianxiang
864210088a 1 2026-06-01 16:28:09 +08:00
litianxiang
5a72e9ca02 1 2026-06-01 16:24:18 +08:00
litianxiang
afdb90e196 商品加入购买状态 商品详情加入水印 2026-06-01 15:53:02 +08:00
litianxiang
3ddf4051e3 水印 2026-06-01 13:30:24 +08:00
litianxiang
db75948bd7 下载商品按照快照状态下载 2026-06-01 11:30:17 +08:00
litianxiang
51491e4493 下载商品按照快照状态下载 2026-06-01 11:30:02 +08:00
litianxiang
269db8a060 购物车为空时报错 2026-05-29 14:28:20 +08:00
litianxiang
54ad0ac05b 筛选不全的bug 2026-05-29 10:06:21 +08:00
litianxiang
65c88c898f 卖家端bug 2026-05-29 09:59:57 +08:00
litianxiang
f9854c9be3 卖家端bug 2026-05-28 14:39:59 +08:00
litianxiang
367e77b782 卖家商品排序 2026-05-28 13:29:49 +08:00
litianxiang
0c47d8906e 卖家字段缺失 2026-05-28 13:24:45 +08:00
litianxiang
554ecf5fe6 工作流 2026-05-28 11:06:12 +08:00
litianxiang
61d67bdadd 没有数据时数字资产报错bugfix 2026-05-27 17:19:21 +08:00
litianxiang
7c2e07798c 下载图片接口改为可多选 2026-05-27 14:30:35 +08:00
litianxiang
69deffc8bc sql表 2026-05-27 13:31:26 +08:00
litianxiang
486134ac3b sql表 2026-05-27 13:31:09 +08:00
litianxiang
2ce5373ad3 Merge remote-tracking branch 'origin/master' 2026-05-27 13:29:46 +08:00
litianxiang
ee666adb8a 销量和浏览量字段设置 2026-05-27 13:29:24 +08:00
77d43ba572 Merge remote-tracking branch 'origin/master' 2026-05-27 13:24:13 +08:00
5c11c6e2b8 TASK:payment_id字段名/类型修改 2026-05-27 13:23:56 +08:00
litianxiang
86dadb3dce 购物车加入销量 2026-05-27 11:31:56 +08:00
litianxiang
60ac110497 接口加入sellerId 2026-05-27 11:29:08 +08:00
litianxiang
e2b507dc1e 获取数字资产没有shopname 2026-05-27 11:18:51 +08:00
litianxiang
2f8abf0d4e 设置语言和资料接口
下载接口
2026-05-27 10:49:19 +08:00
litianxiang
4c073c359d 设置语言和资料接口
下载接口
2026-05-27 10:49:06 +08:00
941e1e04af TASK:买家端/卖家端接入支付服务 2026-05-26 17:41:18 +08:00
litianxiang
ab552fc414 买家端联调bug 2026-05-22 13:02:11 +08:00
litianxiang
28f2d7678c fix商品接口报错 2026-05-21 16:41:17 +08:00
litianxiang
c11ed9bf96 买家端bugfix 2026-05-21 14:42:22 +08:00
litianxiang
18bc9fd5e4 买家端bugfix 2026-05-21 13:33:33 +08:00
litianxiang
c825f6af70 购物车相关代码 2026-05-20 16:53:58 +08:00
litianxiang
45885bf509 订单相关 2026-05-20 15:33:22 +08:00
litianxiang
d004dc75f5 订单相关接口 2026-05-20 11:42:31 +08:00
litianxiang
14efee9c85 订单相关接口 2026-05-20 11:10:01 +08:00
litianxiang
149ee13ec3 订单相关接口 2026-05-20 11:09:30 +08:00
litianxiang
a51bca1867 买家端订单fegin接口 2026-05-19 13:29:17 +08:00
litianxiang
a3020bccae 买家端接口fegin适配 2026-05-18 14:54:08 +08:00
litianxiang
daf40ab224 登录鉴权按照Source判断id来自于何处 2026-05-13 09:40:32 +08:00
litianxiang
912d5efee7 保存卖家信息解析为逻辑路径 2026-05-11 17:18:18 +08:00
litianxiang
0c1b74ddc0 买家端需要的获取商家主页和模糊搜索接口 2026-05-11 16:40:47 +08:00
litianxiang
e1d57f7b37 图片存在性校验 2026-05-07 15:59:04 +08:00
litianxiang
daf4c30a91 商品草稿状态也要校验 2026-05-07 14:11:18 +08:00
litianxiang
08f5a482eb log配置 2026-05-07 13:39:46 +08:00
litianxiang
1ff76957a7 选中状态fix 2026-05-07 11:38:19 +08:00
litianxiang
d77ce701e1 swagger bug 2026-05-07 10:31:28 +08:00
litianxiang
4b309efbb5 swagger bug 2026-05-07 10:31:01 +08:00
litianxiang
73ac643771 商品新增视频类型图片 2026-05-07 10:18:41 +08:00
litianxiang
0b9601278c 商品新增封面源头类型图片 2026-05-07 10:06:08 +08:00
litianxiang
0a1dc1c10d 工作流 2026-05-07 09:39:26 +08:00
litianxiang
9e5ba17dc4 工作流恢复 2026-05-06 17:23:45 +08:00
litianxiang
88c73c4462 工作流恢复 2026-05-06 17:13:36 +08:00
litianxiang
749241f19b 日志
订单表字段改名
视频返回新增字段
2026-05-06 16:58:48 +08:00
litianxiang
f69eca39ff 新工作流 2026-05-06 15:06:31 +08:00
litianxiang
38fb2ec4d5 服务端口号与宿主机统一,方便本地调试不需要修改bootstrap 2026-05-04 14:21:37 +08:00
litianxiang
b56ae5741b 服务端口号与宿主机统一,方便本地调试不需要修改bootstrap 2026-05-04 14:19:38 +08:00
litianxiang
1d4c8ec629 新增删除seller接口 2026-05-04 13:35:34 +08:00
litianxiang
4456722328 nacos注册测试 2026-05-04 10:19:18 +08:00
litianxiang
ad2254bc80 ProductCategory获取不到fix 2026-04-29 16:33:23 +08:00
litianxiang
5569da47f7 ProductCategory获取不到fix 2026-04-29 15:26:59 +08:00
litianxiang
fb892b6b21 商品排序规则按照修改时间 2026-04-29 15:18:23 +08:00
litianxiang
dea2409cea fix:时间自动创建 2026-04-29 14:19:10 +08:00
litianxiang
9d4c675594 fix:发布商品状态错误 2026-04-29 13:51:56 +08:00
litianxiang
da72640783 fix 2026-04-28 17:28:52 +08:00
litianxiang
48c4679820 工作流 2026-04-28 16:35:13 +08:00
litianxiang
86773339ec 工作流 2026-04-28 16:30:07 +08:00
litianxiang
92906881fe 工作流 2026-04-28 16:20:52 +08:00
litianxiang
520627a8fa 工作流 2026-04-28 16:13:30 +08:00
litianxiang
cc839dce1d bootstrap配置 2026-04-28 16:04:49 +08:00
litianxiang
5ceda7991d bootstrap配置 2026-04-28 15:53:56 +08:00
litianxiang
6f4e71b9e9 bootstrap配置 2026-04-28 15:46:47 +08:00
litianxiang
38c12b9ba5 bootstrap配置 2026-04-28 15:44:11 +08:00
litianxiang
232953acb0 bootstrap配置 2026-04-28 15:29:10 +08:00
litianxiang
b862da5b50 bootstrap配置 2026-04-28 14:38:02 +08:00
71 changed files with 3786 additions and 340 deletions

View File

@@ -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"
# 验证
java -version
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
mvn clean package -DskipTests
- name: 5.生成Dockerfile
run: |
@@ -79,10 +48,17 @@ jobs:
cat > Dockerfile << 'EOF'
FROM openjdk:21-ea-21-jdk-slim
VOLUME /tmp
# ===================== 修复字体库缺失=====================
RUN apt-get update && \
apt-get install -y --no-install-recommends \
fontconfig \
libfreetype6 \
&& rm -rf /var/lib/apt/lists/*
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
ENTRYPOINT ["java","-jar","/app.jar"]
ADD ./target/*.jar /app.jar
ENTRYPOINT ["java","-Djava.awt.headless=true","-jar","/app.jar"]
EOF
echo "Dockerfile内容:"
cat Dockerfile
@@ -102,50 +78,41 @@ jobs:
- ./temp:/temp
- ./uploads:/temp/uploads
ports:
- '10093:5568'
networks:
- aida_java_net
- '10093:10093'
restart: always
networks:
aida_java_net:
external: true
name: aida_java_net
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 compose build --no-cache
echo "启动服务..."
docker compose up -d
echo "验证容器状态..."
ls -l
echo "========= 停止旧服务 ========="
docker compose down
echo "========= 启动新服务 ========="
docker compose up -d --build
echo "========= 查看运行状态 ========="
docker compose ps
echo "部署完成!"
EOF_SSH

1
.gitignore vendored
View File

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

24
pom.xml
View File

@@ -25,14 +25,14 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<minio.version>8.5.7</minio.version>
<minio.version>8.0.3</minio.version>
<jwt.version>0.12.3</jwt.version>
<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>
<version>8.2.0</version>
</dependency>
<!-- MinIO -->
@@ -171,6 +176,11 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
@@ -179,6 +189,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>

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
package com.aida.seller.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "watermark.apparel")
public class WatermarkProperties {
private String text = "AiDA";
private float fontSizeRatio = 0.03f;
private String color = "255,255,255,60";
private int rotationDegrees = -30;
private float spacingRatioX = 1.67f;
private float spacingRatioY = 0.9f;
private int ttlDays = 30;
private String bucketName = "aida-user";
public int[] getColorComponents() {
String[] parts = color.split(",");
int[] rgba = new int[4];
for (int i = 0; i < parts.length; i++) {
rgba[i] = Integer.parseInt(parts[i].trim());
}
return rgba;
}
}

View File

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

View File

@@ -1,24 +1,83 @@
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())) {
if ("BUYER".equals(holder.getSource())){
AuthPrincipalVo buyerHolder = getBuyerHolder();
return buyerHolder;
}
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();
}
public static AuthPrincipalVo getBuyerHolder() {
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;
}
public static Long getBuyerIdSafely() {
AuthPrincipalVo holder = userHolder.get();
if (holder == null) {
return null;
}
if (!"BUYER".equals(holder.getSource())) {
return null;
}
return holder.getId();
}
}

View File

@@ -1,9 +1,17 @@
package com.aida.seller.common.exception;
import com.aida.seller.common.context.UserContext;
import com.aida.seller.model.vo.AuthPrincipalVo;
import com.aida.seller.common.result.ResultEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
/**
* @author: dwjian
* @description: 业务异常
@@ -12,31 +20,64 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
private Integer code;
private String msg;
public BusinessException(ResultEnum resultEnum) {
this.code = resultEnum.getCode();
this.msg = resultEnum.getMsg();
this.msg = getMessageFromResource(resultEnum.getMsg(), getUserLocale());
}
public BusinessException(String msg) {
this.code = ResultEnum.FAIL.getCode();
this.msg = msg;
this.msg = getMessageFromResource(msg, getUserLocale());
}
public BusinessException(String msg, Integer code) {
this.code = code;
this.msg = msg;
this.msg = getMessageFromResource(msg, getUserLocale());
}
public BusinessException(Throwable cause) {
this.code = ResultEnum.FAIL.getCode();
this.msg = cause.getMessage();
this.msg = getMessageFromResource(cause.getMessage(), getUserLocale());
}
public BusinessException(ResultEnum resultEnum, String customMsg) {
this.code = resultEnum.getCode();
this.msg = customMsg;
this.msg = getMessageFromResource(customMsg, getUserLocale());
}
private static String getUserLocale() {
try {
AuthPrincipalVo userInfo = UserContext.getUserHolder();
if (userInfo == null) return "en";
String lang = userInfo.getLanguage();
if (lang == null) return "en";
if ("CHINESE_SIMPLIFIED".equalsIgnoreCase(lang)) return "zh";
if ("ENGLISH".equalsIgnoreCase(lang)) return "en";
return "en";
} catch (Exception e) {
return "en";
}
}
public static String getMessageFromResource(String msg, String locale) {
if (msg == null) return null;
try (InputStream is = BusinessException.class.getClassLoader()
.getResourceAsStream("messages_" + locale + ".properties")) {
if (is != null) {
ResourceBundle bundle = new PropertyResourceBundle(
new InputStreamReader(is, StandardCharsets.UTF_8));
if (bundle.containsKey(msg)) {
return bundle.getString(msg);
}
}
} catch (Exception e) {
log.warn("Failed to load messages_{}.properties: {}", locale, e.getMessage());
}
return msg;
}
}

View File

@@ -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());
@@ -45,7 +54,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Response<?> handleException(Exception e) {
log.error("系统异常: ", e);
return Response.error("系统繁忙,请稍后再试");
return Response.error("system error");
}
/**
* 处理MinIO异常
@@ -53,12 +62,9 @@ public class GlobalExceptionHandler {
@ExceptionHandler(MinioException.class)
public ResponseEntity<Object> handleMinioException(MinioException e) {
log.error("[MinioException] {}", e.getMessage(), e);
String message = e.getMessage();
if (message != null && (message.contains("文件不能为空") || message.contains("不能为空"))) {
Response<?> response = Response.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "File cannot be empty");
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
Response<?> response = Response.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "File storage service error");
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>(
Response.error(e.getMessage()),
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}

View File

@@ -1,8 +1,17 @@
package com.aida.seller.common.exception;
import com.aida.seller.common.context.UserContext;
import com.aida.seller.model.vo.AuthPrincipalVo;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
/**
* MinIO 操作异常类
* 用于处理 MinIO 相关的业务异常
* 用于处理 MinIO 相关的业务异常,支持国际化
*
* @author Aida
* @since 2024-01-01
@@ -11,68 +20,55 @@ public class MinioException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private String errorCode;
/**
* 构造函数
*
* @param message 异常信息
*/
public MinioException(String message) {
super(message);
super(getMessageFromResource(message, getUserLocale()));
}
/**
* 构造函数
*
* @param message 异常信息
* @param cause 原因
*/
public MinioException(String message, Throwable cause) {
super(message, cause);
super(getMessageFromResource(message, getUserLocale()), cause);
}
/**
* 构造函数
*
* @param errorCode 错误码
* @param message 异常信息
*/
public MinioException(String errorCode, String message) {
super(message);
super(getMessageFromResource(message, getUserLocale()));
this.errorCode = errorCode;
}
/**
* 构造函数
*
* @param errorCode 错误码
* @param message 异常信息
* @param cause 原因
*/
public MinioException(String errorCode, String message, Throwable cause) {
super(message, cause);
super(getMessageFromResource(message, getUserLocale()), cause);
this.errorCode = errorCode;
}
/**
* 获取错误码
*
* @return 错误码
*/
public String getErrorCode() {
return errorCode;
}
/**
* 设置错误码
*
* @param errorCode 错误码
*/
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
private static String getUserLocale() {
AuthPrincipalVo userInfo = UserContext.getUserHolder();
if (userInfo == null) return "en";
String lang = userInfo.getLanguage();
if (lang == null) return "en";
if ("CHINESE_SIMPLIFIED".equalsIgnoreCase(lang)) return "zh";
if ("ENGLISH".equalsIgnoreCase(lang)) return "en";
return "en";
}
public static String getMessageFromResource(String msg, String locale) {
if (msg == null) return null;
try (InputStream is = MinioException.class.getClassLoader()
.getResourceAsStream("messages_" + locale + ".properties")) {
if (is != null) {
ResourceBundle bundle = new PropertyResourceBundle(
new InputStreamReader(is, StandardCharsets.UTF_8));
if (bundle.containsKey(msg)) return bundle.getString(msg);
}
} catch (Exception ignored) {
}
return msg;
}
}

View File

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

View File

@@ -3,13 +3,18 @@ package com.aida.seller.common.interceptor;
import com.aida.seller.common.context.UserContext;
import com.aida.seller.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) {

View File

@@ -7,13 +7,19 @@ 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.service.DesignerService;
import com.aida.seller.module.designer.vo.DesignerSearchVO;
import com.aida.seller.module.designer.vo.DesignerCheckVO;
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")
@@ -24,10 +30,10 @@ public class DesignerController {
@Operation(summary = "查询设计师是否有售卖资格")
@GetMapping("/check")
public Response<Boolean> check() {
public Response<DesignerCheckVO> check() {
Long userId = UserContext.getUserId();
boolean hasQualification = designerService.checkQualification(userId);
return Response.success(hasQualification);
DesignerCheckVO checkResult = designerService.checkQualification(userId);
return Response.success(checkResult);
}
@Operation(summary = "提交设计师入驻申请", description = "设计师提交入驻申请,系统自动设置为待审核状态")
@@ -87,4 +93,28 @@ public class DesignerController {
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);
}
}

View File

@@ -60,6 +60,9 @@ public class DesignerEntity implements Serializable {
/** 状态: 0-禁用, 1-启用 */
private Integer status;
/** 是否首次进入卖家系统: 0-否, 1-是 */
private Integer firstEnter;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

View File

@@ -0,0 +1,14 @@
package com.aida.seller.module.designer.feign;
import com.aida.seller.common.result.Response;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "aida-back", path = "/api/message")
public interface AiDABackFeignClient {
@PostMapping("/sellerApprovalNotice")
Response<String> sellerApprovalNotice(@RequestParam("userId") Long userId, @RequestParam("isApproved") boolean isApproved);
}

View File

@@ -4,15 +4,20 @@ 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.DesignerCheckVO;
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> {
/**
* 检查设计师是否有售卖资格
*/
Boolean checkQualification(Long userId);
DesignerCheckVO checkQualification(Long userId);
/**
* 提交设计师入驻申请
@@ -49,4 +54,29 @@ public interface DesignerService extends IService<DesignerEntity> {
* 获取设计师详细信息
*/
DesignerDTO getDesignerInfo(Long userId);
/**
* 删除设计师(逻辑删除)及其所有关联数据
* <p>级联删除seller_designer按userId→ seller_orders → seller_order_item、
* seller_listing → seller_listing_image</p>
*
* @param userId 用户ID
*/
void deleteByUserId(Long userId);
/**
* 模糊搜索设计师(不区分大小写),返回设计师信息及关联商品封面列表
*
* @param keyword 关键词(同时匹配店铺名称和所有者姓名,不区分大小写)
* @return 搜索结果列表每条包含设计师基础信息、最近5张商品封面图按updateTime倒序、商品总数
*/
List<DesignerSearchVO> searchDesigners(String keyword);
/**
* 根据 sellerId即 userId获取设计师店铺详情供买家端店铺主页调用
*
* @param sellerId 设计师用户ID
* @return 店铺详情
*/
DesignerShopVO getShopDetailBySellerId(Long sellerId);
}

View File

@@ -8,7 +8,19 @@ 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.feign.AiDABackFeignClient;
import com.aida.seller.module.designer.mapper.DesignerMapper;
import com.aida.seller.module.designer.vo.DesignerCheckVO;
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;
@@ -17,6 +29,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;
@@ -25,20 +42,29 @@ import java.time.LocalDateTime;
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;
private final AiDABackFeignClient aidaBackFeignClient;
@Override
public Boolean checkQualification(Long userId) {
public DesignerCheckVO checkQualification(Long userId) {
DesignerEntity entity = this.getOne(
new LambdaQueryWrapper<DesignerEntity>()
.eq(DesignerEntity::getUserId, userId)
.last("LIMIT 1")
);
if (entity == null) {
return false;
}
return DesignerApplyStatusEnum.APPROVED.getCode().equals(entity.getApplyStatus())
boolean hasQualification = entity != null
&& DesignerApplyStatusEnum.APPROVED.getCode().equals(entity.getApplyStatus())
&& entity.getStatus() != null && entity.getStatus() == 1;
DesignerCheckVO vo = new DesignerCheckVO();
vo.setHasQualification(hasQualification);
vo.setFirstEnter(entity != null ? entity.getFirstEnter() : null);
return vo;
}
@Override
@@ -51,14 +77,22 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
);
if (existDesigner != null) {
throw new BusinessException("该用户已提交过申请或已入驻");
throw new BusinessException("designer.already.applied");
}
DesignerEntity entity = new DesignerEntity();
entity.setUserId(UserContext.getUserId());
entity.setShopName(dto.getShopName());
entity.setAvatar(minioUtil.convertToLogicalPath(dto.getAvatar()));
entity.setBrandBanner(minioUtil.convertToLogicalPath(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());
@@ -68,6 +102,15 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
entity.setStatus(0);
this.save(entity);
// [临时改动] 自动审核通过:在同一事务内完成申请+审核
entity.setApplyStatus(DesignerApplyStatusEnum.APPROVED.getCode());
entity.setAuditRemark("自动审核通过");
entity.setAuditTime(LocalDateTime.now());
entity.setStatus(1);
this.updateById(entity);
aidaBackFeignClient.sellerApprovalNotice(dto.getUserId(), true);
}
@Override
@@ -119,11 +162,11 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
.last("LIMIT 1")
);
if (entity == null) {
throw new BusinessException("申请记录不存在");
throw new BusinessException("designer.application.not.found");
}
if (!DesignerApplyStatusEnum.PENDING.getCode().equals(entity.getApplyStatus())) {
throw new BusinessException("当前状态不支持审核操作");
throw new BusinessException("current.status.not.support.review");
}
entity.setApplyStatus(dto.getAuditStatus());
@@ -135,6 +178,10 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
}
this.updateById(entity);
// 站内信通知和邮件通知
aidaBackFeignClient.sellerApprovalNotice(dto.getUserId(), dto.getAuditStatus().equals(1));
}
@Override
@@ -156,18 +203,22 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
.last("LIMIT 1")
);
if (entity == null) {
throw new BusinessException("设计师记录不存在");
throw new BusinessException("designer.record.not.found");
}
if (dto.getShopName() != null) {
entity.setShopName(dto.getShopName());
}
if (dto.getAvatar() != null) {
entity.setAvatar(dto.getAvatar());
}
if (dto.getBrandBanner() != null) {
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
);
if (dto.getOwnerName() != null) {
entity.setOwnerName(dto.getOwnerName());
}
@@ -195,7 +246,7 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
.last("LIMIT 1")
);
if (entity == null) {
throw new BusinessException("设计师记录不存在");
throw new BusinessException("designer.record.not.found");
}
DesignerDTO dto = new DesignerDTO();
@@ -208,6 +259,167 @@ public class DesignerServiceImpl extends ServiceImpl<DesignerMapper, DesignerEnt
dto.setSocialLinks(entity.getSocialLinks());
dto.setDescription(entity.getDescription());
if (entity.getFirstEnter() == null || entity.getFirstEnter() == 0) {
entity.setFirstEnter(1);
this.updateById(entity);
}
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("designer.record.not.found");
}
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)
.eq(ListingEntity ::getStatus, 1)
.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("designer.not.found");
}
DesignerShopVO vo = new DesignerShopVO();
vo.setShopName(entity.getShopName());
vo.setAvatar(minioUtil.processMinioResource(entity.getAvatar(), CommonConstants.MINIO_PATH_TIMEOUT));
vo.setBrandBanner(minioUtil.processMinioResource(entity.getBrandBanner(), CommonConstants.MINIO_PATH_TIMEOUT));
vo.setOwnerName(entity.getOwnerName());
vo.setDescription(entity.getDescription());
vo.setSocialLinks(entity.getSocialLinks());
vo.setEmail(entity.getEmail());
vo.setMobile(entity.getMobile());
return vo;
}
}

View File

@@ -0,0 +1,22 @@
package com.aida.seller.module.designer.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 设计师售卖资格检查结果VO
*/
@Data
@Schema(description = "设计师售卖资格检查结果")
public class DesignerCheckVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "是否有售卖资格")
private Boolean hasQualification;
@Schema(description = "是否首次进入卖家系统: 0-否, 1-是")
private Integer firstEnter;
}

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ public class FileUploadController {
Long userId = UserContext.getUserId();
if (file.isEmpty()) {
throw new BusinessException("文件不能为空");
throw new BusinessException("file.cannot.be.empty");
}
// 验证文件类型
String contentType = file.getContentType();
@@ -58,12 +58,12 @@ public class FileUploadController {
}
}
if (!validType) {
throw new BusinessException("不支持的文件类型: " + contentType);
throw new BusinessException("file.type.unsupported");
}
}
// 验证文件大小
if (file.getSize() > maxFileSize * 1024 * 1024) {
throw new BusinessException("文件大小超出限制: " + maxFileSize + " MB");
throw new BusinessException("file.size.exceed.limit");
}
try {
// 计算文件MD5可选用于文件完整性校验
@@ -78,7 +78,7 @@ public class FileUploadController {
return Response.success(minioUtil.processMinioResource(filePath, CommonConstants.MINIO_PATH_TIMEOUT));
} catch (IOException e) {
log.error("文件上传失败: {}", e.getMessage(), e);
throw new BusinessException("文件上传失败");
throw new BusinessException("file.upload.fail");
}
}

View File

@@ -40,7 +40,7 @@ public class ListingController {
public Response<ListingSaveDTO> getById(
@Parameter(description = "商品ID") @RequestParam Long id) {
Long sellerId = UserContext.getUserId();
ListingSaveDTO result = listingService.getById(id, sellerId);
ListingSaveDTO result = listingService.getById(id);
return Response.success(result);
}
@@ -83,4 +83,22 @@ public class ListingController {
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));
}
@Operation(summary = "迁移旧商品水印数据", description = "为所有已发布商品中缺失水印记录的图片生成水印并写入数据库,返回处理数量")
@PostMapping("/migrate/watermarks")
public Response<Integer> migrateWatermarks() {
int count = listingService.migrateWatermarks();
return Response.success(count);
}
}

View File

@@ -0,0 +1,65 @@
package com.aida.seller.module.listing.controller;
import com.aida.seller.common.annotation.InternalOnly;
import com.aida.seller.common.result.PageResponse;
import com.aida.seller.common.result.Response;
import com.aida.seller.module.listing.dto.ListingMallQueryDTO;
import com.aida.seller.module.listing.service.ListingMallService;
import com.aida.seller.module.listing.vo.ListingDetailVO;
import com.aida.seller.module.listing.vo.ListingMallVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 商城首页商品 ControllerFeign 端)
*/
@Tag(name = "ListingMall - 商城首页商品")
@RestController
@RequestMapping("/listing")
@RequiredArgsConstructor
public class ListingMallController {
private final ListingMallService listingMallService;
@Operation(summary = "商城首页商品分页列表", description = "")
@PostMapping("/mall")
@InternalOnly
public Response<PageResponse<ListingMallVO>> getMallListings(
@RequestBody ListingMallQueryDTO dto) {
IPage<ListingMallVO> page = listingMallService.getMallListings(dto);
return Response.success(PageResponse.success(page));
}
@Operation(summary = "商品详情(商城详情页)", description = "关联图片与店铺信息")
@GetMapping("/mall/detail")
@InternalOnly
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);
}
@Operation(summary = "获取商品 listing 图片 URL 列表", description = "返回指定商品ID的 listing 分类图片 URL 列表(仅返回已购买该商品的用户)")
@GetMapping("/mall/main-product/urls")
@InternalOnly
public Response<List<String>> getListingUrls(
@Parameter(description = "商品ID") @RequestParam Long id,
@Parameter(description = "买家ID") @RequestParam Long buyerId) {
List<String> urls = listingMallService.getListingUrls(id, buyerId);
return Response.success(urls);
}
}

View File

@@ -20,8 +20,8 @@ public class ListingImageDTO implements Serializable {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 图片类别: cover/main_product/product/sketch/apparel */
@Schema(description = "图片类别: cover/main_product/product/sketch/apparel")
/** 图片类别: 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 */

View File

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

View File

@@ -51,7 +51,7 @@ public class ListingSaveDTO implements Serializable {
@Schema(description = "适用性别: male/female")
private String designFor;
/** 商品分类列表: outwear/trousers/blouse/dress/skirt/accessories */
@Schema(description = "商品分类列表: outwear/trousers/blouse/dress/skirt/accessories")
/** 商品分类列表: outwear/trousers/blouse/dress/skirt/others/tops/bottoms */
@Schema(description = "商品分类列表: outwear/trousers/blouse/dress/skirt/others/tops/bottoms")
private List<String> productCategory;
}

View File

@@ -1,9 +1,7 @@
package com.aida.seller.module.listing.entity;
import com.aida.seller.module.listing.enums.ProductCategoryEnum;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
@@ -17,7 +15,7 @@ import java.util.List;
* 商品实体
*/
@Data
@TableName("seller_listing")
@TableName(value = "seller_listing", autoResultMap = true)
public class ListingEntity implements Serializable {
private static final long serialVersionUID = 1L;
@@ -52,13 +50,11 @@ public class ListingEntity implements Serializable {
private Integer status;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(value = "create_time", fill = FieldFill.INSERT_UPDATE)
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/** 是否删除0-否1-是 */
@@ -70,5 +66,5 @@ public class ListingEntity implements Serializable {
/** 商品分类列表 */
@TableField(typeHandler = JacksonTypeHandler.class)
private List<ProductCategoryEnum> productCategory;
private List<String> productCategory;
}

View File

@@ -1,7 +1,6 @@
package com.aida.seller.module.listing.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
@@ -27,7 +26,7 @@ public class ListingImageEntity implements Serializable {
@JsonSerialize(using = ToStringSerializer.class)
private Long listingId;
/** 图片类别: cover/main_product/product/sketch/apparel */
/** 图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video */
private String category;
/** 图片URL */
@@ -40,7 +39,10 @@ public class ListingImageEntity implements Serializable {
private Integer isSelected;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(value = "create_time", fill = FieldFill.INSERT_UPDATE)
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 是否删除0-否1-是 */
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,45 @@
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_watermark_image")
public class ListingWatermarkImageEntity 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/main_product/product/sketch/apparel */
private String category;
/** 原图 logical path */
private String originalUrl;
/** 加水印图的 logical path */
private String watermarkedUrl;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 是否删除0-否1-是 */
@TableLogic
private Integer deleted;
}

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
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;
@@ -15,17 +17,21 @@ public enum ProductCategoryEnum {
BLOUSE("blouse", "衬衫"),
DRESS("dress", "连衣裙"),
SKIRT("skirt", "半身裙"),
ACCESSORIES("accessories", "配饰");
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.equals(code)) {
if (value.code.equalsIgnoreCase(code)) {
return value;
}
}

View File

@@ -0,0 +1,17 @@
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;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 商城首页商品 Mapper
*/
@Mapper
public interface ListingMallMapper extends BaseMapper<ListingEntity> {
@Update("UPDATE seller_listing SET view_count = view_count + 1, update_time = update_time WHERE id = #{id}")
int incrementViewCount(@Param("id") Long id);
}

View File

@@ -0,0 +1,19 @@
package com.aida.seller.module.listing.mapper;
import com.aida.seller.module.listing.entity.ListingWatermarkImageEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 商品图片水印 Mapper
*/
@Mapper
public interface ListingWatermarkImageMapper extends BaseMapper<ListingWatermarkImageEntity> {
int deleteByListingId(@Param("listingId") Long listingId);
List<ListingWatermarkImageEntity> selectByListingId(@Param("listingId") Long listingId);
}

View File

@@ -0,0 +1,47 @@
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);
/**
* 获取商品的 listing 图片 URL 列表
*
* @param id 商品ID
* @param buyerId 买家ID
* @return listing 图片 URL 列表
*/
List<String> getListingUrls(Long id, Long buyerId);
}

View File

@@ -34,10 +34,9 @@ public interface ListingService extends IService<ListingEntity> {
* 获取商品详情(含所有图片)
*
* @param id 商品ID
* @param sellerId 卖家ID
* @return 商品详情
*/
ListingSaveDTO getById(Long id, Long sellerId);
ListingSaveDTO getById(Long id);
/**
* 分页查询商品列表
@@ -71,4 +70,24 @@ public interface ListingService extends IService<ListingEntity> {
* @return true需要弹窗false不需要
*/
boolean checkPopupReminder(Long sellerId);
/**
* 获取店铺已发布商品列表,供买家端店铺主页调用
* <p>按 status=1、deleted=0、sellerId、designFor 筛选,按 updateTime 倒序</p>
*
* @param sellerId 设计师用户ID
* @param designFor 适用性别 female/maleall 表示不限制性别
* @param pageNum 页码
* @param pageSize 每页数量
* @return 分页商品列表
*/
IPage<ListingPageVO> getShopListings(Long sellerId, String designFor, int pageNum, int pageSize);
/**
* 迁移旧商品图片的水印数据:为所有已发布商品生成缺失的水印记录并存入数据库。
* 仅处理尚无水印记录的原图,已存在水印记录的图片跳过。
*
* @return 迁移处理的商品数量
*/
int migrateWatermarks();
}

View File

@@ -1,17 +1,23 @@
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.entity.ListingWatermarkImageEntity;
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.mapper.ListingWatermarkImageMapper;
import com.aida.seller.util.ImageWatermarkUtil;
import com.aida.seller.module.listing.vo.ListingPageVO;
import com.aida.seller.module.order.mapper.OrderItemMapper;
import com.aida.seller.module.order.entity.OrderItemEntity;
import com.aida.seller.util.MinioUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -26,9 +32,11 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -39,8 +47,11 @@ import java.util.stream.Collectors;
public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity> implements ListingService {
private final ListingImageMapper listingImageMapper;
private final ListingWatermarkImageMapper listingWatermarkImageMapper;
private final OrderItemMapper orderItemMapper;
private final RedisTemplate<String, Object> redisTemplate;
private final MinioUtil minioUtil;
private final ImageWatermarkUtil imageWatermarkUtil;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -52,16 +63,9 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
BeanUtils.copyProperties(dto, entity);
entity.setSellerId(sellerId);
if (!CollectionUtils.isEmpty(dto.getProductCategory())) {
List<ProductCategoryEnum> categories = dto.getProductCategory().stream()
.map(ProductCategoryEnum::of)
.filter(Objects::nonNull)
.collect(Collectors.toList());
entity.setProductCategory(categories.isEmpty() ? null : categories);
}
if (dto.getDesignFor() != null && DesignForEnum.of(dto.getDesignFor()) == null) {
throw new BusinessException("designFor 只能为 male/female");
throw new BusinessException("design.for.only.male.or.female");
}
if (entity.getViewCount() == null) {
@@ -69,26 +73,38 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
}
Long listingId;
Map<String, String> oldWatermarks = Map.of();
if (dto.getId() == null) {
entity.setStatus(ListingStatusEnum.DRAFT.getCode());
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::getSellerId, sellerId)
.eq(ListingEntity::getDeleted, 0));
if (existing == null) {
throw new BusinessException("商品不存在");
throw new BusinessException("product.not.found");
}
entity.setCreateTime(existing.getCreateTime());
this.updateById(entity);
listingImageMapper.delete(new LambdaQueryWrapper<ListingImageEntity>()
.eq(ListingImageEntity::getListingId, dto.getId()));
listingId = dto.getId();
if (Objects.equals(dto.getStatus(), 1)) {
List<ListingWatermarkImageEntity> oldWmList = listingWatermarkImageMapper.selectByListingId(listingId);
oldWatermarks = oldWmList.stream()
.collect(Collectors.toMap(ListingWatermarkImageEntity::getOriginalUrl,
ListingWatermarkImageEntity::getWatermarkedUrl,
(a, b) -> a));
listingWatermarkImageMapper.deleteByListingId(listingId);
}
}
if (!CollectionUtils.isEmpty(dto.getImages())) {
validateImages(dto.getImages());
handleImages(listingId, dto.getImages());
String cover = extractCover(dto.getImages());
if (StringUtils.hasText(cover)) {
@@ -97,6 +113,10 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
update.setCover(minioUtil.convertToLogicalPath(cover));
this.updateById(update);
}
if (Objects.equals(dto.getStatus(), 1)) {
generateWatermarks(listingId, oldWatermarks);
}
}
}
@@ -113,25 +133,18 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
}
@Override
public ListingSaveDTO getById(Long id, Long sellerId) {
public ListingSaveDTO getById(Long id) {
ListingEntity entity = this.getOne(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getId, id)
.eq(ListingEntity::getSellerId, sellerId));
.eq(ListingEntity::getDeleted, 0));
if (entity == null) {
throw new BusinessException("商品不存在");
throw new BusinessException("product.not.found");
}
ListingSaveDTO dto = new ListingSaveDTO();
BeanUtils.copyProperties(entity, dto);
if (!CollectionUtils.isEmpty(entity.getProductCategory())) {
dto.setProductCategory(
entity.getProductCategory().stream()
.map(ProductCategoryEnum::getCode)
.collect(Collectors.toList()));
}
List<ListingImageEntity> images = listingImageMapper.selectList(
new LambdaQueryWrapper<ListingImageEntity>()
.eq(ListingImageEntity::getListingId, id)
@@ -154,12 +167,13 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
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::getCreateTime);
queryWrapper.orderByDesc(ListingEntity::getUpdateTime);
IPage<ListingEntity> page = this.page(pageParam, queryWrapper);
Page<ListingPageVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
@@ -177,9 +191,10 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
ListingEntity existing = this.getOne(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getId, id)
.eq(ListingEntity::getSellerId, sellerId));
.eq(ListingEntity::getSellerId, sellerId)
.eq(ListingEntity::getDeleted, 0));
if (existing == null) {
throw new BusinessException("商品不存在");
throw new BusinessException("product.not.found");
}
ListingEntity update = new ListingEntity();
update.setId(id);
@@ -223,6 +238,34 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
}
}
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.required");
}
}
for (ListingImageDTO img : images) {
String category = img.getCategory();
if (requiredCategories.contains(category)) {
if (!StringUtils.hasText(img.getImageUrl())) {
throw new BusinessException("category.imageUrl.cannot.be.empty");
}
}
}
}
private String extractCover(List<ListingImageDTO> images) {
if (CollectionUtils.isEmpty(images)) {
return null;
@@ -232,12 +275,49 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
return img.getImageUrl();
}
}
if (!CollectionUtils.isEmpty(images)) {
return images.get(0).getImageUrl();
}
return null;
}
private void generateWatermarks(Long listingId, Map<String, String> oldWatermarks) {
Set<String> watermarkCategories = Set.of(
ImageCategoryEnum.COVER.getCode(),
ImageCategoryEnum.MAIN_PRODUCT.getCode(),
ImageCategoryEnum.PRODUCT.getCode(),
ImageCategoryEnum.SKETCH.getCode(),
ImageCategoryEnum.APPAREL.getCode());
List<ListingImageEntity> images = listingImageMapper.selectList(
new LambdaQueryWrapper<ListingImageEntity>()
.eq(ListingImageEntity::getListingId, listingId)
.in(ListingImageEntity::getCategory, watermarkCategories));
for (ListingImageEntity img : images) {
String originalUrl = img.getImageUrl();
String watermarkedUrl;
String existingWm = oldWatermarks.get(originalUrl);
if (existingWm != null) {
watermarkedUrl = existingWm;
} else {
watermarkedUrl = imageWatermarkUtil.applyWatermark(originalUrl);
watermarkedUrl = minioUtil.convertToLogicalPath(watermarkedUrl);
}
ListingWatermarkImageEntity watermark = new ListingWatermarkImageEntity();
watermark.setListingId(listingId);
watermark.setCategory(img.getCategory());
watermark.setOriginalUrl(originalUrl);
watermark.setWatermarkedUrl(watermarkedUrl);
try {
listingWatermarkImageMapper.insert(watermark);
} catch (Exception e) {
// 唯一索引冲突,忽略
}
}
}
@Override
public void setPopupReminder(Long sellerId) {
String key = "popup:reminder:" + sellerId;
@@ -256,30 +336,163 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
* 当 status 为 1已发布检查必填字段
*/
private void validateListingFields(ListingSaveDTO dto) {
if (dto.getStatus() != null && dto.getStatus() == 1) {
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/accessories");
}
if (!StringUtils.hasText(dto.getTitle())) {
throw new BusinessException("product.title.cannot.be.empty");
}
if (!StringUtils.hasText(dto.getDescription())) {
throw new BusinessException("product.description.cannot.be.empty");
}
if (dto.getPrice() == null) {
throw new BusinessException("product.price.cannot.be.empty");
}
if (!StringUtils.hasText(dto.getDesignFor())) {
throw new BusinessException("product.gender.cannot.be.empty");
}
if (DesignForEnum.of(dto.getDesignFor()) == null) {
throw new BusinessException("product.gender.must.be.male.or.female");
}
if (CollectionUtils.isEmpty(dto.getProductCategory())) {
throw new BusinessException("product.category.cannot.be.empty");
}
for (String category : dto.getProductCategory()) {
if (ProductCategoryEnum.of(category) == null) {
throw new BusinessException("product.category.must.be.valid");
}
}
}
@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("design.for.only.female.male.or.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()));
Long buyerId = UserContext.getBuyerIdSafely();
if (buyerId == null) {
result.getRecords().forEach(vo -> vo.setProductStatus(2));
return result;
}
Map<Long, Integer> orderStatusMap = getListingOrderStatusMap(
result.getRecords().stream().map(ListingPageVO::getId).collect(Collectors.toList()),
buyerId
);
for (ListingPageVO vo : result.getRecords()) {
Integer orderStatus = orderStatusMap.get(vo.getId());
if (orderStatus == null) {
vo.setProductStatus(2);
} else if (orderStatus == 1) {
vo.setProductStatus(1);
} else if (orderStatus == 0) {
vo.setProductStatus(0);
} else {
vo.setProductStatus(2);
}
}
return result;
}
private Map<Long, Integer> getListingOrderStatusMap(List<Long> listingIds, Long buyerId) {
if (CollectionUtils.isEmpty(listingIds) || buyerId == null) {
return Collections.emptyMap();
}
LambdaQueryWrapper<OrderItemEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(OrderItemEntity::getListingId, listingIds);
queryWrapper.eq(OrderItemEntity::getBuyerId, buyerId);
queryWrapper.eq(OrderItemEntity::getDeleted, 0);
List<OrderItemEntity> items = orderItemMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(items)) {
return Collections.emptyMap();
}
Map<Long, Integer> result = new HashMap<>();
for (OrderItemEntity item : items) {
Integer current = result.get(item.getListingId());
if (current == null || (item.getStatus() == 1 && current != 1)) {
result.put(item.getListingId(), item.getStatus());
}
}
return result;
}
@Override
public int migrateWatermarks() {
Set<String> watermarkCategories = Set.of(
ImageCategoryEnum.COVER.getCode(),
ImageCategoryEnum.MAIN_PRODUCT.getCode(),
ImageCategoryEnum.PRODUCT.getCode(),
ImageCategoryEnum.SKETCH.getCode(),
ImageCategoryEnum.APPAREL.getCode());
// 查出所有已发布且未删除的商品
List<ListingEntity> listings = this.list(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getStatus, 1)
.eq(ListingEntity::getDeleted, 0));
int count = 0;
for (ListingEntity listing : listings) {
Long listingId = listing.getId();
// 查出该商品已有的水印记录,构建 originalUrl -> watermarkedUrl map
List<ListingWatermarkImageEntity> existingWatermarks =
listingWatermarkImageMapper.selectByListingId(listingId);
Map<String, String> existingMap = existingWatermarks.stream()
.collect(Collectors.toMap(
ListingWatermarkImageEntity::getOriginalUrl,
ListingWatermarkImageEntity::getWatermarkedUrl,
(a, b) -> a));
// 查出该商品所有需要水印的图片
List<ListingImageEntity> images = listingImageMapper.selectList(
new LambdaQueryWrapper<ListingImageEntity>()
.eq(ListingImageEntity::getListingId, listingId)
.in(ListingImageEntity::getCategory, watermarkCategories));
for (ListingImageEntity img : images) {
String originalUrl = img.getImageUrl();
// 已有水印记录则跳过
if (existingMap.containsKey(originalUrl)) {
continue;
}
// 生成水印并入库
String watermarkedUrl = imageWatermarkUtil.applyWatermark(originalUrl);
watermarkedUrl = minioUtil.convertToLogicalPath(watermarkedUrl);
ListingWatermarkImageEntity watermark = new ListingWatermarkImageEntity();
watermark.setListingId(listingId);
watermark.setCategory(img.getCategory());
watermark.setOriginalUrl(originalUrl);
watermark.setWatermarkedUrl(watermarkedUrl);
try {
listingWatermarkImageMapper.insert(watermark);
count++;
} catch (Exception ignored) {
// 唯一索引冲突(并发或重复数据),忽略
}
}
}
return count;
}
}

View File

@@ -0,0 +1,406 @@
package com.aida.seller.module.listing.service.impl;
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.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.entity.ListingWatermarkImageEntity;
import com.aida.seller.module.listing.enums.DesignForEnum;
import com.aida.seller.module.listing.enums.ImageCategoryEnum;
import com.aida.seller.module.listing.mapper.ListingImageMapper;
import com.aida.seller.module.listing.mapper.ListingMallMapper;
import com.aida.seller.module.listing.mapper.ListingWatermarkImageMapper;
import com.aida.seller.module.listing.vo.ListingDetailVO;
import com.aida.seller.module.listing.vo.ListingMallVO;
import com.aida.seller.module.order.entity.OrderItemEntity;
import com.aida.seller.module.order.entity.OrderItemImageEntity;
import com.aida.seller.module.order.mapper.OrderItemImageMapper;
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;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Collections;
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 ListingWatermarkImageMapper listingWatermarkImageMapper;
private final DesignerMapper designerMapper;
private final OrderItemImageMapper orderItemImageMapper;
private final OrderItemMapper orderItemMapper;
public ListingMallServiceImpl(MinioUtil minioUtil, ListingImageMapper listingImageMapper, ListingWatermarkImageMapper listingWatermarkImageMapper, DesignerMapper designerMapper,
OrderItemImageMapper orderItemImageMapper, OrderItemMapper orderItemMapper) {
this.minioUtil = minioUtil;
this.listingImageMapper = listingImageMapper;
this.listingWatermarkImageMapper = listingWatermarkImageMapper;
this.designerMapper = designerMapper;
this.orderItemImageMapper = orderItemImageMapper;
this.orderItemMapper = orderItemMapper;
}
@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())) {
queryWrapper.and(wrapper -> {
for (int i = 0; i < dto.getCategories().size(); i++) {
String cat = dto.getCategories().get(i);
if (i == 0) {
wrapper.apply(
"JSON_CONTAINS(product_category, JSON_QUOTE({0}), '$')",
cat
);
} else {
wrapper.or().apply(
"JSON_CONTAINS(product_category, JSON_QUOTE({0}), '$')",
cat
);
}
}
});
}
applySorting(queryWrapper, dto.getSortField(), dto.getSortOrder());
IPage<ListingEntity> page = this.page(pageParam, queryWrapper);
List<ListingEntity> records = page.getRecords();
List<Long> sellerIds = records.stream()
.map(ListingEntity::getSellerId)
.distinct()
.toList();
if (CollectionUtils.isEmpty(sellerIds)) {
return new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
}
Map<Long, DesignerEntity> designerMap = designerMapper.selectList(
new LambdaQueryWrapper<DesignerEntity>()
.select(DesignerEntity::getUserId, DesignerEntity::getShopName)
.in(DesignerEntity::getUserId, sellerIds)
.eq(DesignerEntity::getDeleted, 0))
.stream()
.collect(Collectors.toMap(
DesignerEntity::getUserId,
designer -> designer,
(existing, replacement) -> existing
));
Page<ListingMallVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
List<ListingMallVO> voList = records.stream().map(entity -> {
DesignerEntity designer = designerMap.get(entity.getSellerId());
ListingMallVO vo = new ListingMallVO();
vo.setId(entity.getId());
vo.setCover(minioUtil.processMinioResource(entity.getCover(), CommonConstants.MINIO_PATH_TIMEOUT));
vo.setTitle(entity.getTitle());
vo.setPrice(entity.getPrice());
vo.setShopName(designer != null ? designer.getShopName() : null);
return vo;
}).toList();
Long buyerId = UserContext.getBuyerIdSafely();
if (buyerId != null) {
List<Long> listingIds = voList.stream().map(ListingMallVO::getId).collect(Collectors.toList());
Map<Long, Integer> orderStatusMap = getListingOrderStatusMap(listingIds, buyerId);
for (ListingMallVO vo : voList) {
Integer orderStatus = orderStatusMap.get(vo.getId());
if (orderStatus == null) {
vo.setProductStatus(2);
} else if (orderStatus == 1) {
vo.setProductStatus(1);
} else if (orderStatus == 0) {
vo.setProductStatus(0);
} else {
vo.setProductStatus(2);
}
}
} else {
voList.forEach(vo -> vo.setProductStatus(2));
}
result.setRecords(voList);
return result;
}
private Map<Long, Integer> getListingOrderStatusMap(List<Long> listingIds, Long buyerId) {
if (CollectionUtils.isEmpty(listingIds) || buyerId == null) {
return Collections.emptyMap();
}
LambdaQueryWrapper<OrderItemEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(OrderItemEntity::getListingId, listingIds);
queryWrapper.eq(OrderItemEntity::getBuyerId, buyerId);
queryWrapper.eq(OrderItemEntity::getDeleted, 0);
List<OrderItemEntity> items = orderItemMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(items)) {
return Collections.emptyMap();
}
Map<Long, Integer> resultMap = new HashMap<>();
for (OrderItemEntity item : items) {
Integer current = resultMap.get(item.getListingId());
if (current == null || (item.getStatus() == 1 && current != 1)) {
resultMap.put(item.getListingId(), item.getStatus());
}
}
return resultMap;
}
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("product.not.found");
}
this.baseMapper.incrementViewCount(id);
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()))
// hasSelection == true 的类别只保留 isSelected == 1 的图片
.filter(img -> {
ImageCategoryEnum categoryEnum = ImageCategoryEnum.of(img.getCategory());
return categoryEnum == null || !categoryEnum.isHasSelection() || Integer.valueOf(1).equals(img.getIsSelected());
})
// 先按 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());
}
}
// 步骤3从数据库读取预生成的水印记录将原图 URL 替换为水印图 URL
List<ListingWatermarkImageEntity> watermarks = listingWatermarkImageMapper.selectByListingId(id);
if (!watermarks.isEmpty()) {
// 以 originalUrl 为 key水印图 logical path 为 value构建查询 map
Map<String, String> watermarkMap = watermarks.stream()
.collect(Collectors.toMap(
ListingWatermarkImageEntity::getOriginalUrl,
ListingWatermarkImageEntity::getWatermarkedUrl,
(a, b) -> a
));
for (Map.Entry<String, List<String>> entry : imageMap.entrySet()) {
List<String> urls = entry.getValue();
if (urls == null || urls.isEmpty()) {
continue;
}
// 遍历该类别的所有图片 URL尝试从 watermarkMap 中找到对应的水印图
// 命中则替换为水印图的 presigned URL未命中则保留原 URL说明该图未生成水印或不在水印表
List<String> watermarkedUrls = urls.stream()
.map(url -> {
try {
String logicalPath = minioUtil.convertToLogicalPath(url);
String wmUrl = watermarkMap.get(logicalPath);
if (wmUrl != null) {
return minioUtil.getImageUrl(wmUrl, CommonConstants.MINIO_PATH_TIMEOUT);
}
} catch (Exception ignored) {
}
return url;
})
.toList();
imageMap.put(entry.getKey(), watermarkedUrls);
}
}
DesignerEntity designer = designerMapper.selectOne(
new LambdaQueryWrapper<DesignerEntity>()
.eq(DesignerEntity::getUserId, entity.getSellerId())
.eq(DesignerEntity::getDeleted, 0));
ListingDetailVO vo = new ListingDetailVO();
vo.setId(entity.getId());
vo.setTitle(entity.getTitle());
vo.setDescription(entity.getDescription());
vo.setPrice(entity.getPrice());
vo.setUpdateTime(entity.getUpdateTime());
vo.setShopName(designer != null ? designer.getShopName() : null);
vo.setImages(imageMap);
vo.setDesignFor(entity.getDesignFor());
vo.setProductCategory(entity.getProductCategory());
vo.setAvatar(minioUtil.processMinioResource(designer != null ? designer.getAvatar() : null, CommonConstants.MINIO_PATH_TIMEOUT));
vo.setSellerId(entity.getSellerId());
Long buyerId = UserContext.getBuyerIdSafely();
if (buyerId == null) {
vo.setProductStatus(2);
} else {
OrderItemEntity orderItem = orderItemMapper.selectOne(
new LambdaQueryWrapper<OrderItemEntity>()
.eq(OrderItemEntity::getListingId, id)
.eq(OrderItemEntity::getBuyerId, buyerId)
.eq(OrderItemEntity::getDeleted, 0)
.last("LIMIT 1"));
Integer orderStatus = orderItem != null ? orderItem.getStatus() : null;
if (orderStatus == null) {
vo.setProductStatus(2);
} else if (orderStatus == 1) {
vo.setProductStatus(1);
} else if (orderStatus == 0) {
vo.setProductStatus(0);
} else {
vo.setProductStatus(2);
}
}
return vo;
}
@Override
public List<String> getListingUrls(Long id, Long buyerId) {
List<OrderItemImageEntity> snapshots = orderItemImageMapper
.selectByListingIdAndBuyerIdWithOrderStatus(id, buyerId);
if (snapshots.isEmpty()) {
throw new BusinessException("product.not.purchased.yet");
}
return snapshots.stream()
.filter(img -> !ImageCategoryEnum.GIF.getCode().equals(img.getCategory())
&& !ImageCategoryEnum.FIRST_FRAME.getCode().equals(img.getCategory())
&& !ImageCategoryEnum.VIDEO.getCode().equals(img.getCategory()))
.sorted(Comparator.comparing(OrderItemImageEntity::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder())))
.map(OrderItemImageEntity::getImageUrl)
.collect(Collectors.toList());
}
@Override
public List<ListingMallVO> getListingSummaries(List<Long> listingIds) {
if (listingIds == null || listingIds.isEmpty()) {
return List.of();
}
List<ListingEntity> entities = this.list(
new LambdaQueryWrapper<ListingEntity>()
.in(ListingEntity::getId, listingIds)
.eq(ListingEntity::getDeleted, 0));
Map<Long, ListingEntity> entityMap = entities.stream()
.collect(Collectors.toMap(ListingEntity::getId, e -> e));
List<Long> sellerIds = entities.stream()
.map(ListingEntity::getSellerId)
.distinct()
.toList();
Map<Long, DesignerEntity> designerMap;
if (CollectionUtils.isEmpty(sellerIds)) {
designerMap = Map.of();
} else {
designerMap = designerMapper.selectList(
new LambdaQueryWrapper<DesignerEntity>()
.select(DesignerEntity::getUserId, DesignerEntity::getShopName)
.in(DesignerEntity::getUserId, sellerIds)
.eq(DesignerEntity::getDeleted, 0))
.stream()
.collect(Collectors.toMap(
DesignerEntity::getUserId,
designer -> designer,
(existing, replacement) -> existing
));
}
List<ListingMallVO> listingMallVOS = listingIds.stream()
.filter(entityMap::containsKey)
.map(id -> {
ListingEntity entity = entityMap.get(id);
DesignerEntity designer = designerMap.get(entity.getSellerId());
ListingMallVO vo = new ListingMallVO();
vo.setId(entity.getId());
vo.setCover(minioUtil.processMinioResource(entity.getCover(), CommonConstants.MINIO_PATH_TIMEOUT));
vo.setTitle(entity.getTitle());
vo.setPrice(entity.getPrice());
vo.setStatus(entity.getStatus());
vo.setProductCategory(entity.getProductCategory());
vo.setShopName(designer != null ? designer.getShopName() : null);
vo.setSellerId(entity.getSellerId());
vo.setSalesVolume(entity.getSalesVolume());
return vo;
})
.toList();
return listingMallVOS;
}
}

View File

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

View File

@@ -0,0 +1,45 @@
package com.aida.seller.module.listing.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 商城首页商品 VO
*/
@Data
public class ListingMallVO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String cover;
private String title;
private BigDecimal price;
private Integer status;
/** 商品分类列表 */
private List<String> productCategory;
/** 店铺名称 */
private String shopName;
@JsonSerialize(using = ToStringSerializer.class)
private Long sellerId;
/** 销量 */
private Integer salesVolume;
/** 商品状态0-已下单未付款1-已购买2-未下单 */
private Integer productStatus;
}

View File

@@ -29,15 +29,15 @@ public class ListingPageVO implements Serializable {
/** 价格 */
private BigDecimal price;
/** 修改时间 */
private LocalDateTime updateTime;
/** 销量 */
private Integer salesVolume;
/** 浏览量 */
private Integer viewCount;
/** 状态 */
private Integer status;
/** 创建时间(用于排序) */
private LocalDateTime createTime;
/** 商品状态0-已下单未付款1-已购买2-未下单 */
private Integer productStatus;
}

View File

@@ -1,20 +1,27 @@
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.dto.UpdatePaymentIdDTO;
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 +68,42 @@ 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)));
}
@InternalOnly
@PutMapping("/payment-id/batch")
@Operation(summary = "批量回填PaymentId仅内部服务调用")
public Response<Void> updatePaymentIdByIds(@RequestBody UpdatePaymentIdDTO dto) {
orderService.updatePaymentIdByIds(dto.getOrderIds(), dto.getPaymentId());
return Response.success();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
package com.aida.seller.module.order.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 批量回填PaymentId请求参数
*/
@Data
@Schema(description = "批量回填PaymentId请求参数")
public class UpdatePaymentIdDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "订单ID列表")
private List<Long> orderIds;
@Schema(description = "支付单ID")
private Long paymentId;
}

View File

@@ -25,6 +25,15 @@ public class OrderInfoEntity implements Serializable {
/** 卖家ID */
private Long sellerId;
/** 买家ID */
private Long buyerId;
/** 订单状态0-未支付1-已支付2-已取消 */
private Integer status;
/** 店铺名称 */
private String shopName;
/** 订单总金额HK$ */
private BigDecimal totalPrice;
@@ -37,7 +46,8 @@ public class OrderInfoEntity implements Serializable {
/** 总浏览量 */
private Long totalViews;
/** 下单时间 */
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@@ -47,4 +57,7 @@ public class OrderInfoEntity implements Serializable {
/** 是否删除0-否1-是 */
@TableLogic
private Integer deleted;
/** 交易流水号 */
private Long paymentId;
}

View File

@@ -1,6 +1,7 @@
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;
@@ -8,12 +9,13 @@ import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 订单商品明细表
*/
@Data
@TableName("seller_order_item")
@TableName(value = "seller_order_item", autoResultMap = true)
public class OrderItemEntity implements Serializable {
private static final long serialVersionUID = 1L;
@@ -27,12 +29,18 @@ public class OrderItemEntity implements Serializable {
@JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
/** 卖家ID */
private Long sellerId;
/** 买家ID */
private Long buyerId;
/** 商品ID */
@JsonSerialize(using = ToStringSerializer.class)
private Long productId;
private Long listingId;
/** 商品名称 */
private String productName;
private String listingName;
/** 商品缩略图URL */
private String thumbnailUrl;
@@ -40,9 +48,6 @@ public class OrderItemEntity implements Serializable {
/** 成交单价HK$ */
private BigDecimal price;
/** 购买数量 */
private Integer quantity;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@@ -50,4 +55,11 @@ public class OrderItemEntity implements Serializable {
/** 是否删除0-否1-是 */
@TableLogic
private Integer deleted;
/** 商品分类列表 */
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> productCategory;
/** 商品状态0-未支付1-已支付2-已取消 */
private Integer status;
}

View File

@@ -0,0 +1,56 @@
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;
import java.time.LocalDateTime;
/**
* 订单商品图片快照表
*/
@Data
@TableName("seller_order_item_image")
public class OrderItemImageEntity implements Serializable {
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 订单商品ID关联 seller_order_item.id */
@JsonSerialize(using = ToStringSerializer.class)
private Long orderItemId;
/** 订单ID */
@JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
/** 原商品ID留作参考 */
@JsonSerialize(using = ToStringSerializer.class)
private Long listingId;
/** 买家ID */
private Long buyerId;
/** 图片类别cover / main_product / product / sketch / apparel / firstFrame / gif / video */
private String category;
/** 图片URL */
private String imageUrl;
/** 排序 */
private Integer sortOrder;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 是否删除0-否1-是 */
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,19 @@
package com.aida.seller.module.order.mapper;
import com.aida.seller.module.order.entity.OrderItemImageEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 订单商品图片快照 Mapper
*/
@Mapper
public interface OrderItemImageMapper extends BaseMapper<OrderItemImageEntity> {
List<OrderItemImageEntity> selectByListingIdAndBuyerIdWithOrderStatus(
@Param("listingId") Long listingId,
@Param("buyerId") Long buyerId);
}

View File

@@ -1,9 +1,20 @@
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);
void incrementSalesVolumeByOrderIds(@Param("orderIds") List<Long> orderIds);
}

View File

@@ -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,36 @@ 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);
/**
* 根据订单号回填交易流水号
* @param orderIds 订单编号列表
* @param paymentId 交易流水号(仅供内部使用,不对外)
*/
void updatePaymentIdByIds(List<Long> orderIds, Long paymentId);
}

View File

@@ -1,20 +1,43 @@
package com.aida.seller.module.order.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.entity.DesignerEntity;
import com.aida.seller.module.designer.mapper.DesignerMapper;
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.mapper.ListingImageMapper;
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.entity.OrderItemImageEntity;
import com.aida.seller.module.order.mapper.OrderInfoMapper;
import com.aida.seller.module.order.mapper.OrderItemImageMapper;
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 lombok.extern.slf4j.Slf4j;
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;
@@ -27,12 +50,17 @@ import java.util.stream.Collectors;
/**
* 订单服务实现
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEntity> implements OrderService {
private final OrderItemMapper orderItemMapper;
private final OrderItemImageMapper orderItemImageMapper;
private final ListingImageMapper listingImageMapper;
private final MinioUtil minioUtil;
private final ListingMapper listingMapper;
private final DesignerMapper designerMapper;
/**
* 查询指定卖家的订单汇总数据
@@ -42,6 +70,7 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
public OrderSummaryVO getSummary(Long sellerId) {
LambdaQueryWrapper<OrderInfoEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderInfoEntity::getSellerId, sellerId);
wrapper.eq(OrderInfoEntity::getStatus, 1);
List<OrderInfoEntity> orders = this.list(wrapper);
@@ -52,10 +81,15 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
Integer totalPurchases = orders.size();
Long totalViews = orders.stream()
.map(OrderInfoEntity::getTotalViews)
// 累加该卖家所有商品的浏览量
Long totalViews = listingMapper.selectList(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getSellerId, sellerId))
.stream()
.map(ListingEntity::getViewCount)
.filter(v -> v != null)
.reduce(0L, Long::sum);
.mapToLong(v -> v)
.sum();
return new OrderSummaryVO(totalRevenue, totalPurchases, totalViews);
}
@@ -78,10 +112,11 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
.like(OrderInfoEntity::getId, keyword)
.or()
.inSql(OrderInfoEntity::getId,
"SELECT order_id FROM seller_order_item WHERE product_name LIKE '%" + keyword + "%'")
"SELECT order_id FROM seller_order_item WHERE deleted = 0 AND listing_name LIKE '%" + keyword + "%'")
);
}
queryWrapper.eq(OrderInfoEntity::getStatus, 1);
queryWrapper.orderByDesc(OrderInfoEntity::getCreateTime);
Page<OrderInfoEntity> page = this.page(pageParam, queryWrapper);
@@ -104,12 +139,13 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
vo.setPrice(order.getTotalPrice());
vo.setBuyerUsername("@" + (order.getBuyerUsername() != null ? order.getBuyerUsername() : ""));
vo.setDate(order.getCreateTime());
vo.setPaymentId(order.getPaymentId());
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.setProductId(item.getListingId());
itemVO.setProductName(item.getListingName());
itemVO.setThumbnailUrl(minioUtil.processMinioResource(item.getThumbnailUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
return itemVO;
}).collect(Collectors.toList());
@@ -122,4 +158,282 @@ 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());
vo.setSellerId(order.getSellerId());
vo.setPaymentId(order.getPaymentId());
List<OrderItemEntity> items = itemsMap.getOrDefault(order.getId(), Collections.emptyList());
List<BuyerOrderItemVO> itemVOs = items.stream().map(item -> {
BuyerOrderItemVO itemVO = new BuyerOrderItemVO();
itemVO.setId(item.getListingId());
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("product.id.list.cannot.be.empty");
}
if (dto.getBuyerId() == null) {
throw new BusinessException("buyer.id.cannot.be.empty");
}
if (!StringUtils.hasText(dto.getBuyerUsername())) {
throw new BusinessException("buyer.account.cannot.be.empty");
}
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("all.products.already.purchased");
}
dto.setListingIds(unpurchasedListingIds);
List<ListingEntity> listings = listingMapper.selectBatchIds(dto.getListingIds());
if (CollectionUtils.isEmpty(listings)) {
throw new BusinessException("product.not.found");
}
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();
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());
item.setStatus(0);
orderItemMapper.insert(item);
List<ListingImageEntity> images = listingImageMapper.selectList(
new LambdaQueryWrapper<ListingImageEntity>()
.eq(ListingImageEntity::getListingId, listing.getId()));
List<OrderItemImageEntity> snapshots = images.stream()
.filter(img -> {
ImageCategoryEnum categoryEnum = ImageCategoryEnum.of(img.getCategory());
return categoryEnum == null || !categoryEnum.isHasSelection() || Integer.valueOf(1).equals(img.getIsSelected());
})
.map(img -> {
OrderItemImageEntity snap = new OrderItemImageEntity();
snap.setOrderItemId(item.getId());
snap.setOrderId(order.getId());
snap.setListingId(listing.getId());
snap.setBuyerId(dto.getBuyerId());
snap.setCategory(img.getCategory());
snap.setImageUrl(img.getImageUrl());
snap.setSortOrder(img.getSortOrder());
return snap;
})
.collect(Collectors.toList());
if (!snapshots.isEmpty()) {
orderItemImageMapper.insert(snapshots);
}
}
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) {
log.info(dto.toString());
if (dto == null) {
throw new BusinessException("order.info.cannot.be.empty");
}
if (dto.getStatus() == null) {
throw new BusinessException("order.status.cannot.be.empty");
}
if (dto.getPaymentId() == null && (dto.getOrderIds() == null || dto.getOrderIds().isEmpty())) {
throw new BusinessException("order.id.list.cannot.be.empty");
}
boolean updated;
LambdaUpdateWrapper<OrderInfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
if (dto.getPaymentId() != null) {
updateWrapper.eq(OrderInfoEntity::getPaymentId, dto.getPaymentId())
.set(OrderInfoEntity::getStatus, dto.getStatus());
} else {
updateWrapper.in(OrderInfoEntity::getId, dto.getOrderIds())
.set(OrderInfoEntity::getStatus, dto.getStatus());
}
updated = this.update(updateWrapper);
if (!updated) {
throw new BusinessException("order.not.found.or.no.permission");
} else {
log.info("[Order] PaymentId:{} / OrderId:{}, 更新订单状态", dto.getPaymentId(), dto.getOrderIds());
List<Long> targetOrderIds;
if (dto.getOrderIds() != null && !dto.getOrderIds().isEmpty()) {
targetOrderIds = dto.getOrderIds();
} else if (dto.getPaymentId() != null) {
LambdaQueryWrapper<OrderInfoEntity> qw = new LambdaQueryWrapper<>();
qw.eq(OrderInfoEntity::getPaymentId, dto.getPaymentId());
targetOrderIds = this.list(qw).stream()
.map(OrderInfoEntity::getId)
.collect(Collectors.toList());
} else {
targetOrderIds = Collections.emptyList();
}
if (!targetOrderIds.isEmpty()) {
LambdaUpdateWrapper<OrderItemEntity> itemUpdateWrapper = new LambdaUpdateWrapper<>();
itemUpdateWrapper.in(OrderItemEntity::getOrderId, targetOrderIds)
.set(OrderItemEntity::getStatus, dto.getStatus());
orderItemMapper.update(null, itemUpdateWrapper);
log.info("[Order] Item status synced for orderIds: {}", targetOrderIds);
}
if (dto.getStatus() != null && dto.getStatus() == 1) {
orderItemMapper.incrementSalesVolumeByOrderIds(targetOrderIds);
log.info("[Order] SalesVolume incremented for orderIds: {}", targetOrderIds);
}
}
}
@Override
public IPage<AssetsItemVO> getAssetsPage(AssetsDTO dto) {
if (dto.getBuyerId() == null) {
throw new BusinessException("buyer.id.cannot.be.empty");
}
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)
.eq(OrderItemEntity::getStatus, 1)
.in(OrderItemEntity::getListingId, listingIds);
return orderItemMapper.selectList(wrapper).stream()
.map(OrderItemEntity::getListingId)
.distinct()
.collect(Collectors.toList());
}
@Override
public void updatePaymentIdByIds(List<Long> orderIds, Long paymentId) {
if (CollectionUtils.isEmpty(orderIds)) {
throw new BusinessException("order.id.list.cannot.be.empty");
}
LambdaUpdateWrapper<OrderInfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.in(OrderInfoEntity::getId, orderIds)
.set(OrderInfoEntity::getPaymentId, paymentId);
this.update(updateWrapper);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
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 = "卖家ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long sellerId;
@Schema(description = "商品明细列表")
private List<BuyerOrderItemVO> items;
@Schema(description = "支付ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long paymentId;
}

View File

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

View File

@@ -20,6 +20,9 @@ public class OrderVO implements Serializable {
@JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
@Schema(description = "交易流水号")
private Long paymentId;
@Schema(description = "商品明细列表")
private List<ItemVO> items;

View File

@@ -0,0 +1,126 @@
package com.aida.seller.util;
import com.aida.seller.common.config.WatermarkProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
@Slf4j
@Component
@RequiredArgsConstructor
public class ImageWatermarkUtil {
private final WatermarkProperties watermarkProperties;
private final MinioUtil minioUtil;
private static final int PRESIGNED_URL_EXPIRE_SECONDS = 7 * 24 * 60 * 60;
/**
* 对指定 MinIO 资源添加平铺文字水印,返回带水印图片的 presigned URL有效期 7 天)。
*/
public String applyWatermark(String minioResource) {
try {
String logicalPath = minioUtil.isPresignedUrl(minioResource)
? minioUtil.getLogicalPathFromPresignedUrl(minioResource)
: minioResource.trim();
try (InputStream originalStream = minioUtil.downloadFile(logicalPath);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] watermarkedBytes = addTextWatermark(originalStream,
watermarkProperties.getText(),
detectContentType(logicalPath));
String newPath = uploadWatermarkedImage(watermarkedBytes, logicalPath);
return minioUtil.getImageUrl(newPath, PRESIGNED_URL_EXPIRE_SECONDS);
}
} catch (Exception e) {
log.error("添加水印失败 resource={}, error={}", minioResource, e.getMessage(), e);
return minioResource;
}
}
/**
* 向图片流添加平铺文字水印,返回处理后的字节数组。
*/
public byte[] addTextWatermark(InputStream imageStream, String text, String contentType) throws Exception {
BufferedImage original = ImageIO.read(imageStream);
if (original == null) {
throw new IllegalArgumentException("无法读取图片格式");
}
int width = original.getWidth();
int height = original.getHeight();
BufferedImage output = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = output.createGraphics();
try {
g2d.setBackground(Color.WHITE);
g2d.clearRect(0, 0, width, height);
g2d.drawImage(original, 0, 0, width, height, null);
int baseFontSize = Math.max(12, (int) (Math.min(width, height) * watermarkProperties.getFontSizeRatio()));
Font font = new Font("Arial", Font.PLAIN, baseFontSize);
g2d.setFont(font);
int[] rgba = watermarkProperties.getColorComponents();
g2d.setColor(new Color(rgba[0], rgba[1], rgba[2], rgba.length > 3 ? rgba[3] : 128));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
double radians = Math.toRadians(watermarkProperties.getRotationDegrees());
int stepX = (int) (baseFontSize * watermarkProperties.getSpacingRatioX());
int stepY = (int) (baseFontSize * watermarkProperties.getSpacingRatioY());
AffineTransform rotateTransform = new AffineTransform();
rotateTransform.translate(width / 2.0, height / 2.0);
rotateTransform.rotate(radians);
rotateTransform.translate(-width / 2.0, -height / 2.0);
g2d.setTransform(rotateTransform);
for (int y = -height; y < height * 2; y += stepY) {
for (int x = -width; x < width * 2; x += stepX) {
g2d.drawString(text, x, y);
}
}
} finally {
g2d.dispose();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String formatName = contentType != null && contentType.contains("png") ? "PNG" : "JPEG";
ImageIO.write(output, formatName, baos);
return baos.toByteArray();
}
private String uploadWatermarkedImage(byte[] watermarkedBytes, String originalPath) {
int lastSlash = originalPath.lastIndexOf('/');
String basePath = lastSlash > 0 ? originalPath.substring(0, lastSlash) : "";
String fileName = lastSlash >= 0 ? originalPath.substring(lastSlash + 1) : originalPath;
String nameWithoutExt = fileName.contains(".")
? fileName.substring(0, fileName.lastIndexOf('.'))
: fileName;
String ext = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf('.')) : ".jpg";
String watermarkedPath = (basePath.isEmpty() ? "" : basePath + "/") + "watermarked/" + nameWithoutExt + "_wm" + ext;
String contentType = ext.contains("png") ? "image/png" : "image/jpeg";
return minioUtil.uploadBytes(watermarkedBytes, watermarkedPath, contentType,
watermarkProperties.getBucketName() != null ? watermarkProperties.getBucketName() : "aida-user");
}
private String detectContentType(String path) {
String lower = path.toLowerCase();
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".gif")) return "image/gif";
if (lower.endsWith(".webp")) return "image/webp";
return "image/jpeg";
}
}

View File

@@ -71,7 +71,7 @@ public class MinioUtil {
return bucketName + "/" + filePath;
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
throw new MinioException("文件上传失败", e);
throw new MinioException("minio.upload.failed", e);
}
}
@@ -111,7 +111,7 @@ public class MinioUtil {
return url;
} catch (Exception e) {
log.error("获取临时访问地址失败: {}", e.getMessage(), e);
throw new MinioException("获取临时访问地址失败", e);
throw new MinioException("minio.get.presigned.url.failed", e);
}
}
@@ -123,7 +123,7 @@ public class MinioUtil {
try {
int index = objectPath.indexOf("/");
if (index == -1) {
throw new MinioException("无效的对象路径,格式应为 bucketName/filePath");
throw new MinioException("minio.invalid.object.path");
}
String bucketName = objectPath.substring(0, index);
String filePath = objectPath.substring(index + 1);
@@ -136,7 +136,7 @@ public class MinioUtil {
log.info("文件删除成功,桶名: {}, 文件路径: {}", bucketName, filePath);
} catch (Exception e) {
log.error("文件删除失败: {}", e.getMessage(), e);
throw new MinioException("文件删除失败", e);
throw new MinioException("minio.delete.failed", e);
}
}
@@ -149,7 +149,7 @@ public class MinioUtil {
String firstPath = objectPaths.get(0);
int index = firstPath.indexOf("/");
if (index == -1) {
throw new MinioException("无效的对象路径,格式应为 bucketName/filePath");
throw new MinioException("minio.invalid.object.path");
}
String bucketName = firstPath.substring(0, index);
@@ -175,7 +175,7 @@ public class MinioUtil {
log.info("批量删除文件成功,桶名: {}, 文件数量: {}", bucketName, objectPaths.size());
} catch (Exception e) {
log.error("批量删除文件失败: {}", e.getMessage(), e);
throw new MinioException("批量删除文件失败", e);
throw new MinioException("minio.batch.delete.failed", e);
}
}
@@ -199,7 +199,7 @@ public class MinioUtil {
return uploadImage(imageBytes, bucketName, filePath, contentType);
} catch (Exception e) {
log.error("base64图片上传失败: {}", e.getMessage(), e);
throw new MinioException("base64图片上传失败", e);
throw new MinioException("minio.base64.upload.failed", e);
}
}
@@ -227,7 +227,7 @@ public class MinioUtil {
return bucketName + "/" + filePath;
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
throw new MinioException("文件上传失败", e);
throw new MinioException("minio.upload.failed", e);
}
}
@@ -238,7 +238,7 @@ public class MinioUtil {
public String uploadBytes(byte[] bytes, String objectName, String contentType, String bucketName) {
if (bytes == null || bytes.length == 0) {
throw new MinioException("文件内容不能为空");
throw new MinioException("minio.file.content.empty");
}
try {
@@ -255,14 +255,14 @@ public class MinioUtil {
return bucketName + "/" + objectName;
} catch (Exception e) {
log.error("字节数组上传失败: {}", e.getMessage(), e);
throw new MinioException("字节数组上传失败", e);
throw new MinioException("minio.bytes.upload.failed", e);
}
}
public InputStream downloadFile(String logicalPath) {
int index = logicalPath.indexOf("/");
if (index <= 0) {
throw new MinioException("逻辑路径格式错误,应包含桶名: " + logicalPath);
throw new MinioException("minio.logical.path.format.error");
}
String bucketName = logicalPath.substring(0, index);
@@ -271,11 +271,11 @@ public class MinioUtil {
try {
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new MinioException("桶不存在: " + bucketName);
throw new MinioException("minio.bucket.not.exists");
}
} catch (Exception e) {
log.error("验证桶存在性失败: {}", e.getMessage(), e);
throw new MinioException("验证桶存在性失败bucketName:{}", bucketName);
throw new MinioException("minio.verify.bucket.failed");
}
try {
@@ -287,7 +287,7 @@ public class MinioUtil {
);
} catch (Exception e) {
log.error("文件下载失败: {}", e.getMessage(), e);
throw new MinioException("文件下载失败", e);
throw new MinioException("minio.download.failed", e);
}
}
@@ -302,7 +302,7 @@ public class MinioUtil {
int firstSlashIndex = path.indexOf("/");
if (firstSlashIndex <= 0) {
throw new MinioException("预签名URL路径格式无效应包含桶名和对象名: " + presignedUrl);
throw new MinioException("minio.presigned.url.format.invalid");
}
String bucketName = path.substring(0, firstSlashIndex);
@@ -311,7 +311,7 @@ public class MinioUtil {
return bucketName + "/" + objectName;
} catch (Exception e) {
log.error("预签名URL解析失败: {}", e.getMessage(), e);
throw new MinioException("预签名URL解析失败", e);
throw new MinioException("minio.presigned.url.parse.failed", e);
}
}
@@ -437,14 +437,14 @@ public class MinioUtil {
public String convertToLogicalPath(String url) {
if (url == null || url.isEmpty()) {
throw new MinioException("URL不能为空");
throw new MinioException("minio.url.cannot.be.empty");
}
if (isMinioLogicalPath(url)) {
return url.trim();
} else if (isPresignedUrl(url)) {
return getLogicalPathFromPresignedUrl(url);
} else {
throw new MinioException("无法识别的MinIO资源格式: " + url + "请提供有效的预签名URL或逻辑路径");
throw new MinioException("minio.resource.format.unrecognized");
}
}

View File

@@ -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
@@ -29,6 +29,16 @@ mybatis-plus:
minio:
default-bucket: aida-user
# ---------- 水印配置apparel 成衣图平铺文字水印) ----------
watermark:
apparel:
text: "AiDA"
font-size-ratio: 0.053
color: "194,194,194,107"
rotation-degrees: 45
spacing-ratio-x: 3.6
spacing-ratio-y: 2.5
ttl-days: 30
bucket-name: "aida-user"
logging:
level:
com.aida: debug

View File

@@ -22,8 +22,11 @@ spring:
namespace: ${nacos.namespace}
username: ${nacos.username}
password: ${nacos.password}
# ip: ${HOSTNAME}
# hostname: master-aida-seller
# ip: 18.167.251.121
port: 10093
# ip-type: ipv4
# prefer-ip-address: true
config:
server-addr: ${nacos.host}
namespace: ${nacos.namespace}

View File

@@ -1,23 +1,24 @@
-- 商品表
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 '价格',
stock INT 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` (
`id` bigint(20) NOT NULL COMMENT '商品ID',
`seller_id` bigint(20) NOT NULL COMMENT '卖家ID',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '商品标题',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商品描述',
`price` decimal(10, 2) NULL DEFAULT NULL COMMENT '价格',
`sales_volume` int(11) NOT NULL DEFAULT 0 COMMENT '',
`cover` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '封面图URL',
`view_count` int(11) NULL DEFAULT 0 COMMENT '浏览量',
`status` int(11) NULL DEFAULT 0 COMMENT '状态: 0-草稿, 1-已发布, 2-已删除',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` int(11) NULL DEFAULT 0 COMMENT '是否删除0-否1-是',
`design_for` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '适用性别: male/female',
`product_category` json NULL COMMENT '商品分类列表',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_seller_id`(`seller_id` ASC) USING BTREE,
INDEX `idx_status`(`status` ASC) USING BTREE,
INDEX `idx_deleted`(`deleted` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '商品表' ROW_FORMAT = Dynamic;
-- 商品图片表
CREATE TABLE seller_listing_image (
@@ -28,7 +29,9 @@ CREATE TABLE seller_listing_image (
sort_order INT DEFAULT 0 COMMENT '排序',
is_selected INT(1) DEFAULT 0 COMMENT '是否选中: 0-未选中, 1-选中(仅product有效)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_listing_id (listing_id)
deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
INDEX idx_listing_id (listing_id),
INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品图片表';
-- 设计师表
@@ -36,7 +39,7 @@ 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',
avatar VARCHAR(200) NOT NULL DEFAULT 'aida-user/388b23f770449d18078b5d54f38be52c.png' COMMENT '店铺头像URL',
brand_banner VARCHAR(200) COMMENT '品牌Banner URL',
owner_name VARCHAR(100) COMMENT '所有者全名',
email VARCHAR(100) COMMENT '邮箱',
@@ -60,6 +63,9 @@ CREATE TABLE seller_designer (
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 '商品总数量',
@@ -67,7 +73,7 @@ CREATE TABLE seller_orders (
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_order_id (order_id),
payment_id BIGINT DEFAULT NULL COMMENT '交易流水号(关联支付表)',
INDEX idx_seller_id (seller_id),
INDEX idx_deleted (deleted),
INDEX idx_create_time (create_time)
@@ -76,15 +82,47 @@ CREATE TABLE seller_orders (
-- 订单商品明细表
CREATE TABLE seller_order_item (
id BIGINT PRIMARY KEY COMMENT '主键ID',
order_id VARCHAR(50) NOT NULL COMMENT '订单ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
product_name VARCHAR(255) COMMENT '商品名称',
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$)',
quantity INT NOT NULL COMMENT '购买数量',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
product_category JSON COMMENT '商品分类列表',
status TINYINT DEFAULT 0 COMMENT '商品状态0-未支付1-已支付2-已取消',
INDEX idx_order_id (order_id),
INDEX idx_product_id (product_id),
INDEX idx_listing_id (listing_id),
INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单商品明细表';
-- 订单商品图片快照表
CREATE TABLE seller_order_item_image (
id BIGINT PRIMARY KEY COMMENT '主键ID',
order_item_id BIGINT NOT NULL COMMENT '订单商品ID',
order_id BIGINT COMMENT '订单ID',
listing_id BIGINT COMMENT '原商品ID',
buyer_id BIGINT COMMENT '买家ID',
category VARCHAR(32) COMMENT '图片类别',
image_url VARCHAR(512) COMMENT '图片URL',
sort_order INT DEFAULT 0 COMMENT '排序',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
INDEX idx_listing_buyer (listing_id, buyer_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单商品图片快照表';
-- 商品图片水印表
CREATE TABLE seller_listing_watermark_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',
original_url VARCHAR(500) NOT NULL COMMENT '原图 logical path',
watermarked_url VARCHAR(500) NOT NULL COMMENT '加水印图的 logical path',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
UNIQUE KEY uk_listing_category (listing_id, category, original_url),
INDEX idx_listing_id (listing_id),
INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品图片水印表';

View File

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

View File

@@ -0,0 +1,16 @@
<?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.listing.mapper.ListingWatermarkImageMapper">
<delete id="deleteByListingId">
DELETE FROM seller_listing_watermark_image
WHERE listing_id = #{listingId} AND deleted = 0
</delete>
<select id="selectByListingId" resultType="com.aida.seller.module.listing.entity.ListingWatermarkImageEntity">
SELECT id, listing_id, category, original_url, watermarked_url, create_time
FROM seller_listing_watermark_image
WHERE listing_id = #{listingId} AND deleted = 0
</select>
</mapper>

View File

@@ -0,0 +1,24 @@
<?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.OrderItemImageMapper">
<select id="selectByListingIdAndBuyerIdWithOrderStatus"
resultType="com.aida.seller.module.order.entity.OrderItemImageEntity">
SELECT
soi.image_url AS imageUrl,
soi.category AS category,
soi.sort_order AS sortOrder,
soi.order_id AS orderId,
soi.listing_id AS listingId,
soi.buyer_id AS buyerId
FROM seller_order_item_image soi
INNER JOIN seller_order_item oi ON soi.order_item_id = oi.id
WHERE soi.listing_id = #{listingId}
AND soi.buyer_id = #{buyerId}
AND soi.deleted = 0
AND oi.deleted = 0
AND oi.status = 1
ORDER BY soi.sort_order ASC
</select>
</mapper>

View File

@@ -0,0 +1,66 @@
<?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 soi.status = 1
<if test="dto.categories != null and dto.categories.size() > 0">
AND (
<foreach collection="dto.categories" item="cat" separator=" OR ">
JSON_CONTAINS(soi.product_category, JSON_QUOTE(#{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 soi.status = 1
<if test="dto.categories != null and dto.categories.size() > 0">
AND (
<foreach collection="dto.categories" item="cat" separator=" OR ">
JSON_CONTAINS(soi.product_category, JSON_QUOTE(#{cat}))
</foreach>
)
</if>
<if test="dto.designFor != null and dto.designFor != '' and dto.designFor != 'all'">
AND l.design_for = #{dto.designFor}
</if>
</select>
<update id="incrementSalesVolumeByOrderIds">
UPDATE seller_listing
SET sales_volume = sales_volume + 1, update_time = update_time
WHERE id IN (
SELECT DISTINCT listing_id FROM seller_order_item
WHERE order_id IN
<foreach collection="orderIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
)
</update>
</mapper>

View File

@@ -0,0 +1,339 @@
# 不易报异常
system.error=System error.
unknown.authentication.operation.type=Unknown authentication operation type.
failed.to.send.mail=Failed to send mail.
unknown.login.type=Unknown login type.
error.login.type=Error login type.
get.moodBoards.data.is.mismatch=Get moodBoards data is mismatch.
get.printBoards.data.is.mismatch=Get printBoards data is mismatch.
get.sketchBoards.data.is.mismatch=Get sketchBoards data is mismatch.
modelPoint.not.found=ModelPoint not found.
collection.not.found=Collection not found.
design.not.found=Design not found.
designItem.not.found=DesignItem not found.
userLikeGroup.not.found=UserLikeGroup not found.
old.elements.not.found=Old elements not found.
new.designItemDetails.not.found=New designItemDetails not found.
save.workspace.failed=Save workspace failed.
save.design.failed=Save design failed.
update.workspace.failed=Update workspace failed.
enumeration.class.not.found=Enumeration class not found.
history.detail.not.found=History detail not found.
designItemDetails.not.found=DesignItemDetails not found.
designPythonOutfit.not.found=DesignPythonOutfit not found.
unknown.parameter.level1Type=Unknown parameter level1Type.
collectionElement.not.found=collectionElement not found.
select1.file.does.not.exist=Select1 file does not exist.
select2.file.does.not.exist=Select2 file does not exist.
save.collectionElement.failed=Save collectionElement failed.
collectionElements.not.found=CollectionElements not found.
batch.save.libraryList.failed=Batch save libraryList failed.
panTones.not.found=tcx value not found.
save.designItem.failed=Save designItem failed.
unknown.type=Unknown type.
unknown.operateType=Unknown operateType.
unknown.level1TypeEnum=Unknown level1TypeEnum.
the.id.value.is.out.of.range=The id value is out of range.
library.not.found=Library not found.
wrong.clothes.type=Wrong clothes type.
sysFile.not.found=SysFile not found.
libraryList.not.found=LibraryList not found.
groupDetails.not.found=GroupDetails not found.
history.not.found=History not found.
unknown.parameter.level2Type=Unknown parameter level2Type.
MARKETING_SKETCH.type.have.been.removed=MARKETING_SKETCH type have been removed.
unknown.modelType=Unknown modelType.
save.library.failed=Save library failed.
get.file.failed=Get file failed.
the.path.is.error=The path is error.
batch.save.colorElements.failed=Batch save colorElements failed.
initSysFile.ioException=InitSysFile ioException.
save.sysFile.failed=Save sysFile failed.
save.pythonTAllInfo.failed=Save pythonTAllInfo failed.
save.collection.failed=Save collection failed.
save.designItemDetail.failed=Save designItemDetail failed.
save.classification.failed=Save classification failed.
update.classification.failed=Update classification failed.
please.input.the.prompt=Please input the prompt.
please.choose.an.image=Please choose an image.
please.input.the.caption.and.choose.an.image=Please input the caption and choose an image.
please.input.the.caption.or.choose.an.image=Please input the caption or choose an image.
duplicate.likes.are.not.allowed=Duplicate likes are not allowed.
layer.information.not.found=Layer information not found.
singleOverall.cannot.be.empty=singleOverall cannot be empty.
colorBoards.cannot.be.empty=colorBoards cannot be empty.
systemScale.cannot.be.empty=systemScale cannot be empty.
modelType.cannot.be.empty=modelType cannot be empty.
modelSex.cannot.be.empty=modelSex cannot be empty.
templateId.cannot.be.empty=templateId cannot be empty.
processId.cannot.be.empty=processId cannot be empty.
timeZone.cannot.be.empty=TimeZone cannot be empty.
userId.cannot.be.empty=userId cannot be empty.
email.cannot.be.empty=email cannot be empty.
operationType.cannot.be.empty=operationType cannot be empty.
password.cannot.be.empty=Password cannot be empty.
emailVerifyCode.cannot.be.empty=emailVerifyCode cannot be empty.
loginType.cannot.be.empty=loginType cannot be empty.
userName.cannot.be.empty=userName cannot be empty.
sketchBoards.designType.cannot.be.empty=sketchBoards designType cannot be empty.
moodBoards.designType.cannot.be.empty=moodBoards designType cannot be empty.
printBoards.designType.cannot.be.empty=printBoards designType cannot be empty.
unknown.parameter.singleOverall=unknown parameter singleOverall.
unknown.parameter.switchCategory=unknown parameter switchCategory.
collectionId.cannot.be.empty=collectionId cannot be empty.
designPythonOutfitId.cannot.be.empty=designPythonOutfitId cannot be empty.
designItemId.cannot.be.empty=designItemId cannot be empty.
designId.cannot.be.empty=designId cannot be empty.
groupDetailId.cannot.be.empty=groupDetailId cannot be empty.
validStartTime.cannot.be.empty=validStartTime cannot be empty.
validEndTime.cannot.be.empty=validEndTime cannot be empty.
user_id.cannot.be.empty=user_id cannot be empty.
session_id.cannot.be.empty=session_id cannot be empty.
rgbValue.cannot.be.empty=rgbValue cannot be empty.
file.cannot.be.empty=file cannot be empty.
file.type.unsupported=Unsupported file type.
file.size.exceed.limit=File size exceeds the limit.
select1Id.cannot.be.empty=select1Id cannot be empty.
select2Id.cannot.be.empty=select2Id cannot be empty.
isPin.cannot.be.empty=isPin cannot be empty.
designType.cannot.be.empty=designType cannot be empty.
priority.cannot.be.empty=priority cannot be empty.
clothes.cannot.be.empty=clothes cannot be empty.
isPreview.cannot.be.empty=isPreview cannot be empty.
h.cannot.be.empty=h cannot be empty.
s.cannot.be.empty=s cannot be empty.
v.cannot.be.empty=v cannot be empty.
userGroupId.cannot.be.empty=userGroupId cannot be empty.
userGroupName.cannot.be.empty=userGroupName cannot be empty.
libraryId.cannot.be.empty=libraryId cannot be empty.
shoulderLeft.cannot.be.empty=shoulderLeft cannot be empty.
shoulderRight.cannot.be.empty=shoulderRight cannot be empty.
waistbandLeft.cannot.be.empty=waistbandLeft cannot be empty.
waistbandRight.cannot.be.empty=waistbandRight cannot be empty.
handLeft.cannot.be.empty=handLeft cannot be empty.
handRight.cannot.be.empty=handRight cannot be empty.
id.cannot.be.empty=id cannot be empty.
url.cannot.be.empty=url cannot be empty.
type.cannot.be.empty=type cannot be empty.
color.cannot.be.empty=color cannot be empty.
generateDetailId.cannot.be.empty=generateDetailId cannot be empty.
level1Type.cannot.be.empty=level1Type cannot be empty.
regionNum.cannot.be.empty=regionNum cannot be empty.
phone.cannot.be.empty=phone cannot be empty.
printId.cannot.be.empty=printId cannot be empty.
path.cannot.be.empty=path cannot be empty.
classificationName.cannot.be.empty=classificationName cannot be empty.
level2Type.cannot.be.empty=level2Type cannot be empty.
generateItem.does.not.exist=generateItem does not exist.
level1Type.does.not.match=level1Type does not match.
the.image.does.not.exist.please.reselect=the image does not exist, please reselect.
design.item.does.not.exist=design item does not exist.
layers.does.not.exists=layers does not exists.
unknown.generate.type=unknown generate type.
the.workspace.lastIndex.not.found=The workspace lastIndex not found.
gender.cannot.be.empty=gender cannot be empty.
image.synthesis.failed=image synthesis failed.
priority.cannot.be.repeated=priority cannot be repeated.
model.not.found=model not found.
libraryIdList.cannot.be.empty=libraryIdList cannot be empty.
the.value.range.of.seed=The value range of seed is 0-99999
image.modify.failed=Image modification failed, please try again later.
slogan.style.cannot.be.empty=Slogan style text cannot be empty.
slogan.image.cannot.be.empty=Slogan image cannot be empty.
questionnaire.filled.out=You have filled out the current questionnaire.
user.has.no.account=The current user has no account.
you.cannot.follow.yourself=You cannot follow yourself.
you.have.already.followed.this.user=You have already followed this user.
subscription.success=Subscription Success.
unsubscribe.success=Unsubscribe Success.
you.have.not.followed.the.current.user=You have not followed the current user.
remaining.modifications=Remaining modifications are 0.
you.have.participated.in.the.event=You have participated in the event.
only.original.works.can.participate.in.the.event=Sorry, only original works can participate in the event.
remaining.credits.insufficient=Your remaining credits are insufficient for this generation. Please recharge.
you.haven't.subscribed.to.any.products.yet=You haven't subscribed to any products yet.
generate.result.below.standard=The quality of the generated images currently falls below standard. Please consider adjusting your prompt and trying again.
partial.design.failed=Partial design failed. Please try again later.
email.count.limit=Rate limit reached. Retry in 1 hour.
model.path.cannot.be.empty=Model path cannot be empty.
this.promotion.code.has.expired=This promotion code has expired.
this.promotion.code.is.invalid=This promotion code is invalid.
one.time.limit.per.customer=This code has already been redeemed. Promo codes are limited to one-time use per customer.
element.already.exists=This element already exists in the public library.
have.no.permission=Sorry, you don't have permission.
permit.bulk.creation=The system permits bulk account creation exclusively when no sub-accounts exist.
school.account.login=School account detected. Please log in through the Academic portal.
enterprise.account.login=Enterprise account detected. Please log in through the Enterprise portal.
white.bg.add.fail=Failed to apply white background to the transparent image.
message.confirm.fail=Message confirmation failed. Switching to manual confirmation.
unknown.designPictureType=unknown designPictureType.
base64.data.empty=Invalid parameter: Image base64 data is empty.
edited.account.information.cannot.be.blank=The edited account information cannot be blank!
oldEmail.cannot.be.empty=oldEmail cannot be empty!
oldUserName.cannot.be.empty=oldUserName cannot be empty!
oldUserName.does.not.exist=oldUserName does not exist!
oldAccount.does.not.exist=oldAccount does not exist!
the.Google.has.been.bound=Cannot link: Google account already in use.
Unable.to.obtain.WeChat.account.information=Unable to obtain WeChat account information. Binding failed.
title.has.been.used=The title of the published work has been used.
source.image.does.not.exist=The source image does not exist.
email.has.been.registered=The email has already been registered.
unknown.subscription.type=Unknown subscription type.
unknown.product.type=Unknown product type.
unknown.Promotion.Code=Unknown Promotion Code.
file.upload.fail=File upload failed.
project.id.cannot.be.empty=Project id cannot be empty.
failed.to.obtain.system.sketch.recommendation=Failed to obtain system sketch recommendation.
pose.transformation.error=Pose transformation failed.
order.creation.failed=Order creation failed.
order.deduction.failed=Order deduction failed.
order.query.failed=Order query failed.
do.not.have.the.permission.to.delete.this.comment=You do not have the permission to delete this comment.
unknow.affiliate=Unknown affiliate id.
unknown.operationType=Unknown operationType.
unknown.mode=unknown mode
unknown.subscription.plan=unknown subscription plan
unknown.subscription.status=Unknown subscription status.
subscription.has.expired=Switch failed. The subscription has expired.
unknown.administrator.user=Operation failed. Unknown administrator user.
no.permission.manage.subscription=Switch failed. You do not have permission to manage this subscription.
unknown.organization=Unknown organization.
valid.subscription.period=The plan is still within its valid period. Please delete it after it expires.
users.currently.using.this.plan=There are users currently using this plan, so it cannot be deleted.
deletion.failed.please.try.again.later=Deletion failed. Please try again later.
subscription.plan.has.been.deleted=This subscription plan has been deleted.
subscription.plan.does.not.exist=The subscription plan does not exist.
ID.cannot.be.empty.and.must.be.greater.than.0=ID cannot be empty and must be greater than 0.
invalid.time.format=Invalid time format. Please use the yyyy-MM-dd HH:mm:ss format.
the.start.time.cannot.be.later.than.the.end.time=The start time cannot be later than the end time.
page.size.limit=The number of items per page must be between 1 and 100.
page.num.limit=The page number must be greater than 0.
end.time.must.be.later.than.the.start.time=The subscription end time must be later than the start time.
please.specify.the.organizationId=Please specify the organizationId.
switch.failed.sub-account.not.under.your.active.subscription=Switch failed. Sub-account not under your active subscription.
Sub-accounts.cannot.be.admins=Sub-accounts in a subscription cannot be designated as admins.
only.subscription.plans.with.a.PENDING.status.can.have.their.start.time.modified=Only subscription plans with a PENDING status can have their start time modified.
end.time.cannot.be.earlier.than.or.equal.to.start.time=End time cannot be earlier than or equal to start time.
end.time.cannot.be.earlier.than.or.equal.to.the.current.time=End time cannot be earlier than or equal to the current time.
the.subscription.end.date.can.be.extended.only.not.reduced=The subscription end date can be extended only, not reduced.
total.sub-account.quota.cannot.be.lower.than.existing.sub-accounts=Total sub-account quota cannot be lower than existing sub-accounts.
the.credit.limit.set.cannot.be.lower.than.the.amount.of.credits.already.used=The credit limit set cannot be lower than the amount of credits already used.
administrator.user.is.already.bound.to.different.organization=This administrator user is already bound to a subscription plan of a different organization.
required.partialDesign='partialDesign' (base64 or path) is required when updating an individual outfit.
account.not.found=Account not found.
verification.code.expired=Verification code has expired. Please request a new code to proceed.
verification.code.error=Verification code entered is incorrect. Please check and try again.
unknown.message=Unknown message.
forbidden.external.access=Forbidden: external access is not allowed for this endpoint.
failed.to.send.mail=Failed to send email.
remote.service.error=Remote service business error.
designer.already.applied=This user has already submitted an application or is already入驻.
designer.application.not.found=Application record not found.
current.status.not.support.review=Current status does not support review operation.
designer.record.not.found=Designer record not found.
designer.not.found=Designer not found.
product.id.list.cannot.be.empty=Product ID list cannot be empty.
buyer.id.cannot.be.empty=Buyer ID cannot be empty.
buyer.account.cannot.be.empty=Buyer account cannot be empty.
all.products.already.purchased=All products have already been purchased, no need to place a duplicate order.
product.not.found=Product not found.
order.info.cannot.be.empty=Order information cannot be empty.
order.status.cannot.be.empty=Order status cannot be empty.
order.id.list.cannot.be.empty=Order ID list cannot be empty.
order.not.found.or.no.permission=Order not found or no permission to modify.
design.for.only.male.or.female=designFor can only be male/female.
product.title.cannot.be.empty=Product title cannot be empty.
product.description.cannot.be.empty=Product description cannot be empty.
product.price.cannot.be.empty=Product price cannot be empty.
product.gender.cannot.be.empty=Product gender cannot be empty.
product.gender.must.be.male.or.female=Product gender can only be male/female.
product.category.cannot.be.empty=Product category cannot be empty.
product.category.must.be.valid=Product category can only be outwear/trousers/blouse/dress/skirt/others/tops/bottoms.
design.for.only.female.male.or.all=designFor can only be female/male/all.
product.not.purchased.yet=This product has not been purchased yet and cannot be viewed.
category.required=category [] is required.
category.imageUrl.cannot.be.empty= category imageUrl cannot be null or empty.
# 可能会报异常
userName.does.not.exist=Username or password is incorrect. Please check your entry and try again.
password.error=Username or password is incorrect. Please check your entry and try again.
email.error=Email is incorrect, please enter the correct bound email.
email.does.not.exist=Email address does not exist in our records. Please check and try again.
the.verification.code.has.expired=Verification code has expired. Please request a new code to proceed.
verification.code.error=Verification code entered is incorrect. Please check and try again.
the.number.of.PIN.top.or.bottom.or.outerwear.sketchBoard.cannot.be.more.than.8=You cannot have more than 8 PIN tops, bottoms, or outerwear in the sketchBoard. Please adjust accordingly.
hsv.value.cannot.exceed.the.maximum.of.8=hsv value cannot exceed the maximum of 8.
the.workspaceName.already.exists=A workspace with this name already exists. Please enter a different name.
unable.to.delete.the.workspace.you.are.currently.using=The workspace you are currently using cannot be deleted. Please select another workspace before trying to delete.
classificationName.already.exists=The label name you've entered already exists. Please enter a different label name to avoid duplication.
account.expired=Your subscription has expired, and your account has been reset to a visitor account. Please log in again from the [Individual] entry. If you have any questions, please contact us at info@code-create.com.hk
relate.to.any.subscription=Your administrator account is not currently linked to any subscription. If you have any questions, please contact us at info@code-create.com.hk
# Warnings
the.classification.you.deleted.has.associated.library=The label you are attempting to delete is associated with existing data. Are you sure you wish to proceed with deletion?
the.model.has.been.referenced.by.the.workspace=This model is currently in use by a workspace. Deleting it might affect the workspace. Confirm deletion only if you are sure.
balance.insufficient.for.trial=Want to continue using it immediately? Please consider upgrading to our subscription plan to get more quota.
balance.insufficient.for.paying=You have reached your usage limit for this month.
# Errors
system.busy=System is currently busy. Please wait a moment and try again.
user.expired=Your user session has expired. Please contact an administrator to renew your usage rights.
attributeRetrieval.interface.exception=We encountered an error retrieving attribute data. (Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
design.interface.exception=We encountered an error with the design interface. (Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
processMannequins.interface.exception=We encountered an error uploading mannequins. (Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
processSketchBoards.interface.exception=We encountered an error uploading sketchBoard. (Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
designProcess.interface.exception=There's been an issue loading the progress bar. (Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
generate.interface.exception=We are currently experiencing a high volume of generating requests. (Please try again later. If the problem continues, reach out to us at help@aida.com.hk for support.)
generate.interface.error=We encountered an error with the generate interface. (Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
chat-bot.interface.exception=We encountered an error with the chat robot interface.(Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
compose-layer.interface.exception=We encountered issues while flattening the layers.(Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
cloth-classification.interface.exception=We encountered some issues while obtaining clothing categories.(Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
sr.interface.error=We encountered an error with the super resolution. (Please try again later. If this issue persists, please contact us at help@aida.com.hk.)
# 多语言返回
OVERALL=Overall
TOPS=Tops
BOTTOMS=Bottoms
OUTWEAR=Outwear
OTHERS=Others
BLOUSE=Blouse
DRESS=Dress
TROUSERS=Trousers
SKIRT=Skirt
FEMALE=Women's Fashion
MALE=Men's Fashion
SLOGAN=Slogan
LOGO=Logo
PATTERN=Pattern
EMBROIDERY=Embroidery
BEADING=Beading
PEARL=Pearl
RIVET=Rivet
BUTTON=Button
BELT=Belt
CORSAGE=Corsage
ZIPPER=Zipper
POCKET=Pocket
THICK=Thick Lines
MEDIUM=Medium Lines
THIN=Thin lines
GENERATE=Generate Sketch
EXTRACT=Extract Sketch
CHILD=Child
ADULT=Adult
minio.upload.failed=File upload failed.
minio.get.presigned.url.failed=Failed to get temporary access URL.
minio.invalid.object.path=Invalid object path, format should be bucketName/filePath.
minio.delete.failed=File deletion failed.
minio.batch.delete.failed=Batch file deletion failed.
minio.base64.upload.failed=Base64 image upload failed.
minio.file.content.empty=File content cannot be empty.
minio.bytes.upload.failed=Byte array upload failed.
minio.logical.path.format.error=Logical path format error, should contain bucket name.
minio.bucket.not.exists=Bucket does not exist: {0}.
minio.verify.bucket.failed=Failed to verify bucket existence: {0}.
minio.download.failed=File download failed.
minio.presigned.url.format.invalid=Presigned URL path format invalid, should contain bucket name and object name.
minio.presigned.url.parse.failed=Failed to parse presigned URL.
minio.url.cannot.be.empty=URL cannot be empty.
minio.resource.format.unrecognized=Unrecognized MinIO resource format: {0}, please provide a valid presigned URL or logical path.

View File

@@ -0,0 +1,336 @@
# 不易报异常
system.error=系统错误。
unknown.authentication.operation.type=未知的身份验证操作类型。
failed.to.send.mail=邮件发送失败。
unknown.login.type=未知的登录类型。
error.login.type=错误的登录类型。
get.moodBoards.data.is.mismatch=获取心情板数据不匹配。
get.printBoards.data.is.mismatch=获取印花板数据不匹配。
get.sketchBoards.data.is.mismatch=获取草图板数据不匹配。
modelPoint.not.found=未找到ModelPoint。
collection.not.found=未找到Collection。
design.not.found=未找到Design。
designItem.not.found=未找到DesignItem。
userLikeGroup.not.found=未找到UserLikeGroup。
old.elements.not.found=未找到旧元素。
new.designItemDetails.not.found=未找到新的DesignItemDetails。
save.workspace.failed=保存工作区失败。
save.design.failed=保存设计失败。
update.workspace.failed=更新工作区失败。
enumeration.class.not.found=未找到枚举类。
history.detail.not.found=未找到历史详情。
designItemDetails.not.found=未找到DesignItemDetails。
designPythonOutfit.not.found=未找到DesignPythonOutfit。
unknown.parameter.level1Type=未知的参数level1Type。
collectionElement.not.found=未找到collectionElement。
select1.file.does.not.exist=选择的文件不存在。
select2.file.does.not.exist=选择的文件不存在。
save.collectionElement.failed=保存collectionElement失败。
collectionElements.not.found=未找到CollectionElements。
batch.save.libraryList.failed=批量保存libraryList失败。
panTones.not.found=未找到tcx value。
save.designItem.failed=保存DesignItem失败。
unknown.type=未知类型。
unknown.operateType=未知操作类型。
unknown.level1TypeEnum=未知的level1TypeEnum。
the.id.value.is.out.of.range=ID值超出范围。
library.not.found=未找到Library。
wrong.clothes.type=错误的服装类型。
sysFile.not.found=未找到SysFile。
libraryList.not.found=未找到LibraryList。
groupDetails.not.found=未找到GroupDetails。
history.not.found=未找到History。
unknown.parameter.level2Type=未知的参数level2Type。
MARKETING_SKETCH.type.have.been.removed=MARKETING_SKETCH类型已移除。
unknown.modelType=未知的modelType。
save.library.failed=保存Library失败。
get.file.failed=获取文件失败。
the.path.is.error=路径错误。
batch.save.colorElements.failed=批量保存colorElements失败。
initSysFile.ioException=初始化SysFile时发生IOException。
save.sysFile.failed=保存SysFile失败。
save.pythonTAllInfo.failed=保存PythonTAllInfo失败。
save.collection.failed=保存Collection失败。
save.designItemDetail.failed=保存DesignItemDetail失败。
save.classification.failed=保存Classification失败。
update.classification.failed=更新Classification失败。
please.input.the.prompt=请输入标题。
please.choose.an.image=请选择图片。
please.input.the.caption.and.choose.an.image=请输入标题并选择图片。
please.input.the.caption.or.choose.an.image=请输入标题或选择图片。
duplicate.likes.are.not.allowed=不允许重复点赞。
layer.information.not.found=未找到图层信息。
singleOverall.cannot.be.empty=singleOverall不能为空。
colorBoards.cannot.be.empty=colorBoards不能为空。
systemScale.cannot.be.empty=systemScale不能为空。
modelType.cannot.be.empty=modelType不能为空。
modelSex.cannot.be.empty=modelSex不能为空。
templateId.cannot.be.empty=templateId不能为空。
processId.cannot.be.empty=processId不能为空。
timeZone.cannot.be.empty=TimeZone不能为空。
userId.cannot.be.empty=userId不能为空。
email.cannot.be.empty=email不能为空。
operationType.cannot.be.empty=operationType不能为空。
password.cannot.be.empty=密码不能为空。
emailVerifyCode.cannot.be.empty=emailVerifyCode不能为空。
loginType.cannot.be.empty=loginType不能为空。
userName.cannot.be.empty=用户名不能为空。
sketchBoards.designType.cannot.be.empty=sketchBoards designType不能为空。
moodBoards.designType.cannot.be.empty=moodBoards designType不能为空。
printBoards.designType.cannot.be.empty=printBoards designType不能为空。
unknown.parameter.singleOverall=未知参数singleOverall。
unknown.parameter.switchCategory=未知参数switchCategory。
collectionId.cannot.be.empty=collectionId不能为空。
designPythonOutfitId.cannot.be.empty=designPythonOutfitId不能为空。
designItemId.cannot.be.empty=designItemId不能为空。
designId.cannot.be.empty=designId不能为空。
groupDetailId.cannot.be.empty=groupDetailId不能为空。
validStartTime.cannot.be.empty=validStartTime不能为空。
validEndTime.cannot.be.empty=validEndTime不能为空。
user_id.cannot.be.empty=user_id不能为空。
session_id.cannot.be.empty=session_id不能为空。
rgbValue.cannot.be.empty=rgbValue不能为空。
file.cannot.be.empty=文件不能为空。
file.type.unsupported=不支持的文件类型。
file.size.exceed.limit=文件大小超出限制。
select1Id.cannot.be.empty=select1Id不能为空。
select2Id.cannot.be.empty=select2Id不能为空。
isPin.cannot.be.empty=isPin不能为空。
designType.cannot.be.empty=designType不能为空。
priority.cannot.be.empty=priority不能为空。
clothes.cannot.be.empty=clothes不能为空。
isPreview.cannot.be.empty=isPreview不能为空。
h.cannot.be.empty=h不能为空。
s.cannot.be.empty=s不能为空。
v.cannot.be.empty=v不能为空。
userGroupId.cannot.be.empty=userGroupId不能为空。
userGroupName.cannot.be.empty=userGroupName不能为空。
libraryId.cannot.be.empty=libraryId不能为空。
shoulderLeft.cannot.be.empty=shoulderLeft不能为空。
shoulderRight.cannot.be.empty=shoulderRight不能为空。
waistbandLeft.cannot.be.empty=waistbandLeft不能为空。
waistbandRight.cannot.be.empty=waistbandRight不能为空。
handLeft.cannot.be.empty=handLeft不能为空。
handRight.cannot.be.empty=handRight不能为空。
id.cannot.be.empty=id不能为空。
url.cannot.be.empty=url不能为空。
type.cannot.be.empty=type不能为空。
color.cannot.be.empty=颜色不能为空。
generateDetailId.cannot.be.empty=generateDetailId不能为空。
level1Type.cannot.be.empty=level1Type不能为空。
regionNum.cannot.be.empty=regionNum不能为空。
phone.cannot.be.empty=手机号不能为空。
printId.cannot.be.empty=printId不能为空。
path.cannot.be.empty=路径不能为空。
classificationName.cannot.be.empty=标签名不能为空。
level2Type.cannot.be.empty=level2Type不能为空。
generateItem.does.not.exist=generateItem不存在。
level1Type.does.not.match=level1Type不匹配。
the.image.does.not.exist.please.reselect=图片不存在,请重新选择。
design.item.does.not.exist=设计项目不存在。
layers.does.not.exists=图层不存在。
unknown.generate.type=未知的生成类型。
the.workspace.lastIndex.not.found=未找到工作区的lastIndex。
gender.cannot.be.empty=性别不能为空。
image.synthesis.failed=图像合成失败。
priority.cannot.be.repeated=优先级不能重复。
image.modify.failed=图片修改失败,请稍后重试。
slogan.style.cannot.be.empty=标语风格文本不能为空。
slogan.image.cannot.be.empty=标语图片不能为空。
questionnaire.filled.out=您已填写过当前问卷。
user.has.no.account=当前用户没有账号。
you.cannot.follow.yourself=您不能关注您自己。
you.have.already.followed.this.user=您已经关注当前用户。
subscription.success=订阅成功。
unsubscribe.success=取消订阅成功。
you.have.not.followed.the.current.user=您还未关注当前用户。
remaining.modifications=剩余修改次数为0。
you.have.participated.in.the.event=您已经参与活动。
only.original.works.can.participate.in.the.event=抱歉,只有原创作品能参与活动。
remaining.credits.insufficient=您的剩余积分不够本次生成消耗,请充值。
you.haven't.subscribed.to.any.products.yet=您还未订阅任何产品。
generate.result.below.standard=当前生成的图像质量低于标准。请考虑调整您的提示词并再次尝试。
partial.design.failed=局部设计失败,请稍后重试。
email.count.limit=您的账号触发邮件发送频率限制,请一小时后重试。
model.path.cannot.be.empty=模特路径不能为空。
this.promotion.code.has.expired=该促销码已过期。
this.promotion.code.is.invalid=该促销码无效。
one.time.limit.per.customer=该码已兑换,每个促销码每位用户仅限使用一次。
element.already.exists=元素已存在于公共库中。
have.no.permission=您没有权限。
permit.bulk.creation=系统仅当不存在任何子账号时,才允许批量创建账号。
school.account.login=检测到学校账号,请从教育版入口登录。
enterprise.account.login=检测到企业账号,请从企业版入口登录。
white.bg.add.fail=透明图添加白色背景失败。
message.confirm.fail=消息确认失败,转为手动确认消息。
unknown.designPictureType=未知图片类型。
base64.data.empty=参数错误图片base64数据为空。
edited.account.information.cannot.be.blank=请提供修改后的用户信息!
oldEmail.cannot.be.empty=旧邮箱不能为空!
oldUserName.cannot.be.empty=旧用户名不能为空!
oldUserName.does.not.exist=旧用户名不存在!
oldAccount.does.not.exist=旧账号不存在!
the.Google.has.been.bound=无法绑定:该 Google 账号已被使用。
Unable.to.obtain.WeChat.account.information=无法获取微信账号信息,绑定失败。
title.has.been.used=标题已被使用。
source.image.does.not.exist=源图片不存在。
email.has.been.registered=邮箱已被使用。
unknown.subscription.type=未知订阅类型。
unknown.product.type=未知商品类型。
unknown.Promotion.Code=未知推广码。
file.upload.fail=文件上传失败。
project.id.cannot.be.empty=项目id不能为空。
failed.to.obtain.system.sketch.recommendation=系统草图推荐获取失败。
pose.transformation.error=图片转视频失败。
order.creation.failed=订单创建失败。
order.deduction.failed=订单金额扣除失败。
order.query.failed=订单查询失败。
do.not.have.the.permission.to.delete.this.comment=您没有权限删除此评论。
unknow.affiliate=未知推广者id。
unknown.operationType=未知操作类型。
unknown.mode=未知模式。
unknown.subscription.plan=未知订阅计划。
unknown.subscription.status=未知订阅状态。
subscription.has.expired=切换失败,订阅已过期。
unknown.administrator.user=操作失败,未知管理员用户。
no.permission.manage.subscription=切换失败,您没有权限管理该订阅。
unknown.organization=未知组织。
valid.subscription.period=计划仍在有效期内,请到期后再删除。
users.currently.using.this.plan=存在用户正在使用此计划,无法删除。
deletion.failed.please.try.again.later=删除失败,请稍后重试。
subscription.plan.has.been.deleted=该订阅计划已被删除。
subscription.plan.does.not.exist=订阅计划不存在。
ID.cannot.be.empty.and.must.be.greater.than.0=ID不能为空且必须大于0。
invalid.time.format=时间格式错误请使用yyyy-MM-dd HH:mm:ss格式。
the.start.time.cannot.be.later.than.the.end.time=开始时间不能晚于结束时间。
page.size.limit=每页数量必须在1-100之间。
page.num.limit=页码必须大于0。
end.time.must.be.later.than.the.start.time=订阅结束时间必须晚于开始时间。
please.specify.the.organizationId=请指定organizationId。
switch.failed.sub-account.not.under.your.active.subscription=切换失败,该子账号不属于您当前管理的订阅计划。
Sub-accounts.cannot.be.admins=在订阅中的子账号不能被指定为管理员。
only.subscription.plans.with.a.PENDING.status.can.have.their.start.time.modified=只有PENDING状态的订阅计划可以修改订阅开始时间。
end.time.cannot.be.earlier.than.or.equal.to.start.time=订阅结束时间不能早于或等于开始时间。
end.time.cannot.be.earlier.than.or.equal.to.the.current.time=订阅结束时间不能早于或等于当前时间。
the.subscription.end.date.can.be.extended.only.not.reduced=订阅的到期时间不能缩短,只能延长。
total.sub-account.quota.cannot.be.lower.than.existing.sub-accounts=设置的子账号总数量不能低于现存已添加的子账号数量。
the.credit.limit.set.cannot.be.lower.than.the.amount.of.credits.already.used=设置的积分上限不能低于已使用的积分量。
administrator.user.is.already.bound.to.different.organization=该管理员用户已与其他组织的订阅计划绑定。
required.partialDesign=修改单套搭配必须提供'partialDesign'(base64 或 path)。
account.not.found=账号不存在。
verification.code.expired=验证码已过期,请请求新的验证码继续。
verification.code.error=输入的验证码不正确,请检查并重试。
unknown.message=未知消息。
forbidden.external.access=禁止外部直接访问此接口。
failed.to.send.mail=邮件发送失败。
remote.service.error=远程服务业务错误。
designer.already.applied=该用户已提交过申请或已入驻。
designer.application.not.found=申请记录不存在。
current.status.not.support.review=当前状态不支持审核操作。
designer.record.not.found=设计师记录不存在。
designer.not.found=设计师不存在。
product.id.list.cannot.be.empty=商品ID列表不能为空。
buyer.id.cannot.be.empty=买家ID不能为空。
buyer.account.cannot.be.empty=买家账号不能为空。
all.products.already.purchased=所有商品均已购买,无需重复下单。
product.not.found=未找到对应的商品。
order.info.cannot.be.empty=订单信息为空。
order.status.cannot.be.empty=订单状态不能为空。
order.id.list.cannot.be.empty=订单ID列表不能为空。
order.not.found.or.no.permission=订单不存在或无权修改。
design.for.only.male.or.female=designFor 只能为 male/female。
product.title.cannot.be.empty=商品标题不能为空。
product.description.cannot.be.empty=商品描述不能为空。
product.price.cannot.be.empty=商品价格不能为空。
product.gender.cannot.be.empty=适用性别不能为空。
product.gender.must.be.male.or.female=适用性别只能为 male/female。
product.category.cannot.be.empty=商品分类不能为空。
product.category.must.be.valid=商品分类只能为 outwear/trousers/blouse/dress/skirt/others/tops/bottoms。
design.for.only.female.male.or.all=designFor 只能为 female/male/all。
product.not.purchased.yet=该商品尚未被购买,无法查看。
category.required=category [] is required。
category.imageUrl.cannot.be.empty=category imageUrl cannot be null or empty。
# 可能会报异常
userName.does.not.exist=用户名或密码不正确,请检查您的输入并重试。
password.error=用户名或密码不正确,请检查您的输入并重试。
email.error=电子邮件不正确,请输入正确的绑定电子邮件。
email.does.not.exist=电子邮件地址在我们的记录中不存在,请检查并重试。
the.verification.code.has.expired=验证码已过期,请请求新的验证码继续。
verification.code.error=输入的验证码不正确,请检查并重试。
the.number.of.PIN.top.or.bottom.or.outerwear.sketchBoard.cannot.be.more.than.8=在草图板中PIN上装、下装或外套的数量不能超过8件请进行相应调整。
hsv.value.cannot.exceed.the.maximum.of.8=hsv值不能超过8的最大值。
the.workspaceName.already.exists=具有此名称的工作区已存在,请输入不同的名称。
unable.to.delete.the.workspace.you.are.currently.using=无法删除当前正在使用的工作区,在尝试删除之前,请选择另一个工作区。
classificationName.already.exists=您输入的标签名已存在,请输入不同的标签名以避免重复。
account.expired=您的订阅已过期,账号已被重置为访客身份,请从【个人账号】入口重新登录,如有疑问,请联系 info@code-create.com.hk。
relate.to.any.subscription=您的管理员账号暂未关联任何订阅,如有疑问,请联系我们 info@code-create.com.hk。
# Warnings
the.classification.you.deleted.has.associated.library=您正在尝试删除的标签与现有数据相关联,您确定要继续删除吗?
the.model.has.been.referenced.by.the.workspace=此模型当前正在工作区中使用,删除它可能会影响工作区,仅在确信后再确认删除。
balance.insufficient.for.trial=想要立即继续使用?请考虑升级到我们的订阅计划,以获得更多额度。
balance.insufficient.for.paying=您已达到本月的使用额度限制。
# Errors
system.busy=系统当前繁忙,请稍候再试。
user.expired=您的用户会话已过期,请联系管理员更新您的使用权限。
attributeRetrieval.interface.exception=检索属性数据时发生错误。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
design.interface.exception=设计接口出现错误。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
processMannequins.interface.exception=上传模特时出现错误。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
processSketchBoards.interface.exception=上传草图板时出现错误。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
designProcess.interface.exception=加载进度条时出现问题。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
generate.interface.exception=我们当前正经历大量生成请求。请稍后再试。如果问题继续请联系我们的help@aida.com.hk寻求支持。
generate.interface.error=生成接口出现错误。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
chat-bot.interface.exception=聊天机器人接口出现错误。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
compose-layer.interface.exception=图层合并时出现问题。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
cloth-classification.interface.exception=获取服装类别时出现问题。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
sr.interface.error=超分接口出现错误。请稍后再试。如果问题持续请联系我们的help@aida.com.hk
# 多语言返回
OVERALL=整体
TOPS=上装
BOTTOMS=下装
OUTWEAR=外套
OTHERS=其他
BLOUSE=上衣
DRESS=连衣裙
TROUSERS=裤子
SKIRT=短裙
FEMALE=女装
MALE=男装
SLOGAN=标语
LOGO=标志
PATTERN=图案
EMBROIDERY=刺绣
BEADING=钉珠
PEARL=珍珠
RIVET=铆钉
BUTTON=纽扣
BELT=腰带
CORSAGE=胸花
ZIPPER=拉链
POCKET=口袋
THICK=粗线条
MEDIUM=中线条
THIN=细线条
GENERATE=生成线稿
EXTRACT=提取线稿
CHILD=儿童
ADULT=成人
minio.upload.failed=文件上传失败。
minio.get.presigned.url.failed=获取临时访问地址失败。
minio.invalid.object.path=无效的对象路径,格式应为 bucketName/filePath。
minio.delete.failed=文件删除失败。
minio.batch.delete.failed=批量删除文件失败。
minio.base64.upload.failed=base64图片上传失败。
minio.file.content.empty=文件内容不能为空。
minio.bytes.upload.failed=字节数组上传失败。
minio.logical.path.format.error=逻辑路径格式错误,应包含桶名。
minio.bucket.not.exists=桶不存在: {0}。
minio.verify.bucket.failed=验证桶存在性失败: {0}。
minio.download.failed=文件下载失败。
minio.presigned.url.format.invalid=预签名URL路径格式无效应包含桶名和对象名。
minio.presigned.url.parse.failed=预签名URL解析失败。
minio.url.cannot.be.empty=URL不能为空。
minio.resource.format.unrecognized=无法识别的MinIO资源格式: {0}请提供有效的预签名URL或逻辑路径。