Browse Source

1.午休脚本提交

2.销售统计功能提交
main
wangqiang 3 days ago
parent
commit
ddd7e69507
54 changed files with 4507 additions and 0 deletions
  1. 4
    0
      script/sql/20250711.sql
  2. 11
    0
      zs-manager/pom.xml
  3. 50
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/common/ApiResponse.java
  4. 39
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/common/PageResponse.java
  5. 357
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/controller/AnalysisDataController.java
  6. 126
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/controller/BaseDataController.java
  7. 78
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/controller/CategoryController.java
  8. 77
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/controller/ReportController.java
  9. 140
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/controller/ShopCustomerController.java
  10. 135
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/AnalysisData.java
  11. 68
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/BaseData.java
  12. 26
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/BaseDataExcelDTO.java
  13. 18
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/BaseDataVO.java
  14. 26
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/BasicStatsVO.java
  15. 17
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/BrandDataVO.java
  16. 51
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/Category.java
  17. 18
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/CategoryAnalysisVO.java
  18. 17
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/CategoryTrendVO.java
  19. 17
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/DimensionChartVO.java
  20. 29
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/DimensionStatsVO.java
  21. 12
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/FieldConfig.java
  22. 21
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/FilterOptionsVO.java
  23. 20
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/GrowthRatesVO.java
  24. 22
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/OverallAnalysisVO.java
  25. 15
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/ProductAnalysisVO.java
  26. 16
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/ProductCodeSuggestionVO.java
  27. 17
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/RankingDataVO.java
  28. 17
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopAmountVO.java
  29. 19
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopAnalysisVO.java
  30. 43
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopCustomer.java
  31. 14
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopCustomerExcelDTO.java
  32. 17
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopQuantityVO.java
  33. 19
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/TopProductVO.java
  34. 17
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/entity/TrendDataVO.java
  35. 56
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/mapper/AnalysisDataMapper.java
  36. 38
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/mapper/BaseDataMapper.java
  37. 12
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/mapper/CategoryMapper.java
  38. 35
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/mapper/ReportMapper.java
  39. 18
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/mapper/ShopCustomerMapper.java
  40. 76
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/AnalysisDataService.java
  41. 26
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/BaseDataService.java
  42. 18
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/CategoryService.java
  43. 14
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/ReportService.java
  44. 25
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/ShopCustomerService.java
  45. 639
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/AnalysisDataServiceImpl.java
  46. 328
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/BaseDataServiceImpl.java
  47. 80
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/CategoryServiceImpl.java
  48. 274
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/ReportServiceImpl.java
  49. 229
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/ShopCustomerServiceImpl.java
  50. 379
    0
      zs-manager/src/main/java/com/ruoyi/tjfx/util/ExcelUtil.java
  51. 91
    0
      zs-manager/src/main/resources/mapper/AnalysisDataMapper.xml
  52. 80
    0
      zs-manager/src/main/resources/mapper/BaseDataMapper.xml
  53. 504
    0
      zs-manager/src/main/resources/mapper/ReportMapper.xml
  54. 12
    0
      zs-manager/src/main/resources/mapper/ShopCustomerMapper.xml

+ 4
- 0
script/sql/20250711.sql View File

@@ -0,0 +1,4 @@
ALTER TABLE dk_check_in_attendance_team
ADD COLUMN lunch_start_time VARCHAR(255) COMMENT '午休开始时间',
ADD COLUMN lunch_end_time VARCHAR(255) COMMENT '午休结束时间';
ADD COLUMN lunch_time VARCHAR(255) COMMENT '午休时间 单位小时';

+ 11
- 0
zs-manager/pom.xml View File

@@ -75,5 +75,16 @@
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<!-- jsoup HTML 解析器库 -->
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.21.1</version>
</dependency>
</dependencies>

</project>

+ 50
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/common/ApiResponse.java View File

@@ -0,0 +1,50 @@
package com.ruoyi.tjfx.common;

import lombok.Data;

/**
* 通用API响应类
*/
@Data
public class ApiResponse<T> {

private Integer code;
private String message;
private T data;
private Long timestamp;

public ApiResponse() {
this.timestamp = System.currentTimeMillis();
}

public ApiResponse(Integer code, String message) {
this();
this.code = code;
this.message = message;
}

public ApiResponse(Integer code, String message, T data) {
this(code, message);
this.data = data;
}

public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "操作成功", data);
}

public static <T> ApiResponse<T> success(T data, String message) {
return new ApiResponse<>(200, message, data);
}

public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(500, message);
}

public static <T> ApiResponse<T> error(Integer code, String message) {
return new ApiResponse<>(code, message);
}

public static <T> ApiResponse<T> validationError(String message) {
return new ApiResponse<>(400, message);
}
}

+ 39
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/common/PageResponse.java View File

@@ -0,0 +1,39 @@
package com.ruoyi.tjfx.common;

import lombok.Data;

import java.util.List;

/**
* 分页响应类
*/
@Data
public class PageResponse<T> {

private List<T> list;
private Pagination pagination;

public PageResponse(List<T> list, Pagination pagination) {
this.list = list;
this.pagination = pagination;
}

@Data
public static class Pagination {
private Long total;
private Integer page;
private Integer pageSize;
private Integer totalPages;
private Boolean hasNext;
private Boolean hasPrev;

public Pagination(Long total, Integer page, Integer pageSize) {
this.total = total;
this.page = page;
this.pageSize = pageSize;
this.totalPages = (int) Math.ceil((double) total / pageSize);
this.hasNext = page < totalPages;
this.hasPrev = page > 1;
}
}
}

+ 357
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/controller/AnalysisDataController.java View File

@@ -0,0 +1,357 @@
package com.ruoyi.tjfx.controller;


import cn.dev33.satoken.annotation.SaIgnore;
import com.ruoyi.tjfx.common.ApiResponse;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.AnalysisData;
import com.ruoyi.tjfx.service.AnalysisDataService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.HashMap;

/**
* 分析数据Controller
*/
@SaIgnore
@Slf4j
@RestController
@RequestMapping("/analysis-data")
public class AnalysisDataController {

@Autowired
private AnalysisDataService analysisDataService;

/**
* 分页查询分析数据
*/
@GetMapping
public ApiResponse<PageResponse<AnalysisData>> getPage(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String productCode,
@RequestParam(required = false) String productName,
@RequestParam(required = false) String customerName,
@RequestParam(required = false) String shopName,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {

try {
PageResponse<AnalysisData> result = analysisDataService.getPage(
page, pageSize, productCode, productName, customerName, shopName, status, startDate, endDate);
return ApiResponse.success(result, "获取成功");
} catch (Exception e) {
log.error("获取分析数据列表失败", e);
return ApiResponse.error("获取分析数据列表失败");
}
}

/**
* 根据ID获取分析数据详情
*/
@GetMapping("/{id}")
public ApiResponse<AnalysisData> getById(@PathVariable Long id) {
try {
AnalysisData analysisData = analysisDataService.getById(id);
if (analysisData == null) {
return ApiResponse.error(404, "分析数据不存在");
}
return ApiResponse.success(analysisData, "获取成功");
} catch (Exception e) {
log.error("获取分析数据详情失败", e);
return ApiResponse.error("获取分析数据详情失败");
}
}

/**
* 更新分析数据
*/
@PutMapping("/{id}")
public ApiResponse<Void> updateById(@PathVariable Long id, @RequestBody AnalysisData analysisData) {
try {
analysisDataService.updateById(id, analysisData);
return ApiResponse.success(null, "更新成功");
} catch (Exception e) {
log.error("更新分析数据失败", e);
return ApiResponse.error("更新分析数据失败");
}
}

/**
* 删除分析数据
*/
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteById(@PathVariable Long id) {
try {
analysisDataService.deleteById(id);
return ApiResponse.success(null, "删除成功");
} catch (Exception e) {
log.error("删除分析数据失败", e);
return ApiResponse.error("删除分析数据失败");
}
}

/**
* 批量删除分析数据
*/
@DeleteMapping("/batch/{ids}")
public ApiResponse<Map<String, Object>> deleteBatchByIds(@PathVariable String ids) {
try {
String[] idArray = ids.split(",");
List<Long> idList = new ArrayList<>();
for (String id : idArray) {
try {
idList.add(Long.parseLong(id.trim()));
} catch (NumberFormatException ignored) {
// 忽略无效的ID
}
}

if (idList.isEmpty()) {
return ApiResponse.validationError("请选择要删除的数据");
}

analysisDataService.deleteBatchByIds(idList);

Map<String, Object> result = new HashMap<>();
result.put("deletedCount", idList.size());
return ApiResponse.success(result, "批量删除成功");
} catch (Exception e) {
log.error("批量删除分析数据失败", e);
return ApiResponse.error("批量删除分析数据失败");
}
}

/**
* 导出Excel(支持分页)
*/
@GetMapping("/export")
public void exportExcel(HttpServletResponse response,
@RequestParam(required = false) String productCode,
@RequestParam(required = false) String productName,
@RequestParam(required = false) String customerName,
@RequestParam(required = false) String shopName,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate,
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer pageSize) {
try {
analysisDataService.exportExcel(response, productCode, productName, customerName, shopName, status, startDate, endDate, page, pageSize);
} catch (Exception e) {
log.error("Excel导出失败", e);
try {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"code\":500,\"message\":\"Excel导出失败\"}");
} catch (Exception ex) {
log.error("写入错误响应失败", ex);
}
}
}

/**
* 导入Excel数据(单文件)
*/
@PostMapping("/import")
public ApiResponse<Map<String, Object>> importExcel(
@RequestParam("file") MultipartFile file,
@RequestParam("categoryId") Long categoryId) {
try {
if (file.isEmpty()) {
return ApiResponse.validationError("请选择Excel文件");
}

// 保存上传的文件
String filePath = saveUploadedFile(file);

Map<String, Object> result = analysisDataService.importExcel(filePath, categoryId);
return ApiResponse.success(result, "数据处理完成,请确认后保存");
} catch (Exception e) {
log.error("Excel导入失败", e);
return ApiResponse.error(e.getMessage());
}
}

/**
* 批量导入Excel数据
*/
/* @PostMapping("/import-batch")
public ApiResponse<Map<String, Object>> importBatchExcel(
@RequestParam("files") MultipartFile[] files,
@RequestParam("categoryId") Long categoryId) {
try {
if (files == null || files.length == 0) {
return ApiResponse.validationError("请选择Excel文件");
}

List<String> filePaths = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
filePaths.add(saveUploadedFile(file));
}
}

if (filePaths.isEmpty()) {
return ApiResponse.validationError("没有有效的Excel文件");
}

Map<String, Object> result = analysisDataService.importBatchExcel(filePaths, categoryId);
return ApiResponse.success(result, "批量数据处理完成,请确认后保存");
} catch (Exception e) {
log.error("批量Excel导入失败", e);
return ApiResponse.error(e.getMessage());
}
}*/
/**
* 批量导入Excel数据
*/
@PostMapping("/import-batch")
public ApiResponse<Map<String, Object>> importBatchExcel(
@RequestParam("files") MultipartFile[] files,
@RequestParam("categoryId") Long categoryId) {
try {
if (files == null || files.length == 0) {
return ApiResponse.validationError("请选择Excel文件");
}

List<String> filePaths = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
filePaths.add(saveUploadedFile(file));
}
}

if (filePaths.isEmpty()) {
return ApiResponse.validationError("没有有效的Excel文件");
}

Map<String, Object> result = analysisDataService.importBatchExcel(filePaths, categoryId);
return ApiResponse.success(result, "批量数据处理完成,请确认后保存");
} catch (Exception e) {
log.error("批量Excel导入失败", e);
return ApiResponse.error("导入失败:" + e.getMessage());
}
}

/**
* 导入FBA数据(单文件)
*/
@PostMapping("/import/fba")
public ApiResponse<Map<String, Object>> importFbaExcel(
@RequestParam("file") MultipartFile file,
@RequestParam("shopName") String shopName) {
try {
if (file.isEmpty()) {
return ApiResponse.validationError("请选择Excel文件");
}

String filePath = saveUploadedFile(file);
Map<String, Object> result = analysisDataService.importFbaExcel(filePath, shopName);
return ApiResponse.success(result, "FBA数据处理完成,请确认后保存");
} catch (Exception e) {
log.error("FBA数据导入失败", e);
return ApiResponse.error(e.getMessage());
}
}

/**
* 批量导入FBA数据
*/
@PostMapping("/import/fba/batch")
public ApiResponse<Map<String, Object>> importBatchFbaExcel(
@RequestParam("files") MultipartFile[] files,
@RequestParam("shopName") String shopName) {
try {
if (files == null || files.length == 0) {
return ApiResponse.validationError("请选择Excel文件");
}

List<String> filePaths = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
filePaths.add(saveUploadedFile(file));
}
}

if (filePaths.isEmpty()) {
return ApiResponse.validationError("没有有效的Excel文件");
}

Map<String, Object> result = analysisDataService.importBatchFbaExcel(filePaths, shopName);
return ApiResponse.success(result, "FBA批量数据处理完成,请确认后保存");
} catch (Exception e) {
log.error("FBA批量数据导入失败", e);
return ApiResponse.error(e.getMessage());
}
}

/**
* 保存导入的数据
*/
@PostMapping("/save-imported")
public ApiResponse<Map<String, Object>> saveImportedData(@RequestBody List<AnalysisData> dataList) {
try {
if (dataList == null || dataList.isEmpty()) {
return ApiResponse.validationError("没有可保存的数据");
}

Map<String, Object> result = analysisDataService.saveImportedData(dataList);
return ApiResponse.success(result, "数据保存完成");
} catch (Exception e) {
log.error("保存数据失败", e);
return ApiResponse.error("保存数据失败");
}
}

/**
* 保存上传的文件到系统临时目录
*/
private String saveUploadedFile(MultipartFile file) throws Exception {
// 获取系统临时目录
String tempDir = System.getProperty("java.io.tmpdir");

// 防止文件名冲突,加时间戳
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();

// 构建目标文件路径
String filePath = tempDir + java.io.File.separator + fileName;

// 创建文件对象并保存
java.io.File destFile = new java.io.File(filePath);
file.transferTo(destFile);

return filePath;
}

/**
* 保存上传的文件
*/
/* private String saveUploadedFile(MultipartFile file) throws Exception {
// 这里应该实现文件保存逻辑
// 为了简化,这里只是返回一个临时路径
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
String uploadPath = "./uploads/";

// 确保目录存在
java.io.File uploadDir = new java.io.File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}

String filePath = uploadPath + fileName;
file.transferTo(new java.io.File(filePath));

return filePath;
}*/
}

+ 126
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/controller/BaseDataController.java View File

@@ -0,0 +1,126 @@
package com.ruoyi.tjfx.controller;

import cn.dev33.satoken.annotation.SaIgnore;
import com.ruoyi.tjfx.common.ApiResponse;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.BaseDataVO;
import com.ruoyi.tjfx.entity.Category;
import com.ruoyi.tjfx.service.BaseDataService;
import com.ruoyi.tjfx.entity.BaseData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@SaIgnore
@Slf4j
@RestController
@RequestMapping("/basedata")
public class BaseDataController {

@Autowired
private BaseDataService baseDataService;

/**
* 分页查询基础数据
*/
@GetMapping()
public ApiResponse<PageResponse<BaseDataVO>> getBaseDataPage(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String productCode,
@RequestParam(required = false) String productName,
@RequestParam(required = false) Long categoryId) {
try {
return ApiResponse.success(baseDataService.getPage(page, pageSize, productCode, productName, categoryId));
} catch (Exception e) {
log.error("分页查询基础数据失败", e);
return ApiResponse.error("获取基础数据失败");
}


}

@GetMapping("/{id}")
public ApiResponse<BaseData> getById(@PathVariable Long id) {
BaseData data = baseDataService.getById(id);
if (data == null) return ApiResponse.error(404, "数据不存在");
return ApiResponse.success(data, "获取成功");
}

@PostMapping
public ApiResponse<Void> add(@RequestBody BaseData baseData) {
baseDataService.add(baseData);
return ApiResponse.success(null, "新增成功");
}

@PutMapping("/{id}")
public ApiResponse<Void> update(@PathVariable Long id, @RequestBody BaseData baseData) {
baseDataService.update(id, baseData);
return ApiResponse.success(null, "更新成功");
}

@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
baseDataService.delete(id);
return ApiResponse.success(null, "删除成功");
}

@DeleteMapping("/batch/{ids}")
public ApiResponse<Void> deleteBatch(@PathVariable String ids) {
List<Long> idList = Arrays.stream(ids.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Long::valueOf)
.collect(Collectors.toList());

baseDataService.deleteBatch(idList);
return ApiResponse.success(null, "批量删除成功");
}
@PostMapping("/import")
public ApiResponse<?> importExcel(@RequestParam("file") MultipartFile file,
@RequestParam("categoryId") Long categoryId) {
try {
if (file.isEmpty()) {
return ApiResponse.validationError("请选择Excel文件");
}

baseDataService.importExcel(file, categoryId);
return ApiResponse.success(null, "Excel导入成功");
} catch (RuntimeException e) {
return ApiResponse.validationError(e.getMessage());
} catch (Exception e) {
log.error("Excel导入失败", e);
return ApiResponse.error("Excel导入失败");
}
}

@GetMapping("/export/{categoryId}")
public void exportExcel(@PathVariable Long categoryId, HttpServletResponse response) {
try {
baseDataService.exportExcel(categoryId, response);
} catch (RuntimeException e) {
try {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"success\":false,\"message\":\"" + e.getMessage() + "\"}");
} catch (Exception ex) {
log.error("写入错误响应失败", ex);
}
} catch (Exception e) {
log.error("Excel导出失败", e);
try {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"success\":false,\"message\":\"Excel导出失败\"}");
} catch (Exception ex) {
log.error("写入错误响应失败", ex);
}
}
}
}

+ 78
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/controller/CategoryController.java View File

@@ -0,0 +1,78 @@
package com.ruoyi.tjfx.controller;

import cn.dev33.satoken.annotation.SaIgnore;
import com.ruoyi.tjfx.common.ApiResponse;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.service.CategoryService;
import com.ruoyi.tjfx.entity.Category;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
@SaIgnore
@Slf4j
@RestController
@RequestMapping("/categories")
public class CategoryController {

@Autowired
private CategoryService categoryService;
@GetMapping("/simple")
public ApiResponse<List<Category>> getSimpleCategories() {
try {
List<Category> categories = categoryService.getAllSimple();
return ApiResponse.success(categories);
} catch (Exception e) {
log.error("获取分类列表失败", e);
return ApiResponse.error("获取分类列表失败");
}
}
/*
@GetMapping
public ApiResponse<List<Category>> listAll() {
return ApiResponse.success(categoryService.listAll(), "获取成功");
}*/

/**
* 分页查询分类
*/
@GetMapping()
public ApiResponse<PageResponse<Category>> getPage(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name) {
try {
PageResponse<Category> result = categoryService.getPage(page, pageSize, name);
return ApiResponse.success(result, "获取成功");
} catch (Exception e) {
log.error("获取分类列表失败", e);
return ApiResponse.error("获取分类列表失败");
}
}

@GetMapping("/{id}")
public ApiResponse<Category> getById(@PathVariable Long id) {
Category data = categoryService.getById(id);
if (data == null) return ApiResponse.error(404, "数据不存在");
return ApiResponse.success(data, "获取成功");
}

@PostMapping
public ApiResponse<Void> add(@RequestBody Category category) {
categoryService.add(category);
return ApiResponse.success(null, "新增成功");
}

@PutMapping("/{id}")
public ApiResponse<Void> update(@PathVariable Long id, @RequestBody Category category) {
categoryService.update(id, category);
return ApiResponse.success(null, "更新成功");
}

@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
categoryService.delete(id);
return ApiResponse.success(null, "删除成功");
}
}

+ 77
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/controller/ReportController.java View File

@@ -0,0 +1,77 @@
package com.ruoyi.tjfx.controller;

import com.ruoyi.tjfx.entity.*;
import com.ruoyi.tjfx.service.ReportService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.tjfx.common.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
@RestController
@RequestMapping("/reports")
public class ReportController {

@Autowired
private ReportService reportService;

// 1. 整体销售分析
@GetMapping("/overall-analysis")
public ApiResponse<OverallAnalysisVO> overallAnalysis(
@RequestParam String startDate,
@RequestParam String endDate,
@RequestParam(required = false) String category,
@RequestParam(required = false) String categorySpecs,
@RequestParam(required = false) String customer,
@RequestParam(required = false) String shop,
@RequestParam(required = false) String brand,
@RequestParam(required = false) String productCode
) {
return ApiResponse.success(reportService.overallAnalysis(
startDate, endDate, category, categorySpecs, customer, shop, brand, productCode
), "获取整体销售分析数据成功");
}

// 2. 单品销售分析
@GetMapping("/product-analysis")
public ApiResponse<ProductAnalysisVO> productAnalysis(
@RequestParam String startDate,
@RequestParam String endDate,
@RequestParam String productCode
) {
return ApiResponse.success(reportService.productAnalysis(startDate, endDate, productCode), "获取单品销售分析数据成功");
}

// 3. 店铺销售分析
@GetMapping("/shop-analysis")
public ApiResponse<ShopAnalysisVO> shopAnalysis(
@RequestParam String startDate,
@RequestParam String endDate,
@RequestParam(required = false) String shop
) {
return ApiResponse.success(reportService.shopAnalysis(startDate, endDate, shop), "获取店铺销售分析数据成功");
}

// 4. 分类销售分析
@GetMapping("/category-analysis")
public ApiResponse<CategoryAnalysisVO> categoryAnalysis(
@RequestParam String startDate,
@RequestParam String endDate,
@RequestParam(required = false) String category
) {
return ApiResponse.success(reportService.categoryAnalysis(startDate, endDate, category), "获取分类销售分析数据成功");
}

// 5. 筛选选项
@GetMapping("/filter-options")
public ApiResponse<FilterOptionsVO> filterOptions() {
return ApiResponse.success(reportService.filterOptions(), "获取筛选选项成功");
}

// 6. 商品编码联想
@GetMapping("/product-code-suggestions")
public ApiResponse<List<ProductCodeSuggestionVO>> productCodeSuggestions(@RequestParam String keyword) {
return ApiResponse.success(reportService.productCodeSuggestions(keyword), "获取商品编码联想数据成功");
}
}

+ 140
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/controller/ShopCustomerController.java View File

@@ -0,0 +1,140 @@
package com.ruoyi.tjfx.controller;

import cn.dev33.satoken.annotation.SaIgnore;
import com.ruoyi.tjfx.common.ApiResponse;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.ShopCustomer;
import com.ruoyi.tjfx.service.ShopCustomerService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@SaIgnore
@Slf4j
@RestController
@RequestMapping("/shop-customer")
public class ShopCustomerController {

@Autowired
private ShopCustomerService shopCustomerService;

/* @GetMapping
public ApiResponse<List<ShopCustomer>> listAll() {
return ApiResponse.success(shopCustomerService.listAll(), "获取成功");
}*/

/**
* 分页查询店铺客户
*/
@GetMapping()
public ApiResponse<PageResponse<ShopCustomer>> getPage(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String shopName,
@RequestParam(required = false) String customerName) {
try {
PageResponse<ShopCustomer> result = shopCustomerService.getPage(page, pageSize, shopName, customerName);
return ApiResponse.success(result, "获取成功");
} catch (Exception e) {
log.error("获取店铺客户列表失败", e);
return ApiResponse.error("获取店铺客户列表失败");
}
}

@GetMapping("/{id}")
public ApiResponse<ShopCustomer> getById(@PathVariable Long id) {
ShopCustomer data = shopCustomerService.getById(id);
if (data == null) return ApiResponse.error(404, "数据不存在");
return ApiResponse.success(data, "获取成功");
}

@PostMapping
public ApiResponse<Void> add(@RequestBody ShopCustomer shopCustomer) {
shopCustomerService.add(shopCustomer);
return ApiResponse.success(null, "新增成功");
}

@PutMapping("/{id}")
public ApiResponse<Void> update(@PathVariable Long id, @RequestBody ShopCustomer shopCustomer) {
shopCustomerService.update(id, shopCustomer);
return ApiResponse.success(null, "更新成功");
}

@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
shopCustomerService.delete(id);
return ApiResponse.success(null, "删除成功");
}

@DeleteMapping("/batch/{ids}")
public ApiResponse<Void> deleteBatch(@PathVariable String ids) {
List<Long> idList = Arrays.stream(ids.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Long::valueOf)
.collect(Collectors.toList());

shopCustomerService.deleteBatch(idList);
return ApiResponse.success(null, "批量删除成功");
}

@PostMapping("/import")
public ApiResponse<?> importExcel(@RequestParam("file") MultipartFile file) {
try {
if (file.isEmpty()) {
return ApiResponse.validationError("请选择Excel文件");
}
Map<String, Object> result = shopCustomerService.importExcel(file);
int imported = (int) result.getOrDefault("imported", 0);
int total = (int) result.getOrDefault("total", 0);
int errors = (int) result.getOrDefault("errors", 0);
String msg = "Excel导入完成,共处理" + total + "行,成功导入" + imported + "条";
if (errors > 0) msg += ",跳过" + errors + "条";
return ApiResponse.success(result, msg);
} catch (RuntimeException e) {
return ApiResponse.validationError(e.getMessage());
} catch (Exception e) {
log.error("Excel导入失败", e);
return ApiResponse.error("Excel导入失败");
}
}
@GetMapping("/export")
public void exportExcel(HttpServletResponse response) {
try {
shopCustomerService.exportExcel(response);
} catch (Exception e) {
log.error("Excel导出失败", e);
// 不建议再写响应体了,可能已经写入了一部分内容,会报 IllegalStateException
}
}
/* @GetMapping("/export")
public void exportExcel(HttpServletResponse response) {
try {
shopCustomerService.exportExcel(response);
} catch (RuntimeException e) {
try {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"success\":false,\"message\":\"" + e.getMessage() + "\"}");
} catch (Exception ex) {
log.error("写入错误响应失败", ex);
}
} catch (Exception e) {
log.error("Excel导出失败", e);
try {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"success\":false,\"message\":\"Excel导出失败\"}");
} catch (Exception ex) {
log.error("写入错误响应失败", ex);
}
}
}*/
}

+ 135
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/AnalysisData.java View File

@@ -0,0 +1,135 @@
package com.ruoyi.tjfx.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;

/**
* 分析数据实体类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("zs_tjfx_analysis_data")
public class AnalysisData {

@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 日期
*/
@TableField("date")
private LocalDate date;

/**
* 店铺名称
*/
@TableField("shop_name")
private String shopName;

/**
* 商品编号
*/
@TableField("product_code")
private String productCode;

/**
* 商品名称
*/
@TableField("product_name")
private String productName;

/**
* 客户名称
*/
@TableField("customer_name")
private String customerName;

/**
* 分类
*/
@TableField("category")
private String category;

/**
* 分类规格JSON
*/
@TableField("category_specs")
private String categorySpecs;

/**
* 数量
*/
@TableField("quantity")
private Integer quantity;

/**
* 合计金额
*/
@TableField("total_amount")
private BigDecimal totalAmount;

/**
* 数据来源
*/
@TableField("source")
private String source;

/**
* 状态:1-正常,2-客户名称未匹配,3-分类规格未匹配
*/
@TableField("status")
private Integer status;

/**
* 出库类型
*/
@TableField("delivery_type")
private String deliveryType;

/**
* 目的地
*/
@TableField("destination")
private String destination;

/**
* 备注
*/
@TableField("remarks")
private String remarks;

/**
* 订单编号
*/
@TableField("order_number")
private String orderNumber;

/**
* 行号
*/
@TableField("row_number")
private Integer rowNumber;

/**
* 品牌
*/
@TableField("brand")
private String brand;

/**
* 创建时间
*/
@TableField(value = "created_at", fill = FieldFill.INSERT)
private LocalDateTime createdAt;

/**
* 更新时间
*/
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

+ 68
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/BaseData.java View File

@@ -0,0 +1,68 @@
package com.ruoyi.tjfx.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.time.LocalDateTime;

/**
* 基准数据实体类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("zs_tjfx_base_data")
public class BaseData {

@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 商品编号
*/
@TableField("product_code")
private String productCode;

/**
* 商品名称
*/
@TableField("product_name")
private String productName;

/**
* 品牌
*/
@TableField("brand")
private String brand;

/**
* 分类ID
*/
@TableField("category_id")
private Long categoryId;


/**
* 分类名称
*/
private String categoryName;


/**
* 分类规格JSON
*/
@TableField("category_specs")
private String categorySpecs;

/**
* 创建时间
*/
@TableField(value = "created_at", fill = FieldFill.INSERT)
private LocalDateTime createdAt;

/**
* 更新时间
*/
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

+ 26
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/BaseDataExcelDTO.java View File

@@ -0,0 +1,26 @@
package com.ruoyi.tjfx.entity;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Data
public class BaseDataExcelDTO {
@ExcelProperty(value = "商品编码",index =0)
private String productCode;
@ExcelProperty(value = "商品名称",index =1)
private String productName;
@ExcelProperty(value = "品牌",index =2)
private String brand;
@ExcelProperty(value = "所属分类名称",index =3)
private String categoryName;
@ExcelProperty(value = "分类规格",index =4)
private String categorySpecs;
/* private LocalDateTime createdAt;
private LocalDateTime updatedAt;*/
// 动态扩展字段(不确定)
// private Map<String, String> categorySpecs = new HashMap<>();
}

+ 18
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/BaseDataVO.java View File

@@ -0,0 +1,18 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class BaseDataVO {
private Long id;
private String productCode;
private String productName;
private String brand;
private Long categoryId;
private String categoryName; // 注意:这里是字符串
private String categorySpecs;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

+ 26
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/BasicStatsVO.java View File

@@ -0,0 +1,26 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;

import java.math.BigDecimal;

/**
* 基础统计数据
*/
@Data
public class BasicStatsVO {
/** 总记录数 */
private Integer totalRecords;
/** 总销售数量 */
private BigDecimal totalQuantity;
/** 总销售金额 */
private BigDecimal totalAmount;
/** 不同商品数 */
private Integer uniqueProducts;
/** 不同客户数 */
private Integer uniqueCustomers;
/** 店铺分析用-不同店铺数 */
private Integer totalShops;
/** 店铺分析用-不同品类数 */
private Integer totalCategories;
}

+ 17
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/BrandDataVO.java View File

@@ -0,0 +1,17 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.math.BigDecimal;

/**
* 品牌销售数据
*/
@Data
public class BrandDataVO {
/** 品牌名称 */
private String brand;
/** 销售金额 */
private Double amount;
/** 销售数量 */
private Double quantity;
}

+ 51
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/Category.java View File

@@ -0,0 +1,51 @@
package com.ruoyi.tjfx.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.time.LocalDateTime;
import java.util.List;

/**
* 分类实体类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("zs_tjfx_categories")
public class Category {

@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 分类名称
*/
@TableField("name")
private String name;

/**
* 分类描述
*/
@TableField("description")
private String description;
/**
* 字段配置JSON string 格式
*/
@TableField("field_config")
// @JsonProperty("field_config")
private String fieldConfig;

/**
* 创建时间
*/
@TableField(value = "created_at", fill = FieldFill.INSERT)
private LocalDateTime createdAt;

/**
* 更新时间
*/
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

+ 18
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/CategoryAnalysisVO.java View File

@@ -0,0 +1,18 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.util.List;
import java.util.Map;

/**
* 分类销售分析数据
*/
@Data
public class CategoryAnalysisVO {
/** 各规格维度的图表数据,key为维度名 */
private Map<String, DimensionChartVO> dimensionCharts;
/** 可用的规格维度列表 */
private List<String> availableDimensions;
/** 当前分析的分类名称 */
private String currentCategory;
}

+ 17
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/CategoryTrendVO.java View File

@@ -0,0 +1,17 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.math.BigDecimal;

/**
* 品类销售趋势数据
*/
@Data
public class CategoryTrendVO {
/** 品类名称 */
private String category;
/** 销售金额 */
private Double amount;
/** 销售数量 */
private Double quantity;
}

+ 17
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/DimensionChartVO.java View File

@@ -0,0 +1,17 @@
package com.ruoyi.tjfx.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;

/**
* 规格维度图表数据
*/
@Data
@AllArgsConstructor
public class DimensionChartVO {
/** 按金额统计的维度数据列表 */
private List<DimensionStatsVO> amountData;
/** 按数量统计的维度数据列表 */
private List<DimensionStatsVO> quantityData;
}

+ 29
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/DimensionStatsVO.java View File

@@ -0,0 +1,29 @@
package com.ruoyi.tjfx.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;

/**
* 规格维度统计数据
*/
@Data
@AllArgsConstructor
public class DimensionStatsVO {
/** 维度值名称 */
private String name;

/** 销售数量 */
private Double quantity;
/** 销售金额 */
private Double amount;
/* *//** 数量占比(百分比字符串) *//*
private String quantityPercentage;
*//** 金额占比(百分比字符串) *//*
private String amountPercentage;*/

/** 数量或者 金额占比(百分比字符串) */
private String percentage;
//价格或者销量
private Double value;
}

+ 12
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/FieldConfig.java View File

@@ -0,0 +1,12 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;

@Data
public class FieldConfig {
private Long id;
private String fieldName;
private String displayLabel;
private String fieldType;
private Boolean isRequired;
}

+ 21
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/FilterOptionsVO.java View File

@@ -0,0 +1,21 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.util.List;

/**
* 筛选选项数据
*/
@Data
public class FilterOptionsVO {
/** 所有分类列表 */
private List<String> categories;
/** 所有规格列表 */
private List<String> categorySpecs;
/** 所有客户列表 */
private List<String> customers;
/** 所有店铺列表 */
private List<String> shops;
/** 所有品牌列表 */
private List<String> brands;
}

+ 20
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/GrowthRatesVO.java View File

@@ -0,0 +1,20 @@
package com.ruoyi.tjfx.entity;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
* 增长率数据(同比/环比)
*/
@Data
@AllArgsConstructor
public class GrowthRatesVO {
/** 金额同比增长率(%) */
private String amountYoY;
/** 数量同比增长率(%) */
private String quantityYoY;
/** 金额环比增长率(%) */
private String amountMoM;
/** 数量环比增长率(%) */
private String quantityMoM;
}

+ 22
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/OverallAnalysisVO.java View File

@@ -0,0 +1,22 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;

import java.util.List;

/**
* 整体销售分析数据
*/
@Data
public class OverallAnalysisVO {
/** 基础统计数据 */
private BasicStatsVO basicStats;
/** 同比/环比增长率 */
private GrowthRatesVO growthRates;
/** 销售趋势数据 */
private List<TrendDataVO> trendData;
/** TOP商品销售数据 */
private List<TopProductVO> topProducts;
/** 品牌销售数据 */
private List<BrandDataVO> brandData;
}

+ 15
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/ProductAnalysisVO.java View File

@@ -0,0 +1,15 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.util.List;

/**
* 单品销售分析数据
*/
@Data
public class ProductAnalysisVO {
/** 单品每日销售排行数据 */
private List<RankingDataVO> rankingData;
/** 单品销售趋势数据 */
private List<TrendDataVO> trendData;
}

+ 16
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/ProductCodeSuggestionVO.java View File

@@ -0,0 +1,16 @@
package com.ruoyi.tjfx.entity;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
* 商品编码联想提示数据
*/
@Data
@AllArgsConstructor
public class ProductCodeSuggestionVO {
/** 商品编码 */
private String value;
/** 显示文本(商品名+规格-分类名) */
private String label;
}

+ 17
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/RankingDataVO.java View File

@@ -0,0 +1,17 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.math.BigDecimal;

/**
* 单品每日销售排行数据
*/
@Data
public class RankingDataVO {
/** 日期 */
private String date;
/** 销售金额 */
private BigDecimal totalAmount;
/** 销售数量 */
private BigDecimal totalQuantity;
}

+ 17
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopAmountVO.java View File

@@ -0,0 +1,17 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.math.BigDecimal;

/**
* 店铺销售金额数据
*/
@Data
public class ShopAmountVO {
/** 店铺名称 */
private String shopName;
/** 销售金额 */
private Double amount;
/** 金额占比(百分比字符串) */
private String percentage;
}

+ 19
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopAnalysisVO.java View File

@@ -0,0 +1,19 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.util.List;

/**
* 店铺销售分析数据
*/
@Data
public class ShopAnalysisVO {
/** 基础统计数据 */
private BasicStatsVO basicStats;
/** 各店铺销售金额数据 */
private List<ShopAmountVO> shopAmountData;
/** 各店铺销售数量数据 */
private List<ShopQuantityVO> shopQuantityData;
/** 各品类销售趋势数据 */
private List<CategoryTrendVO> categoryTrendData;
}

+ 43
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopCustomer.java View File

@@ -0,0 +1,43 @@
package com.ruoyi.tjfx.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.time.LocalDateTime;

/**
* 店铺客户关联实体类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("zs_tjfx_shop_customer")
public class ShopCustomer {

@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 店铺名称
*/
@TableField("shop_name")
private String shopName;

/**
* 客户名称
*/
@TableField("customer_name")
private String customerName;

/**
* 创建时间
*/
@TableField(value = "created_at", fill = FieldFill.INSERT)
private LocalDateTime createdAt;

/**
* 更新时间
*/
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

+ 14
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopCustomerExcelDTO.java View File

@@ -0,0 +1,14 @@
package com.ruoyi.tjfx.entity;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

@Data
public class ShopCustomerExcelDTO {

@ExcelProperty(value = "店铺名称",index =0)
private String shopName;

@ExcelProperty(value = "客户名称",index =1)
private String customerName;
}

+ 17
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/ShopQuantityVO.java View File

@@ -0,0 +1,17 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.math.BigDecimal;

/**
* 店铺销售数量数据
*/
@Data
public class ShopQuantityVO {
/** 店铺名称 */
private String shopName;
/** 销售数量 */
private Double quantity;
/** 数量占比(百分比字符串) */
private String percentage;
}

+ 19
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/TopProductVO.java View File

@@ -0,0 +1,19 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.math.BigDecimal;

/**
* TOP商品销售数据
*/
@Data
public class TopProductVO {
/** 商品编码 */
private String productCode;
/** 商品名称 */
private String productName;
/** 销售数量 */
private Double quantity;
/** 销售金额 */
private Double amount;
}

+ 17
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/entity/TrendDataVO.java View File

@@ -0,0 +1,17 @@
package com.ruoyi.tjfx.entity;

import lombok.Data;
import java.math.BigDecimal;

/**
* 销售趋势数据
*/
@Data
public class TrendDataVO {
/** 日期 */
private String date;
/** 销售金额(单品趋势用) */
private Double amount;
/** 销售数量(单品趋势用) */
private Double quantity;
}

+ 56
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/mapper/AnalysisDataMapper.java View File

@@ -0,0 +1,56 @@
package com.ruoyi.tjfx.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.tjfx.entity.AnalysisData;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.time.LocalDate;
import java.util.List;

/**
* 分析数据Mapper接口
*/
@Mapper
public interface AnalysisDataMapper extends BaseMapper<AnalysisData> {

/**
* 分页查询分析数据
*/
IPage<AnalysisData> selectPageWithConditions(Page<AnalysisData> page,
@Param("productCode") String productCode,
@Param("productName") String productName,
@Param("customerName") String customerName,
@Param("shopName") String shopName,
@Param("status") Integer status,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);

/**
* 根据条件查询所有数据(用于导出)
*/
List<AnalysisData> selectAllWithConditions(@Param("productCode") String productCode,
@Param("productName") String productName,
@Param("customerName") String customerName,
@Param("shopName") String shopName,
@Param("status") Integer status,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);

/**
* 批量插入数据
*/
int batchInsert(@Param("list") List<AnalysisData> list);

/**
* 根据唯一条件查询记录
*/
AnalysisData selectByUniqueCondition(@Param("date") LocalDate date,
@Param("shopName") String shopName,
@Param("productCode") String productCode,
@Param("deliveryType") String deliveryType,
@Param("orderNumber") String orderNumber,
@Param("rowNumber") Integer rowNumber);
}

+ 38
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/mapper/BaseDataMapper.java View File

@@ -0,0 +1,38 @@
package com.ruoyi.tjfx.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.tjfx.entity.BaseData;
import com.ruoyi.tjfx.entity.BaseDataVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
* 基准数据Mapper接口
*/
@Mapper
public interface BaseDataMapper extends BaseMapper<BaseData> {

/**
* 根据商品编号和分类ID查询基准数据
*/
BaseData selectByProductCodeAndCategoryId(@Param("productCode") String productCode,
@Param("categoryId") Long categoryId);
BaseDataVO selectByProductCode(@Param("productCode") String productCode);


List<BaseDataVO> findByCategoryId( @Param("categoryId") Long categoryId);
List<String> selectExistingProductCodes(@Param("codes") List<String> codes);

IPage<BaseDataVO> selectPageWithJoin(
Page<BaseDataVO> page,
@Param("productCode") String productCode,
@Param("productName") String productName,
@Param("categoryId") Long categoryId
);
void insertBatch(@Param("list") List<BaseData> list);

}

+ 12
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/mapper/CategoryMapper.java View File

@@ -0,0 +1,12 @@
package com.ruoyi.tjfx.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.tjfx.entity.Category;
import org.apache.ibatis.annotations.Mapper;

/**
* 分类Mapper接口
*/
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

+ 35
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/mapper/ReportMapper.java View File

@@ -0,0 +1,35 @@
package com.ruoyi.tjfx.mapper;

import com.ruoyi.tjfx.entity.*;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;
import java.util.Map;

@Mapper
public interface ReportMapper {
BasicStatsVO selectBasicStats(Map<String, Object> params);
BasicStatsVO selectLastYearStats(Map<String, Object> params);
BasicStatsVO selectPrevStats(Map<String, Object> params);
List<TrendDataVO> selectTrendData(Map<String, Object> params);
List<TopProductVO> selectTopProducts(Map<String, Object> params);
List<BrandDataVO> selectBrandData(Map<String, Object> params);

List<RankingDataVO> selectProductRanking(Map<String, Object> params);
List<TrendDataVO> selectProductTrend(Map<String, Object> params);

BasicStatsVO selectShopBasicStats(Map<String, Object> params);
List<ShopAmountVO> selectShopAmountData(Map<String, Object> params);
List<ShopQuantityVO> selectShopQuantityData(Map<String, Object> params);
List<CategoryTrendVO> selectCategoryTrendData(Map<String, Object> params);

List<Map<String, Object>> selectSpecsRawData(Map<String, Object> params);

List<String> selectCategories();
List<String> selectCategorySpecs();
List<String> selectCustomers();
List<String> selectShops();
List<String> selectBrands();

List<Map<String, Object>> selectProductCodeSuggestions(String keyword);
}

+ 18
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/mapper/ShopCustomerMapper.java View File

@@ -0,0 +1,18 @@
package com.ruoyi.tjfx.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.tjfx.entity.ShopCustomer;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
* 店铺客户Mapper接口
*/
@Mapper
public interface ShopCustomerMapper extends BaseMapper<ShopCustomer> {

/**
* 根据店铺名称查询客户名称
*/
ShopCustomer selectByShopName(@Param("shopName") String shopName);
}

+ 76
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/AnalysisDataService.java View File

@@ -0,0 +1,76 @@
package com.ruoyi.tjfx.service;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.AnalysisData;

import javax.servlet.http.HttpServletResponse;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;

/**
* 分析数据Service接口
*/
public interface AnalysisDataService {

/**
* 分页查询分析数据
*/
PageResponse<AnalysisData> getPage(Integer page, Integer pageSize, String productCode,
String productName, String customerName, String shopName,
Integer status, LocalDate startDate, LocalDate endDate);

/**
* 根据ID获取分析数据详情
*/
AnalysisData getById(Long id);

/**
* 更新分析数据
*/
void updateById(Long id, AnalysisData analysisData);

/**
* 删除分析数据
*/
void deleteById(Long id);

/**
* 批量删除分析数据
*/
void deleteBatchByIds(List<Long> ids);

/**
* 导出Excel(支持分页)
*/
void exportExcel(HttpServletResponse response, String productCode, String productName,
String customerName, String shopName, Integer status,
LocalDate startDate, LocalDate endDate, Integer page, Integer pageSize);

/**
* 导入Excel数据(单文件)
*/
Map<String, Object> importExcel(String filePath, Long categoryId);

/**
* 批量导入Excel数据
*/
Map<String, Object> importBatchExcel(List<String> filePaths, Long categoryId);

/**
* 导入FBA数据(单文件)
*/
Map<String, Object> importFbaExcel(String filePath, String shopName);

/**
* 批量导入FBA数据
*/
Map<String, Object> importBatchFbaExcel(List<String> filePaths, String shopName);

/**
* 保存导入的数据
*/
Map<String, Object> saveImportedData(List<AnalysisData> dataList);
}

+ 26
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/BaseDataService.java View File

@@ -0,0 +1,26 @@
package com.ruoyi.tjfx.service;

import com.ruoyi.tjfx.common.ApiResponse;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.BaseData;
import com.ruoyi.tjfx.entity.BaseDataVO;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.List;

public interface BaseDataService {
List<BaseData> listAll();
BaseData getById(Long id);
void add(BaseData baseData);
void update(Long id, BaseData baseData);
void delete(Long id);
void deleteBatch(List<Long> ids);
// 分页查询
PageResponse<BaseDataVO> getPage(Integer page, Integer pageSize, String productCode, String productName, Long categoryId);

void importExcel(MultipartFile file, Long categoryId);

void exportExcel(Long categoryId, HttpServletResponse response);

}

+ 18
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/CategoryService.java View File

@@ -0,0 +1,18 @@
package com.ruoyi.tjfx.service;

import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.Category;
import java.util.List;

public interface CategoryService {
List<Category> getAllSimple();
List<Category> listAll();
Category getById(Long id);
void add(Category category);
void update(Long id, Category category);
void delete(Long id);
void addBatch(List<Category> categorys);

// 分页查询
PageResponse<Category> getPage(Integer page, Integer pageSize, String name);
}

+ 14
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/ReportService.java View File

@@ -0,0 +1,14 @@
package com.ruoyi.tjfx.service;

import com.ruoyi.tjfx.entity.*;

import java.util.List;

public interface ReportService {
OverallAnalysisVO overallAnalysis(String startDate, String endDate, String category, String categorySpecs, String customer, String shop, String brand, String productCode);
ProductAnalysisVO productAnalysis(String startDate, String endDate, String productCode);
ShopAnalysisVO shopAnalysis(String startDate, String endDate, String shop);
CategoryAnalysisVO categoryAnalysis(String startDate, String endDate, String category);
FilterOptionsVO filterOptions();
List<ProductCodeSuggestionVO> productCodeSuggestions(String keyword);
}

+ 25
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/ShopCustomerService.java View File

@@ -0,0 +1,25 @@
package com.ruoyi.tjfx.service;

import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.ShopCustomer;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;

public interface ShopCustomerService {
List<ShopCustomer> listAll();
ShopCustomer getById(Long id);
void add(ShopCustomer shopCustomer);
void update(Long id, ShopCustomer shopCustomer);
void delete(Long id);

void deleteBatch(List<Long> ids);

// 分页查询
PageResponse<ShopCustomer> getPage(Integer page, Integer pageSize, String shopName, String customerName);
Map<String, Object> importExcel(MultipartFile file);

void exportExcel(HttpServletResponse response);
}

+ 639
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/AnalysisDataServiceImpl.java View File

@@ -0,0 +1,639 @@
package com.ruoyi.tjfx.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.*;
import com.ruoyi.tjfx.mapper.AnalysisDataMapper;
import com.ruoyi.tjfx.mapper.BaseDataMapper;
import com.ruoyi.tjfx.mapper.CategoryMapper;
import com.ruoyi.tjfx.mapper.ShopCustomerMapper;
import com.ruoyi.tjfx.service.AnalysisDataService;
import com.ruoyi.tjfx.util.ExcelUtil;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;

/**
* 分析数据Service实现类
*/
@Slf4j
@Service
public class AnalysisDataServiceImpl implements AnalysisDataService {

@Autowired
private AnalysisDataMapper analysisDataMapper;

@Autowired
private CategoryMapper categoryMapper;

@Autowired
private BaseDataMapper baseDataMapper;

@Autowired
private ShopCustomerMapper shopCustomerMapper;

@Autowired
private ExcelUtil excelUtil;

@Override
public PageResponse<AnalysisData> getPage(Integer page, Integer pageSize, String productCode,
String productName, String customerName, String shopName,
Integer status, LocalDate startDate, LocalDate endDate) {

Page<AnalysisData> pageParam = new Page<>(page, pageSize);
IPage<AnalysisData> result = analysisDataMapper.selectPageWithConditions(pageParam,
productCode, productName, customerName, shopName, status, startDate, endDate);

PageResponse.Pagination pagination = new PageResponse.Pagination(
result.getTotal(), page, pageSize);

return new PageResponse<>(result.getRecords(), pagination);
}

@Override
public AnalysisData getById(Long id) {
return analysisDataMapper.selectById(id);
}

@Override
@Transactional
public void updateById(Long id, AnalysisData analysisData) {
AnalysisData existing = analysisDataMapper.selectById(id);
if (existing == null) {
throw new RuntimeException("分析数据不存在");
}

analysisData.setId(id);
analysisDataMapper.updateById(analysisData);
}

@Override
@Transactional
public void deleteById(Long id) {
AnalysisData existing = analysisDataMapper.selectById(id);
if (existing == null) {
throw new RuntimeException("分析数据不存在");
}

analysisDataMapper.deleteById(id);
}

@Override
@Transactional
public void deleteBatchByIds(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
throw new RuntimeException("请选择要删除的数据");
}

analysisDataMapper.deleteBatchIds(ids);
}

@Override
public void exportExcel(HttpServletResponse response, String productCode, String productName,
String customerName, String shopName, Integer status,
LocalDate startDate, LocalDate endDate, Integer page, Integer pageSize) {

// 如果page和pageSize为null,则导出全部数据
if (page == null || pageSize == null) {
List<AnalysisData> dataList = analysisDataMapper.selectAllWithConditions(
productCode, productName, customerName, shopName, status, startDate, endDate);

if (dataList.isEmpty()) {
throw new RuntimeException("暂无数据可导出");
}

// 按分类分组
Map<String, List<AnalysisData>> groupedData = dataList.stream()
.collect(Collectors.groupingBy(data ->
StringUtils.hasText(data.getCategory()) ? data.getCategory() : "未分类"));

excelUtil.exportAnalysisData(response, groupedData);
} else {
// 分页导出
Page<AnalysisData> pageParam = new Page<>(page, pageSize);

// 构建查询条件
QueryWrapper<AnalysisData> queryWrapper = new QueryWrapper<>();
if (StringUtils.hasText(productCode)) {
queryWrapper.like("product_code", productCode);
}
if (StringUtils.hasText(productName)) {
queryWrapper.like("product_name", productName);
}
if (StringUtils.hasText(customerName)) {
queryWrapper.like("customer_name", customerName);
}
if (StringUtils.hasText(shopName)) {
queryWrapper.like("shop_name", shopName);
}
if (status != null) {
queryWrapper.eq("status", status);
}
if (startDate != null) {
queryWrapper.ge("date", startDate);
}
if (endDate != null) {
queryWrapper.le("date", endDate);
}
queryWrapper.orderByDesc("id");

// 执行分页查询
IPage<AnalysisData> pageResult = analysisDataMapper.selectPage(pageParam, queryWrapper);

if (pageResult.getRecords().isEmpty()) {
throw new RuntimeException("暂无数据可导出");
}

// 按分类分组
Map<String, List<AnalysisData>> groupedData = pageResult.getRecords().stream()
.collect(Collectors.groupingBy(data ->
StringUtils.hasText(data.getCategory()) ? data.getCategory() : "未分类"));

excelUtil.exportAnalysisData(response, groupedData);
}
}

@Override
public Map<String, Object> importExcel(String filePath, Long categoryId) {
// 验证分类是否存在
Category category = categoryMapper.selectById(categoryId);
if (category == null) {
throw new RuntimeException("分类不存在");
}

// 读取Excel文件
List<Map<String, Object>> excelData = excelUtil.readExcel(filePath);
if (excelData.isEmpty()) {
throw new RuntimeException("Excel文件为空或格式不正确");
}

return processExcelData(excelData, category, filePath);
}

@Override
public Map<String, Object> importBatchExcel(List<String> filePaths, Long categoryId) {
// 验证分类是否存在
Category category = categoryMapper.selectById(categoryId);
if (category == null) {
throw new RuntimeException("分类不存在");
}

List<AnalysisData> allProcessedData = new ArrayList<>();
List<String> allErrors = new ArrayList<>();
int totalRows = 0;

for (String filePath : filePaths) {
// 1. 读取 Excel 数据(第一行为表头)
/* List<ShopCustomerExcelDTO> excelDTOList = EasyExcel.read(file.getInputStream())
.head(ShopCustomerExcelDTO.class).headRowNumber(1).sheet().doReadSync();*/
try {
List<Map<String, Object>> excelData = excelUtil.readExcel(filePath);
if (excelData.isEmpty()) {
allErrors.add("文件为空或格式不正确: " + filePath);
continue;
}
//删除里面都是null或者""的map
excelData.removeIf(map ->
map.values().stream()
.allMatch(value -> value == null || "".equals(value))
);

totalRows += excelData.size();
Map<String, Object> result = processExcelData(excelData, category, filePath);

@SuppressWarnings("unchecked")
List<AnalysisData> processedData = (List<AnalysisData>) result.get("data");
@SuppressWarnings("unchecked")
List<String> errors = (List<String>) result.get("errorDetails");

allProcessedData.addAll(processedData);
allErrors.addAll(errors);

} catch (Exception e) {
allErrors.add("处理文件失败: " + filePath + " - " + e.getMessage());
}
}

Map<String, Object> result = new HashMap<>();
result.put("total", totalRows);
result.put("processed", allProcessedData.size());
result.put("errors", allErrors.size());
result.put("data", allProcessedData);
result.put("errorDetails", allErrors.size() > 0 ? allErrors.subList(0, Math.min(20, allErrors.size())) : new ArrayList<>());
result.put("filesProcessed", filePaths.size());

return result;
}

@Override
public Map<String, Object> importFbaExcel(String filePath, String shopName) {
// 验证店铺是否存在
ShopCustomer shopCustomer = shopCustomerMapper.selectByShopName(shopName);
if (shopCustomer == null) {
throw new RuntimeException("店铺不存在");
}

// 读取Excel文件
List<Map<String, Object>> excelData = excelUtil.readFbaExcel(filePath);
if (excelData.isEmpty()) {
throw new RuntimeException("Excel文件为空或格式不正确");
}

return processFbaData(excelData, shopCustomer, filePath);
}

@Override
public Map<String, Object> importBatchFbaExcel(List<String> filePaths, String shopName) {
// 验证店铺是否存在
ShopCustomer shopCustomer = shopCustomerMapper.selectByShopName(shopName);
if (shopCustomer == null) {
throw new RuntimeException("店铺不存在");
}

List<AnalysisData> allProcessedData = new ArrayList<>();
List<String> allErrors = new ArrayList<>();
int totalRows = 0;

for (String filePath : filePaths) {
try {
List<Map<String, Object>> excelData = excelUtil.readFbaExcel(filePath);
if (excelData.isEmpty()) {
allErrors.add("文件为空或格式不正确: " + filePath);
continue;
}

totalRows += excelData.size();
Map<String, Object> result = processFbaData(excelData, shopCustomer, filePath);

@SuppressWarnings("unchecked")
List<AnalysisData> processedData = (List<AnalysisData>) result.get("data");
@SuppressWarnings("unchecked")
List<String> errors = (List<String>) result.get("errorDetails");

allProcessedData.addAll(processedData);
allErrors.addAll(errors);

} catch (Exception e) {
allErrors.add("处理文件失败: " + filePath + " - " + e.getMessage());
}
}

Map<String, Object> result = new HashMap<>();
result.put("total", totalRows);
result.put("processed", allProcessedData.size());
result.put("errors", allErrors.size());
result.put("data", allProcessedData);
result.put("errorDetails", allErrors.size() > 0 ? allErrors.subList(0, Math.min(20, allErrors.size())) : new ArrayList<>());
result.put("filesProcessed", filePaths.size());

return result;
}

@Override
@Transactional
public Map<String, Object> saveImportedData(List<AnalysisData> dataList) {
if (dataList == null || dataList.isEmpty()) {
throw new RuntimeException("没有可保存的数据");
}

int insertedCount = 0;
int updatedCount = 0;
List<String> errors = new ArrayList<>();

for (AnalysisData data : dataList) {
try {
// 检查是否存在相同的记录
AnalysisData existing = analysisDataMapper.selectByUniqueCondition(
data.getDate(), data.getShopName(), data.getProductCode(),
data.getDeliveryType(), data.getOrderNumber(), data.getRowNumber());

if (existing != null) {
// 更新现有记录
data.setId(existing.getId());
analysisDataMapper.updateById(data);
updatedCount++;
} else {
// 插入新记录
analysisDataMapper.insert(data);
insertedCount++;
}
} catch (Exception e) {
errors.add("保存数据失败: " + e.getMessage());
}
}

Map<String, Object> result = new HashMap<>();
result.put("total", dataList.size());
result.put("saved", insertedCount + updatedCount);
result.put("inserted", insertedCount);
result.put("updated", updatedCount);
result.put("failed", errors.size());
result.put("errors", errors.size() > 0 ? errors.subList(0, Math.min(10, errors.size())) : new ArrayList<>());

return result;
}

/**
* 处理Excel数据
*/
private Map<String, Object> processExcelData(List<Map<String, Object>> excelData,
Category category, String fileName) {
List<AnalysisData> processedData = new ArrayList<>();
List<String> errors = new ArrayList<>();
int filteredCount = 0;

for (int i = 0; i < excelData.size(); i++) {
Map<String, Object> row = excelData.get(i);
int rowIndex = i + 2; // Excel行号

try {
// 检查是否需要过滤
if (shouldFilterData(row)) {
filteredCount++;
continue;
}

// 验证必填字段
if (!validateRequiredFields(row, rowIndex, fileName, errors)) {
continue;
}

// 处理数据
AnalysisData analysisData = processRowData(row, category, rowIndex, fileName);
if (analysisData != null) {
processedData.add(analysisData);
}

} catch (Exception e) {
errors.add(String.format("文件[%s]第(%d)行: 数据处理失败 - %s", fileName, rowIndex, e.getMessage()));
}
}

Map<String, Object> result = new HashMap<>();
result.put("total", excelData.size());
result.put("processed", processedData.size());
result.put("filtered", filteredCount);
result.put("errors", errors.size());
result.put("data", processedData);
result.put("errorDetails", errors.size() > 0 ? errors.subList(0, Math.min(10, errors.size())) : new ArrayList<>());

return result;
}

/**
* 处理FBA数据
*/
private Map<String, Object> processFbaData(List<Map<String, Object>> excelData,
ShopCustomer shopCustomer, String fileName) {
List<AnalysisData> processedData = new ArrayList<>();
List<String> errors = new ArrayList<>();

for (int i = 0; i < excelData.size(); i++) {
Map<String, Object> row = excelData.get(i);
int rowIndex = i + 2; // Excel行号

try {
// 验证必填字段
if (!validateFbaRequiredFields(row, rowIndex, fileName, errors)) {
continue;
}

// 处理FBA数据
AnalysisData analysisData = processFbaRowData(row, shopCustomer, rowIndex, fileName);
if (analysisData != null) {
processedData.add(analysisData);
}

} catch (Exception e) {
errors.add(String.format("文件[%s]第(%d)行: 数据处理失败 - %s", fileName, rowIndex, e.getMessage()));
}
}

Map<String, Object> result = new HashMap<>();
result.put("total", excelData.size());
result.put("processed", processedData.size());
result.put("filtered", 0); // FBA数据不需要过滤
result.put("errors", errors.size());
result.put("data", processedData);
result.put("errorDetails", errors.size() > 0 ? errors.subList(0, Math.min(10, errors.size())) : new ArrayList<>());

return result;
}

/**
* 检查是否需要过滤数据
*/
private boolean shouldFilterData(Map<String, Object> row) {
String destination = String.valueOf(row.getOrDefault("目的地", ""));
String deliveryType = String.valueOf(row.getOrDefault("出库类型", ""));
String remarks = String.valueOf(row.getOrDefault("备注", ""));

// 过滤条件
return (destination.equals("SPD") && deliveryType.equals("批发出库") && remarks.trim().isEmpty()) ||
deliveryType.equals("库内换码") ||
deliveryType.equals("返品交互") ||
destination.equals("Amazon") ||
(destination.equals("開元") && deliveryType.equals("批发出库") && remarks.trim().isEmpty());
}

/**
* 验证必填字段
*/
private boolean validateRequiredFields(Map<String, Object> row, int rowIndex,
String fileName, List<String> errors) {
if (!row.containsKey("店铺名称") || StringUtils.isEmpty(row.get("店铺名称"))) {
errors.add(String.format("文件[%s]第(%d)行: 店铺名称不能为空", fileName, rowIndex));
return false;
}
if (!row.containsKey("商品编号") || StringUtils.isEmpty(row.get("商品编号"))) {
errors.add(String.format("文件[%s]第(%d)行: 商品编号不能为空", fileName, rowIndex));
return false;
}
if (!row.containsKey("商品名称") || StringUtils.isEmpty(row.get("商品名称"))) {
errors.add(String.format("文件[%s]第(%d)行: 商品名称不能为空", fileName, rowIndex));
return false;
}
if (!row.containsKey("数量") || StringUtils.isEmpty(row.get("数量"))) {
errors.add(String.format("文件[%s]第(%d)行: 数量不能为空", fileName, rowIndex));
return false;
}
if (!row.containsKey("单价") || StringUtils.isEmpty(row.get("单价"))) {
errors.add(String.format("文件[%s]第(%d)行: 单价不能为空", fileName, rowIndex));
return false;
}
return true;
}

/**
* 验证FBA必填字段
*/
private boolean validateFbaRequiredFields(Map<String, Object> row, int rowIndex,
String fileName, List<String> errors) {
if (!row.containsKey("出荷日") || StringUtils.isEmpty(row.get("出荷日"))) {
errors.add(String.format("文件[%s]第(%d)行: 出荷日不能为空", fileName, rowIndex));
return false;
}
if (!row.containsKey("出品者SKU") || StringUtils.isEmpty(row.get("出品者SKU"))) {
errors.add(String.format("文件[%s]第(%d)行: 出品者SKU不能为空", fileName, rowIndex));
return false;
}
if (!row.containsKey("数量") || StringUtils.isEmpty(row.get("数量"))) {
errors.add(String.format("文件[%s]第(%d)行: 数量不能为空", fileName, rowIndex));
return false;
}
return true;
}

/**
* 处理行数据
*/
private AnalysisData processRowData(Map<String, Object> row, Category category,
int rowIndex, String fileName) {
try {
// 格式化日期
LocalDate formattedDate = excelUtil.processExcelDate(row.get("出库日期"));
if (formattedDate == null) {
throw new RuntimeException("日期格式不正确");
}

String shopName = String.valueOf(row.get("店铺名称")).trim();
String productCode = String.valueOf(row.get("商品编号")).trim();
String productName = String.valueOf(row.get("商品名称")).trim();

// 匹配店铺客户关系
ShopCustomer shopCustomer = shopCustomerMapper.selectByShopName(shopName);
String customerName = shopCustomer != null ? shopCustomer.getCustomerName() :
String.valueOf(row.getOrDefault("客户名称", ""));

// 匹配基准数据分类规格
BaseData baseData = baseDataMapper.selectByProductCodeAndCategoryId(productCode, category.getId());
String categoryName = baseData != null ? category.getName() : category.getName();
String categorySpecs = baseData != null ? baseData.getCategorySpecs() : "";

// 计算合计金额
BigDecimal quantity = new BigDecimal(String.valueOf(row.get("数量")));
BigDecimal unitPrice = new BigDecimal(String.valueOf(row.get("单价")));
BigDecimal shipping = new BigDecimal(String.valueOf(row.getOrDefault("送料", "0")));
BigDecimal cod = new BigDecimal(String.valueOf(row.getOrDefault("代引", "0")));
BigDecimal totalAmount = quantity.multiply(unitPrice).add(shipping).add(cod);

// 确定状态
int status = 1; // 默认正常
if (StringUtils.isEmpty(customerName)) {
status = 2; // 客户名称未匹配
} else if (baseData == null) {
status = 3; // 分类规格未匹配
}

AnalysisData analysisData = new AnalysisData();
analysisData.setDate(formattedDate);
analysisData.setShopName(shopName);
analysisData.setProductCode(productCode);
analysisData.setProductName(productName);
analysisData.setCustomerName(customerName);
analysisData.setCategory(categoryName);
analysisData.setCategorySpecs(categorySpecs);
analysisData.setQuantity(quantity.intValue());
analysisData.setTotalAmount(totalAmount);
analysisData.setSource("物流出库数据");
analysisData.setStatus(status);
analysisData.setDeliveryType(String.valueOf(row.getOrDefault("出库类型", "")));
analysisData.setDestination(String.valueOf(row.getOrDefault("目的地", "")));
analysisData.setRemarks(String.valueOf(row.getOrDefault("备注", "")));
analysisData.setOrderNumber(String.valueOf(row.getOrDefault("注文番号", "")));
analysisData.setRowNumber(rowIndex);
analysisData.setBrand(baseData != null ? baseData.getBrand() : "");

return analysisData;

} catch (Exception e) {
throw new RuntimeException("数据处理失败: " + e.getMessage());
}
}

/**
* 处理FBA行数据
*/
private AnalysisData processFbaRowData(Map<String, Object> row, ShopCustomer shopCustomer,
int rowIndex, String fileName) {
try {
// 处理日期
LocalDate formattedDate = excelUtil.processFbaDate(row.get("出荷日"));
if (formattedDate == null) {
throw new RuntimeException("日期格式不正确");
}

// 处理SKU
String rawSku = String.valueOf(row.get("出品者SKU")).trim();
String processedSku = excelUtil.processFbaSku(rawSku);
if (StringUtils.isEmpty(processedSku)) {
throw new RuntimeException("SKU格式不正确");
}

// 匹配商品信息
BaseDataVO baseData = baseDataMapper.selectByProductCode(processedSku);
String productName = baseData != null ? baseData.getProductName() : "";
String category = baseData != null ? baseData.getCategoryName() : "";//这里存名字
String categorySpecs = baseData != null ? baseData.getCategorySpecs() : "";
String brand = baseData != null ? baseData.getBrand() : "";

// 获取客户名称
String customerName = shopCustomer.getCustomerName();

// 计算合计金额
BigDecimal quantity = new BigDecimal(String.valueOf(row.get("数量")));
BigDecimal unitPrice = new BigDecimal(String.valueOf(row.getOrDefault("商品金額(商品1点ごと)", "0")));
BigDecimal shipping = new BigDecimal(String.valueOf(row.getOrDefault("配送料", "0")));
BigDecimal giftWrap = new BigDecimal(String.valueOf(row.getOrDefault("ギフト包装手数料", "0")));
BigDecimal totalAmount = quantity.multiply(unitPrice).add(shipping).add(giftWrap);

// 确定状态
int status = 1; // 默认正常
if (StringUtils.isEmpty(customerName)) {
status = 2; // 客户名称未匹配
} else if (baseData == null) {
status = 3; // 分类规格未匹配
}

// FBA 只保存正常的数据
if (status != 1) {
return null;
}

AnalysisData analysisData = new AnalysisData();
analysisData.setDate(formattedDate);
analysisData.setShopName(shopCustomer.getShopName());
analysisData.setProductCode(processedSku);
analysisData.setProductName(productName);
analysisData.setCustomerName(customerName);
analysisData.setCategory(category);
analysisData.setCategorySpecs(categorySpecs);
analysisData.setQuantity(quantity.intValue());
analysisData.setTotalAmount(totalAmount);
analysisData.setSource("FBA出库数据");
analysisData.setStatus(status);
analysisData.setDeliveryType("FBA");
analysisData.setDestination("");
analysisData.setRemarks("");
analysisData.setOrderNumber(String.valueOf(row.getOrDefault("Amazon注文番号", "")));
analysisData.setRowNumber(rowIndex);
analysisData.setBrand(brand);

return analysisData;

} catch (Exception e) {
throw new RuntimeException("数据处理失败: " + e.getMessage());
}
}
}

+ 328
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/BaseDataServiceImpl.java View File

@@ -0,0 +1,328 @@
package com.ruoyi.tjfx.service.impl;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.*;
import com.ruoyi.tjfx.mapper.BaseDataMapper;
import com.ruoyi.tjfx.mapper.CategoryMapper;
import com.ruoyi.tjfx.service.BaseDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;

@Service
public class BaseDataServiceImpl implements BaseDataService {
@Autowired
private BaseDataMapper baseDataMapper;
@Autowired
private CategoryMapper categoryMapper;

@Override
public List<BaseData> listAll() {
return baseDataMapper.selectList(null);
}

@Override
public BaseData getById(Long id) {
return baseDataMapper.selectById(id);
}

@Override
public void add(BaseData baseData) {
baseDataMapper.insert(baseData);
}

@Override
public void update(Long id, BaseData baseData) {
baseData.setId(id);
baseDataMapper.updateById(baseData);
}

@Override
public void delete(Long id) {
baseDataMapper.deleteById(id);
}

@Override
public void deleteBatch(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return;
}
baseDataMapper.deleteBatchIds(ids);
}
@Override
public PageResponse<BaseDataVO> getPage(Integer page, Integer pageSize, String productCode, String productName, Long categoryId) {
Page<BaseDataVO> pageParam = new Page<>(page, pageSize);

// 执行 join 查询
IPage<BaseDataVO> pageResult = baseDataMapper.selectPageWithJoin(
pageParam, productCode, productName, categoryId
);

// 构建 PageResponse(关键在这)
PageResponse.Pagination pagination = new PageResponse.Pagination(
pageResult.getTotal(),
Math.toIntExact(pageResult.getCurrent()), // 当前页
Math.toIntExact(pageResult.getSize()) // 页大小
);

return new PageResponse<>(pageResult.getRecords(), pagination);
}
@Override
@Transactional
public void importExcel(MultipartFile file, Long categoryId) {
try {
Category category = categoryMapper.selectById(categoryId);
if (category == null) {
throw new RuntimeException("分类不存在");
}

// 初始化数据容器
List<Map<Integer, String>> dataList = new ArrayList<>();
List<String> headers = new ArrayList<>();

// 使用 EasyExcel 读取表头 + 数据
try (InputStream inputStream = file.getInputStream()) {
EasyExcel.read(inputStream)
.headRowNumber(0) // 表头行为第0行
.sheet()
.registerReadListener(new AnalysisEventListener<Map<Integer, String>>() {
private boolean isHeaderParsed = false;

@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
if (!isHeaderParsed) {
for (Map.Entry<Integer, String> entry : data.entrySet()) {
headers.add(entry.getValue());
}
isHeaderParsed = true;
} else {
dataList.add(data);
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// nothing
}
}).doRead();
}

if (dataList.isEmpty()) {
throw new RuntimeException("导入数据为空");
}

List<BaseData> insertList = new ArrayList<>();
List<String> errors = new ArrayList<>();
Set<String> duplicateCheck = new HashSet<>();

// 固定列索引
int codeCol = 0;
int nameCol = 1;
int brandCol = 2;
int categoryNameCol = 3;
int extendStartCol = 4; // E列开始(0-based index)

ObjectMapper objectMapper = new ObjectMapper();

for (int i = 0; i < dataList.size(); i++) {
Map<Integer, String> rowMap = dataList.get(i);
int rowNum = i + 2;

String productCode = rowMap.getOrDefault(codeCol, "").trim();
String productName = rowMap.getOrDefault(nameCol, "").trim();
String brand = rowMap.getOrDefault(brandCol, "").trim();
String categoryNameVal = rowMap.getOrDefault(categoryNameCol, "").trim();

if (productCode.isEmpty() || productName.isEmpty()) {
errors.add("第" + rowNum + "行:商品编号或商品名称不能为空");
continue;
}

if (!duplicateCheck.add(productCode)) {
errors.add("第" + rowNum + "行:商品编号\"" + productCode + "\"在Excel中重复");
continue;
}

// 处理分类规格字段
Map<String, String> categorySpecsMap = new LinkedHashMap<>();
for (int col = extendStartCol; col < headers.size(); col++) {
String key = headers.get(col);
String value = rowMap.getOrDefault(col, "").trim();
categorySpecsMap.put(key, value);
}

BaseData data = new BaseData();
data.setProductCode(productCode);
data.setProductName(productName);
data.setBrand(brand);
data.setCategoryId(categoryId);
data.setCategoryName(categoryNameVal);
data.setCategorySpecs(objectMapper.writeValueAsString(categorySpecsMap));
data.setCreatedAt(LocalDateTime.now());
data.setUpdatedAt(LocalDateTime.now());

insertList.add(data);
}

// 去重:数据库中已存在的商品编号
if (!insertList.isEmpty()) {
List<String> existCodes = baseDataMapper.selectExistingProductCodes(
insertList.stream().map(BaseData::getProductCode).collect(Collectors.toList())
);
Set<String> existSet = new HashSet<>(existCodes);
insertList = insertList.stream()
.filter(item -> {
if (existSet.contains(item.getProductCode())) {
errors.add("商品编号\"" + item.getProductCode() + "\"已存在数据库中");
return false;
}
return true;
}).collect(Collectors.toList());
}

// 批量插入
if (!insertList.isEmpty()) {
baseDataMapper.insertBatch(insertList);
}

if (!errors.isEmpty()) {
throw new RuntimeException("导入完成,但存在以下错误:\n" + String.join("\n", errors));
}

} catch (Exception e) {
throw new RuntimeException("Excel导入失败: " + e.getMessage(), e);
}
}

@Override
public void exportExcel(Long categoryId, HttpServletResponse response) {
try (Workbook workbook = new XSSFWorkbook()) {
// 查询分类
Category category = categoryMapper.selectById(categoryId);
if (category == null) {
throw new RuntimeException("分类不存在");
}

// 查询数据
List<BaseDataVO> baseDataList = baseDataMapper.findByCategoryId(categoryId);
if (baseDataList.isEmpty()) {
throw new RuntimeException("该分类下暂无数据");
}

// 提取所有扩展字段 key(即分类规格里的字段)
Set<String> specKeys = new LinkedHashSet<>();
for (BaseDataVO baseData : baseDataList) {
String specJson = baseData.getCategorySpecs();
if (specJson != null && !specJson.isEmpty()) {
try {
Map<String, String> specMap = new ObjectMapper().readValue(specJson, new TypeReference<Map<String, String>>() {});
specKeys.addAll(specMap.keySet());
} catch (Exception e) {
throw new RuntimeException("分类规格格式有误:" + specJson);
}
}
}

// 创建 Sheet
Sheet sheet = workbook.createSheet("基准数据");

// 设置表头:固定字段 + 动态分类规格字段(不包含时间)
String[] fixedHeaders = {"商品编号", "商品名称", "品牌", "所属分类"};
List<String> allHeaders = new ArrayList<>(Arrays.asList(fixedHeaders));
allHeaders.addAll(specKeys);

Row headerRow = sheet.createRow(0);
for (int i = 0; i < allHeaders.size(); i++) {
headerRow.createCell(i).setCellValue(allHeaders.get(i));
}

// 写入数据
for (int i = 0; i < baseDataList.size(); i++) {
BaseDataVO baseData = baseDataList.get(i);
Row row = sheet.createRow(i + 1);
int col = 0;

row.createCell(col++).setCellValue(Optional.ofNullable(baseData.getProductCode()).orElse(""));
row.createCell(col++).setCellValue(Optional.ofNullable(baseData.getProductName()).orElse(""));
row.createCell(col++).setCellValue(Optional.ofNullable(baseData.getBrand()).orElse(""));
row.createCell(col++).setCellValue(category.getName());

// 解析分类规格
Map<String, String> specMap = new HashMap<>();
if (baseData.getCategorySpecs() != null && !baseData.getCategorySpecs().isEmpty()) {
try {
specMap = new ObjectMapper().readValue(baseData.getCategorySpecs(), new TypeReference<Map<String, String>>() {});
} catch (Exception e) {
// 忽略解析失败的行
}
}

// 写入扩展字段
for (String key : specKeys) {
row.createCell(col++).setCellValue(specMap.getOrDefault(key, ""));
}
}

// 自动列宽
for (int i = 0; i < allHeaders.size(); i++) {
sheet.autoSizeColumn(i);
}

// 设置响应头
String fileName = URLEncoder.encode("基准数据_" + category.getName() + "_" + System.currentTimeMillis() + ".xlsx", "UTF-8")
.replaceAll("\\+", "%20");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName);

// 输出到响应流
workbook.write(response.getOutputStream());
response.flushBuffer();
} catch (Exception e) {
try {
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"success\":false,\"message\":\"" + e.getMessage() + "\"}");
} catch (IOException ex) {
throw new RuntimeException("写入错误信息失败: " + ex.getMessage(), ex);
}
}
}

private List<List<String>> buildHead(List<Map<String, Object>> data) {
if (data == null || data.isEmpty()) {
return new ArrayList<>();
}

List<List<String>> headList = new ArrayList<>();
for (String key : data.get(0).keySet()) {
headList.add(Collections.singletonList(key));
}
return headList;
}
}

+ 80
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/CategoryServiceImpl.java View File

@@ -0,0 +1,80 @@
package com.ruoyi.tjfx.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.mapper.CategoryMapper;
import com.ruoyi.tjfx.service.CategoryService;
import com.ruoyi.tjfx.entity.Category;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Override
public List<Category> getAllSimple() {
return categoryMapper.selectList(null);
}

@Override
public List<Category> listAll() {
return categoryMapper.selectList(null);
}

@Override
public Category getById(Long id) {
return categoryMapper.selectById(id);
}

@Override
public void add(Category category) {
categoryMapper.insert(category);
}

@Override
public void update(Long id, Category category) {
category.setId(id);
categoryMapper.updateById(category);
}

@Override
public void delete(Long id) {
categoryMapper.deleteById(id);
}

@Override
public void addBatch(List<Category> categorys) {
if (categorys != null && !categorys.isEmpty()) {
for (Category category : categorys) {
categoryMapper.insert(category);
}
}
}

@Override
public PageResponse<Category> getPage(Integer page, Integer pageSize, String name) {
// 创建分页对象
Page<Category> pageParam = new Page<>(page, pageSize);

// 构建查询条件
QueryWrapper<Category> queryWrapper = new QueryWrapper<>();
if (StringUtils.hasText(name)) {
queryWrapper.like("name", name);
}
queryWrapper.orderByDesc("id");

// 执行分页查询
IPage<Category> pageResult = categoryMapper.selectPage(pageParam, queryWrapper);

// 构建分页响应
PageResponse.Pagination pagination = new PageResponse.Pagination(
pageResult.getTotal(), page, pageSize);

return new PageResponse<>(pageResult.getRecords(), pagination);
}
}

+ 274
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/ReportServiceImpl.java View File

@@ -0,0 +1,274 @@
package com.ruoyi.tjfx.service.impl;

import com.alibaba.fastjson.JSON;
import com.ruoyi.tjfx.entity.*;
import com.ruoyi.tjfx.mapper.ReportMapper;
import com.ruoyi.tjfx.service.ReportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class ReportServiceImpl implements ReportService {

@Autowired
private ReportMapper reportMapper;

/**
* 整体销售分析
* @param startDate
* @param endDate
* @param category
* @param categorySpecs
* @param customer
* @param shop
* @param brand
* @param productCode
* @return
*/
@Override
public OverallAnalysisVO overallAnalysis(String startDate, String endDate, String category, String categorySpecs, String customer, String shop, String brand, String productCode) {
Map<String, Object> params = new HashMap<>();
params.put("startDate", startDate);
params.put("endDate", endDate);
params.put("category", category);
params.put("categorySpecs", categorySpecs);
params.put("customer", customer);
params.put("shop", shop);
params.put("brand", brand);
params.put("productCode", productCode);

// 基础统计
BasicStatsVO basicStats = reportMapper.selectBasicStats(params);

// 去年同期
String lastYearStart = LocalDate.parse(startDate).minusYears(1).toString();
String lastYearEnd = LocalDate.parse(endDate).minusYears(1).toString();
Map<String, Object> lastYearParams = new HashMap<>(params);
lastYearParams.put("startDate", lastYearStart);
lastYearParams.put("endDate", lastYearEnd);
BasicStatsVO lastYearStats = reportMapper.selectLastYearStats(lastYearParams);

// 上周期
long days = LocalDate.parse(endDate).toEpochDay() - LocalDate.parse(startDate).toEpochDay();
String prevStart = LocalDate.parse(startDate).minusDays(days + 1).toString();
String prevEnd = LocalDate.parse(startDate).minusDays(1).toString();
Map<String, Object> prevParams = new HashMap<>(params);
prevParams.put("startDate", prevStart);
prevParams.put("endDate", prevEnd);
BasicStatsVO prevStats = reportMapper.selectPrevStats(prevParams);

// 趋势
List<TrendDataVO> trendData = reportMapper.selectTrendData(params);

// TOP20
List<TopProductVO> topProducts = reportMapper.selectTopProducts(params);

// 品牌
List<BrandDataVO> brandData = reportMapper.selectBrandData(params);

// 计算同比、环比
OverallAnalysisVO vo = new OverallAnalysisVO();
vo.setBasicStats(basicStats);
vo.setGrowthRates(
new GrowthRatesVO((basicStats==null||lastYearStats==null)?"0":calcRate(basicStats.getTotalAmount(), lastYearStats.getTotalAmount()),
(basicStats==null||lastYearStats==null)?"0":calcRate(basicStats.getTotalQuantity(), lastYearStats.getTotalQuantity()),
(basicStats==null||prevStats==null)?"0":calcRate(basicStats.getTotalAmount(), prevStats.getTotalAmount()),
(basicStats==null||prevStats==null)?"0":calcRate(basicStats.getTotalQuantity(), prevStats.getTotalQuantity()))
);
vo.setTrendData(trendData);
vo.setTopProducts(topProducts);
vo.setBrandData(brandData);
return vo;
}

private String calcRate(BigDecimal now, BigDecimal prev) {
if (prev == null || prev.compareTo(BigDecimal.ZERO) == 0) return "0";
return now.subtract(prev).divide(prev, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString();
}

@Override
public ProductAnalysisVO productAnalysis(String startDate, String endDate, String productCode) {
Map<String, Object> params = new HashMap<>();
params.put("startDate", startDate);
params.put("endDate", endDate);
params.put("productCode", productCode);

List<RankingDataVO> rankingData = reportMapper.selectProductRanking(params);
List<TrendDataVO> trendData = reportMapper.selectProductTrend(params);

ProductAnalysisVO vo = new ProductAnalysisVO();
vo.setRankingData(rankingData);
vo.setTrendData(trendData);
return vo;
}

/**
* 店铺销售分析
* @param startDate
* @param endDate
* @param shop
* @return
*/
@Override
public ShopAnalysisVO shopAnalysis(String startDate, String endDate, String shop) {
Map<String, Object> params = new HashMap<>();
params.put("startDate", startDate);
params.put("endDate", endDate);
params.put("shop", shop);

BasicStatsVO basicStats = reportMapper.selectShopBasicStats(params);
List<ShopAmountVO> shopAmountData = reportMapper.selectShopAmountData(params);
List<ShopQuantityVO> shopQuantityData = reportMapper.selectShopQuantityData(params);
List<CategoryTrendVO> categoryTrendData = reportMapper.selectCategoryTrendData(params);

// 计算占比
BigDecimal totalAmount = shopAmountData.stream()
.map(vo -> BigDecimal.valueOf(vo.getAmount()))
.reduce(BigDecimal.ZERO, BigDecimal::add);

BigDecimal totalQuantity = shopQuantityData.stream()
.map(vo -> BigDecimal.valueOf(vo.getQuantity()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
shopAmountData.forEach(item -> {
item.setPercentage(totalAmount.compareTo(BigDecimal.ZERO) == 0 ? "0" :
new BigDecimal(item.getAmount()).divide(totalAmount, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString());
});
shopQuantityData.forEach(item -> {
item.setPercentage(totalQuantity.compareTo(BigDecimal.ZERO) == 0 ? "0" :
new BigDecimal(item.getQuantity()).divide(totalQuantity, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString());
});

ShopAnalysisVO vo = new ShopAnalysisVO();
vo.setBasicStats(basicStats);
vo.setShopAmountData(shopAmountData);
vo.setShopQuantityData(shopQuantityData);
vo.setCategoryTrendData(categoryTrendData);
return vo;
}

/**
* 品类销售分析
* @param startDate
* @param endDate
* @param category
* @return
*/
@Override
public CategoryAnalysisVO categoryAnalysis(String startDate, String endDate, String category) {
Map<String, Object> params = new HashMap<>();
params.put("startDate", startDate);
params.put("endDate", endDate);
params.put("category", category);

List<Map<String, Object>> specsRawData = reportMapper.selectSpecsRawData(params);

// 解析规格数据
Map<String, Map<String, DimensionStatsVO>> dimensionStats = new HashMap<>();
Set<String> availableDimensions = new HashSet<>();
for (Map<String, Object> item : specsRawData) {
String specsStr = (String) item.get("category_specs");
BigDecimal quantity = (BigDecimal) item.get("quantity");
BigDecimal amount = (BigDecimal) item.get("amount");
try {
Map<String, Object> specsObj = JSON.parseObject(specsStr, Map.class);
for (Map.Entry<String, Object> entry : specsObj.entrySet()) {
String dim = entry.getKey();
String val = entry.getValue() == null ? "" : entry.getValue().toString();
if (!val.trim().isEmpty()) {
availableDimensions.add(dim);
dimensionStats.putIfAbsent(dim, new HashMap<>());
Map<String, DimensionStatsVO> dimMap = dimensionStats.get(dim);
dimMap.putIfAbsent(val, new DimensionStatsVO(val, 0d, 0d, "0",0d));
DimensionStatsVO stats = dimMap.get(val);
stats.setQuantity(new BigDecimal(stats.getQuantity()).add(quantity == null ? BigDecimal.ZERO : quantity).doubleValue());
stats.setAmount(new BigDecimal(stats.getAmount()).add(amount == null ? BigDecimal.ZERO : amount).doubleValue());
}
}
} catch (Exception e) {
// 非JSON格式跳过
}
}
// 计算占比
Map<String, DimensionChartVO> dimensionCharts = new HashMap<>();
for (String dim : dimensionStats.keySet()) {
Map<String, DimensionStatsVO> dimMap = dimensionStats.get(dim);
//BigDecimal totalAmount = dimMap.values().stream().map(DimensionStatsVO::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
//BigDecimal totalQuantity = dimMap.values().stream().map(DimensionStatsVO::getQuantity).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal totalAmount = dimMap.values().stream()
.map(vo -> BigDecimal.valueOf(vo.getAmount()))
.reduce(BigDecimal.ZERO, BigDecimal::add);

BigDecimal totalQuantity = dimMap.values().stream()
.map(vo -> BigDecimal.valueOf(vo.getQuantity()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
List<DimensionStatsVO> amountData = new ArrayList<>();
List<DimensionStatsVO> quantityData = new ArrayList<>();
for (DimensionStatsVO stats : dimMap.values()) {
//金额集合
stats.setPercentage(totalAmount.compareTo(BigDecimal.ZERO) == 0 ? "0" :
new BigDecimal(stats.getAmount()).divide(totalAmount, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString());
stats.setValue(stats.getAmount());
amountData.add(stats);

//销量集合
DimensionStatsVO statsQuantity = new DimensionStatsVO(
stats.getName(),
stats.getQuantity(),
stats.getAmount(),
stats.getPercentage(),
stats.getValue());
statsQuantity.setPercentage(totalQuantity.compareTo(BigDecimal.ZERO) == 0 ? "0" :
new BigDecimal(statsQuantity.getQuantity()).divide(totalQuantity, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString());
statsQuantity.setValue(statsQuantity.getQuantity());
quantityData.add(statsQuantity);
}
amountData.sort((a, b) -> b.getAmount().compareTo(a.getAmount()));
quantityData.sort((a, b) -> b.getQuantity().compareTo(a.getQuantity()));
dimensionCharts.put(dim, new DimensionChartVO(amountData, quantityData));
}
CategoryAnalysisVO vo = new CategoryAnalysisVO();
vo.setDimensionCharts(dimensionCharts);
vo.setAvailableDimensions(new ArrayList<>(availableDimensions));
vo.setCurrentCategory(category == null ? "全部分类" : category);
return vo;
}

@Override
public FilterOptionsVO filterOptions() {
FilterOptionsVO vo = new FilterOptionsVO();
vo.setCategories(reportMapper.selectCategories());
vo.setCategorySpecs(reportMapper.selectCategorySpecs());
vo.setCustomers(reportMapper.selectCustomers());
vo.setShops(reportMapper.selectShops());
vo.setBrands(reportMapper.selectBrands());
return vo;
}

@Override
public List<ProductCodeSuggestionVO> productCodeSuggestions(String keyword) {
List<Map<String, Object>> suggestions = reportMapper.selectProductCodeSuggestions("%" + keyword + "%");
return suggestions.stream().map(item -> {
String productName = (String) item.get("product_name");
String categorySpecs = (String) item.get("category_specs");
String categoryName = (String) item.get("category_name");
String displayText = productName == null ? "" : productName;
String specsText = "";
if (categorySpecs != null) {
try {
Map<String, Object> specs = JSON.parseObject(categorySpecs, Map.class);
specsText = specs.values().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.joining("+"));
} catch (Exception e) {
specsText = categorySpecs;
}
}
if (!specsText.isEmpty()) displayText += "+" + specsText;
if (categoryName != null && !categoryName.isEmpty()) displayText += "-" + categoryName;
return new ProductCodeSuggestionVO((String) item.get("product_code"), displayText);
}).collect(Collectors.toList());
}
}

+ 229
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/service/impl/ShopCustomerServiceImpl.java View File

@@ -0,0 +1,229 @@
package com.ruoyi.tjfx.service.impl;

import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.tjfx.common.PageResponse;
import com.ruoyi.tjfx.entity.ShopCustomer;
import com.ruoyi.tjfx.entity.ShopCustomerExcelDTO;
import com.ruoyi.tjfx.mapper.ShopCustomerMapper;
import com.ruoyi.tjfx.service.ShopCustomerService;
import com.ruoyi.tjfx.util.ExcelUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class ShopCustomerServiceImpl implements ShopCustomerService {
@Autowired
private ShopCustomerMapper shopCustomerMapper;

@Override
public List<ShopCustomer> listAll() {
return shopCustomerMapper.selectList(null);
}

@Override
public ShopCustomer getById(Long id) {
return shopCustomerMapper.selectById(id);
}

@Override
public void add(ShopCustomer shopCustomer) {
shopCustomerMapper.insert(shopCustomer);
}

@Override
public void update(Long id, ShopCustomer shopCustomer) {
shopCustomer.setId(id);
shopCustomerMapper.updateById(shopCustomer);
}

@Override
public void delete(Long id) {
shopCustomerMapper.deleteById(id);
}

@Override
public void deleteBatch(List<Long> ids) {
if (ids != null && !ids.isEmpty()) {
shopCustomerMapper.deleteBatchIds(ids);
}
}

@Override
public PageResponse<ShopCustomer> getPage(Integer page, Integer pageSize, String shopName, String customerName) {
// 创建分页对象
Page<ShopCustomer> pageParam = new Page<>(page, pageSize);

// 构建查询条件
QueryWrapper<ShopCustomer> queryWrapper = new QueryWrapper<>();
if (StringUtils.hasText(shopName)) {
queryWrapper.like("shop_name", shopName);
}
if (StringUtils.hasText(customerName)) {
queryWrapper.like("customer_name", customerName);
}
queryWrapper.orderByDesc("id");

// 执行分页查询
IPage<ShopCustomer> pageResult = shopCustomerMapper.selectPage(pageParam, queryWrapper);

// 构建分页响应
PageResponse.Pagination pagination = new PageResponse.Pagination(
pageResult.getTotal(), page, pageSize);

return new PageResponse<>(pageResult.getRecords(), pagination);
}

@Override
public Map<String, Object> importExcel(MultipartFile file) {
Map<String, Object> result = new HashMap<>();
List<String> errors = new ArrayList<>();
int successCount = 0;
int totalRows = 0;

try {
// 1. 读取 Excel 数据(第一行为表头)
List<ShopCustomerExcelDTO> excelDTOList = EasyExcel.read(file.getInputStream())
.head(ShopCustomerExcelDTO.class).headRowNumber(1).sheet().doReadSync();
totalRows = excelDTOList.size();
if (totalRows == 0) {
result.put("total", 0);
result.put("imported", 0);
result.put("errors", 1);
result.put("errorDetails", Collections.singletonList("Excel文件为空或格式不正确"));
return result;
}

// 2. Excel内部去重、必填校验
Set<String> duplicateCheck = new HashSet<>();
List<ShopCustomer> validList = new ArrayList<>();

for (int i = 0; i < excelDTOList.size(); i++) {
ShopCustomerExcelDTO dto = excelDTOList.get(i);
int rowIndex = i + 2; // Excel行号

String shopName = dto.getShopName() != null ? dto.getShopName().trim() : "";
String customerName = dto.getCustomerName() != null ? dto.getCustomerName().trim() : "";

if (shopName.isEmpty() || customerName.isEmpty()) {
errors.add("第" + rowIndex + "行: 店铺名称和客户名称不能为空");
continue;
}

String key = shopName + "-" + customerName;
if (!duplicateCheck.add(key)) {
errors.add("第" + rowIndex + "行: 店铺'" + shopName + "'与客户'" + customerName + "'的关联在Excel中重复");
continue;
}

ShopCustomer sc = new ShopCustomer();
sc.setShopName(shopName);
sc.setCustomerName(customerName);
validList.add(sc);
}

// 3. 数据库去重
if (!validList.isEmpty()) {
List<String> dbKeys = validList.stream()
.map(sc -> sc.getShopName() + "-" + sc.getCustomerName())
.collect(Collectors.toList());

QueryWrapper<ShopCustomer> queryWrapper = new QueryWrapper<>();
queryWrapper.select("shop_name", "customer_name");
queryWrapper.inSql("concat(shop_name,'-',customer_name)",
dbKeys.stream()
.map(k -> "'" + k.replace("'", "''") + "'")
.collect(Collectors.joining(",")));

List<ShopCustomer> existList = shopCustomerMapper.selectList(queryWrapper);
Set<String> existKeys = existList.stream()
.map(sc -> sc.getShopName() + "-" + sc.getCustomerName())
.collect(Collectors.toSet());

// 过滤掉数据库中已存在的记录
validList = validList.stream()
.filter(sc -> !existKeys.contains(sc.getShopName() + "-" + sc.getCustomerName()))
.collect(Collectors.toList());

for (String k : dbKeys) {
if (existKeys.contains(k)) {
String[] parts = k.split("-", 2);
errors.add("店铺'" + parts[0] + "'与客户'" + parts[1] + "'的关联已存在于数据库中");
}
}
}

// 4. 批量插入
for (ShopCustomer sc : validList) {
try {
shopCustomerMapper.insert(sc);
successCount++;
} catch (Exception e) {
errors.add("插入失败: 店铺'" + sc.getShopName() + "', 客户'" + sc.getCustomerName() + "' - " + e.getMessage());
}
}
} catch (Exception e) {
errors.add("导入异常: " + e.getMessage());
}

result.put("total", totalRows);
result.put("imported", successCount);
result.put("errors", errors.size());
result.put("errorDetails", errors.size() > 0 ? errors.subList(0, Math.min(10, errors.size())) : new ArrayList<>());
return result;
}


@Override
public void exportExcel(HttpServletResponse response) {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("店铺客户关联");

String[] headers = {"店铺名称", "客户名称", "创建时间", "更新时间"};
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
headerRow.createCell(i).setCellValue(headers[i]);
}

List<ShopCustomer> list = shopCustomerMapper.selectList(null);
for (int i = 0; i < list.size(); i++) {
ShopCustomer sc = list.get(i);
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(sc.getShopName() != null ? sc.getShopName() : "");
row.createCell(1).setCellValue(sc.getCustomerName() != null ? sc.getCustomerName() : "");
row.createCell(2).setCellValue(sc.getCreatedAt() != null ? sc.getCreatedAt().toString() : "");
row.createCell(3).setCellValue(sc.getUpdatedAt() != null ? sc.getUpdatedAt().toString() : "");
}

for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
}

// 设置文件名
String fileName = URLEncoder.encode("shop_customer.xlsx", "UTF-8").replaceAll("\\+", "%20");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName);

workbook.write(response.getOutputStream());
response.flushBuffer(); // 强制写出
} catch (Exception e) {
throw new RuntimeException("导出Excel失败: " + e.getMessage(), e);
}
}

}

+ 379
- 0
zs-manager/src/main/java/com/ruoyi/tjfx/util/ExcelUtil.java View File

@@ -0,0 +1,379 @@
package com.ruoyi.tjfx.util;

import com.ruoyi.tjfx.entity.AnalysisData;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.jsoup.Jsoup;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
//import javax.swing.text.Document;
import java.io.*;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.jsoup.nodes.Document;



import java.util.stream.Collectors;




/**
* Excel工具类
*/
@Slf4j
@Component
public class ExcelUtil {

/** 检测文件是否为 HTML (简单检查首行) */
private boolean isHtmlFile(File file) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String firstLine = reader.readLine();
return firstLine != null && firstLine.trim().startsWith("<");
}
}

/** 用 Jsoup 解析 HTML 表格 */
private List<Map<String, Object>> readHtmlTable(File file) throws IOException {
List<Map<String, Object>> dataList = new ArrayList<>();
Document doc = Jsoup.parse(file, "UTF-8");
Element table = doc.selectFirst("table");
if (table == null) {
throw new RuntimeException("HTML 中找不到 <table> 元素");
}

// 读取表头
Elements headerCells = table.select("tr").get(0).select("th,td");
List<String> headers = headerCells.stream()
.map(Element::text)
.collect(Collectors.toList());

// 读取每一行数据
Elements rows = table.select("tr");
for (int i = 1; i < rows.size(); i++) {
Elements cells = rows.get(i).select("td");
Map<String, Object> rowMap = new HashMap<>();
for (int j = 0; j < headers.size() && j < cells.size(); j++) {
rowMap.put(headers.get(j), cells.get(j).text());
}
dataList.add(rowMap);
}
return dataList;
}

/** 根据 Cell 类型取值的通用方法 */
private Object getCellValue2(Cell cell) {
if (cell == null) return null;
switch (cell.getCellType()) {
case STRING: return cell.getStringCellValue();
case NUMERIC: return DateUtil.isCellDateFormatted(cell)
? cell.getDateCellValue()
: cell.getNumericCellValue();
case BOOLEAN: return cell.getBooleanCellValue();
case FORMULA: return cell.getCellFormula();
case BLANK: return "";
default: return cell.toString();
}
}


/**
* 读取Excel文件
*/
public List<Map<String, Object>> readExcel(String filePath) {
File file = new File(filePath);
List<Map<String, Object>> dataList = new ArrayList<>();

try {
// 1. 检测是否为 HTML 文件(第一行以 '<' 开头)
if (isHtmlFile(file)) {
return readHtmlTable(file);
}

// 2. 如果不是 HTML,就用 POI 自动识别 xls/xlsx
try (FileInputStream fis = new FileInputStream(file);
Workbook workbook = WorkbookFactory.create(fis)) {

Sheet sheet = workbook.getSheetAt(0);
Row headerRow = sheet.getRow(0);
if (headerRow == null) {
return dataList;
}

// 3. 读取表头
List<String> headers = new ArrayList<>();
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
Cell cell = headerRow.getCell(i);
headers.add(cell != null ? cell.toString() : "");
}

// 4. 读取数据行
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) continue;

Map<String, Object> rowData = new HashMap<>();
for (int j = 0; j < headers.size(); j++) {
Cell cell = row.getCell(j);
rowData.put(headers.get(j), getCellValue2(cell));
}
dataList.add(rowData);
}
}

} catch (Exception e) {
log.error("读取Excel文件失败: {}", filePath, e);
throw new RuntimeException("读取Excel失败:" + e.getMessage());
}

return dataList;
}

/* public List<Map<String, Object>> readExcel(String filePath) {
List<Map<String, Object>> dataList = new ArrayList<>();

try (FileInputStream fis = new FileInputStream(filePath);
Workbook workbook = WorkbookFactory.create(fis)) { // 自动识别xls或xlsx

Sheet sheet = workbook.getSheetAt(0);
Row headerRow = sheet.getRow(0);

if (headerRow == null) {
return dataList;
}

List<String> headers = new ArrayList<>();
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
Cell cell = headerRow.getCell(i);
headers.add(cell != null ? cell.toString() : "");
}

for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) continue;

Map<String, Object> rowData = new HashMap<>();
for (int j = 0; j < headers.size(); j++) {
Cell cell = row.getCell(j);
String header = headers.get(j);
Object value = getCellValue(cell);
rowData.put(header, value);
}
dataList.add(rowData);
}

} catch (Exception e) {
log.error("读取Excel文件失败: {}", filePath, e);
}

return dataList;
}*/

/**
* 读取FBA Excel文件
*/
public List<Map<String, Object>> readFbaExcel(String filePath) {
return readExcel(filePath); // 使用相同的读取方法
}

/**
* 导出分析数据到Excel
*/
public void exportAnalysisData(HttpServletResponse response, Map<String, List<AnalysisData>> groupedData) {
try (Workbook workbook = new XSSFWorkbook()) {

for (Map.Entry<String, List<AnalysisData>> entry : groupedData.entrySet()) {
String categoryName = entry.getKey();
List<AnalysisData> dataList = entry.getValue();

// 清理工作表名称中的非法字符
String safeSheetName = categoryName.replaceAll("[\\\\/*?:\"<>|]", "_").substring(0, Math.min(31, categoryName.length()));

Sheet sheet = workbook.createSheet(safeSheetName);

// 创建表头
Row headerRow = sheet.createRow(0);
String[] headers = {
"日期", "店铺", "商品编号", "商品名称", "客户", "品牌", "数量", "合计",
"来源", "状态", "出库类型", "目的地", "备注", "订单编号", "行号"
};

for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
}

// 填充数据
for (int i = 0; i < dataList.size(); i++) {
AnalysisData data = dataList.get(i);
Row row = sheet.createRow(i + 1);

row.createCell(0).setCellValue(data.getDate().toString());
row.createCell(1).setCellValue(data.getShopName());
row.createCell(2).setCellValue(data.getProductCode());
row.createCell(3).setCellValue(data.getProductName());
row.createCell(4).setCellValue(data.getCustomerName());
row.createCell(5).setCellValue(data.getBrand());
row.createCell(6).setCellValue(data.getQuantity());
row.createCell(7).setCellValue(data.getTotalAmount().doubleValue());
row.createCell(8).setCellValue(data.getSource());
row.createCell(9).setCellValue(getStatusText(data.getStatus()));
row.createCell(10).setCellValue(data.getDeliveryType());
row.createCell(11).setCellValue(data.getDestination());
row.createCell(12).setCellValue(data.getRemarks());
row.createCell(13).setCellValue(data.getOrderNumber());
row.createCell(14).setCellValue(data.getRowNumber());
}

// 自动调整列宽
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
}
}

// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=sales_analysis_data.xlsx");

// 写入响应流
workbook.write(response.getOutputStream());

} catch (IOException e) {
log.error("导出Excel失败", e);
throw new RuntimeException("导出Excel失败");
}
}

/**
* 处理Excel日期
*/
public LocalDate processExcelDate(Object dateValue) {
if (dateValue == null) {
return null;
}

try {
if (dateValue instanceof Date) {
return ((Date) dateValue).toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
} else if (dateValue instanceof String) {
String dateStr = (String) dateValue;
// 尝试多种日期格式
String[] formats = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyy.MM.dd","yyyy/M/d", "yyyy年MM月dd日"};
for (String format : formats) {
try {
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(format));
} catch (Exception ignored) {
// 继续尝试下一个格式
}
}
} else if (dateValue instanceof Number) {
// Excel日期数字格式
double excelDate = ((Number) dateValue).doubleValue();
return DateUtil.getJavaDate(excelDate).toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
}
} catch (Exception e) {
log.error("日期处理失败: {}", dateValue, e);
}

return null;
}

/**
* 处理FBA日期
*/
public LocalDate processFbaDate(Object dateValue) {
if (dateValue == null) {
return null;
}

try {
if (dateValue instanceof String) {
String dateStr = (String) dateValue;
// 处理ISO格式日期
if (dateStr.contains("T")) {
return LocalDate.parse(dateStr.substring(0, 10));
}
}

// 使用通用的Excel日期处理
return processExcelDate(dateValue);

} catch (Exception e) {
log.error("FBA日期处理失败: {}", dateValue, e);
}

return null;
}

/**
* 处理FBA SKU
*/
public String processFbaSku(String sku) {
if (sku == null || sku.trim().isEmpty()) {
return "";
}

String trimmedSku = sku.trim();

// 具体的后缀模式
String[] specificSuffixes = {"-F", "-SLP", "-SF", "-F1", "-2K", "-5F", "-D", "-E"};

// 首先尝试匹配具体的后缀
for (String suffix : specificSuffixes) {
if (trimmedSku.endsWith(suffix)) {
return trimmedSku.substring(0, trimmedSku.length() - suffix.length());
}
}

// 如果没有匹配到具体后缀,尝试通用模式
String result = trimmedSku.replaceAll("^(.+?)[-_][A-Z]{1,4}$", "$1");
return result.equals(trimmedSku) ? trimmedSku : result;
}

/**
* 获取单元格值
*/
private Object getCellValue(Cell cell) {
if (cell == null) {
return "";
}

switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue();
} else {
return cell.getNumericCellValue();
}
case BOOLEAN:
return cell.getBooleanCellValue();
case FORMULA:
return cell.getCellFormula();
default:
return "";
}
}

/**
* 获取状态文本
*/
private String getStatusText(Integer status) {
if (status == null) return "";
switch (status) {
case 1: return "正常";
case 2: return "客户名称未匹配";
case 3: return "分类规格未匹配";
default: return "未知";
}
}
}

+ 91
- 0
zs-manager/src/main/resources/mapper/AnalysisDataMapper.xml View File

@@ -0,0 +1,91 @@
<?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.ruoyi.tjfx.mapper.AnalysisDataMapper">

<!-- 分页查询分析数据 -->
<select id="selectPageWithConditions" resultType="com.ruoyi.tjfx.entity.AnalysisData">
SELECT * FROM zs_tjfx_analysis_data
<where>
<if test="productCode != null and productCode != ''">
AND product_code LIKE CONCAT('%', #{productCode}, '%')
</if>
<if test="productName != null and productName != ''">
AND product_name LIKE CONCAT('%', #{productName}, '%')
</if>
<if test="customerName != null and customerName != ''">
AND customer_name LIKE CONCAT('%', #{customerName}, '%')
</if>
<if test="shopName != null and shopName != ''">
AND shop_name LIKE CONCAT('%', #{shopName}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="startDate != null">
AND date >= #{startDate}
</if>
<if test="endDate != null">
AND date &lt;= #{endDate}
</if>
</where>
ORDER BY created_at DESC
</select>

<!-- 根据条件查询所有数据(用于导出) -->
<select id="selectAllWithConditions" resultType="com.ruoyi.tjfx.entity.AnalysisData">
SELECT * FROM zs_tjfx_analysis_data
<where>
<if test="productCode != null and productCode != ''">
AND product_code LIKE CONCAT('%', #{productCode}, '%')
</if>
<if test="productName != null and productName != ''">
AND product_name LIKE CONCAT('%', #{productName}, '%')
</if>
<if test="customerName != null and customerName != ''">
AND customer_name LIKE CONCAT('%', #{customerName}, '%')
</if>
<if test="shopName != null and shopName != ''">
AND shop_name LIKE CONCAT('%', #{shopName}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="startDate != null">
AND date >= #{startDate}
</if>
<if test="endDate != null">
AND date &lt;= #{endDate}
</if>
</where>
ORDER BY created_at DESC
</select>

<!-- 批量插入数据 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO zs_tjfx_analysis_data (
date, shop_name, product_code, product_name, customer_name,
category, category_specs, quantity, total_amount, source,
status, delivery_type, destination, remarks, order_number, row_number, brand
) VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.date}, #{item.shopName}, #{item.productCode}, #{item.productName}, #{item.customerName},
#{item.category}, #{item.categorySpecs}, #{item.quantity}, #{item.totalAmount}, #{item.source},
#{item.status}, #{item.deliveryType}, #{item.destination}, #{item.remarks}, #{item.orderNumber}, #{item.rowNumber}, #{item.brand}
)
</foreach>
</insert>

<!-- 根据唯一条件查询记录 -->
<select id="selectByUniqueCondition" resultType="com.ruoyi.tjfx.entity.AnalysisData">
SELECT * FROM zs_tjfx_analysis_data
WHERE date = #{date}
AND shop_name = #{shopName}
AND product_code = #{productCode}
AND delivery_type = #{deliveryType}
AND order_number = #{orderNumber}
AND row_number = #{rowNumber}
LIMIT 1
</select>

</mapper>

+ 80
- 0
zs-manager/src/main/resources/mapper/BaseDataMapper.xml View File

@@ -0,0 +1,80 @@
<?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.ruoyi.tjfx.mapper.BaseDataMapper">

<!-- 根据商品编号和分类ID查询基准数据 -->
<select id="selectByProductCodeAndCategoryId" resultType="com.ruoyi.tjfx.entity.BaseData">
SELECT bd.*, c.name as category_name
FROM zs_tjfx_base_data bd
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id
WHERE bd.product_code = #{productCode}
<if test="categoryId != null">
AND bd.category_id = #{categoryId}
</if>
LIMIT 1
</select>
<select id="findByCategoryId" resultType="com.ruoyi.tjfx.entity.BaseDataVO">
SELECT bd.*, c.name as category_name
FROM zs_tjfx_base_data bd
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id
WHERE 1=1
<if test="categoryId != null">
AND bd.category_id = #{categoryId}
</if>
</select>
<select id="selectByProductCode" resultType="com.ruoyi.tjfx.entity.BaseDataVO">
SELECT bd.*, c.name as category_name
FROM zs_tjfx_base_data bd
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id
WHERE bd.product_code = #{productCode}
LIMIT 1
</select>

<select id="selectExistingProductCodes" resultType="string">
SELECT product_code
FROM zs_tjfx_base_data
WHERE product_code IN
<foreach collection="codes" item="code" open="(" separator="," close=")">
#{code}
</foreach>
</select>
<select id="selectPageWithJoin" resultType="com.ruoyi.tjfx.entity.BaseDataVO">
SELECT
bd.id, bd.product_code, bd.product_name, bd.brand,
bd.category_id, c.name AS category_name,
bd.category_specs, bd.created_at, bd.updated_at
FROM zs_tjfx_base_data bd
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id
<where>
<if test="productCode != null and productCode != ''">
AND bd.product_code LIKE CONCAT('%', #{productCode}, '%')
</if>
<if test="productName != null and productName != ''">
AND bd.product_name LIKE CONCAT('%', #{productName}, '%')
</if>
<if test="categoryId != null">
AND bd.category_id = #{categoryId}
</if>
</where>
ORDER BY bd.created_at DESC
</select>
<insert id="insertBatch">
INSERT INTO zs_tjfx_base_data (
product_code, product_name, brand,
category_id, category_specs,
created_at, updated_at
)
VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.productCode},
#{item.productName},
#{item.brand},
#{item.categoryId},
#{item.categorySpecs},
#{item.createdAt},
#{item.updatedAt}
)
</foreach>
</insert>
</mapper>

+ 504
- 0
zs-manager/src/main/resources/mapper/ReportMapper.xml View File

@@ -0,0 +1,504 @@
<?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.ruoyi.tjfx.mapper.ReportMapper">
<!-- 1. 整体销售分析 基础统计 -->
<select id="selectBasicStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO">
SELECT
COUNT(*) AS totalRecords,
SUM(quantity) AS totalQuantity,
SUM(total_amount) AS totalAmount,
COUNT(DISTINCT product_code) AS uniqueProducts,
COUNT(DISTINCT customer_name) AS uniqueCustomers
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND category = #{category}</if>
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if>
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if>
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
<if test="brand != null and brand != ''">AND brand = #{brand}</if>
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if>
AND status = 1
</select>

<!-- 2. 去年同期统计 -->
<select id="selectLastYearStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO">
SELECT
SUM(quantity) AS totalQuantity,
SUM(total_amount) AS totalAmount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
AND status = 1
</select>

<!-- 3. 上周期统计 -->
<select id="selectPrevStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO">
SELECT
SUM(quantity) AS totalQuantity,
SUM(total_amount) AS totalAmount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
AND status = 1
</select>

<!-- 4. 销售趋势数据 -->
<select id="selectTrendData" resultType="com.ruoyi.tjfx.entity.TrendDataVO">
SELECT
date,
SUM(quantity) AS quantity,
SUM(total_amount) AS amount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND category = #{category}</if>
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if>
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if>
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
<if test="brand != null and brand != ''">AND brand = #{brand}</if>
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if>
AND status = 1
GROUP BY date
ORDER BY date
</select>

<!-- 5. TOP20商品销售 -->
<select id="selectTopProducts" resultType="com.ruoyi.tjfx.entity.TopProductVO">
SELECT
product_code AS productCode,
product_name AS productName,
SUM(quantity) AS quantity,
SUM(total_amount) AS amount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND category = #{category}</if>
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if>
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if>
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
<if test="brand != null and brand != ''">AND brand = #{brand}</if>
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if>
AND status = 1
GROUP BY product_code, product_name
ORDER BY amount DESC
LIMIT 20
</select>

<!-- 6. 品牌销售数据 -->
<select id="selectBrandData" resultType="com.ruoyi.tjfx.entity.BrandDataVO">
SELECT
bd.brand AS brand,
SUM(ad.quantity) AS quantity,
SUM(ad.total_amount) AS amount
FROM zs_tjfx_analysis_data ad
JOIN zs_tjfx_base_data bd ON ad.product_code = bd.product_code
WHERE ad.date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND ad.category = #{category}</if>
<if test="categorySpecs != null and categorySpecs != ''">AND ad.category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if>
<if test="customer != null and customer != ''">AND ad.customer_name = #{customer}</if>
<if test="shop != null and shop != ''">AND ad.shop_name = #{shop}</if>
<if test="brand != null and brand != ''">AND ad.brand = #{brand}</if>
<if test="productCode != null and productCode != ''">AND ad.product_code LIKE CONCAT('%', #{productCode}, '%')</if>
AND ad.status = 1
AND bd.brand IS NOT NULL
AND bd.brand != ''
GROUP BY bd.brand
ORDER BY amount DESC
</select>

<!-- 7. 单品销售排行 -->
<select id="selectProductRanking" resultType="com.ruoyi.tjfx.entity.RankingDataVO">
SELECT
date,
SUM(total_amount) AS totalAmount,
SUM(quantity) AS totalQuantity
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
AND status = 1
AND product_code = #{productCode}
GROUP BY date
ORDER BY totalAmount DESC
LIMIT 20
</select>

<!-- 8. 单品销售趋势 -->
<select id="selectProductTrend" resultType="com.ruoyi.tjfx.entity.TrendDataVO">
SELECT
date,
SUM(total_amount) AS amount,
SUM(quantity) AS quantity
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
AND status = 1
AND product_code = #{productCode}
GROUP BY date
ORDER BY date ASC
</select>

<!-- 9. 店铺销售基础统计 -->
<select id="selectShopBasicStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO">
SELECT
SUM(quantity) AS totalQuantity,
SUM(total_amount) AS totalAmount,
COUNT(DISTINCT shop_name) AS totalShops,
COUNT(DISTINCT category) AS totalCategories
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
AND status = 1
</select>

<!-- 10. 各店铺销售金额 -->
<select id="selectShopAmountData" resultType="com.ruoyi.tjfx.entity.ShopAmountVO">
SELECT
shop_name AS shopName,
SUM(total_amount) AS amount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
AND status = 1
GROUP BY shop_name
ORDER BY amount DESC
</select>

<!-- 11. 各店铺销售数量 -->
<select id="selectShopQuantityData" resultType="com.ruoyi.tjfx.entity.ShopQuantityVO">
SELECT
shop_name AS shopName,
SUM(quantity) AS quantity
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
AND status = 1
GROUP BY shop_name
ORDER BY quantity DESC
</select>

<!-- 12. 各品类销售趋势 -->
<select id="selectCategoryTrendData" resultType="com.ruoyi.tjfx.entity.CategoryTrendVO">
SELECT
category,
SUM(quantity) AS quantity,
SUM(total_amount) AS amount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
AND status = 1
AND category IS NOT NULL
AND category != ''
GROUP BY category
ORDER BY amount DESC
</select>

<!-- 13. 分类规格原始数据 -->
<select id="selectSpecsRawData" resultType="map">
SELECT
category_specs,
SUM(quantity) AS quantity,
SUM(total_amount) AS amount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND category = #{category}</if>
AND status = 1
AND category_specs IS NOT NULL
AND category_specs != ''
GROUP BY category_specs
ORDER BY amount DESC
</select>

<!-- 14. 筛选选项 -->
<select id="selectCategories" resultType="string">
SELECT DISTINCT category
FROM zs_tjfx_analysis_data
WHERE category IS NOT NULL AND category != ''
ORDER BY category
</select>

<select id="selectCategorySpecs" resultType="string">
SELECT DISTINCT category_specs
FROM zs_tjfx_analysis_data
WHERE category_specs IS NOT NULL AND category_specs != ''
ORDER BY category_specs
</select>

<select id="selectCustomers" resultType="string">
SELECT DISTINCT customer_name
FROM zs_tjfx_analysis_data
WHERE customer_name IS NOT NULL AND customer_name != ''
ORDER BY customer_name
</select>

<select id="selectShops" resultType="string">
SELECT DISTINCT shop_name
FROM zs_tjfx_analysis_data
WHERE shop_name IS NOT NULL AND shop_name != ''
ORDER BY shop_name
</select>

<select id="selectBrands" resultType="string">
SELECT DISTINCT brand
FROM zs_tjfx_base_data
WHERE brand IS NOT NULL AND brand != ''
ORDER BY brand
</select>

<!-- 15. 商品编码联想 -->
<select id="selectProductCodeSuggestions" resultType="map">
SELECT
bd.product_code AS product_code,
bd.product_name AS product_name,
bd.category_specs AS category_specs,
c.name AS category_name
FROM zs_tjfx_base_data bd
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id
WHERE bd.product_code LIKE #{keyword}
ORDER BY bd.product_code
LIMIT 10
</select>

<!-- 1. 整体销售分析 基础统计 -->
<!-- <select id="selectBasicStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO">
SELECT
COUNT(*) as totalRecords,
SUM(quantity) as totalQuantity,
SUM(total_amount) as totalAmount,
COUNT(DISTINCT product_code) as uniqueProducts,
COUNT(DISTINCT customer_name) as uniqueCustomers
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND category = #{category}</if>
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if>
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if>
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
<if test="brand != null and brand != ''">AND brand = #{brand}</if>
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if>
AND status = 1
</select>

&lt;!&ndash; 2. 去年同期统计 &ndash;&gt;
<select id="selectLastYearStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO">
SELECT
SUM(quantity) as totalQuantity,
SUM(total_amount) as totalAmount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
AND status = 1
</select>

&lt;!&ndash; 3. 上周期统计 &ndash;&gt;
<select id="selectPrevStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO">
SELECT
SUM(quantity) as totalQuantity,
SUM(total_amount) as totalAmount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
AND status = 1
</select>

&lt;!&ndash; 4. 销售趋势数据 &ndash;&gt;
<select id="selectTrendData" resultType="com.ruoyi.tjfx.entity.TrendDataVO">
SELECT
date,
SUM(quantity) as dailyQuantity,
SUM(total_amount) as dailyAmount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND category = #{category}</if>
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if>
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if>
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
<if test="brand != null and brand != ''">AND brand = #{brand}</if>
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if>
AND status = 1
GROUP BY date
ORDER BY date
</select>

&lt;!&ndash; 5. TOP20商品销售 &ndash;&gt;
<select id="selectTopProducts" resultType="com.ruoyi.tjfx.entity.TopProductVO">
SELECT
product_code as productCode,
product_name as productName,
SUM(quantity) as totalQuantity,
SUM(total_amount) as totalAmount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND category = #{category}</if>
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if>
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if>
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
<if test="brand != null and brand != ''">AND brand = #{brand}</if>
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if>
AND status = 1
GROUP BY product_code, product_name
ORDER BY totalAmount DESC
LIMIT 20
</select>

&lt;!&ndash; 6. 品牌销售数据 &ndash;&gt;
<select id="selectBrandData" resultType="com.ruoyi.tjfx.entity.BrandDataVO">
SELECT
bd.brand as brand,
SUM(ad.quantity) as quantity,
SUM(ad.total_amount) as amount
FROM zs_tjfx_analysis_data ad
JOIN zs_tjfx_base_data bd ON ad.product_code = bd.product_code
WHERE ad.date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND ad.category = #{category}</if>
<if test="categorySpecs != null and categorySpecs != ''">AND ad.category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if>
<if test="customer != null and customer != ''">AND ad.customer_name = #{customer}</if>
<if test="shop != null and shop != ''">AND ad.shop_name = #{shop}</if>
<if test="brand != null and brand != ''">AND ad.brand = #{brand}</if>
<if test="productCode != null and productCode != ''">AND ad.product_code LIKE CONCAT('%', #{productCode}, '%')</if>
AND ad.status = 1
AND bd.brand IS NOT NULL
AND bd.brand != ''
GROUP BY bd.brand
ORDER BY amount DESC
</select>

&lt;!&ndash; 7. 单品销售排行 &ndash;&gt;
<select id="selectProductRanking" resultType="com.ruoyi.tjfx.entity.RankingDataVO">
SELECT
date,
SUM(total_amount) as totalAmount,
SUM(quantity) as totalQuantity
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
AND status = 1
AND product_code = #{productCode}
GROUP BY date
ORDER BY totalAmount DESC
LIMIT 20
</select>

&lt;!&ndash; 8. 单品销售趋势 &ndash;&gt;
<select id="selectProductTrend" resultType="com.ruoyi.tjfx.entity.TrendDataVO">
SELECT
date,
SUM(total_amount) as amount,
SUM(quantity) as quantity
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
AND status = 1
AND product_code = #{productCode}
GROUP BY date
ORDER BY date ASC
</select>

&lt;!&ndash; 9. 店铺销售基础统计 &ndash;&gt;
<select id="selectShopBasicStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO">
SELECT
SUM(quantity) as totalQuantity,
SUM(total_amount) as totalAmount,
COUNT(DISTINCT shop_name) as totalShops,
COUNT(DISTINCT category) as totalCategories
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
AND status = 1
</select>

&lt;!&ndash; 10. 各店铺销售金额 &ndash;&gt;
<select id="selectShopAmountData" resultType="com.ruoyi.tjfx.entity.ShopAmountVO">
SELECT
shop_name as shopName,
SUM(total_amount) as amount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
AND status = 1
GROUP BY shop_name
ORDER BY amount DESC
</select>

&lt;!&ndash; 11. 各店铺销售数量 &ndash;&gt;
<select id="selectShopQuantityData" resultType="com.ruoyi.tjfx.entity.ShopQuantityVO">
SELECT
shop_name as shopName,
SUM(quantity) as quantity
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
AND status = 1
GROUP BY shop_name
ORDER BY quantity DESC
</select>

&lt;!&ndash; 12. 各品类销售趋势 &ndash;&gt;
<select id="selectCategoryTrendData" resultType="com.ruoyi.tjfx.entity.CategoryTrendVO">
SELECT
category,
SUM(quantity) as quantity,
SUM(total_amount) as amount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if>
AND status = 1
AND category IS NOT NULL
AND category != ''
GROUP BY category
ORDER BY amount DESC
</select>

&lt;!&ndash; 13. 分类规格原始数据 &ndash;&gt;
<select id="selectSpecsRawData" resultType="map">
SELECT
category_specs,
SUM(quantity) as quantity,
SUM(total_amount) as amount
FROM zs_tjfx_analysis_data
WHERE date BETWEEN #{startDate} AND #{endDate}
<if test="category != null and category != ''">AND category = #{category}</if>
AND status = 1
AND category_specs IS NOT NULL
AND category_specs != ''
GROUP BY category_specs
ORDER BY amount DESC
</select>

&lt;!&ndash; 14. 筛选选项 &ndash;&gt;
<select id="selectCategories" resultType="string">
SELECT DISTINCT category
FROM zs_tjfx_analysis_data
WHERE category IS NOT NULL AND category != ''
ORDER BY category
</select>
<select id="selectCategorySpecs" resultType="string">
SELECT DISTINCT category_specs
FROM zs_tjfx_analysis_data
WHERE category_specs IS NOT NULL AND category_specs != ''
ORDER BY category_specs
</select>
<select id="selectCustomers" resultType="string">
SELECT DISTINCT customer_name
FROM zs_tjfx_analysis_data
WHERE customer_name IS NOT NULL AND customer_name != ''
ORDER BY customer_name
</select>
<select id="selectShops" resultType="string">
SELECT DISTINCT shop_name
FROM zs_tjfx_analysis_data
WHERE shop_name IS NOT NULL AND shop_name != ''
ORDER BY shop_name
</select>
<select id="selectBrands" resultType="string">
SELECT DISTINCT brand
FROM zs_tjfx_base_data
WHERE brand IS NOT NULL AND brand != ''
ORDER BY brand
</select>

&lt;!&ndash; 15. 商品编码联想 &ndash;&gt;
<select id="selectProductCodeSuggestions" resultType="map">
SELECT
bd.product_code as product_code,
bd.product_name as product_name,
bd.category_specs as category_specs,
c.name as category_name
FROM zs_tjfx_base_data bd
LEFT JOIN categories c ON bd.category_id = c.id
WHERE bd.product_code LIKE #{keyword}
ORDER BY bd.product_code
LIMIT 10
</select>-->
</mapper>

+ 12
- 0
zs-manager/src/main/resources/mapper/ShopCustomerMapper.xml View File

@@ -0,0 +1,12 @@
<?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.ruoyi.tjfx.mapper.ShopCustomerMapper">

<!-- 根据店铺名称查询客户名称 -->
<select id="selectByShopName" resultType="com.ruoyi.tjfx.entity.ShopCustomer">
SELECT * FROM zs_tjfx_shop_customer
WHERE shop_name = #{shopName}
LIMIT 1
</select>

</mapper>

Loading…
Cancel
Save