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 '午休时间 单位小时'; |
<artifactId>fastjson</artifactId> | <artifactId>fastjson</artifactId> | ||||
<version>1.2.83</version> | <version>1.2.83</version> | ||||
</dependency> | </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> | </dependencies> | ||||
</project> | </project> |
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); | |||||
} | |||||
} |
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; | |||||
} | |||||
} | |||||
} |
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; | |||||
}*/ | |||||
} |
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); | |||||
} | |||||
} | |||||
} | |||||
} |
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, "删除成功"); | |||||
} | |||||
} |
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), "获取商品编码联想数据成功"); | |||||
} | |||||
} |
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); | |||||
} | |||||
} | |||||
}*/ | |||||
} |
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; | |||||
} |
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; | |||||
} |
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<>(); | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
package com.ruoyi.tjfx.entity; | |||||
import lombok.Data; | |||||
import java.util.List; | |||||
/** | |||||
* 单品销售分析数据 | |||||
*/ | |||||
@Data | |||||
public class ProductAnalysisVO { | |||||
/** 单品每日销售排行数据 */ | |||||
private List<RankingDataVO> rankingData; | |||||
/** 单品销售趋势数据 */ | |||||
private List<TrendDataVO> trendData; | |||||
} |
package com.ruoyi.tjfx.entity; | |||||
import lombok.AllArgsConstructor; | |||||
import lombok.Data; | |||||
/** | |||||
* 商品编码联想提示数据 | |||||
*/ | |||||
@Data | |||||
@AllArgsConstructor | |||||
public class ProductCodeSuggestionVO { | |||||
/** 商品编码 */ | |||||
private String value; | |||||
/** 显示文本(商品名+规格-分类名) */ | |||||
private String label; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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; | |||||
} |
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); | |||||
} |
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); | |||||
} |
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> { | |||||
} |
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); | |||||
} |
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); | |||||
} |
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); | |||||
} |
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); | |||||
} |
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); | |||||
} |
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); | |||||
} |
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); | |||||
} |
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()); | |||||
} | |||||
} | |||||
} |
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; | |||||
} | |||||
} |
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); | |||||
} | |||||
} |
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()); | |||||
} | |||||
} |
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); | |||||
} | |||||
} | |||||
} |
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 "未知"; | |||||
} | |||||
} | |||||
} |
<?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 <= #{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 <= #{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> |
<?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> |
<?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> | |||||
<!– 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 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> | |||||
<!– 5. TOP20商品销售 –> | |||||
<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> | |||||
<!– 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 categories c ON bd.category_id = c.id | |||||
WHERE bd.product_code LIKE #{keyword} | |||||
ORDER BY bd.product_code | |||||
LIMIT 10 | |||||
</select>--> | |||||
</mapper> |
<?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> |