Compare commits

...

47 Commits

Author SHA1 Message Date
litianxiang
912d5efee7 保存卖家信息解析为逻辑路径 2026-05-11 17:18:18 +08:00
litianxiang
0c1b74ddc0 买家端需要的获取商家主页和模糊搜索接口 2026-05-11 16:40:47 +08:00
litianxiang
e1d57f7b37 图片存在性校验 2026-05-07 15:59:04 +08:00
litianxiang
daf4c30a91 商品草稿状态也要校验 2026-05-07 14:11:18 +08:00
litianxiang
08f5a482eb log配置 2026-05-07 13:39:46 +08:00
litianxiang
1ff76957a7 选中状态fix 2026-05-07 11:38:19 +08:00
litianxiang
d77ce701e1 swagger bug 2026-05-07 10:31:28 +08:00
litianxiang
4b309efbb5 swagger bug 2026-05-07 10:31:01 +08:00
litianxiang
73ac643771 商品新增视频类型图片 2026-05-07 10:18:41 +08:00
litianxiang
0b9601278c 商品新增封面源头类型图片 2026-05-07 10:06:08 +08:00
litianxiang
0a1dc1c10d 工作流 2026-05-07 09:39:26 +08:00
litianxiang
9e5ba17dc4 工作流恢复 2026-05-06 17:23:45 +08:00
litianxiang
88c73c4462 工作流恢复 2026-05-06 17:13:36 +08:00
litianxiang
749241f19b 日志
订单表字段改名
视频返回新增字段
2026-05-06 16:58:48 +08:00
litianxiang
f69eca39ff 新工作流 2026-05-06 15:06:31 +08:00
litianxiang
38fb2ec4d5 服务端口号与宿主机统一,方便本地调试不需要修改bootstrap 2026-05-04 14:21:37 +08:00
litianxiang
b56ae5741b 服务端口号与宿主机统一,方便本地调试不需要修改bootstrap 2026-05-04 14:19:38 +08:00
litianxiang
1d4c8ec629 新增删除seller接口 2026-05-04 13:35:34 +08:00
litianxiang
4456722328 nacos注册测试 2026-05-04 10:19:18 +08:00
litianxiang
ad2254bc80 ProductCategory获取不到fix 2026-04-29 16:33:23 +08:00
litianxiang
5569da47f7 ProductCategory获取不到fix 2026-04-29 15:26:59 +08:00
litianxiang
fb892b6b21 商品排序规则按照修改时间 2026-04-29 15:18:23 +08:00
litianxiang
dea2409cea fix:时间自动创建 2026-04-29 14:19:10 +08:00
litianxiang
9d4c675594 fix:发布商品状态错误 2026-04-29 13:51:56 +08:00
litianxiang
da72640783 fix 2026-04-28 17:28:52 +08:00
litianxiang
48c4679820 工作流 2026-04-28 16:35:13 +08:00
litianxiang
86773339ec 工作流 2026-04-28 16:30:07 +08:00
litianxiang
92906881fe 工作流 2026-04-28 16:20:52 +08:00
litianxiang
520627a8fa 工作流 2026-04-28 16:13:30 +08:00
litianxiang
cc839dce1d bootstrap配置 2026-04-28 16:04:49 +08:00
litianxiang
5ceda7991d bootstrap配置 2026-04-28 15:53:56 +08:00
litianxiang
6f4e71b9e9 bootstrap配置 2026-04-28 15:46:47 +08:00
litianxiang
38c12b9ba5 bootstrap配置 2026-04-28 15:44:11 +08:00
litianxiang
232953acb0 bootstrap配置 2026-04-28 15:29:10 +08:00
litianxiang
b862da5b50 bootstrap配置 2026-04-28 14:38:02 +08:00
litianxiang
0605839c87 1 2026-04-28 13:30:57 +08:00
litianxiang
774e6e0c6b 1 2026-04-28 13:28:31 +08:00
litianxiang
1802b2b500 1 2026-04-28 13:22:14 +08:00
litianxiang
f0f772ae89 加入销量字段,解决数据库自动填充字段问题 2026-04-28 13:16:45 +08:00
litianxiang
e433921abe 商品bug 2026-04-28 09:39:28 +08:00
litianxiang
259a7c8c2a host配置 2026-04-27 16:43:20 +08:00
litianxiang
0740aefa1a host配置 2026-04-27 16:29:56 +08:00
f25029be87 更新 .gitea/workflows/master_sellrt_build_manual.yaml 2026-04-27 16:02:34 +08:00
litianxiang
1744480822 端口号错误 2026-04-27 15:09:53 +08:00
litianxiang
9cc302fa53 微服务改造 2026-04-27 13:54:05 +08:00
litianxiang
8c29d292d5 Merge remote-tracking branch 'origin/master' 2026-04-27 11:47:26 +08:00
litianxiang
0019ae01ea 微服务改造 2026-04-27 11:47:17 +08:00
43 changed files with 1845 additions and 229 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"
# 验证
mvn -v
- name: 4.构建jar包
run: |
echo "===== 开始构建JAR包 ====="
# 新增:打印当前构建分支(两种方式双重确认)
echo "当前工作目录分支:$(git branch --show-current)"
echo "Gitea检出分支:${{ github.ref_name }}"
echo "预期构建分支: master"
echo "========================"
mvn -B clean install -DskipTests -Pdev 2>&1
# 检查构建是否成功
if [ $? -ne 0 ]; then
echo "JAR包构建失败!"
exit 1
fi
java -version
mvn -v
mvn clean package -DskipTests
- name: 5.生成Dockerfile
run: |
@@ -81,7 +50,7 @@ jobs:
VOLUME /tmp
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' > /etc/timezone
ADD ./aida-seller-1.0.0.jar /app.jar
ADD ./*.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
EOF
echo "Dockerfile内容:"
@@ -93,7 +62,7 @@ jobs:
cat > docker-compose.yml << 'EOF'
version: '3'
services:
aida_back:
master-aida-seller:
container_name: master-aida-seller
build: .
volumes:
@@ -102,46 +71,41 @@ jobs:
- ./temp:/temp
- ./uploads:/temp/uploads
ports:
- '10093:5567'
- '10093:10093'
restart: always
EOF
# 验证docker-compose.yml生成
echo "docker-compose.yml内容:"
cat docker-compose.yml
- name: 7.安装SSH工具
run: |
$SUDO apt install -y sshpass openssh-client --no-install-recommends
# 配置SSH免密
mkdir -p ~/.ssh
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
- name: 7.上传jar到远程服务器
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_HOST }}
port: 22
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_KEY }}
source: "target/*.jar,Dockerfile,docker-compose.yml"
target: ${{ env.REMOTE_DEPLOY_PATH }}
preserve_host_directory_structure: false
- name: 8.同步文件到远程服务
run: |
echo "===== 同步文件到远程服务器 ====="
# 使用scp同步文件
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
./target/*.jar ./Dockerfile ./docker-compose.yml \
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ env.REMOTE_DEPLOY_PATH }} 2>&1
- name: 9.部署和运行服务
run: |
echo "===== 开始部署服务 ====="
# SSH执行部署命令
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF_SSH'
- name: 8. 重启 Docker 服务
uses: appleboy/ssh-action@master # 👈 专门执行命令的 action
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_KEY }}
key_base64: true
script: |
echo "========= 进入部署目录 ========="
cd ${{ env.REMOTE_DEPLOY_PATH }}
echo "停止旧容器..."
docker compose down || true
echo "清理Docker资源..."
docker system prune -f
echo "构建镜像..."
docker compose build --no-cache
echo "启动服务..."
docker compose up -d
echo "验证容器状态..."
docker compose ps
echo "部署完成!"
EOF_SSH
ls -l
echo "========= 停止旧服务 ========="
docker compose down
echo "========= 启动新服务 ========="
docker compose up -d --build
echo "========= 查看运行状态 ========="
docker compose ps

1
.gitignore vendored
View File

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

18
pom.xml
View File

@@ -24,7 +24,7 @@
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<minio.version>8.5.7</minio.version>
<jwt.version>0.12.3</jwt.version>
<hutool.version>5.8.26</hutool.version>
@@ -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,7 @@
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
<version>8.2.0</version>
</dependency>
<!-- MinIO -->
@@ -149,12 +155,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- ==================== 微服务 ==================== -->
<dependency>
@@ -185,6 +185,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

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

View File

@@ -14,7 +14,7 @@ import java.util.List;
@Data
@NoArgsConstructor
@Schema(description = "分页响应结果")
public class PageResponse<T> extends Response<List<T>> {
public class PageResponse<T> {
@Schema(description = "页码")
private long page;
@Schema(description = "每页数量")
@@ -26,21 +26,16 @@ public class PageResponse<T> extends Response<List<T>> {
@Schema(description = "结果集")
private List<T> content;
public PageResponse(Response<List<T>> response, long page, long size, long total, long pages) {
if (response != null) {
this.setData(response.getData());
this.setErrCode(response.getErrCode());
this.setErrMsg(response.getErrMsg());
}
public PageResponse(List<T> list, long page, long size, long total, long pages) {
this.page = page;
this.size = size;
this.total = total;
this.pages = pages;
this.content = response.getData();
this.content = list;
}
public static <T> PageResponse<T> success(IPage<T> page) {
Response<List<T>> response = success(page.getRecords());
return new PageResponse<>(response, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
return new PageResponse<>(page.getRecords(), page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
}
}

View File

@@ -5,8 +5,10 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class MyBatisPlusConfig {
@Bean

View File

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

View File

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

View File

@@ -1,17 +1,23 @@
package com.aida.seller.module.designer.controller;
import com.aida.seller.common.context.UserContext;
import com.aida.seller.common.result.PageResponse;
import com.aida.seller.common.result.Response;
import com.aida.seller.module.designer.dto.DesignerApplyDTO;
import com.aida.seller.module.designer.dto.DesignerAuditDTO;
import com.aida.seller.module.designer.dto.DesignerDTO;
import com.aida.seller.module.designer.entity.DesignerEntity;
import com.aida.seller.module.designer.service.DesignerService;
import com.aida.seller.module.designer.vo.DesignerSearchVO;
import com.aida.seller.module.designer.vo.DesignerShopVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "设计师入驻管理")
@RestController
@RequestMapping("/designer")
@@ -22,8 +28,8 @@ public class DesignerController {
@Operation(summary = "查询设计师是否有售卖资格")
@GetMapping("/check")
public Response<Boolean> check(
@Parameter(description = "用户ID") @RequestParam Long userId) {
public Response<Boolean> check() {
Long userId = UserContext.getUserId();
boolean hasQualification = designerService.checkQualification(userId);
return Response.success(hasQualification);
}
@@ -61,4 +67,52 @@ public class DesignerController {
designerService.audit(dto);
return Response.success();
}
@Operation(summary = "获取设计师申请状态", description = "根据当前登录用户ID获取设计师申请状态0-待审核, 1-审核通过, 2-审核拒绝,null-未申请过")
@GetMapping("/apply/status")
public Response<Integer> getApplyStatus() {
Long userId = UserContext.getUserId();
Integer applyStatus = designerService.getApplyStatus(userId);
return Response.success(applyStatus);
}
@Operation(summary = "更新设计师信息", description = "当前登录设计师更新自身信息")
@PutMapping("/update")
public Response<Void> update(
@Parameter(description = "更新表单") @RequestBody DesignerDTO dto) {
designerService.update(dto);
return Response.success();
}
@Operation(summary = "获取设计师详细信息", description = "根据当前登录用户ID获取设计师详细信息")
@GetMapping("/info")
public Response<DesignerDTO> getInfo() {
Long userId = UserContext.getUserId();
DesignerDTO designerInfo = designerService.getDesignerInfo(userId);
return Response.success(designerInfo);
}
@Operation(summary = "删除设计师", description = "根据当前登录用户ID逻辑删除设计师及其所有关联数据订单、订单明细、商品、商品图片")
@DeleteMapping("/delete")
public Response<Void> delete() {
Long userId = UserContext.getUserId();
designerService.deleteByUserId(userId);
return Response.success();
}
@Operation(summary = "搜索设计师", description = "根据关键词不区分大小写同时匹配店铺名称和所有者姓名返回设计师信息、最近5张商品封面图按updateTime倒序、商品总数")
@GetMapping("/search")
public Response<List<DesignerSearchVO>> search(
@Parameter(description = "关键词(同时匹配店铺名称和所有者姓名,不区分大小写)") @RequestParam String keyword) {
List<DesignerSearchVO> result = designerService.searchDesigners(keyword);
return Response.success(result);
}
@Operation(summary = "获取设计师店铺详情", description = "根据 sellerId即 卖家userId获取店铺公开信息供买家端店铺主页调用")
@GetMapping("/shop/{sellerId}")
public Response<DesignerShopVO> getShopDetail(
@Parameter(description = "设计师用户ID") @PathVariable Long sellerId) {
DesignerShopVO vo = designerService.getShopDetailBySellerId(sellerId);
return Response.success(vo);
}
}

View File

@@ -40,9 +40,11 @@ public class DesignerApplyDTO implements Serializable {
@Schema(description = "手机号")
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
@Schema(description = "作品集/社交媒体链接(JSON数组)")
private String socialLinks;
@Schema(description = "设计师简介")
private String description;
}

View File

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

View File

@@ -0,0 +1,40 @@
package com.aida.seller.module.designer.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
/**
* 设计师信息DTO
*/
@Data
@Schema(description = "设计师信息表单")
public class DesignerDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "店铺名称")
private String shopName;
@Schema(description = "店铺头像URL")
private String avatar;
@Schema(description = "品牌Banner URL")
private String brandBanner;
@Schema(description = "所有者全名")
private String ownerName;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机号")
private String mobile;
@Schema(description = "作品集/社交媒体链接(JSON数组)")
private String socialLinks;
@Schema(description = "设计师简介")
private String description;
}

View File

@@ -1,6 +1,8 @@
package com.aida.seller.module.designer.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@@ -9,13 +11,14 @@ import java.time.LocalDateTime;
* 设计师表实体类
*/
@Data
@TableName("designer")
@TableName("seller_designer")
public class DesignerEntity implements Serializable {
private static final long serialVersionUID = 1L;
/** 设计师ID */
@TableId(type = IdType.AUTO)
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 用户ID(关联用户表) */
@@ -41,6 +44,9 @@ public class DesignerEntity implements Serializable {
/** 作品集/社交媒体链接(JSON数组) */
private String socialLinks;
/** 设计师简介 */
private String description;
/** 申请状态: 0-待审核, 1-审核通过, 2-审核拒绝 */
private Integer applyStatus;

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,87 @@
package com.aida.seller.module.file;
import cn.hutool.crypto.digest.DigestUtil;
import com.aida.seller.common.constants.CommonConstants;
import com.aida.seller.common.context.UserContext;
import com.aida.seller.common.exception.BusinessException;
import com.aida.seller.common.result.Response;
import com.aida.seller.util.MinioUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Arrays;
/**
* 文件上传控制器
*
* @author Fida Team
* @date 2026-02-03
*/
@Slf4j
@RestController
@RequestMapping("/file")
@Tag(name = "文件管理", description = "文件上传、下载等功能接口")
@RequiredArgsConstructor
public class FileUploadController {
private final MinioUtil minioUtil;
@Value("${multipart.max-file-size}")
private Long maxFileSize;
@PostMapping("/upload")
@Operation(summary = "文件上传", description = "上传文件到Minio服务器")
public Response<String> uploadFile(
@Parameter(description = "文件", required = true) @RequestParam("file") MultipartFile file,
@Parameter(description = "允许的文件类型") @RequestParam(value = "allowedTypes", required = false) String[] allowedTypes
) {
Long userId = UserContext.getUserId();
if (file.isEmpty()) {
throw new BusinessException("文件不能为空");
}
// 验证文件类型
String contentType = file.getContentType();
if (allowedTypes != null && allowedTypes.length > 0) {
boolean validType = false;
for (String allowedType : allowedTypes) {
if (contentType != null && contentType.startsWith(allowedType)) {
validType = true;
break;
}
}
if (!validType) {
throw new BusinessException("不支持的文件类型: " + contentType);
}
}
// 验证文件大小
if (file.getSize() > maxFileSize * 1024 * 1024) {
throw new BusinessException("文件大小超出限制: " + maxFileSize + " MB");
}
try {
// 计算文件MD5可选用于文件完整性校验
String md5 = DigestUtil.md5Hex(file.getInputStream());
log.info("文件MD5: {}", md5);
// 调用MinioUtil上传文件使用默认桶名按userId划分文件夹
String filePath = minioUtil.uploadImage(file, String.valueOf(userId));
log.info("文件上传成功: {}, 文件路径: {}", file.getOriginalFilename(), filePath);
return Response.success(minioUtil.processMinioResource(filePath, CommonConstants.MINIO_PATH_TIMEOUT));
} catch (IOException e) {
log.error("文件上传失败: {}", e.getMessage(), e);
throw new BusinessException("文件上传失败");
}
}
}

View File

@@ -0,0 +1,97 @@
package com.aida.seller.module.listing.controller;
import com.aida.seller.common.context.UserContext;
import com.aida.seller.common.result.PageResponse;
import com.aida.seller.common.result.Response;
import com.aida.seller.module.listing.dto.*;
import com.aida.seller.module.listing.service.ListingService;
import com.aida.seller.module.listing.vo.ListingPageVO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 商品管理 Controller
*/
@Tag(name = "Listing - 商品管理")
@RestController
@RequestMapping("/listing")
@RequiredArgsConstructor
public class ListingController {
private final ListingService listingService;
@Operation(summary = "批量保存/更新商品", description = "根据 id 是否存在判断新增或更新,同时保存图片")
@PostMapping("/batch")
public Response<Void> saveOrUpdate(
@Parameter(description = "商品保存/更新表单列表") @RequestBody List<ListingSaveDTO> dtoList) {
Long sellerId = UserContext.getUserId();
listingService.saveOrUpdate(dtoList, sellerId);
return Response.success();
}
@Operation(summary = "获取商品详情", description = "返回商品信息及所有图片")
@GetMapping("/detail")
public Response<ListingSaveDTO> getById(
@Parameter(description = "商品ID") @RequestParam Long id) {
Long sellerId = UserContext.getUserId();
ListingSaveDTO result = listingService.getById(id, sellerId);
return Response.success(result);
}
@Operation(summary = "分页查询商品列表", description = "按 status 过滤,返回 ListingPageVO 不含图片详情")
@GetMapping("/page")
public Response<PageResponse<ListingPageVO>> getPage(
@Parameter(description = "商品状态可选0-草稿, 1-已发布, 2-已删除") @RequestParam(required = false) Integer status,
@Parameter(description = "页码默认1") @RequestParam(defaultValue = "1") Integer pageNum,
@Parameter(description = "每页数量默认10") @RequestParam(defaultValue = "10") Integer pageSize) {
Long sellerId = UserContext.getUserId();
ListingQueryDTO dto = new ListingQueryDTO();
dto.setStatus(status);
dto.setPageNum(pageNum);
dto.setPageSize(pageSize);
IPage<ListingPageVO> page = listingService.getPage(dto, sellerId);
return Response.success(PageResponse.success(page));
}
@Operation(summary = "更新商品状态", description = "支持设为已删除或恢复为草稿")
@PutMapping("/status")
public Response<Void> updateStatus(
@Parameter(description = "状态更新表单") @RequestBody ListingStatusUpdateDTO dto) {
Long sellerId = UserContext.getUserId();
listingService.updateStatus(dto.getId(), dto.getStatus(), sellerId);
return Response.success();
}
@Operation(summary = "设置弹窗提醒标志", description = "在Redis中设置7天过期的弹窗提醒标志")
@PostMapping("/popup/set")
public Response<Void> setPopupReminder() {
Long sellerId = UserContext.getUserId();
listingService.setPopupReminder(sellerId);
return Response.success();
}
@Operation(summary = "检查是否需要弹窗提醒", description = "检查Redis中是否存在未过期的弹窗提醒标志")
@GetMapping("/popup/check")
public Response<Integer> checkPopupReminder() {
Long sellerId = UserContext.getUserId();
boolean needPopup = listingService.checkPopupReminder(sellerId);
return Response.success(needPopup ? 1 : 0);
}
@Operation(summary = "获取店铺商品列表", description = "按 status=1、deleted=0、designFor 筛选,返回店铺已发布商品分页列表")
@GetMapping("/shop")
public Response<PageResponse<ListingPageVO>> getShopListings(
@Parameter(description = "设计师用户ID") @RequestParam Long sellerId,
@Parameter(description = "适用性别 female/male/all") @RequestParam String designFor,
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") int pageSize) {
IPage<ListingPageVO> page = listingService.getShopListings(sellerId, designFor, pageNum, pageSize);
return Response.success(PageResponse.success(page));
}
}

View File

@@ -0,0 +1,38 @@
package com.aida.seller.module.listing.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 商品图片 DTO入参/出参复用)
*/
@Data
public class ListingImageDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** 图片ID有值则更新无则新增 */
@Schema(description = "图片ID有值则更新无则新增")
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video */
@Schema(description = "图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video")
private String category;
/** 图片URL */
@Schema(description = "图片URL")
private String imageUrl;
/** 排序 */
@Schema(description = "排序")
private Integer sortOrder;
/** 是否选中: 0-未选中, 1-选中(仅 product 有效) */
@Schema(description = "是否选中: 0-未选中, 1-选中(仅 product 有效)")
private Boolean isSelected;
}

View File

@@ -0,0 +1,25 @@
package com.aida.seller.module.listing.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 商品分页查询 DTO
*/
@Data
public class ListingQueryDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** 按状态过滤(选填,不传则查所有非删除) */
@Schema(description = "按状态过滤(选填,不传则查所有非删除)")
private Integer status;
@Schema(description = "页码默认1")
private Integer pageNum = 1;
@Schema(description = "每页数量默认10")
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,57 @@
package com.aida.seller.module.listing.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 商品保存/更新 DTO入参/出参复用)
*/
@Data
public class ListingSaveDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** 商品ID无则新建有则更新 */
@Schema(description = "商品ID无则新建有则更新")
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 商品标题 */
@Schema(description = "商品标题")
private String title;
/** 商品描述 */
@Schema(description = "商品描述")
private String description;
/** 价格 */
@Schema(description = "价格")
private BigDecimal price;
/** 浏览量(更新时传入) */
@Schema(description = "浏览量(更新时传入)")
private Integer viewCount;
/** 状态: 0-草稿, 1-已发布, 2-已删除 */
@Schema(description = "状态: 0-草稿, 1-已发布, 2-已删除")
private Integer status;
/** 图片列表(更新时全量覆盖) */
@Schema(description = "图片列表(更新时全量覆盖)")
private List<ListingImageDTO> images;
/** 适用性别: male/female */
@Schema(description = "适用性别: male/female")
private String designFor;
/** 商品分类列表: outwear/trousers/blouse/dress/skirt/others/tops/bottoms */
@Schema(description = "商品分类列表: outwear/trousers/blouse/dress/skirt/others/tops/bottoms")
private List<String> productCategory;
}

View File

@@ -0,0 +1,23 @@
package com.aida.seller.module.listing.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 商品状态更新 DTO
*/
@Data
public class ListingStatusUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** 商品ID */
@Schema(description = "商品ID")
private Long id;
/** 目标状态: 0-草稿, 1-已发布, 2-已删除 */
@Schema(description = "目标状态: 0-草稿, 1-已发布, 2-已删除")
private Integer status;
}

View File

@@ -0,0 +1,70 @@
package com.aida.seller.module.listing.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 商品实体
*/
@Data
@TableName(value = "seller_listing", autoResultMap = true)
public class ListingEntity implements Serializable {
private static final long serialVersionUID = 1L;
/** 商品ID */
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 卖家ID */
private Long sellerId;
/** 商品标题 */
private String title;
/** 商品描述 */
private String description;
/** 价格 */
private BigDecimal price;
/** 销量 */
private Integer salesVolume;
/** 封面图URL列表页展示用 */
private String cover;
/** 浏览量 */
private Integer viewCount;
/** 状态: 0-草稿, 1-已发布, 2-已删除 */
private Integer status;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/** 是否删除0-否1-是 */
@TableLogic
private Integer deleted;
/** 适用性别: male/female */
private String designFor;
/** 商品分类列表 */
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> productCategory;
}

View File

@@ -0,0 +1,48 @@
package com.aida.seller.module.listing.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 商品图片实体
*/
@Data
@TableName("seller_listing_image")
public class ListingImageEntity implements Serializable {
private static final long serialVersionUID = 1L;
/** 图片ID */
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 商品ID */
@JsonSerialize(using = ToStringSerializer.class)
private Long listingId;
/** 图片类别: cover/cover_from/main_product/product/sketch/apparel/firstFrame/gif/video */
private String category;
/** 图片URL */
private String imageUrl;
/** 排序 */
private Integer sortOrder;
/** 是否选中: 0-未选中, 1-选中(仅 product 有效) */
private Integer isSelected;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 是否删除0-否1-是 */
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,31 @@
package com.aida.seller.module.listing.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 适用性别枚举
*/
@Getter
@AllArgsConstructor
public enum DesignForEnum {
MALE("male", "男性"),
FEMALE("female", "女性"),
ALL("all", "全部");
private final String code;
private final String desc;
public static DesignForEnum of(String code) {
if (code == null) {
return null;
}
for (DesignForEnum value : values()) {
if (value.code.equals(code)) {
return value;
}
}
return null;
}
}

View File

@@ -0,0 +1,38 @@
package com.aida.seller.module.listing.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 商品图片类别枚举
*/
@Getter
@AllArgsConstructor
public enum ImageCategoryEnum {
COVER("cover", "封面图", false),
MAIN_PRODUCT("main_product", "主产品图", false),
PRODUCT("product", "产品图", true),
SKETCH("sketch", "草图", false),
COVERFROM("cover_from", "封面源", false),
APPAREL("apparel", "成衣图", false),
FIRST_FRAME("firstFrame", "首帧图", true),
GIF("gif", "GIF图", true),
VIDEO("video", "视频", true);
private final String code;
private final String desc;
private final boolean hasSelection;
public static ImageCategoryEnum of(String code) {
if (code == null) {
return null;
}
for (ImageCategoryEnum value : values()) {
if (value.code.equals(code)) {
return value;
}
}
return null;
}
}

View File

@@ -0,0 +1,19 @@
package com.aida.seller.module.listing.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 商品状态枚举
*/
@Getter
@AllArgsConstructor
public enum ListingStatusEnum {
DRAFT(0, "草稿"),
PUBLISHED(1, "已发布"),
DELETED(2, "已删除");
private final Integer code;
private final String desc;
}

View File

@@ -0,0 +1,40 @@
package com.aida.seller.module.listing.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 商品分类枚举
*/
@Getter
@AllArgsConstructor
public enum ProductCategoryEnum {
OUTWEAR("outwear", "外套"),
TROUSERS("trousers", "裤装"),
BLOUSE("blouse", "衬衫"),
DRESS("dress", "连衣裙"),
SKIRT("skirt", "半身裙"),
OTHERS("others", "其他"),
TOP("tops", "上装"),
BOTTOMS("bottoms", "下装");
@JsonValue
private final String code;
private final String desc;
@JsonCreator
public static ProductCategoryEnum of(String code) {
if (code == null) {
return null;
}
for (ProductCategoryEnum value : values()) {
if (value.code.equalsIgnoreCase(code)) {
return value;
}
}
return null;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,333 @@
package com.aida.seller.module.listing.service;
import com.aida.seller.common.constants.CommonConstants;
import com.aida.seller.common.context.UserContext;
import com.aida.seller.common.exception.BusinessException;
import com.aida.seller.module.listing.dto.*;
import com.aida.seller.module.listing.entity.ListingEntity;
import com.aida.seller.module.listing.entity.ListingImageEntity;
import com.aida.seller.module.listing.enums.ImageCategoryEnum;
import com.aida.seller.module.listing.enums.ListingStatusEnum;
import com.aida.seller.module.listing.enums.DesignForEnum;
import com.aida.seller.module.listing.enums.ProductCategoryEnum;
import com.aida.seller.module.listing.mapper.ListingImageMapper;
import com.aida.seller.module.listing.mapper.ListingMapper;
import com.aida.seller.module.listing.vo.ListingPageVO;
import com.aida.seller.util.MinioUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 商品 Service 实现
*/
@Service
@RequiredArgsConstructor
public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity> implements ListingService {
private final ListingImageMapper listingImageMapper;
private final RedisTemplate<String, Object> redisTemplate;
private final MinioUtil minioUtil;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(ListingSaveDTO dto, Long sellerId) {
// 当 status 为 1已发布检查必填字段
validateListingFields(dto);
ListingEntity entity = new ListingEntity();
BeanUtils.copyProperties(dto, entity);
entity.setSellerId(sellerId);
if (dto.getDesignFor() != null && DesignForEnum.of(dto.getDesignFor()) == null) {
throw new BusinessException("designFor 只能为 male/female");
}
if (entity.getViewCount() == null) {
entity.setViewCount(0);
}
Long listingId;
if (dto.getId() == null) {
entity.setStatus(dto.getStatus());
this.save(entity);
listingId = entity.getId();
} else {
ListingEntity existing = this.getOne(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getId, dto.getId())
.eq(ListingEntity::getSellerId, sellerId)
.eq(ListingEntity::getDeleted, 0));
if (existing == null) {
throw new BusinessException("商品不存在");
}
entity.setCreateTime(existing.getCreateTime());
this.updateById(entity);
listingImageMapper.delete(new LambdaQueryWrapper<ListingImageEntity>()
.eq(ListingImageEntity::getListingId, dto.getId()));
listingId = dto.getId();
}
if (!CollectionUtils.isEmpty(dto.getImages())) {
validateImages(dto.getImages());
handleImages(listingId, dto.getImages());
String cover = extractCover(dto.getImages());
if (StringUtils.hasText(cover)) {
ListingEntity update = new ListingEntity();
update.setId(listingId);
update.setCover(minioUtil.convertToLogicalPath(cover));
this.updateById(update);
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(List<ListingSaveDTO> dtoList, Long sellerId) {
if (CollectionUtils.isEmpty(dtoList)) {
return;
}
for (ListingSaveDTO dto : dtoList) {
saveOrUpdate(dto, sellerId);
}
}
@Override
public ListingSaveDTO getById(Long id, Long sellerId) {
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("商品不存在");
}
ListingSaveDTO dto = new ListingSaveDTO();
BeanUtils.copyProperties(entity, dto);
List<ListingImageEntity> images = listingImageMapper.selectList(
new LambdaQueryWrapper<ListingImageEntity>()
.eq(ListingImageEntity::getListingId, id)
.orderByAsc(ListingImageEntity::getSortOrder));
if (!CollectionUtils.isEmpty(images)) {
List<ListingImageDTO> imageDTOs = images.stream().map(img -> {
ListingImageDTO imgDto = new ListingImageDTO();
BeanUtils.copyProperties(img, imgDto);
imgDto.setImageUrl(minioUtil.processMinioResource(imgDto.getImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
imgDto.setIsSelected(img.getIsSelected() != null && img.getIsSelected() == 1);
return imgDto;
}).collect(Collectors.toList());
dto.setImages(imageDTOs);
}
return dto;
}
@Override
public IPage<ListingPageVO> getPage(ListingQueryDTO dto, Long sellerId) {
Page<ListingEntity> pageParam = new Page<>(dto.getPageNum(), dto.getPageSize());
LambdaQueryWrapper<ListingEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ListingEntity::getSellerId, sellerId);
queryWrapper.eq(ListingEntity::getDeleted, 0);
if (dto.getStatus() != null) {
queryWrapper.eq(ListingEntity::getStatus, dto.getStatus());
} else {
queryWrapper.ne(ListingEntity::getStatus, ListingStatusEnum.DELETED.getCode());
}
queryWrapper.orderByDesc(ListingEntity::getUpdateTime);
IPage<ListingEntity> page = this.page(pageParam, queryWrapper);
Page<ListingPageVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
result.setRecords(page.getRecords().stream().map(entity -> {
ListingPageVO vo = new ListingPageVO();
BeanUtils.copyProperties(entity, vo);
vo.setCover(minioUtil.processMinioResource(vo.getCover(), CommonConstants.MINIO_PATH_TIMEOUT));
return vo;
}).collect(Collectors.toList()));
return result;
}
@Override
public void updateStatus(Long id, Integer status, Long sellerId) {
ListingEntity existing = this.getOne(
new LambdaQueryWrapper<ListingEntity>()
.eq(ListingEntity::getId, id)
.eq(ListingEntity::getSellerId, sellerId)
.eq(ListingEntity::getDeleted, 0));
if (existing == null) {
throw new BusinessException("商品不存在");
}
ListingEntity update = new ListingEntity();
update.setId(id);
update.setStatus(status);
this.updateById(update);
}
private void handleImages(Long listingId, List<ListingImageDTO> images) {
Map<String, List<ListingImageDTO>> byCategory = images.stream()
.collect(Collectors.groupingBy(img -> img.getCategory() == null ? "" : img.getCategory()));
for (Map.Entry<String, List<ListingImageDTO>> entry : byCategory.entrySet()) {
String category = entry.getKey();
List<ListingImageDTO> categoryImages = entry.getValue();
if (!StringUtils.hasText(category)) {
continue;
}
ImageCategoryEnum categoryEnum = ImageCategoryEnum.of(category);
boolean supportsSelection = categoryEnum != null && categoryEnum.isHasSelection();
for (int i = 0; i < categoryImages.size(); i++) {
ListingImageDTO imgDto = categoryImages.get(i);
if (imgDto.getImageUrl() != null && !imgDto.getImageUrl().isEmpty()) {
ListingImageEntity imgEntity = new ListingImageEntity();
imgEntity.setListingId(listingId);
imgEntity.setCategory(category);
imgEntity.setImageUrl(minioUtil.convertToLogicalPath(imgDto.getImageUrl()));
imgEntity.setSortOrder(imgDto.getSortOrder() != null ? imgDto.getSortOrder() : i);
if (supportsSelection) {
imgEntity.setIsSelected(Boolean.TRUE.equals(imgDto.getIsSelected()) ? 1 : 0);
} else {
imgEntity.setIsSelected(0);
}
listingImageMapper.insert(imgEntity);
}
}
}
}
private void validateImages(List<ListingImageDTO> images) {
Set<String> requiredCategories = Set.of(
ImageCategoryEnum.COVER.getCode(),
ImageCategoryEnum.COVERFROM.getCode(),
ImageCategoryEnum.SKETCH.getCode(),
ImageCategoryEnum.APPAREL.getCode());
Set<String> presentCategories = images.stream()
.map(ListingImageDTO::getCategory)
.filter(StringUtils::hasText)
.collect(Collectors.toSet());
for (String category : requiredCategories) {
if (!presentCategories.contains(category)) {
throw new BusinessException("category [" + category + "] is required");
}
}
for (ListingImageDTO img : images) {
String category = img.getCategory();
if (requiredCategories.contains(category)) {
if (!StringUtils.hasText(img.getImageUrl())) {
throw new BusinessException(category + " category imageUrl cannot be null or empty");
}
}
}
}
private String extractCover(List<ListingImageDTO> images) {
if (CollectionUtils.isEmpty(images)) {
return null;
}
for (ListingImageDTO img : images) {
if (ImageCategoryEnum.COVER.getCode().equals(img.getCategory())) {
return img.getImageUrl();
}
}
return null;
}
@Override
public void setPopupReminder(Long sellerId) {
String key = "popup:reminder:" + sellerId;
redisTemplate.opsForValue().set(key, true, 7, java.util.concurrent.TimeUnit.DAYS);
}
@Override
public boolean checkPopupReminder(Long sellerId) {
String key = "popup:reminder:" + sellerId;
Object value = redisTemplate.opsForValue().get(key);
return value == null;
}
/**
* 校验商品字段
* 当 status 为 1已发布检查必填字段
*/
private void validateListingFields(ListingSaveDTO dto) {
if (!StringUtils.hasText(dto.getTitle())) {
throw new BusinessException("商品标题不能为空");
}
if (!StringUtils.hasText(dto.getDescription())) {
throw new BusinessException("商品描述不能为空");
}
if (dto.getPrice() == null) {
throw new BusinessException("商品价格不能为空");
}
if (!StringUtils.hasText(dto.getDesignFor())) {
throw new BusinessException("适用性别不能为空");
}
if (DesignForEnum.of(dto.getDesignFor()) == null) {
throw new BusinessException("适用性别只能为 male/female");
}
if (CollectionUtils.isEmpty(dto.getProductCategory())) {
throw new BusinessException("商品分类不能为空");
}
for (String category : dto.getProductCategory()) {
if (ProductCategoryEnum.of(category) == null) {
throw new BusinessException("商品分类只能为 outwear/trousers/blouse/dress/skirt/others/tops/bottoms");
}
}
}
@Override
public IPage<ListingPageVO> getShopListings(Long sellerId, String designFor, int pageNum, int pageSize) {
Page<ListingEntity> pageParam = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<ListingEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ListingEntity::getSellerId, sellerId);
DesignForEnum designForEnum = DesignForEnum.of(designFor);
if (designForEnum == null) {
throw new BusinessException("designFor 只能为 female/male/all");
}
if (designForEnum != DesignForEnum.ALL) {
queryWrapper.eq(ListingEntity::getDesignFor, designForEnum.getCode());
}
queryWrapper.eq(ListingEntity::getStatus, 1);
queryWrapper.eq(ListingEntity::getDeleted, 0);
queryWrapper.orderByDesc(ListingEntity::getUpdateTime);
IPage<ListingEntity> page = this.page(pageParam, queryWrapper);
Page<ListingPageVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
boolean loggedIn = UserContext.getUserId() != null;
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));
if (!loggedIn) {
vo.setPrice(null);
}
return vo;
}).collect(Collectors.toList()));
return result;
}
}

View File

@@ -0,0 +1,34 @@
package com.aida.seller.module.listing.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 商品分页列表 VO不含图片详情
*/
@Data
public class ListingPageVO implements Serializable {
private static final long serialVersionUID = 1L;
/** 商品ID */
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 封面图URL列表页直返无须关联图片表 */
private String cover;
/** 商品标题 */
private String title;
/** 价格 */
private BigDecimal price;
/** 修改时间 */
private LocalDateTime updateTime;
}

View File

@@ -1,6 +1,8 @@
package com.aida.seller.module.order.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
@@ -11,14 +13,14 @@ import java.time.LocalDateTime;
* 订单主表
*/
@Data
@TableName("order_info")
@TableName("seller_orders")
public class OrderInfoEntity implements Serializable {
private static final long serialVersionUID = 1L;
/** 订单唯一标识(如 SP897772698 */
@TableId(type = IdType.INPUT)
private String orderId;
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 卖家ID */
private Long sellerId;
@@ -35,7 +37,8 @@ public class OrderInfoEntity implements Serializable {
/** 总浏览量 */
private Long totalViews;
/** 下单时间 */
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */

View File

@@ -1,6 +1,8 @@
package com.aida.seller.module.order.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.io.Serializable;
@@ -11,20 +13,23 @@ import java.time.LocalDateTime;
* 订单商品明细表
*/
@Data
@TableName("order_item")
@TableName("seller_order_item")
public class OrderItemEntity implements Serializable {
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.AUTO)
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/** 订单ID关联 order_info */
private String orderId;
/** 订单ID关联 seller_orders */
@JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
/** 商品ID */
private Long productId;
@JsonSerialize(using = ToStringSerializer.class)
private Long listingId;
/** 商品名称 */
private String productName;
@@ -35,9 +40,6 @@ public class OrderItemEntity implements Serializable {
/** 成交单价HK$ */
private BigDecimal price;
/** 购买数量 */
private Integer quantity;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

View File

@@ -1,5 +1,6 @@
package com.aida.seller.module.order.service;
import com.aida.seller.common.constants.CommonConstants;
import com.aida.seller.module.order.dto.OrderListDTO;
import com.aida.seller.module.order.entity.OrderInfoEntity;
import com.aida.seller.module.order.entity.OrderItemEntity;
@@ -7,6 +8,7 @@ import com.aida.seller.module.order.mapper.OrderInfoMapper;
import com.aida.seller.module.order.mapper.OrderItemMapper;
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.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -30,6 +32,7 @@ import java.util.stream.Collectors;
public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEntity> implements OrderService {
private final OrderItemMapper orderItemMapper;
private final MinioUtil minioUtil;
/**
* 查询指定卖家的订单汇总数据
@@ -72,10 +75,10 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
if (StringUtils.hasText(dto.getKeyword())) {
String keyword = dto.getKeyword().trim();
queryWrapper.and(w -> w
.like(OrderInfoEntity::getOrderId, keyword)
.like(OrderInfoEntity::getId, keyword)
.or()
.inSql(OrderInfoEntity::getOrderId,
"SELECT order_id FROM order_item WHERE product_name LIKE '%" + keyword + "%'")
.inSql(OrderInfoEntity::getId,
"SELECT order_id FROM seller_order_item WHERE deleted = 0 AND product_name LIKE '%" + keyword + "%'")
);
}
@@ -83,11 +86,11 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
Page<OrderInfoEntity> page = this.page(pageParam, queryWrapper);
List<String> orderIds = page.getRecords().stream()
.map(OrderInfoEntity::getOrderId)
List<Long> orderIds = page.getRecords().stream()
.map(OrderInfoEntity::getId)
.collect(Collectors.toList());
Map<String, List<OrderItemEntity>> itemsMap = orderIds.isEmpty()
Map<Long, List<OrderItemEntity>> itemsMap = orderIds.isEmpty()
? Collections.emptyMap()
: orderItemMapper.selectList(
new LambdaQueryWrapper<OrderItemEntity>()
@@ -97,17 +100,17 @@ public class OrderServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
List<OrderVO> voList = page.getRecords().stream().map(order -> {
OrderVO vo = new OrderVO();
vo.setOrderId(order.getOrderId());
vo.setOrderId(order.getId());
vo.setPrice(order.getTotalPrice());
vo.setBuyerUsername("@" + (order.getBuyerUsername() != null ? order.getBuyerUsername() : ""));
vo.setDate(order.getCreateTime());
List<OrderItemEntity> items = itemsMap.getOrDefault(order.getOrderId(), new ArrayList<>());
List<OrderItemEntity> items = itemsMap.getOrDefault(order.getId(), new ArrayList<>());
List<OrderVO.ItemVO> itemVOs = items.stream().map(item -> {
OrderVO.ItemVO itemVO = new OrderVO.ItemVO();
itemVO.setProductId(item.getProductId());
itemVO.setProductId(item.getListingId());
itemVO.setProductName(item.getProductName());
itemVO.setThumbnailUrl(item.getThumbnailUrl());
itemVO.setThumbnailUrl(minioUtil.processMinioResource(item.getThumbnailUrl(), CommonConstants.MINIO_PATH_TIMEOUT));
return itemVO;
}).collect(Collectors.toList());
vo.setItems(itemVOs);

View File

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

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
@@ -30,5 +30,3 @@ minio:
default-bucket: aida-user
logging:
level:
com.aida: debug

View File

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

View File

@@ -1,87 +1,90 @@
-- 商品表
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_image (
id BIGINT PRIMARY KEY COMMENT '图片ID',
listing_id BIGINT NOT NULL COMMENT '商品ID',
category VARCHAR(50) NOT NULL COMMENT '图片类别: cover/main_product/product/sketch/apparel',
image_url VARCHAR(500) NOT NULL COMMENT '图片URL',
sort_order INT DEFAULT 0 COMMENT '排序',
is_selected INT(1) DEFAULT 0 COMMENT '是否选中: 0-未选中, 1-选中(仅product有效)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
INDEX idx_listing_id (listing_id),
INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品图片表';
CREATE DATABASE IF NOT EXISTS aida_seller DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE aida_seller;
-- ==================== 1. 设计师表 ====================
DROP TABLE IF EXISTS `designer`;
CREATE TABLE `designer` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '设计师ID',
`user_id` BIGINT DEFAULT NULL COMMENT '用户ID(关联用户表)',
`shop_name` VARCHAR(100) NOT NULL COMMENT '店铺名称',
`owner_name` VARCHAR(50) NOT NULL COMMENT '所有者全名',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`mobile` VARCHAR(20) NOT NULL COMMENT '手机号',
`social_links` TEXT DEFAULT NULL COMMENT '作品集/社交媒体链接(JSON数组)',
`apply_status` TINYINT NOT NULL DEFAULT 0 COMMENT '申请状态: 0-待审核, 1-审核通过, 2-审核拒绝',
`audit_remark` VARCHAR(500) DEFAULT NULL COMMENT '审核备注',
`audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-禁用, 1-启用',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0-否, 1-是',
PRIMARY KEY (`id`),
KEY `idx_mobile` (`mobile`),
KEY `idx_email` (`email`),
KEY `idx_apply_status` (`apply_status`),
KEY `idx_status` (`status`)
-- 设计师表
CREATE TABLE seller_designer (
id BIGINT PRIMARY KEY COMMENT '设计师ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
shop_name VARCHAR(100) NOT NULL COMMENT '店铺名称',
avatar VARCHAR(200) COMMENT '店铺头像URL',
brand_banner VARCHAR(200) COMMENT '品牌Banner URL',
owner_name VARCHAR(100) COMMENT '所有者全名',
email VARCHAR(100) COMMENT '邮箱',
mobile VARCHAR(30) COMMENT '手机号',
social_links JSON COMMENT '作品集/社交媒体链接',
description TEXT COMMENT '设计师简介',
apply_status INT(1) DEFAULT 0 COMMENT '申请状态: 0-待审核, 1-审核通过, 2-审核拒绝',
audit_remark VARCHAR(500) COMMENT '审核备注',
audit_time DATETIME COMMENT '审核时间',
status INT(1) DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted INT(1) DEFAULT 0 COMMENT '是否删除: 0-否, 1-是',
INDEX idx_user_id (user_id),
INDEX idx_apply_status (apply_status),
INDEX idx_status (status),
INDEX idx_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设计师表';
-- ==================== 2. 订单表 ====================
-- 注意: 代码中 OrderInfoEntity 使用 @TableName("order_info")
-- 若生产库表名为 "orders" 请改为 "order_info",列名 "order_no" 建议改为 "order_id"
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_id` VARCHAR(32) NOT NULL COMMENT '订单号',
`seller_id` BIGINT NOT NULL COMMENT '商家ID',
`buyer_id` BIGINT NOT NULL COMMENT '买家ID',
`buyer_name` VARCHAR(100) DEFAULT NULL COMMENT '买家名称',
`total_price` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '商品总金额',
`order_status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态: 0-待支付, 1-已支付, 2-已发货, 3-已完成, 4-已取消, 5-退款中',
`total_views` BIGINT NOT NULL DEFAULT 0 COMMENT '商品浏览量(订单关联商品的总浏览数)',
`shipping_address` TEXT DEFAULT NULL COMMENT '收货地址',
`receiver_name` VARCHAR(50) DEFAULT NULL COMMENT '收货人',
`receiver_phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
`receiver_address` VARCHAR(500) DEFAULT NULL COMMENT '详细地址',
`tracking_number` VARCHAR(100) DEFAULT NULL COMMENT '快递单号',
`tracking_company` VARCHAR(100) DEFAULT NULL COMMENT '快递公司',
`pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
`ship_time` DATETIME DEFAULT NULL COMMENT '发货时间',
`receive_time` DATETIME DEFAULT NULL COMMENT '收货时间',
`cancel_time` DATETIME DEFAULT NULL COMMENT '取消时间',
`cancel_reason` VARCHAR(500) DEFAULT NULL COMMENT '取消原因',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0-否, 1-是',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id` (`order_id`),
KEY `idx_seller_id` (`seller_id`),
KEY `idx_buyer_id` (`buyer_id`),
KEY `idx_order_status` (`order_status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
-- 订单主表
CREATE TABLE seller_orders (
id BIGINT PRIMARY KEY COMMENT '主键ID',
seller_id BIGINT NOT NULL COMMENT '卖家ID',
total_price DECIMAL(10,2) COMMENT '订单总金额(HK$)',
buyer_username VARCHAR(100) COMMENT '买家账号',
total_items INT COMMENT '商品总数量',
total_views BIGINT DEFAULT 0 COMMENT '总浏览量',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
INDEX idx_seller_id (seller_id),
INDEX idx_deleted (deleted),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';
-- ==================== 3. 订单项表 ====================
DROP TABLE IF EXISTS `order_item`;
CREATE TABLE `order_item` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_id` BIGINT NOT NULL COMMENT '订单ID(关联orders.id)',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`sku_id` BIGINT DEFAULT NULL COMMENT 'SKU ID',
`product_name` VARCHAR(200) NOT NULL COMMENT '商品名称',
`sku_name` VARCHAR(200) DEFAULT NULL COMMENT 'SKU名称',
`product_image` VARCHAR(500) DEFAULT NULL COMMENT '商品图片',
`price` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '商品单价',
`quantity` INT NOT NULL DEFAULT 1 COMMENT '购买数量',
`total_amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '小计金额',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0-否, 1-是',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_order_no` (`order_no`),
KEY `idx_product_id` (`product_id`),
KEY `idx_sku_id` (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项表';
-- 订单商品明细表
CREATE TABLE seller_order_item (
id BIGINT PRIMARY KEY COMMENT '主键ID',
order_id VARCHAR(50) NOT NULL COMMENT '订单ID',
listing_id BIGINT NOT NULL COMMENT '商品ID',
product_name VARCHAR(255) COMMENT '商品名称',
thumbnail_url VARCHAR(200) COMMENT '商品缩略图URL',
price DECIMAL(10,2) COMMENT '成交单价(HK$)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
deleted INT(1) DEFAULT 0 COMMENT '是否删除0-否1-是',
INDEX idx_order_id (order_id),
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>