@@ -0,0 +1,4 @@ | |||
ALTER TABLE dk_check_in_attendance_team | |||
ADD COLUMN lunch_start_time VARCHAR(255) COMMENT '午休开始时间', | |||
ADD COLUMN lunch_end_time VARCHAR(255) COMMENT '午休结束时间'; | |||
ADD COLUMN lunch_time VARCHAR(255) COMMENT '午休时间 单位小时'; |
@@ -75,5 +75,16 @@ | |||
<artifactId>fastjson</artifactId> | |||
<version>1.2.83</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-data-jpa</artifactId> | |||
</dependency> | |||
<dependency> | |||
<!-- jsoup HTML 解析器库 --> | |||
<groupId>org.jsoup</groupId> | |||
<artifactId>jsoup</artifactId> | |||
<version>1.21.1</version> | |||
</dependency> | |||
</dependencies> | |||
</project> |
@@ -0,0 +1,50 @@ | |||
package com.ruoyi.tjfx.common; | |||
import lombok.Data; | |||
/** | |||
* 通用API响应类 | |||
*/ | |||
@Data | |||
public class ApiResponse<T> { | |||
private Integer code; | |||
private String message; | |||
private T data; | |||
private Long timestamp; | |||
public ApiResponse() { | |||
this.timestamp = System.currentTimeMillis(); | |||
} | |||
public ApiResponse(Integer code, String message) { | |||
this(); | |||
this.code = code; | |||
this.message = message; | |||
} | |||
public ApiResponse(Integer code, String message, T data) { | |||
this(code, message); | |||
this.data = data; | |||
} | |||
public static <T> ApiResponse<T> success(T data) { | |||
return new ApiResponse<>(200, "操作成功", data); | |||
} | |||
public static <T> ApiResponse<T> success(T data, String message) { | |||
return new ApiResponse<>(200, message, data); | |||
} | |||
public static <T> ApiResponse<T> error(String message) { | |||
return new ApiResponse<>(500, message); | |||
} | |||
public static <T> ApiResponse<T> error(Integer code, String message) { | |||
return new ApiResponse<>(code, message); | |||
} | |||
public static <T> ApiResponse<T> validationError(String message) { | |||
return new ApiResponse<>(400, message); | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
package com.ruoyi.tjfx.common; | |||
import lombok.Data; | |||
import java.util.List; | |||
/** | |||
* 分页响应类 | |||
*/ | |||
@Data | |||
public class PageResponse<T> { | |||
private List<T> list; | |||
private Pagination pagination; | |||
public PageResponse(List<T> list, Pagination pagination) { | |||
this.list = list; | |||
this.pagination = pagination; | |||
} | |||
@Data | |||
public static class Pagination { | |||
private Long total; | |||
private Integer page; | |||
private Integer pageSize; | |||
private Integer totalPages; | |||
private Boolean hasNext; | |||
private Boolean hasPrev; | |||
public Pagination(Long total, Integer page, Integer pageSize) { | |||
this.total = total; | |||
this.page = page; | |||
this.pageSize = pageSize; | |||
this.totalPages = (int) Math.ceil((double) total / pageSize); | |||
this.hasNext = page < totalPages; | |||
this.hasPrev = page > 1; | |||
} | |||
} | |||
} |
@@ -0,0 +1,357 @@ | |||
package com.ruoyi.tjfx.controller; | |||
import cn.dev33.satoken.annotation.SaIgnore; | |||
import com.ruoyi.tjfx.common.ApiResponse; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.AnalysisData; | |||
import com.ruoyi.tjfx.service.AnalysisDataService; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.format.annotation.DateTimeFormat; | |||
import org.springframework.web.bind.annotation.*; | |||
import org.springframework.web.multipart.MultipartFile; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.time.LocalDate; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.ArrayList; | |||
import java.util.HashMap; | |||
/** | |||
* 分析数据Controller | |||
*/ | |||
@SaIgnore | |||
@Slf4j | |||
@RestController | |||
@RequestMapping("/analysis-data") | |||
public class AnalysisDataController { | |||
@Autowired | |||
private AnalysisDataService analysisDataService; | |||
/** | |||
* 分页查询分析数据 | |||
*/ | |||
@GetMapping | |||
public ApiResponse<PageResponse<AnalysisData>> getPage( | |||
@RequestParam(defaultValue = "1") Integer page, | |||
@RequestParam(defaultValue = "10") Integer pageSize, | |||
@RequestParam(required = false) String productCode, | |||
@RequestParam(required = false) String productName, | |||
@RequestParam(required = false) String customerName, | |||
@RequestParam(required = false) String shopName, | |||
@RequestParam(required = false) Integer status, | |||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, | |||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) { | |||
try { | |||
PageResponse<AnalysisData> result = analysisDataService.getPage( | |||
page, pageSize, productCode, productName, customerName, shopName, status, startDate, endDate); | |||
return ApiResponse.success(result, "获取成功"); | |||
} catch (Exception e) { | |||
log.error("获取分析数据列表失败", e); | |||
return ApiResponse.error("获取分析数据列表失败"); | |||
} | |||
} | |||
/** | |||
* 根据ID获取分析数据详情 | |||
*/ | |||
@GetMapping("/{id}") | |||
public ApiResponse<AnalysisData> getById(@PathVariable Long id) { | |||
try { | |||
AnalysisData analysisData = analysisDataService.getById(id); | |||
if (analysisData == null) { | |||
return ApiResponse.error(404, "分析数据不存在"); | |||
} | |||
return ApiResponse.success(analysisData, "获取成功"); | |||
} catch (Exception e) { | |||
log.error("获取分析数据详情失败", e); | |||
return ApiResponse.error("获取分析数据详情失败"); | |||
} | |||
} | |||
/** | |||
* 更新分析数据 | |||
*/ | |||
@PutMapping("/{id}") | |||
public ApiResponse<Void> updateById(@PathVariable Long id, @RequestBody AnalysisData analysisData) { | |||
try { | |||
analysisDataService.updateById(id, analysisData); | |||
return ApiResponse.success(null, "更新成功"); | |||
} catch (Exception e) { | |||
log.error("更新分析数据失败", e); | |||
return ApiResponse.error("更新分析数据失败"); | |||
} | |||
} | |||
/** | |||
* 删除分析数据 | |||
*/ | |||
@DeleteMapping("/{id}") | |||
public ApiResponse<Void> deleteById(@PathVariable Long id) { | |||
try { | |||
analysisDataService.deleteById(id); | |||
return ApiResponse.success(null, "删除成功"); | |||
} catch (Exception e) { | |||
log.error("删除分析数据失败", e); | |||
return ApiResponse.error("删除分析数据失败"); | |||
} | |||
} | |||
/** | |||
* 批量删除分析数据 | |||
*/ | |||
@DeleteMapping("/batch/{ids}") | |||
public ApiResponse<Map<String, Object>> deleteBatchByIds(@PathVariable String ids) { | |||
try { | |||
String[] idArray = ids.split(","); | |||
List<Long> idList = new ArrayList<>(); | |||
for (String id : idArray) { | |||
try { | |||
idList.add(Long.parseLong(id.trim())); | |||
} catch (NumberFormatException ignored) { | |||
// 忽略无效的ID | |||
} | |||
} | |||
if (idList.isEmpty()) { | |||
return ApiResponse.validationError("请选择要删除的数据"); | |||
} | |||
analysisDataService.deleteBatchByIds(idList); | |||
Map<String, Object> result = new HashMap<>(); | |||
result.put("deletedCount", idList.size()); | |||
return ApiResponse.success(result, "批量删除成功"); | |||
} catch (Exception e) { | |||
log.error("批量删除分析数据失败", e); | |||
return ApiResponse.error("批量删除分析数据失败"); | |||
} | |||
} | |||
/** | |||
* 导出Excel(支持分页) | |||
*/ | |||
@GetMapping("/export") | |||
public void exportExcel(HttpServletResponse response, | |||
@RequestParam(required = false) String productCode, | |||
@RequestParam(required = false) String productName, | |||
@RequestParam(required = false) String customerName, | |||
@RequestParam(required = false) String shopName, | |||
@RequestParam(required = false) Integer status, | |||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, | |||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate, | |||
@RequestParam(required = false) Integer page, | |||
@RequestParam(required = false) Integer pageSize) { | |||
try { | |||
analysisDataService.exportExcel(response, productCode, productName, customerName, shopName, status, startDate, endDate, page, pageSize); | |||
} catch (Exception e) { | |||
log.error("Excel导出失败", e); | |||
try { | |||
response.setContentType("application/json"); | |||
response.setCharacterEncoding("UTF-8"); | |||
response.getWriter().write("{\"code\":500,\"message\":\"Excel导出失败\"}"); | |||
} catch (Exception ex) { | |||
log.error("写入错误响应失败", ex); | |||
} | |||
} | |||
} | |||
/** | |||
* 导入Excel数据(单文件) | |||
*/ | |||
@PostMapping("/import") | |||
public ApiResponse<Map<String, Object>> importExcel( | |||
@RequestParam("file") MultipartFile file, | |||
@RequestParam("categoryId") Long categoryId) { | |||
try { | |||
if (file.isEmpty()) { | |||
return ApiResponse.validationError("请选择Excel文件"); | |||
} | |||
// 保存上传的文件 | |||
String filePath = saveUploadedFile(file); | |||
Map<String, Object> result = analysisDataService.importExcel(filePath, categoryId); | |||
return ApiResponse.success(result, "数据处理完成,请确认后保存"); | |||
} catch (Exception e) { | |||
log.error("Excel导入失败", e); | |||
return ApiResponse.error(e.getMessage()); | |||
} | |||
} | |||
/** | |||
* 批量导入Excel数据 | |||
*/ | |||
/* @PostMapping("/import-batch") | |||
public ApiResponse<Map<String, Object>> importBatchExcel( | |||
@RequestParam("files") MultipartFile[] files, | |||
@RequestParam("categoryId") Long categoryId) { | |||
try { | |||
if (files == null || files.length == 0) { | |||
return ApiResponse.validationError("请选择Excel文件"); | |||
} | |||
List<String> filePaths = new ArrayList<>(); | |||
for (MultipartFile file : files) { | |||
if (!file.isEmpty()) { | |||
filePaths.add(saveUploadedFile(file)); | |||
} | |||
} | |||
if (filePaths.isEmpty()) { | |||
return ApiResponse.validationError("没有有效的Excel文件"); | |||
} | |||
Map<String, Object> result = analysisDataService.importBatchExcel(filePaths, categoryId); | |||
return ApiResponse.success(result, "批量数据处理完成,请确认后保存"); | |||
} catch (Exception e) { | |||
log.error("批量Excel导入失败", e); | |||
return ApiResponse.error(e.getMessage()); | |||
} | |||
}*/ | |||
/** | |||
* 批量导入Excel数据 | |||
*/ | |||
@PostMapping("/import-batch") | |||
public ApiResponse<Map<String, Object>> importBatchExcel( | |||
@RequestParam("files") MultipartFile[] files, | |||
@RequestParam("categoryId") Long categoryId) { | |||
try { | |||
if (files == null || files.length == 0) { | |||
return ApiResponse.validationError("请选择Excel文件"); | |||
} | |||
List<String> filePaths = new ArrayList<>(); | |||
for (MultipartFile file : files) { | |||
if (!file.isEmpty()) { | |||
filePaths.add(saveUploadedFile(file)); | |||
} | |||
} | |||
if (filePaths.isEmpty()) { | |||
return ApiResponse.validationError("没有有效的Excel文件"); | |||
} | |||
Map<String, Object> result = analysisDataService.importBatchExcel(filePaths, categoryId); | |||
return ApiResponse.success(result, "批量数据处理完成,请确认后保存"); | |||
} catch (Exception e) { | |||
log.error("批量Excel导入失败", e); | |||
return ApiResponse.error("导入失败:" + e.getMessage()); | |||
} | |||
} | |||
/** | |||
* 导入FBA数据(单文件) | |||
*/ | |||
@PostMapping("/import/fba") | |||
public ApiResponse<Map<String, Object>> importFbaExcel( | |||
@RequestParam("file") MultipartFile file, | |||
@RequestParam("shopName") String shopName) { | |||
try { | |||
if (file.isEmpty()) { | |||
return ApiResponse.validationError("请选择Excel文件"); | |||
} | |||
String filePath = saveUploadedFile(file); | |||
Map<String, Object> result = analysisDataService.importFbaExcel(filePath, shopName); | |||
return ApiResponse.success(result, "FBA数据处理完成,请确认后保存"); | |||
} catch (Exception e) { | |||
log.error("FBA数据导入失败", e); | |||
return ApiResponse.error(e.getMessage()); | |||
} | |||
} | |||
/** | |||
* 批量导入FBA数据 | |||
*/ | |||
@PostMapping("/import/fba/batch") | |||
public ApiResponse<Map<String, Object>> importBatchFbaExcel( | |||
@RequestParam("files") MultipartFile[] files, | |||
@RequestParam("shopName") String shopName) { | |||
try { | |||
if (files == null || files.length == 0) { | |||
return ApiResponse.validationError("请选择Excel文件"); | |||
} | |||
List<String> filePaths = new ArrayList<>(); | |||
for (MultipartFile file : files) { | |||
if (!file.isEmpty()) { | |||
filePaths.add(saveUploadedFile(file)); | |||
} | |||
} | |||
if (filePaths.isEmpty()) { | |||
return ApiResponse.validationError("没有有效的Excel文件"); | |||
} | |||
Map<String, Object> result = analysisDataService.importBatchFbaExcel(filePaths, shopName); | |||
return ApiResponse.success(result, "FBA批量数据处理完成,请确认后保存"); | |||
} catch (Exception e) { | |||
log.error("FBA批量数据导入失败", e); | |||
return ApiResponse.error(e.getMessage()); | |||
} | |||
} | |||
/** | |||
* 保存导入的数据 | |||
*/ | |||
@PostMapping("/save-imported") | |||
public ApiResponse<Map<String, Object>> saveImportedData(@RequestBody List<AnalysisData> dataList) { | |||
try { | |||
if (dataList == null || dataList.isEmpty()) { | |||
return ApiResponse.validationError("没有可保存的数据"); | |||
} | |||
Map<String, Object> result = analysisDataService.saveImportedData(dataList); | |||
return ApiResponse.success(result, "数据保存完成"); | |||
} catch (Exception e) { | |||
log.error("保存数据失败", e); | |||
return ApiResponse.error("保存数据失败"); | |||
} | |||
} | |||
/** | |||
* 保存上传的文件到系统临时目录 | |||
*/ | |||
private String saveUploadedFile(MultipartFile file) throws Exception { | |||
// 获取系统临时目录 | |||
String tempDir = System.getProperty("java.io.tmpdir"); | |||
// 防止文件名冲突,加时间戳 | |||
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename(); | |||
// 构建目标文件路径 | |||
String filePath = tempDir + java.io.File.separator + fileName; | |||
// 创建文件对象并保存 | |||
java.io.File destFile = new java.io.File(filePath); | |||
file.transferTo(destFile); | |||
return filePath; | |||
} | |||
/** | |||
* 保存上传的文件 | |||
*/ | |||
/* private String saveUploadedFile(MultipartFile file) throws Exception { | |||
// 这里应该实现文件保存逻辑 | |||
// 为了简化,这里只是返回一个临时路径 | |||
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename(); | |||
String uploadPath = "./uploads/"; | |||
// 确保目录存在 | |||
java.io.File uploadDir = new java.io.File(uploadPath); | |||
if (!uploadDir.exists()) { | |||
uploadDir.mkdirs(); | |||
} | |||
String filePath = uploadPath + fileName; | |||
file.transferTo(new java.io.File(filePath)); | |||
return filePath; | |||
}*/ | |||
} |
@@ -0,0 +1,126 @@ | |||
package com.ruoyi.tjfx.controller; | |||
import cn.dev33.satoken.annotation.SaIgnore; | |||
import com.ruoyi.tjfx.common.ApiResponse; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.BaseDataVO; | |||
import com.ruoyi.tjfx.entity.Category; | |||
import com.ruoyi.tjfx.service.BaseDataService; | |||
import com.ruoyi.tjfx.entity.BaseData; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.web.bind.annotation.*; | |||
import org.springframework.web.multipart.MultipartFile; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
import java.util.stream.Collectors; | |||
@SaIgnore | |||
@Slf4j | |||
@RestController | |||
@RequestMapping("/basedata") | |||
public class BaseDataController { | |||
@Autowired | |||
private BaseDataService baseDataService; | |||
/** | |||
* 分页查询基础数据 | |||
*/ | |||
@GetMapping() | |||
public ApiResponse<PageResponse<BaseDataVO>> getBaseDataPage( | |||
@RequestParam(defaultValue = "1") Integer page, | |||
@RequestParam(defaultValue = "10") Integer pageSize, | |||
@RequestParam(required = false) String productCode, | |||
@RequestParam(required = false) String productName, | |||
@RequestParam(required = false) Long categoryId) { | |||
try { | |||
return ApiResponse.success(baseDataService.getPage(page, pageSize, productCode, productName, categoryId)); | |||
} catch (Exception e) { | |||
log.error("分页查询基础数据失败", e); | |||
return ApiResponse.error("获取基础数据失败"); | |||
} | |||
} | |||
@GetMapping("/{id}") | |||
public ApiResponse<BaseData> getById(@PathVariable Long id) { | |||
BaseData data = baseDataService.getById(id); | |||
if (data == null) return ApiResponse.error(404, "数据不存在"); | |||
return ApiResponse.success(data, "获取成功"); | |||
} | |||
@PostMapping | |||
public ApiResponse<Void> add(@RequestBody BaseData baseData) { | |||
baseDataService.add(baseData); | |||
return ApiResponse.success(null, "新增成功"); | |||
} | |||
@PutMapping("/{id}") | |||
public ApiResponse<Void> update(@PathVariable Long id, @RequestBody BaseData baseData) { | |||
baseDataService.update(id, baseData); | |||
return ApiResponse.success(null, "更新成功"); | |||
} | |||
@DeleteMapping("/{id}") | |||
public ApiResponse<Void> delete(@PathVariable Long id) { | |||
baseDataService.delete(id); | |||
return ApiResponse.success(null, "删除成功"); | |||
} | |||
@DeleteMapping("/batch/{ids}") | |||
public ApiResponse<Void> deleteBatch(@PathVariable String ids) { | |||
List<Long> idList = Arrays.stream(ids.split(",")) | |||
.map(String::trim) | |||
.filter(s -> !s.isEmpty()) | |||
.map(Long::valueOf) | |||
.collect(Collectors.toList()); | |||
baseDataService.deleteBatch(idList); | |||
return ApiResponse.success(null, "批量删除成功"); | |||
} | |||
@PostMapping("/import") | |||
public ApiResponse<?> importExcel(@RequestParam("file") MultipartFile file, | |||
@RequestParam("categoryId") Long categoryId) { | |||
try { | |||
if (file.isEmpty()) { | |||
return ApiResponse.validationError("请选择Excel文件"); | |||
} | |||
baseDataService.importExcel(file, categoryId); | |||
return ApiResponse.success(null, "Excel导入成功"); | |||
} catch (RuntimeException e) { | |||
return ApiResponse.validationError(e.getMessage()); | |||
} catch (Exception e) { | |||
log.error("Excel导入失败", e); | |||
return ApiResponse.error("Excel导入失败"); | |||
} | |||
} | |||
@GetMapping("/export/{categoryId}") | |||
public void exportExcel(@PathVariable Long categoryId, HttpServletResponse response) { | |||
try { | |||
baseDataService.exportExcel(categoryId, response); | |||
} catch (RuntimeException e) { | |||
try { | |||
response.setContentType("application/json"); | |||
response.setCharacterEncoding("UTF-8"); | |||
response.getWriter().write("{\"success\":false,\"message\":\"" + e.getMessage() + "\"}"); | |||
} catch (Exception ex) { | |||
log.error("写入错误响应失败", ex); | |||
} | |||
} catch (Exception e) { | |||
log.error("Excel导出失败", e); | |||
try { | |||
response.setContentType("application/json"); | |||
response.setCharacterEncoding("UTF-8"); | |||
response.getWriter().write("{\"success\":false,\"message\":\"Excel导出失败\"}"); | |||
} catch (Exception ex) { | |||
log.error("写入错误响应失败", ex); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,78 @@ | |||
package com.ruoyi.tjfx.controller; | |||
import cn.dev33.satoken.annotation.SaIgnore; | |||
import com.ruoyi.tjfx.common.ApiResponse; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.service.CategoryService; | |||
import com.ruoyi.tjfx.entity.Category; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.web.bind.annotation.*; | |||
import java.util.List; | |||
@SaIgnore | |||
@Slf4j | |||
@RestController | |||
@RequestMapping("/categories") | |||
public class CategoryController { | |||
@Autowired | |||
private CategoryService categoryService; | |||
@GetMapping("/simple") | |||
public ApiResponse<List<Category>> getSimpleCategories() { | |||
try { | |||
List<Category> categories = categoryService.getAllSimple(); | |||
return ApiResponse.success(categories); | |||
} catch (Exception e) { | |||
log.error("获取分类列表失败", e); | |||
return ApiResponse.error("获取分类列表失败"); | |||
} | |||
} | |||
/* | |||
@GetMapping | |||
public ApiResponse<List<Category>> listAll() { | |||
return ApiResponse.success(categoryService.listAll(), "获取成功"); | |||
}*/ | |||
/** | |||
* 分页查询分类 | |||
*/ | |||
@GetMapping() | |||
public ApiResponse<PageResponse<Category>> getPage( | |||
@RequestParam(defaultValue = "1") Integer page, | |||
@RequestParam(defaultValue = "10") Integer pageSize, | |||
@RequestParam(required = false) String name) { | |||
try { | |||
PageResponse<Category> result = categoryService.getPage(page, pageSize, name); | |||
return ApiResponse.success(result, "获取成功"); | |||
} catch (Exception e) { | |||
log.error("获取分类列表失败", e); | |||
return ApiResponse.error("获取分类列表失败"); | |||
} | |||
} | |||
@GetMapping("/{id}") | |||
public ApiResponse<Category> getById(@PathVariable Long id) { | |||
Category data = categoryService.getById(id); | |||
if (data == null) return ApiResponse.error(404, "数据不存在"); | |||
return ApiResponse.success(data, "获取成功"); | |||
} | |||
@PostMapping | |||
public ApiResponse<Void> add(@RequestBody Category category) { | |||
categoryService.add(category); | |||
return ApiResponse.success(null, "新增成功"); | |||
} | |||
@PutMapping("/{id}") | |||
public ApiResponse<Void> update(@PathVariable Long id, @RequestBody Category category) { | |||
categoryService.update(id, category); | |||
return ApiResponse.success(null, "更新成功"); | |||
} | |||
@DeleteMapping("/{id}") | |||
public ApiResponse<Void> delete(@PathVariable Long id) { | |||
categoryService.delete(id); | |||
return ApiResponse.success(null, "删除成功"); | |||
} | |||
} |
@@ -0,0 +1,77 @@ | |||
package com.ruoyi.tjfx.controller; | |||
import com.ruoyi.tjfx.entity.*; | |||
import com.ruoyi.tjfx.service.ReportService; | |||
import org.springframework.web.bind.annotation.RequestMapping; | |||
import org.springframework.web.bind.annotation.RestController; | |||
import com.ruoyi.tjfx.common.ApiResponse; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.web.bind.annotation.*; | |||
import java.util.List; | |||
@RestController | |||
@RequestMapping("/reports") | |||
public class ReportController { | |||
@Autowired | |||
private ReportService reportService; | |||
// 1. 整体销售分析 | |||
@GetMapping("/overall-analysis") | |||
public ApiResponse<OverallAnalysisVO> overallAnalysis( | |||
@RequestParam String startDate, | |||
@RequestParam String endDate, | |||
@RequestParam(required = false) String category, | |||
@RequestParam(required = false) String categorySpecs, | |||
@RequestParam(required = false) String customer, | |||
@RequestParam(required = false) String shop, | |||
@RequestParam(required = false) String brand, | |||
@RequestParam(required = false) String productCode | |||
) { | |||
return ApiResponse.success(reportService.overallAnalysis( | |||
startDate, endDate, category, categorySpecs, customer, shop, brand, productCode | |||
), "获取整体销售分析数据成功"); | |||
} | |||
// 2. 单品销售分析 | |||
@GetMapping("/product-analysis") | |||
public ApiResponse<ProductAnalysisVO> productAnalysis( | |||
@RequestParam String startDate, | |||
@RequestParam String endDate, | |||
@RequestParam String productCode | |||
) { | |||
return ApiResponse.success(reportService.productAnalysis(startDate, endDate, productCode), "获取单品销售分析数据成功"); | |||
} | |||
// 3. 店铺销售分析 | |||
@GetMapping("/shop-analysis") | |||
public ApiResponse<ShopAnalysisVO> shopAnalysis( | |||
@RequestParam String startDate, | |||
@RequestParam String endDate, | |||
@RequestParam(required = false) String shop | |||
) { | |||
return ApiResponse.success(reportService.shopAnalysis(startDate, endDate, shop), "获取店铺销售分析数据成功"); | |||
} | |||
// 4. 分类销售分析 | |||
@GetMapping("/category-analysis") | |||
public ApiResponse<CategoryAnalysisVO> categoryAnalysis( | |||
@RequestParam String startDate, | |||
@RequestParam String endDate, | |||
@RequestParam(required = false) String category | |||
) { | |||
return ApiResponse.success(reportService.categoryAnalysis(startDate, endDate, category), "获取分类销售分析数据成功"); | |||
} | |||
// 5. 筛选选项 | |||
@GetMapping("/filter-options") | |||
public ApiResponse<FilterOptionsVO> filterOptions() { | |||
return ApiResponse.success(reportService.filterOptions(), "获取筛选选项成功"); | |||
} | |||
// 6. 商品编码联想 | |||
@GetMapping("/product-code-suggestions") | |||
public ApiResponse<List<ProductCodeSuggestionVO>> productCodeSuggestions(@RequestParam String keyword) { | |||
return ApiResponse.success(reportService.productCodeSuggestions(keyword), "获取商品编码联想数据成功"); | |||
} | |||
} |
@@ -0,0 +1,140 @@ | |||
package com.ruoyi.tjfx.controller; | |||
import cn.dev33.satoken.annotation.SaIgnore; | |||
import com.ruoyi.tjfx.common.ApiResponse; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.ShopCustomer; | |||
import com.ruoyi.tjfx.service.ShopCustomerService; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.web.bind.annotation.*; | |||
import org.springframework.web.multipart.MultipartFile; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.stream.Collectors; | |||
@SaIgnore | |||
@Slf4j | |||
@RestController | |||
@RequestMapping("/shop-customer") | |||
public class ShopCustomerController { | |||
@Autowired | |||
private ShopCustomerService shopCustomerService; | |||
/* @GetMapping | |||
public ApiResponse<List<ShopCustomer>> listAll() { | |||
return ApiResponse.success(shopCustomerService.listAll(), "获取成功"); | |||
}*/ | |||
/** | |||
* 分页查询店铺客户 | |||
*/ | |||
@GetMapping() | |||
public ApiResponse<PageResponse<ShopCustomer>> getPage( | |||
@RequestParam(defaultValue = "1") Integer page, | |||
@RequestParam(defaultValue = "10") Integer pageSize, | |||
@RequestParam(required = false) String shopName, | |||
@RequestParam(required = false) String customerName) { | |||
try { | |||
PageResponse<ShopCustomer> result = shopCustomerService.getPage(page, pageSize, shopName, customerName); | |||
return ApiResponse.success(result, "获取成功"); | |||
} catch (Exception e) { | |||
log.error("获取店铺客户列表失败", e); | |||
return ApiResponse.error("获取店铺客户列表失败"); | |||
} | |||
} | |||
@GetMapping("/{id}") | |||
public ApiResponse<ShopCustomer> getById(@PathVariable Long id) { | |||
ShopCustomer data = shopCustomerService.getById(id); | |||
if (data == null) return ApiResponse.error(404, "数据不存在"); | |||
return ApiResponse.success(data, "获取成功"); | |||
} | |||
@PostMapping | |||
public ApiResponse<Void> add(@RequestBody ShopCustomer shopCustomer) { | |||
shopCustomerService.add(shopCustomer); | |||
return ApiResponse.success(null, "新增成功"); | |||
} | |||
@PutMapping("/{id}") | |||
public ApiResponse<Void> update(@PathVariable Long id, @RequestBody ShopCustomer shopCustomer) { | |||
shopCustomerService.update(id, shopCustomer); | |||
return ApiResponse.success(null, "更新成功"); | |||
} | |||
@DeleteMapping("/{id}") | |||
public ApiResponse<Void> delete(@PathVariable Long id) { | |||
shopCustomerService.delete(id); | |||
return ApiResponse.success(null, "删除成功"); | |||
} | |||
@DeleteMapping("/batch/{ids}") | |||
public ApiResponse<Void> deleteBatch(@PathVariable String ids) { | |||
List<Long> idList = Arrays.stream(ids.split(",")) | |||
.map(String::trim) | |||
.filter(s -> !s.isEmpty()) | |||
.map(Long::valueOf) | |||
.collect(Collectors.toList()); | |||
shopCustomerService.deleteBatch(idList); | |||
return ApiResponse.success(null, "批量删除成功"); | |||
} | |||
@PostMapping("/import") | |||
public ApiResponse<?> importExcel(@RequestParam("file") MultipartFile file) { | |||
try { | |||
if (file.isEmpty()) { | |||
return ApiResponse.validationError("请选择Excel文件"); | |||
} | |||
Map<String, Object> result = shopCustomerService.importExcel(file); | |||
int imported = (int) result.getOrDefault("imported", 0); | |||
int total = (int) result.getOrDefault("total", 0); | |||
int errors = (int) result.getOrDefault("errors", 0); | |||
String msg = "Excel导入完成,共处理" + total + "行,成功导入" + imported + "条"; | |||
if (errors > 0) msg += ",跳过" + errors + "条"; | |||
return ApiResponse.success(result, msg); | |||
} catch (RuntimeException e) { | |||
return ApiResponse.validationError(e.getMessage()); | |||
} catch (Exception e) { | |||
log.error("Excel导入失败", e); | |||
return ApiResponse.error("Excel导入失败"); | |||
} | |||
} | |||
@GetMapping("/export") | |||
public void exportExcel(HttpServletResponse response) { | |||
try { | |||
shopCustomerService.exportExcel(response); | |||
} catch (Exception e) { | |||
log.error("Excel导出失败", e); | |||
// 不建议再写响应体了,可能已经写入了一部分内容,会报 IllegalStateException | |||
} | |||
} | |||
/* @GetMapping("/export") | |||
public void exportExcel(HttpServletResponse response) { | |||
try { | |||
shopCustomerService.exportExcel(response); | |||
} catch (RuntimeException e) { | |||
try { | |||
response.setContentType("application/json"); | |||
response.setCharacterEncoding("UTF-8"); | |||
response.getWriter().write("{\"success\":false,\"message\":\"" + e.getMessage() + "\"}"); | |||
} catch (Exception ex) { | |||
log.error("写入错误响应失败", ex); | |||
} | |||
} catch (Exception e) { | |||
log.error("Excel导出失败", e); | |||
try { | |||
response.setContentType("application/json"); | |||
response.setCharacterEncoding("UTF-8"); | |||
response.getWriter().write("{\"success\":false,\"message\":\"Excel导出失败\"}"); | |||
} catch (Exception ex) { | |||
log.error("写入错误响应失败", ex); | |||
} | |||
} | |||
}*/ | |||
} |
@@ -0,0 +1,135 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import com.baomidou.mybatisplus.annotation.*; | |||
import lombok.Data; | |||
import lombok.EqualsAndHashCode; | |||
import java.math.BigDecimal; | |||
import java.time.LocalDate; | |||
import java.time.LocalDateTime; | |||
/** | |||
* 分析数据实体类 | |||
*/ | |||
@Data | |||
@EqualsAndHashCode(callSuper = false) | |||
@TableName("zs_tjfx_analysis_data") | |||
public class AnalysisData { | |||
@TableId(value = "id", type = IdType.AUTO) | |||
private Long id; | |||
/** | |||
* 日期 | |||
*/ | |||
@TableField("date") | |||
private LocalDate date; | |||
/** | |||
* 店铺名称 | |||
*/ | |||
@TableField("shop_name") | |||
private String shopName; | |||
/** | |||
* 商品编号 | |||
*/ | |||
@TableField("product_code") | |||
private String productCode; | |||
/** | |||
* 商品名称 | |||
*/ | |||
@TableField("product_name") | |||
private String productName; | |||
/** | |||
* 客户名称 | |||
*/ | |||
@TableField("customer_name") | |||
private String customerName; | |||
/** | |||
* 分类 | |||
*/ | |||
@TableField("category") | |||
private String category; | |||
/** | |||
* 分类规格JSON | |||
*/ | |||
@TableField("category_specs") | |||
private String categorySpecs; | |||
/** | |||
* 数量 | |||
*/ | |||
@TableField("quantity") | |||
private Integer quantity; | |||
/** | |||
* 合计金额 | |||
*/ | |||
@TableField("total_amount") | |||
private BigDecimal totalAmount; | |||
/** | |||
* 数据来源 | |||
*/ | |||
@TableField("source") | |||
private String source; | |||
/** | |||
* 状态:1-正常,2-客户名称未匹配,3-分类规格未匹配 | |||
*/ | |||
@TableField("status") | |||
private Integer status; | |||
/** | |||
* 出库类型 | |||
*/ | |||
@TableField("delivery_type") | |||
private String deliveryType; | |||
/** | |||
* 目的地 | |||
*/ | |||
@TableField("destination") | |||
private String destination; | |||
/** | |||
* 备注 | |||
*/ | |||
@TableField("remarks") | |||
private String remarks; | |||
/** | |||
* 订单编号 | |||
*/ | |||
@TableField("order_number") | |||
private String orderNumber; | |||
/** | |||
* 行号 | |||
*/ | |||
@TableField("row_number") | |||
private Integer rowNumber; | |||
/** | |||
* 品牌 | |||
*/ | |||
@TableField("brand") | |||
private String brand; | |||
/** | |||
* 创建时间 | |||
*/ | |||
@TableField(value = "created_at", fill = FieldFill.INSERT) | |||
private LocalDateTime createdAt; | |||
/** | |||
* 更新时间 | |||
*/ | |||
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE) | |||
private LocalDateTime updatedAt; | |||
} |
@@ -0,0 +1,68 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import com.baomidou.mybatisplus.annotation.*; | |||
import lombok.Data; | |||
import lombok.EqualsAndHashCode; | |||
import java.time.LocalDateTime; | |||
/** | |||
* 基准数据实体类 | |||
*/ | |||
@Data | |||
@EqualsAndHashCode(callSuper = false) | |||
@TableName("zs_tjfx_base_data") | |||
public class BaseData { | |||
@TableId(value = "id", type = IdType.AUTO) | |||
private Long id; | |||
/** | |||
* 商品编号 | |||
*/ | |||
@TableField("product_code") | |||
private String productCode; | |||
/** | |||
* 商品名称 | |||
*/ | |||
@TableField("product_name") | |||
private String productName; | |||
/** | |||
* 品牌 | |||
*/ | |||
@TableField("brand") | |||
private String brand; | |||
/** | |||
* 分类ID | |||
*/ | |||
@TableField("category_id") | |||
private Long categoryId; | |||
/** | |||
* 分类名称 | |||
*/ | |||
private String categoryName; | |||
/** | |||
* 分类规格JSON | |||
*/ | |||
@TableField("category_specs") | |||
private String categorySpecs; | |||
/** | |||
* 创建时间 | |||
*/ | |||
@TableField(value = "created_at", fill = FieldFill.INSERT) | |||
private LocalDateTime createdAt; | |||
/** | |||
* 更新时间 | |||
*/ | |||
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE) | |||
private LocalDateTime updatedAt; | |||
} |
@@ -0,0 +1,26 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import com.alibaba.excel.annotation.ExcelProperty; | |||
import lombok.Data; | |||
import java.time.LocalDateTime; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
@Data | |||
public class BaseDataExcelDTO { | |||
@ExcelProperty(value = "商品编码",index =0) | |||
private String productCode; | |||
@ExcelProperty(value = "商品名称",index =1) | |||
private String productName; | |||
@ExcelProperty(value = "品牌",index =2) | |||
private String brand; | |||
@ExcelProperty(value = "所属分类名称",index =3) | |||
private String categoryName; | |||
@ExcelProperty(value = "分类规格",index =4) | |||
private String categorySpecs; | |||
/* private LocalDateTime createdAt; | |||
private LocalDateTime updatedAt;*/ | |||
// 动态扩展字段(不确定) | |||
// private Map<String, String> categorySpecs = new HashMap<>(); | |||
} |
@@ -0,0 +1,18 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.time.LocalDateTime; | |||
@Data | |||
public class BaseDataVO { | |||
private Long id; | |||
private String productCode; | |||
private String productName; | |||
private String brand; | |||
private Long categoryId; | |||
private String categoryName; // 注意:这里是字符串 | |||
private String categorySpecs; | |||
private LocalDateTime createdAt; | |||
private LocalDateTime updatedAt; | |||
} |
@@ -0,0 +1,26 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.math.BigDecimal; | |||
/** | |||
* 基础统计数据 | |||
*/ | |||
@Data | |||
public class BasicStatsVO { | |||
/** 总记录数 */ | |||
private Integer totalRecords; | |||
/** 总销售数量 */ | |||
private BigDecimal totalQuantity; | |||
/** 总销售金额 */ | |||
private BigDecimal totalAmount; | |||
/** 不同商品数 */ | |||
private Integer uniqueProducts; | |||
/** 不同客户数 */ | |||
private Integer uniqueCustomers; | |||
/** 店铺分析用-不同店铺数 */ | |||
private Integer totalShops; | |||
/** 店铺分析用-不同品类数 */ | |||
private Integer totalCategories; | |||
} |
@@ -0,0 +1,17 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.math.BigDecimal; | |||
/** | |||
* 品牌销售数据 | |||
*/ | |||
@Data | |||
public class BrandDataVO { | |||
/** 品牌名称 */ | |||
private String brand; | |||
/** 销售金额 */ | |||
private Double amount; | |||
/** 销售数量 */ | |||
private Double quantity; | |||
} |
@@ -0,0 +1,51 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import com.baomidou.mybatisplus.annotation.*; | |||
import com.fasterxml.jackson.annotation.JsonProperty; | |||
import lombok.Data; | |||
import lombok.EqualsAndHashCode; | |||
import java.time.LocalDateTime; | |||
import java.util.List; | |||
/** | |||
* 分类实体类 | |||
*/ | |||
@Data | |||
@EqualsAndHashCode(callSuper = false) | |||
@TableName("zs_tjfx_categories") | |||
public class Category { | |||
@TableId(value = "id", type = IdType.AUTO) | |||
private Long id; | |||
/** | |||
* 分类名称 | |||
*/ | |||
@TableField("name") | |||
private String name; | |||
/** | |||
* 分类描述 | |||
*/ | |||
@TableField("description") | |||
private String description; | |||
/** | |||
* 字段配置JSON string 格式 | |||
*/ | |||
@TableField("field_config") | |||
// @JsonProperty("field_config") | |||
private String fieldConfig; | |||
/** | |||
* 创建时间 | |||
*/ | |||
@TableField(value = "created_at", fill = FieldFill.INSERT) | |||
private LocalDateTime createdAt; | |||
/** | |||
* 更新时间 | |||
*/ | |||
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE) | |||
private LocalDateTime updatedAt; | |||
} |
@@ -0,0 +1,18 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.util.List; | |||
import java.util.Map; | |||
/** | |||
* 分类销售分析数据 | |||
*/ | |||
@Data | |||
public class CategoryAnalysisVO { | |||
/** 各规格维度的图表数据,key为维度名 */ | |||
private Map<String, DimensionChartVO> dimensionCharts; | |||
/** 可用的规格维度列表 */ | |||
private List<String> availableDimensions; | |||
/** 当前分析的分类名称 */ | |||
private String currentCategory; | |||
} |
@@ -0,0 +1,17 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.math.BigDecimal; | |||
/** | |||
* 品类销售趋势数据 | |||
*/ | |||
@Data | |||
public class CategoryTrendVO { | |||
/** 品类名称 */ | |||
private String category; | |||
/** 销售金额 */ | |||
private Double amount; | |||
/** 销售数量 */ | |||
private Double quantity; | |||
} |
@@ -0,0 +1,17 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Data; | |||
import java.util.List; | |||
/** | |||
* 规格维度图表数据 | |||
*/ | |||
@Data | |||
@AllArgsConstructor | |||
public class DimensionChartVO { | |||
/** 按金额统计的维度数据列表 */ | |||
private List<DimensionStatsVO> amountData; | |||
/** 按数量统计的维度数据列表 */ | |||
private List<DimensionStatsVO> quantityData; | |||
} |
@@ -0,0 +1,29 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Data; | |||
import java.math.BigDecimal; | |||
/** | |||
* 规格维度统计数据 | |||
*/ | |||
@Data | |||
@AllArgsConstructor | |||
public class DimensionStatsVO { | |||
/** 维度值名称 */ | |||
private String name; | |||
/** 销售数量 */ | |||
private Double quantity; | |||
/** 销售金额 */ | |||
private Double amount; | |||
/* *//** 数量占比(百分比字符串) *//* | |||
private String quantityPercentage; | |||
*//** 金额占比(百分比字符串) *//* | |||
private String amountPercentage;*/ | |||
/** 数量或者 金额占比(百分比字符串) */ | |||
private String percentage; | |||
//价格或者销量 | |||
private Double value; | |||
} |
@@ -0,0 +1,12 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
@Data | |||
public class FieldConfig { | |||
private Long id; | |||
private String fieldName; | |||
private String displayLabel; | |||
private String fieldType; | |||
private Boolean isRequired; | |||
} |
@@ -0,0 +1,21 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.util.List; | |||
/** | |||
* 筛选选项数据 | |||
*/ | |||
@Data | |||
public class FilterOptionsVO { | |||
/** 所有分类列表 */ | |||
private List<String> categories; | |||
/** 所有规格列表 */ | |||
private List<String> categorySpecs; | |||
/** 所有客户列表 */ | |||
private List<String> customers; | |||
/** 所有店铺列表 */ | |||
private List<String> shops; | |||
/** 所有品牌列表 */ | |||
private List<String> brands; | |||
} |
@@ -0,0 +1,20 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Data; | |||
/** | |||
* 增长率数据(同比/环比) | |||
*/ | |||
@Data | |||
@AllArgsConstructor | |||
public class GrowthRatesVO { | |||
/** 金额同比增长率(%) */ | |||
private String amountYoY; | |||
/** 数量同比增长率(%) */ | |||
private String quantityYoY; | |||
/** 金额环比增长率(%) */ | |||
private String amountMoM; | |||
/** 数量环比增长率(%) */ | |||
private String quantityMoM; | |||
} |
@@ -0,0 +1,22 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.util.List; | |||
/** | |||
* 整体销售分析数据 | |||
*/ | |||
@Data | |||
public class OverallAnalysisVO { | |||
/** 基础统计数据 */ | |||
private BasicStatsVO basicStats; | |||
/** 同比/环比增长率 */ | |||
private GrowthRatesVO growthRates; | |||
/** 销售趋势数据 */ | |||
private List<TrendDataVO> trendData; | |||
/** TOP商品销售数据 */ | |||
private List<TopProductVO> topProducts; | |||
/** 品牌销售数据 */ | |||
private List<BrandDataVO> brandData; | |||
} |
@@ -0,0 +1,15 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.util.List; | |||
/** | |||
* 单品销售分析数据 | |||
*/ | |||
@Data | |||
public class ProductAnalysisVO { | |||
/** 单品每日销售排行数据 */ | |||
private List<RankingDataVO> rankingData; | |||
/** 单品销售趋势数据 */ | |||
private List<TrendDataVO> trendData; | |||
} |
@@ -0,0 +1,16 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Data; | |||
/** | |||
* 商品编码联想提示数据 | |||
*/ | |||
@Data | |||
@AllArgsConstructor | |||
public class ProductCodeSuggestionVO { | |||
/** 商品编码 */ | |||
private String value; | |||
/** 显示文本(商品名+规格-分类名) */ | |||
private String label; | |||
} |
@@ -0,0 +1,17 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.math.BigDecimal; | |||
/** | |||
* 单品每日销售排行数据 | |||
*/ | |||
@Data | |||
public class RankingDataVO { | |||
/** 日期 */ | |||
private String date; | |||
/** 销售金额 */ | |||
private BigDecimal totalAmount; | |||
/** 销售数量 */ | |||
private BigDecimal totalQuantity; | |||
} |
@@ -0,0 +1,17 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.math.BigDecimal; | |||
/** | |||
* 店铺销售金额数据 | |||
*/ | |||
@Data | |||
public class ShopAmountVO { | |||
/** 店铺名称 */ | |||
private String shopName; | |||
/** 销售金额 */ | |||
private Double amount; | |||
/** 金额占比(百分比字符串) */ | |||
private String percentage; | |||
} |
@@ -0,0 +1,19 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.util.List; | |||
/** | |||
* 店铺销售分析数据 | |||
*/ | |||
@Data | |||
public class ShopAnalysisVO { | |||
/** 基础统计数据 */ | |||
private BasicStatsVO basicStats; | |||
/** 各店铺销售金额数据 */ | |||
private List<ShopAmountVO> shopAmountData; | |||
/** 各店铺销售数量数据 */ | |||
private List<ShopQuantityVO> shopQuantityData; | |||
/** 各品类销售趋势数据 */ | |||
private List<CategoryTrendVO> categoryTrendData; | |||
} |
@@ -0,0 +1,43 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import com.baomidou.mybatisplus.annotation.*; | |||
import lombok.Data; | |||
import lombok.EqualsAndHashCode; | |||
import java.time.LocalDateTime; | |||
/** | |||
* 店铺客户关联实体类 | |||
*/ | |||
@Data | |||
@EqualsAndHashCode(callSuper = false) | |||
@TableName("zs_tjfx_shop_customer") | |||
public class ShopCustomer { | |||
@TableId(value = "id", type = IdType.AUTO) | |||
private Long id; | |||
/** | |||
* 店铺名称 | |||
*/ | |||
@TableField("shop_name") | |||
private String shopName; | |||
/** | |||
* 客户名称 | |||
*/ | |||
@TableField("customer_name") | |||
private String customerName; | |||
/** | |||
* 创建时间 | |||
*/ | |||
@TableField(value = "created_at", fill = FieldFill.INSERT) | |||
private LocalDateTime createdAt; | |||
/** | |||
* 更新时间 | |||
*/ | |||
@TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE) | |||
private LocalDateTime updatedAt; | |||
} |
@@ -0,0 +1,14 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import com.alibaba.excel.annotation.ExcelProperty; | |||
import lombok.Data; | |||
@Data | |||
public class ShopCustomerExcelDTO { | |||
@ExcelProperty(value = "店铺名称",index =0) | |||
private String shopName; | |||
@ExcelProperty(value = "客户名称",index =1) | |||
private String customerName; | |||
} |
@@ -0,0 +1,17 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.math.BigDecimal; | |||
/** | |||
* 店铺销售数量数据 | |||
*/ | |||
@Data | |||
public class ShopQuantityVO { | |||
/** 店铺名称 */ | |||
private String shopName; | |||
/** 销售数量 */ | |||
private Double quantity; | |||
/** 数量占比(百分比字符串) */ | |||
private String percentage; | |||
} |
@@ -0,0 +1,19 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.math.BigDecimal; | |||
/** | |||
* TOP商品销售数据 | |||
*/ | |||
@Data | |||
public class TopProductVO { | |||
/** 商品编码 */ | |||
private String productCode; | |||
/** 商品名称 */ | |||
private String productName; | |||
/** 销售数量 */ | |||
private Double quantity; | |||
/** 销售金额 */ | |||
private Double amount; | |||
} |
@@ -0,0 +1,17 @@ | |||
package com.ruoyi.tjfx.entity; | |||
import lombok.Data; | |||
import java.math.BigDecimal; | |||
/** | |||
* 销售趋势数据 | |||
*/ | |||
@Data | |||
public class TrendDataVO { | |||
/** 日期 */ | |||
private String date; | |||
/** 销售金额(单品趋势用) */ | |||
private Double amount; | |||
/** 销售数量(单品趋势用) */ | |||
private Double quantity; | |||
} |
@@ -0,0 +1,56 @@ | |||
package com.ruoyi.tjfx.mapper; | |||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |||
import com.baomidou.mybatisplus.core.metadata.IPage; | |||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
import com.ruoyi.tjfx.entity.AnalysisData; | |||
import org.apache.ibatis.annotations.Mapper; | |||
import org.apache.ibatis.annotations.Param; | |||
import java.time.LocalDate; | |||
import java.util.List; | |||
/** | |||
* 分析数据Mapper接口 | |||
*/ | |||
@Mapper | |||
public interface AnalysisDataMapper extends BaseMapper<AnalysisData> { | |||
/** | |||
* 分页查询分析数据 | |||
*/ | |||
IPage<AnalysisData> selectPageWithConditions(Page<AnalysisData> page, | |||
@Param("productCode") String productCode, | |||
@Param("productName") String productName, | |||
@Param("customerName") String customerName, | |||
@Param("shopName") String shopName, | |||
@Param("status") Integer status, | |||
@Param("startDate") LocalDate startDate, | |||
@Param("endDate") LocalDate endDate); | |||
/** | |||
* 根据条件查询所有数据(用于导出) | |||
*/ | |||
List<AnalysisData> selectAllWithConditions(@Param("productCode") String productCode, | |||
@Param("productName") String productName, | |||
@Param("customerName") String customerName, | |||
@Param("shopName") String shopName, | |||
@Param("status") Integer status, | |||
@Param("startDate") LocalDate startDate, | |||
@Param("endDate") LocalDate endDate); | |||
/** | |||
* 批量插入数据 | |||
*/ | |||
int batchInsert(@Param("list") List<AnalysisData> list); | |||
/** | |||
* 根据唯一条件查询记录 | |||
*/ | |||
AnalysisData selectByUniqueCondition(@Param("date") LocalDate date, | |||
@Param("shopName") String shopName, | |||
@Param("productCode") String productCode, | |||
@Param("deliveryType") String deliveryType, | |||
@Param("orderNumber") String orderNumber, | |||
@Param("rowNumber") Integer rowNumber); | |||
} |
@@ -0,0 +1,38 @@ | |||
package com.ruoyi.tjfx.mapper; | |||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |||
import com.baomidou.mybatisplus.core.metadata.IPage; | |||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
import com.ruoyi.tjfx.entity.BaseData; | |||
import com.ruoyi.tjfx.entity.BaseDataVO; | |||
import org.apache.ibatis.annotations.Mapper; | |||
import org.apache.ibatis.annotations.Param; | |||
import java.util.List; | |||
/** | |||
* 基准数据Mapper接口 | |||
*/ | |||
@Mapper | |||
public interface BaseDataMapper extends BaseMapper<BaseData> { | |||
/** | |||
* 根据商品编号和分类ID查询基准数据 | |||
*/ | |||
BaseData selectByProductCodeAndCategoryId(@Param("productCode") String productCode, | |||
@Param("categoryId") Long categoryId); | |||
BaseDataVO selectByProductCode(@Param("productCode") String productCode); | |||
List<BaseDataVO> findByCategoryId( @Param("categoryId") Long categoryId); | |||
List<String> selectExistingProductCodes(@Param("codes") List<String> codes); | |||
IPage<BaseDataVO> selectPageWithJoin( | |||
Page<BaseDataVO> page, | |||
@Param("productCode") String productCode, | |||
@Param("productName") String productName, | |||
@Param("categoryId") Long categoryId | |||
); | |||
void insertBatch(@Param("list") List<BaseData> list); | |||
} |
@@ -0,0 +1,12 @@ | |||
package com.ruoyi.tjfx.mapper; | |||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |||
import com.ruoyi.tjfx.entity.Category; | |||
import org.apache.ibatis.annotations.Mapper; | |||
/** | |||
* 分类Mapper接口 | |||
*/ | |||
@Mapper | |||
public interface CategoryMapper extends BaseMapper<Category> { | |||
} |
@@ -0,0 +1,35 @@ | |||
package com.ruoyi.tjfx.mapper; | |||
import com.ruoyi.tjfx.entity.*; | |||
import org.apache.ibatis.annotations.Mapper; | |||
import java.util.List; | |||
import java.util.Map; | |||
@Mapper | |||
public interface ReportMapper { | |||
BasicStatsVO selectBasicStats(Map<String, Object> params); | |||
BasicStatsVO selectLastYearStats(Map<String, Object> params); | |||
BasicStatsVO selectPrevStats(Map<String, Object> params); | |||
List<TrendDataVO> selectTrendData(Map<String, Object> params); | |||
List<TopProductVO> selectTopProducts(Map<String, Object> params); | |||
List<BrandDataVO> selectBrandData(Map<String, Object> params); | |||
List<RankingDataVO> selectProductRanking(Map<String, Object> params); | |||
List<TrendDataVO> selectProductTrend(Map<String, Object> params); | |||
BasicStatsVO selectShopBasicStats(Map<String, Object> params); | |||
List<ShopAmountVO> selectShopAmountData(Map<String, Object> params); | |||
List<ShopQuantityVO> selectShopQuantityData(Map<String, Object> params); | |||
List<CategoryTrendVO> selectCategoryTrendData(Map<String, Object> params); | |||
List<Map<String, Object>> selectSpecsRawData(Map<String, Object> params); | |||
List<String> selectCategories(); | |||
List<String> selectCategorySpecs(); | |||
List<String> selectCustomers(); | |||
List<String> selectShops(); | |||
List<String> selectBrands(); | |||
List<Map<String, Object>> selectProductCodeSuggestions(String keyword); | |||
} |
@@ -0,0 +1,18 @@ | |||
package com.ruoyi.tjfx.mapper; | |||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |||
import com.ruoyi.tjfx.entity.ShopCustomer; | |||
import org.apache.ibatis.annotations.Mapper; | |||
import org.apache.ibatis.annotations.Param; | |||
/** | |||
* 店铺客户Mapper接口 | |||
*/ | |||
@Mapper | |||
public interface ShopCustomerMapper extends BaseMapper<ShopCustomer> { | |||
/** | |||
* 根据店铺名称查询客户名称 | |||
*/ | |||
ShopCustomer selectByShopName(@Param("shopName") String shopName); | |||
} |
@@ -0,0 +1,76 @@ | |||
package com.ruoyi.tjfx.service; | |||
import com.baomidou.mybatisplus.core.metadata.IPage; | |||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.AnalysisData; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.time.LocalDate; | |||
import java.util.List; | |||
import java.util.Map; | |||
/** | |||
* 分析数据Service接口 | |||
*/ | |||
public interface AnalysisDataService { | |||
/** | |||
* 分页查询分析数据 | |||
*/ | |||
PageResponse<AnalysisData> getPage(Integer page, Integer pageSize, String productCode, | |||
String productName, String customerName, String shopName, | |||
Integer status, LocalDate startDate, LocalDate endDate); | |||
/** | |||
* 根据ID获取分析数据详情 | |||
*/ | |||
AnalysisData getById(Long id); | |||
/** | |||
* 更新分析数据 | |||
*/ | |||
void updateById(Long id, AnalysisData analysisData); | |||
/** | |||
* 删除分析数据 | |||
*/ | |||
void deleteById(Long id); | |||
/** | |||
* 批量删除分析数据 | |||
*/ | |||
void deleteBatchByIds(List<Long> ids); | |||
/** | |||
* 导出Excel(支持分页) | |||
*/ | |||
void exportExcel(HttpServletResponse response, String productCode, String productName, | |||
String customerName, String shopName, Integer status, | |||
LocalDate startDate, LocalDate endDate, Integer page, Integer pageSize); | |||
/** | |||
* 导入Excel数据(单文件) | |||
*/ | |||
Map<String, Object> importExcel(String filePath, Long categoryId); | |||
/** | |||
* 批量导入Excel数据 | |||
*/ | |||
Map<String, Object> importBatchExcel(List<String> filePaths, Long categoryId); | |||
/** | |||
* 导入FBA数据(单文件) | |||
*/ | |||
Map<String, Object> importFbaExcel(String filePath, String shopName); | |||
/** | |||
* 批量导入FBA数据 | |||
*/ | |||
Map<String, Object> importBatchFbaExcel(List<String> filePaths, String shopName); | |||
/** | |||
* 保存导入的数据 | |||
*/ | |||
Map<String, Object> saveImportedData(List<AnalysisData> dataList); | |||
} |
@@ -0,0 +1,26 @@ | |||
package com.ruoyi.tjfx.service; | |||
import com.ruoyi.tjfx.common.ApiResponse; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.BaseData; | |||
import com.ruoyi.tjfx.entity.BaseDataVO; | |||
import org.springframework.web.multipart.MultipartFile; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.util.List; | |||
public interface BaseDataService { | |||
List<BaseData> listAll(); | |||
BaseData getById(Long id); | |||
void add(BaseData baseData); | |||
void update(Long id, BaseData baseData); | |||
void delete(Long id); | |||
void deleteBatch(List<Long> ids); | |||
// 分页查询 | |||
PageResponse<BaseDataVO> getPage(Integer page, Integer pageSize, String productCode, String productName, Long categoryId); | |||
void importExcel(MultipartFile file, Long categoryId); | |||
void exportExcel(Long categoryId, HttpServletResponse response); | |||
} |
@@ -0,0 +1,18 @@ | |||
package com.ruoyi.tjfx.service; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.Category; | |||
import java.util.List; | |||
public interface CategoryService { | |||
List<Category> getAllSimple(); | |||
List<Category> listAll(); | |||
Category getById(Long id); | |||
void add(Category category); | |||
void update(Long id, Category category); | |||
void delete(Long id); | |||
void addBatch(List<Category> categorys); | |||
// 分页查询 | |||
PageResponse<Category> getPage(Integer page, Integer pageSize, String name); | |||
} |
@@ -0,0 +1,14 @@ | |||
package com.ruoyi.tjfx.service; | |||
import com.ruoyi.tjfx.entity.*; | |||
import java.util.List; | |||
public interface ReportService { | |||
OverallAnalysisVO overallAnalysis(String startDate, String endDate, String category, String categorySpecs, String customer, String shop, String brand, String productCode); | |||
ProductAnalysisVO productAnalysis(String startDate, String endDate, String productCode); | |||
ShopAnalysisVO shopAnalysis(String startDate, String endDate, String shop); | |||
CategoryAnalysisVO categoryAnalysis(String startDate, String endDate, String category); | |||
FilterOptionsVO filterOptions(); | |||
List<ProductCodeSuggestionVO> productCodeSuggestions(String keyword); | |||
} |
@@ -0,0 +1,25 @@ | |||
package com.ruoyi.tjfx.service; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.ShopCustomer; | |||
import org.springframework.web.multipart.MultipartFile; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.util.List; | |||
import java.util.Map; | |||
public interface ShopCustomerService { | |||
List<ShopCustomer> listAll(); | |||
ShopCustomer getById(Long id); | |||
void add(ShopCustomer shopCustomer); | |||
void update(Long id, ShopCustomer shopCustomer); | |||
void delete(Long id); | |||
void deleteBatch(List<Long> ids); | |||
// 分页查询 | |||
PageResponse<ShopCustomer> getPage(Integer page, Integer pageSize, String shopName, String customerName); | |||
Map<String, Object> importExcel(MultipartFile file); | |||
void exportExcel(HttpServletResponse response); | |||
} |
@@ -0,0 +1,639 @@ | |||
package com.ruoyi.tjfx.service.impl; | |||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |||
import com.baomidou.mybatisplus.core.metadata.IPage; | |||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.*; | |||
import com.ruoyi.tjfx.mapper.AnalysisDataMapper; | |||
import com.ruoyi.tjfx.mapper.BaseDataMapper; | |||
import com.ruoyi.tjfx.mapper.CategoryMapper; | |||
import com.ruoyi.tjfx.mapper.ShopCustomerMapper; | |||
import com.ruoyi.tjfx.service.AnalysisDataService; | |||
import com.ruoyi.tjfx.util.ExcelUtil; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
import org.springframework.transaction.annotation.Transactional; | |||
import org.springframework.util.StringUtils; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.math.BigDecimal; | |||
import java.time.LocalDate; | |||
import java.util.*; | |||
import java.util.stream.Collectors; | |||
/** | |||
* 分析数据Service实现类 | |||
*/ | |||
@Slf4j | |||
@Service | |||
public class AnalysisDataServiceImpl implements AnalysisDataService { | |||
@Autowired | |||
private AnalysisDataMapper analysisDataMapper; | |||
@Autowired | |||
private CategoryMapper categoryMapper; | |||
@Autowired | |||
private BaseDataMapper baseDataMapper; | |||
@Autowired | |||
private ShopCustomerMapper shopCustomerMapper; | |||
@Autowired | |||
private ExcelUtil excelUtil; | |||
@Override | |||
public PageResponse<AnalysisData> getPage(Integer page, Integer pageSize, String productCode, | |||
String productName, String customerName, String shopName, | |||
Integer status, LocalDate startDate, LocalDate endDate) { | |||
Page<AnalysisData> pageParam = new Page<>(page, pageSize); | |||
IPage<AnalysisData> result = analysisDataMapper.selectPageWithConditions(pageParam, | |||
productCode, productName, customerName, shopName, status, startDate, endDate); | |||
PageResponse.Pagination pagination = new PageResponse.Pagination( | |||
result.getTotal(), page, pageSize); | |||
return new PageResponse<>(result.getRecords(), pagination); | |||
} | |||
@Override | |||
public AnalysisData getById(Long id) { | |||
return analysisDataMapper.selectById(id); | |||
} | |||
@Override | |||
@Transactional | |||
public void updateById(Long id, AnalysisData analysisData) { | |||
AnalysisData existing = analysisDataMapper.selectById(id); | |||
if (existing == null) { | |||
throw new RuntimeException("分析数据不存在"); | |||
} | |||
analysisData.setId(id); | |||
analysisDataMapper.updateById(analysisData); | |||
} | |||
@Override | |||
@Transactional | |||
public void deleteById(Long id) { | |||
AnalysisData existing = analysisDataMapper.selectById(id); | |||
if (existing == null) { | |||
throw new RuntimeException("分析数据不存在"); | |||
} | |||
analysisDataMapper.deleteById(id); | |||
} | |||
@Override | |||
@Transactional | |||
public void deleteBatchByIds(List<Long> ids) { | |||
if (ids == null || ids.isEmpty()) { | |||
throw new RuntimeException("请选择要删除的数据"); | |||
} | |||
analysisDataMapper.deleteBatchIds(ids); | |||
} | |||
@Override | |||
public void exportExcel(HttpServletResponse response, String productCode, String productName, | |||
String customerName, String shopName, Integer status, | |||
LocalDate startDate, LocalDate endDate, Integer page, Integer pageSize) { | |||
// 如果page和pageSize为null,则导出全部数据 | |||
if (page == null || pageSize == null) { | |||
List<AnalysisData> dataList = analysisDataMapper.selectAllWithConditions( | |||
productCode, productName, customerName, shopName, status, startDate, endDate); | |||
if (dataList.isEmpty()) { | |||
throw new RuntimeException("暂无数据可导出"); | |||
} | |||
// 按分类分组 | |||
Map<String, List<AnalysisData>> groupedData = dataList.stream() | |||
.collect(Collectors.groupingBy(data -> | |||
StringUtils.hasText(data.getCategory()) ? data.getCategory() : "未分类")); | |||
excelUtil.exportAnalysisData(response, groupedData); | |||
} else { | |||
// 分页导出 | |||
Page<AnalysisData> pageParam = new Page<>(page, pageSize); | |||
// 构建查询条件 | |||
QueryWrapper<AnalysisData> queryWrapper = new QueryWrapper<>(); | |||
if (StringUtils.hasText(productCode)) { | |||
queryWrapper.like("product_code", productCode); | |||
} | |||
if (StringUtils.hasText(productName)) { | |||
queryWrapper.like("product_name", productName); | |||
} | |||
if (StringUtils.hasText(customerName)) { | |||
queryWrapper.like("customer_name", customerName); | |||
} | |||
if (StringUtils.hasText(shopName)) { | |||
queryWrapper.like("shop_name", shopName); | |||
} | |||
if (status != null) { | |||
queryWrapper.eq("status", status); | |||
} | |||
if (startDate != null) { | |||
queryWrapper.ge("date", startDate); | |||
} | |||
if (endDate != null) { | |||
queryWrapper.le("date", endDate); | |||
} | |||
queryWrapper.orderByDesc("id"); | |||
// 执行分页查询 | |||
IPage<AnalysisData> pageResult = analysisDataMapper.selectPage(pageParam, queryWrapper); | |||
if (pageResult.getRecords().isEmpty()) { | |||
throw new RuntimeException("暂无数据可导出"); | |||
} | |||
// 按分类分组 | |||
Map<String, List<AnalysisData>> groupedData = pageResult.getRecords().stream() | |||
.collect(Collectors.groupingBy(data -> | |||
StringUtils.hasText(data.getCategory()) ? data.getCategory() : "未分类")); | |||
excelUtil.exportAnalysisData(response, groupedData); | |||
} | |||
} | |||
@Override | |||
public Map<String, Object> importExcel(String filePath, Long categoryId) { | |||
// 验证分类是否存在 | |||
Category category = categoryMapper.selectById(categoryId); | |||
if (category == null) { | |||
throw new RuntimeException("分类不存在"); | |||
} | |||
// 读取Excel文件 | |||
List<Map<String, Object>> excelData = excelUtil.readExcel(filePath); | |||
if (excelData.isEmpty()) { | |||
throw new RuntimeException("Excel文件为空或格式不正确"); | |||
} | |||
return processExcelData(excelData, category, filePath); | |||
} | |||
@Override | |||
public Map<String, Object> importBatchExcel(List<String> filePaths, Long categoryId) { | |||
// 验证分类是否存在 | |||
Category category = categoryMapper.selectById(categoryId); | |||
if (category == null) { | |||
throw new RuntimeException("分类不存在"); | |||
} | |||
List<AnalysisData> allProcessedData = new ArrayList<>(); | |||
List<String> allErrors = new ArrayList<>(); | |||
int totalRows = 0; | |||
for (String filePath : filePaths) { | |||
// 1. 读取 Excel 数据(第一行为表头) | |||
/* List<ShopCustomerExcelDTO> excelDTOList = EasyExcel.read(file.getInputStream()) | |||
.head(ShopCustomerExcelDTO.class).headRowNumber(1).sheet().doReadSync();*/ | |||
try { | |||
List<Map<String, Object>> excelData = excelUtil.readExcel(filePath); | |||
if (excelData.isEmpty()) { | |||
allErrors.add("文件为空或格式不正确: " + filePath); | |||
continue; | |||
} | |||
//删除里面都是null或者""的map | |||
excelData.removeIf(map -> | |||
map.values().stream() | |||
.allMatch(value -> value == null || "".equals(value)) | |||
); | |||
totalRows += excelData.size(); | |||
Map<String, Object> result = processExcelData(excelData, category, filePath); | |||
@SuppressWarnings("unchecked") | |||
List<AnalysisData> processedData = (List<AnalysisData>) result.get("data"); | |||
@SuppressWarnings("unchecked") | |||
List<String> errors = (List<String>) result.get("errorDetails"); | |||
allProcessedData.addAll(processedData); | |||
allErrors.addAll(errors); | |||
} catch (Exception e) { | |||
allErrors.add("处理文件失败: " + filePath + " - " + e.getMessage()); | |||
} | |||
} | |||
Map<String, Object> result = new HashMap<>(); | |||
result.put("total", totalRows); | |||
result.put("processed", allProcessedData.size()); | |||
result.put("errors", allErrors.size()); | |||
result.put("data", allProcessedData); | |||
result.put("errorDetails", allErrors.size() > 0 ? allErrors.subList(0, Math.min(20, allErrors.size())) : new ArrayList<>()); | |||
result.put("filesProcessed", filePaths.size()); | |||
return result; | |||
} | |||
@Override | |||
public Map<String, Object> importFbaExcel(String filePath, String shopName) { | |||
// 验证店铺是否存在 | |||
ShopCustomer shopCustomer = shopCustomerMapper.selectByShopName(shopName); | |||
if (shopCustomer == null) { | |||
throw new RuntimeException("店铺不存在"); | |||
} | |||
// 读取Excel文件 | |||
List<Map<String, Object>> excelData = excelUtil.readFbaExcel(filePath); | |||
if (excelData.isEmpty()) { | |||
throw new RuntimeException("Excel文件为空或格式不正确"); | |||
} | |||
return processFbaData(excelData, shopCustomer, filePath); | |||
} | |||
@Override | |||
public Map<String, Object> importBatchFbaExcel(List<String> filePaths, String shopName) { | |||
// 验证店铺是否存在 | |||
ShopCustomer shopCustomer = shopCustomerMapper.selectByShopName(shopName); | |||
if (shopCustomer == null) { | |||
throw new RuntimeException("店铺不存在"); | |||
} | |||
List<AnalysisData> allProcessedData = new ArrayList<>(); | |||
List<String> allErrors = new ArrayList<>(); | |||
int totalRows = 0; | |||
for (String filePath : filePaths) { | |||
try { | |||
List<Map<String, Object>> excelData = excelUtil.readFbaExcel(filePath); | |||
if (excelData.isEmpty()) { | |||
allErrors.add("文件为空或格式不正确: " + filePath); | |||
continue; | |||
} | |||
totalRows += excelData.size(); | |||
Map<String, Object> result = processFbaData(excelData, shopCustomer, filePath); | |||
@SuppressWarnings("unchecked") | |||
List<AnalysisData> processedData = (List<AnalysisData>) result.get("data"); | |||
@SuppressWarnings("unchecked") | |||
List<String> errors = (List<String>) result.get("errorDetails"); | |||
allProcessedData.addAll(processedData); | |||
allErrors.addAll(errors); | |||
} catch (Exception e) { | |||
allErrors.add("处理文件失败: " + filePath + " - " + e.getMessage()); | |||
} | |||
} | |||
Map<String, Object> result = new HashMap<>(); | |||
result.put("total", totalRows); | |||
result.put("processed", allProcessedData.size()); | |||
result.put("errors", allErrors.size()); | |||
result.put("data", allProcessedData); | |||
result.put("errorDetails", allErrors.size() > 0 ? allErrors.subList(0, Math.min(20, allErrors.size())) : new ArrayList<>()); | |||
result.put("filesProcessed", filePaths.size()); | |||
return result; | |||
} | |||
@Override | |||
@Transactional | |||
public Map<String, Object> saveImportedData(List<AnalysisData> dataList) { | |||
if (dataList == null || dataList.isEmpty()) { | |||
throw new RuntimeException("没有可保存的数据"); | |||
} | |||
int insertedCount = 0; | |||
int updatedCount = 0; | |||
List<String> errors = new ArrayList<>(); | |||
for (AnalysisData data : dataList) { | |||
try { | |||
// 检查是否存在相同的记录 | |||
AnalysisData existing = analysisDataMapper.selectByUniqueCondition( | |||
data.getDate(), data.getShopName(), data.getProductCode(), | |||
data.getDeliveryType(), data.getOrderNumber(), data.getRowNumber()); | |||
if (existing != null) { | |||
// 更新现有记录 | |||
data.setId(existing.getId()); | |||
analysisDataMapper.updateById(data); | |||
updatedCount++; | |||
} else { | |||
// 插入新记录 | |||
analysisDataMapper.insert(data); | |||
insertedCount++; | |||
} | |||
} catch (Exception e) { | |||
errors.add("保存数据失败: " + e.getMessage()); | |||
} | |||
} | |||
Map<String, Object> result = new HashMap<>(); | |||
result.put("total", dataList.size()); | |||
result.put("saved", insertedCount + updatedCount); | |||
result.put("inserted", insertedCount); | |||
result.put("updated", updatedCount); | |||
result.put("failed", errors.size()); | |||
result.put("errors", errors.size() > 0 ? errors.subList(0, Math.min(10, errors.size())) : new ArrayList<>()); | |||
return result; | |||
} | |||
/** | |||
* 处理Excel数据 | |||
*/ | |||
private Map<String, Object> processExcelData(List<Map<String, Object>> excelData, | |||
Category category, String fileName) { | |||
List<AnalysisData> processedData = new ArrayList<>(); | |||
List<String> errors = new ArrayList<>(); | |||
int filteredCount = 0; | |||
for (int i = 0; i < excelData.size(); i++) { | |||
Map<String, Object> row = excelData.get(i); | |||
int rowIndex = i + 2; // Excel行号 | |||
try { | |||
// 检查是否需要过滤 | |||
if (shouldFilterData(row)) { | |||
filteredCount++; | |||
continue; | |||
} | |||
// 验证必填字段 | |||
if (!validateRequiredFields(row, rowIndex, fileName, errors)) { | |||
continue; | |||
} | |||
// 处理数据 | |||
AnalysisData analysisData = processRowData(row, category, rowIndex, fileName); | |||
if (analysisData != null) { | |||
processedData.add(analysisData); | |||
} | |||
} catch (Exception e) { | |||
errors.add(String.format("文件[%s]第(%d)行: 数据处理失败 - %s", fileName, rowIndex, e.getMessage())); | |||
} | |||
} | |||
Map<String, Object> result = new HashMap<>(); | |||
result.put("total", excelData.size()); | |||
result.put("processed", processedData.size()); | |||
result.put("filtered", filteredCount); | |||
result.put("errors", errors.size()); | |||
result.put("data", processedData); | |||
result.put("errorDetails", errors.size() > 0 ? errors.subList(0, Math.min(10, errors.size())) : new ArrayList<>()); | |||
return result; | |||
} | |||
/** | |||
* 处理FBA数据 | |||
*/ | |||
private Map<String, Object> processFbaData(List<Map<String, Object>> excelData, | |||
ShopCustomer shopCustomer, String fileName) { | |||
List<AnalysisData> processedData = new ArrayList<>(); | |||
List<String> errors = new ArrayList<>(); | |||
for (int i = 0; i < excelData.size(); i++) { | |||
Map<String, Object> row = excelData.get(i); | |||
int rowIndex = i + 2; // Excel行号 | |||
try { | |||
// 验证必填字段 | |||
if (!validateFbaRequiredFields(row, rowIndex, fileName, errors)) { | |||
continue; | |||
} | |||
// 处理FBA数据 | |||
AnalysisData analysisData = processFbaRowData(row, shopCustomer, rowIndex, fileName); | |||
if (analysisData != null) { | |||
processedData.add(analysisData); | |||
} | |||
} catch (Exception e) { | |||
errors.add(String.format("文件[%s]第(%d)行: 数据处理失败 - %s", fileName, rowIndex, e.getMessage())); | |||
} | |||
} | |||
Map<String, Object> result = new HashMap<>(); | |||
result.put("total", excelData.size()); | |||
result.put("processed", processedData.size()); | |||
result.put("filtered", 0); // FBA数据不需要过滤 | |||
result.put("errors", errors.size()); | |||
result.put("data", processedData); | |||
result.put("errorDetails", errors.size() > 0 ? errors.subList(0, Math.min(10, errors.size())) : new ArrayList<>()); | |||
return result; | |||
} | |||
/** | |||
* 检查是否需要过滤数据 | |||
*/ | |||
private boolean shouldFilterData(Map<String, Object> row) { | |||
String destination = String.valueOf(row.getOrDefault("目的地", "")); | |||
String deliveryType = String.valueOf(row.getOrDefault("出库类型", "")); | |||
String remarks = String.valueOf(row.getOrDefault("备注", "")); | |||
// 过滤条件 | |||
return (destination.equals("SPD") && deliveryType.equals("批发出库") && remarks.trim().isEmpty()) || | |||
deliveryType.equals("库内换码") || | |||
deliveryType.equals("返品交互") || | |||
destination.equals("Amazon") || | |||
(destination.equals("開元") && deliveryType.equals("批发出库") && remarks.trim().isEmpty()); | |||
} | |||
/** | |||
* 验证必填字段 | |||
*/ | |||
private boolean validateRequiredFields(Map<String, Object> row, int rowIndex, | |||
String fileName, List<String> errors) { | |||
if (!row.containsKey("店铺名称") || StringUtils.isEmpty(row.get("店铺名称"))) { | |||
errors.add(String.format("文件[%s]第(%d)行: 店铺名称不能为空", fileName, rowIndex)); | |||
return false; | |||
} | |||
if (!row.containsKey("商品编号") || StringUtils.isEmpty(row.get("商品编号"))) { | |||
errors.add(String.format("文件[%s]第(%d)行: 商品编号不能为空", fileName, rowIndex)); | |||
return false; | |||
} | |||
if (!row.containsKey("商品名称") || StringUtils.isEmpty(row.get("商品名称"))) { | |||
errors.add(String.format("文件[%s]第(%d)行: 商品名称不能为空", fileName, rowIndex)); | |||
return false; | |||
} | |||
if (!row.containsKey("数量") || StringUtils.isEmpty(row.get("数量"))) { | |||
errors.add(String.format("文件[%s]第(%d)行: 数量不能为空", fileName, rowIndex)); | |||
return false; | |||
} | |||
if (!row.containsKey("单价") || StringUtils.isEmpty(row.get("单价"))) { | |||
errors.add(String.format("文件[%s]第(%d)行: 单价不能为空", fileName, rowIndex)); | |||
return false; | |||
} | |||
return true; | |||
} | |||
/** | |||
* 验证FBA必填字段 | |||
*/ | |||
private boolean validateFbaRequiredFields(Map<String, Object> row, int rowIndex, | |||
String fileName, List<String> errors) { | |||
if (!row.containsKey("出荷日") || StringUtils.isEmpty(row.get("出荷日"))) { | |||
errors.add(String.format("文件[%s]第(%d)行: 出荷日不能为空", fileName, rowIndex)); | |||
return false; | |||
} | |||
if (!row.containsKey("出品者SKU") || StringUtils.isEmpty(row.get("出品者SKU"))) { | |||
errors.add(String.format("文件[%s]第(%d)行: 出品者SKU不能为空", fileName, rowIndex)); | |||
return false; | |||
} | |||
if (!row.containsKey("数量") || StringUtils.isEmpty(row.get("数量"))) { | |||
errors.add(String.format("文件[%s]第(%d)行: 数量不能为空", fileName, rowIndex)); | |||
return false; | |||
} | |||
return true; | |||
} | |||
/** | |||
* 处理行数据 | |||
*/ | |||
private AnalysisData processRowData(Map<String, Object> row, Category category, | |||
int rowIndex, String fileName) { | |||
try { | |||
// 格式化日期 | |||
LocalDate formattedDate = excelUtil.processExcelDate(row.get("出库日期")); | |||
if (formattedDate == null) { | |||
throw new RuntimeException("日期格式不正确"); | |||
} | |||
String shopName = String.valueOf(row.get("店铺名称")).trim(); | |||
String productCode = String.valueOf(row.get("商品编号")).trim(); | |||
String productName = String.valueOf(row.get("商品名称")).trim(); | |||
// 匹配店铺客户关系 | |||
ShopCustomer shopCustomer = shopCustomerMapper.selectByShopName(shopName); | |||
String customerName = shopCustomer != null ? shopCustomer.getCustomerName() : | |||
String.valueOf(row.getOrDefault("客户名称", "")); | |||
// 匹配基准数据分类规格 | |||
BaseData baseData = baseDataMapper.selectByProductCodeAndCategoryId(productCode, category.getId()); | |||
String categoryName = baseData != null ? category.getName() : category.getName(); | |||
String categorySpecs = baseData != null ? baseData.getCategorySpecs() : ""; | |||
// 计算合计金额 | |||
BigDecimal quantity = new BigDecimal(String.valueOf(row.get("数量"))); | |||
BigDecimal unitPrice = new BigDecimal(String.valueOf(row.get("单价"))); | |||
BigDecimal shipping = new BigDecimal(String.valueOf(row.getOrDefault("送料", "0"))); | |||
BigDecimal cod = new BigDecimal(String.valueOf(row.getOrDefault("代引", "0"))); | |||
BigDecimal totalAmount = quantity.multiply(unitPrice).add(shipping).add(cod); | |||
// 确定状态 | |||
int status = 1; // 默认正常 | |||
if (StringUtils.isEmpty(customerName)) { | |||
status = 2; // 客户名称未匹配 | |||
} else if (baseData == null) { | |||
status = 3; // 分类规格未匹配 | |||
} | |||
AnalysisData analysisData = new AnalysisData(); | |||
analysisData.setDate(formattedDate); | |||
analysisData.setShopName(shopName); | |||
analysisData.setProductCode(productCode); | |||
analysisData.setProductName(productName); | |||
analysisData.setCustomerName(customerName); | |||
analysisData.setCategory(categoryName); | |||
analysisData.setCategorySpecs(categorySpecs); | |||
analysisData.setQuantity(quantity.intValue()); | |||
analysisData.setTotalAmount(totalAmount); | |||
analysisData.setSource("物流出库数据"); | |||
analysisData.setStatus(status); | |||
analysisData.setDeliveryType(String.valueOf(row.getOrDefault("出库类型", ""))); | |||
analysisData.setDestination(String.valueOf(row.getOrDefault("目的地", ""))); | |||
analysisData.setRemarks(String.valueOf(row.getOrDefault("备注", ""))); | |||
analysisData.setOrderNumber(String.valueOf(row.getOrDefault("注文番号", ""))); | |||
analysisData.setRowNumber(rowIndex); | |||
analysisData.setBrand(baseData != null ? baseData.getBrand() : ""); | |||
return analysisData; | |||
} catch (Exception e) { | |||
throw new RuntimeException("数据处理失败: " + e.getMessage()); | |||
} | |||
} | |||
/** | |||
* 处理FBA行数据 | |||
*/ | |||
private AnalysisData processFbaRowData(Map<String, Object> row, ShopCustomer shopCustomer, | |||
int rowIndex, String fileName) { | |||
try { | |||
// 处理日期 | |||
LocalDate formattedDate = excelUtil.processFbaDate(row.get("出荷日")); | |||
if (formattedDate == null) { | |||
throw new RuntimeException("日期格式不正确"); | |||
} | |||
// 处理SKU | |||
String rawSku = String.valueOf(row.get("出品者SKU")).trim(); | |||
String processedSku = excelUtil.processFbaSku(rawSku); | |||
if (StringUtils.isEmpty(processedSku)) { | |||
throw new RuntimeException("SKU格式不正确"); | |||
} | |||
// 匹配商品信息 | |||
BaseDataVO baseData = baseDataMapper.selectByProductCode(processedSku); | |||
String productName = baseData != null ? baseData.getProductName() : ""; | |||
String category = baseData != null ? baseData.getCategoryName() : "";//这里存名字 | |||
String categorySpecs = baseData != null ? baseData.getCategorySpecs() : ""; | |||
String brand = baseData != null ? baseData.getBrand() : ""; | |||
// 获取客户名称 | |||
String customerName = shopCustomer.getCustomerName(); | |||
// 计算合计金额 | |||
BigDecimal quantity = new BigDecimal(String.valueOf(row.get("数量"))); | |||
BigDecimal unitPrice = new BigDecimal(String.valueOf(row.getOrDefault("商品金額(商品1点ごと)", "0"))); | |||
BigDecimal shipping = new BigDecimal(String.valueOf(row.getOrDefault("配送料", "0"))); | |||
BigDecimal giftWrap = new BigDecimal(String.valueOf(row.getOrDefault("ギフト包装手数料", "0"))); | |||
BigDecimal totalAmount = quantity.multiply(unitPrice).add(shipping).add(giftWrap); | |||
// 确定状态 | |||
int status = 1; // 默认正常 | |||
if (StringUtils.isEmpty(customerName)) { | |||
status = 2; // 客户名称未匹配 | |||
} else if (baseData == null) { | |||
status = 3; // 分类规格未匹配 | |||
} | |||
// FBA 只保存正常的数据 | |||
if (status != 1) { | |||
return null; | |||
} | |||
AnalysisData analysisData = new AnalysisData(); | |||
analysisData.setDate(formattedDate); | |||
analysisData.setShopName(shopCustomer.getShopName()); | |||
analysisData.setProductCode(processedSku); | |||
analysisData.setProductName(productName); | |||
analysisData.setCustomerName(customerName); | |||
analysisData.setCategory(category); | |||
analysisData.setCategorySpecs(categorySpecs); | |||
analysisData.setQuantity(quantity.intValue()); | |||
analysisData.setTotalAmount(totalAmount); | |||
analysisData.setSource("FBA出库数据"); | |||
analysisData.setStatus(status); | |||
analysisData.setDeliveryType("FBA"); | |||
analysisData.setDestination(""); | |||
analysisData.setRemarks(""); | |||
analysisData.setOrderNumber(String.valueOf(row.getOrDefault("Amazon注文番号", ""))); | |||
analysisData.setRowNumber(rowIndex); | |||
analysisData.setBrand(brand); | |||
return analysisData; | |||
} catch (Exception e) { | |||
throw new RuntimeException("数据处理失败: " + e.getMessage()); | |||
} | |||
} | |||
} |
@@ -0,0 +1,328 @@ | |||
package com.ruoyi.tjfx.service.impl; | |||
import com.alibaba.excel.EasyExcel; | |||
import com.alibaba.excel.context.AnalysisContext; | |||
import com.alibaba.excel.event.AnalysisEventListener; | |||
import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder; | |||
import com.baomidou.mybatisplus.core.metadata.IPage; | |||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
import com.fasterxml.jackson.core.type.TypeReference; | |||
import com.fasterxml.jackson.databind.ObjectMapper; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.*; | |||
import com.ruoyi.tjfx.mapper.BaseDataMapper; | |||
import com.ruoyi.tjfx.mapper.CategoryMapper; | |||
import com.ruoyi.tjfx.service.BaseDataService; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
import org.springframework.transaction.annotation.Transactional; | |||
import org.springframework.web.multipart.MultipartFile; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.net.URLEncoder; | |||
import java.time.LocalDateTime; | |||
import java.util.*; | |||
import java.util.stream.Collectors; | |||
import org.apache.poi.ss.usermodel.Row; | |||
import org.apache.poi.ss.usermodel.Sheet; | |||
import org.apache.poi.ss.usermodel.Workbook; | |||
import org.apache.poi.xssf.usermodel.XSSFWorkbook; | |||
import com.fasterxml.jackson.databind.ObjectMapper; | |||
import com.fasterxml.jackson.core.type.TypeReference; | |||
@Service | |||
public class BaseDataServiceImpl implements BaseDataService { | |||
@Autowired | |||
private BaseDataMapper baseDataMapper; | |||
@Autowired | |||
private CategoryMapper categoryMapper; | |||
@Override | |||
public List<BaseData> listAll() { | |||
return baseDataMapper.selectList(null); | |||
} | |||
@Override | |||
public BaseData getById(Long id) { | |||
return baseDataMapper.selectById(id); | |||
} | |||
@Override | |||
public void add(BaseData baseData) { | |||
baseDataMapper.insert(baseData); | |||
} | |||
@Override | |||
public void update(Long id, BaseData baseData) { | |||
baseData.setId(id); | |||
baseDataMapper.updateById(baseData); | |||
} | |||
@Override | |||
public void delete(Long id) { | |||
baseDataMapper.deleteById(id); | |||
} | |||
@Override | |||
public void deleteBatch(List<Long> ids) { | |||
if (ids == null || ids.isEmpty()) { | |||
return; | |||
} | |||
baseDataMapper.deleteBatchIds(ids); | |||
} | |||
@Override | |||
public PageResponse<BaseDataVO> getPage(Integer page, Integer pageSize, String productCode, String productName, Long categoryId) { | |||
Page<BaseDataVO> pageParam = new Page<>(page, pageSize); | |||
// 执行 join 查询 | |||
IPage<BaseDataVO> pageResult = baseDataMapper.selectPageWithJoin( | |||
pageParam, productCode, productName, categoryId | |||
); | |||
// 构建 PageResponse(关键在这) | |||
PageResponse.Pagination pagination = new PageResponse.Pagination( | |||
pageResult.getTotal(), | |||
Math.toIntExact(pageResult.getCurrent()), // 当前页 | |||
Math.toIntExact(pageResult.getSize()) // 页大小 | |||
); | |||
return new PageResponse<>(pageResult.getRecords(), pagination); | |||
} | |||
@Override | |||
@Transactional | |||
public void importExcel(MultipartFile file, Long categoryId) { | |||
try { | |||
Category category = categoryMapper.selectById(categoryId); | |||
if (category == null) { | |||
throw new RuntimeException("分类不存在"); | |||
} | |||
// 初始化数据容器 | |||
List<Map<Integer, String>> dataList = new ArrayList<>(); | |||
List<String> headers = new ArrayList<>(); | |||
// 使用 EasyExcel 读取表头 + 数据 | |||
try (InputStream inputStream = file.getInputStream()) { | |||
EasyExcel.read(inputStream) | |||
.headRowNumber(0) // 表头行为第0行 | |||
.sheet() | |||
.registerReadListener(new AnalysisEventListener<Map<Integer, String>>() { | |||
private boolean isHeaderParsed = false; | |||
@Override | |||
public void invoke(Map<Integer, String> data, AnalysisContext context) { | |||
if (!isHeaderParsed) { | |||
for (Map.Entry<Integer, String> entry : data.entrySet()) { | |||
headers.add(entry.getValue()); | |||
} | |||
isHeaderParsed = true; | |||
} else { | |||
dataList.add(data); | |||
} | |||
} | |||
@Override | |||
public void doAfterAllAnalysed(AnalysisContext context) { | |||
// nothing | |||
} | |||
}).doRead(); | |||
} | |||
if (dataList.isEmpty()) { | |||
throw new RuntimeException("导入数据为空"); | |||
} | |||
List<BaseData> insertList = new ArrayList<>(); | |||
List<String> errors = new ArrayList<>(); | |||
Set<String> duplicateCheck = new HashSet<>(); | |||
// 固定列索引 | |||
int codeCol = 0; | |||
int nameCol = 1; | |||
int brandCol = 2; | |||
int categoryNameCol = 3; | |||
int extendStartCol = 4; // E列开始(0-based index) | |||
ObjectMapper objectMapper = new ObjectMapper(); | |||
for (int i = 0; i < dataList.size(); i++) { | |||
Map<Integer, String> rowMap = dataList.get(i); | |||
int rowNum = i + 2; | |||
String productCode = rowMap.getOrDefault(codeCol, "").trim(); | |||
String productName = rowMap.getOrDefault(nameCol, "").trim(); | |||
String brand = rowMap.getOrDefault(brandCol, "").trim(); | |||
String categoryNameVal = rowMap.getOrDefault(categoryNameCol, "").trim(); | |||
if (productCode.isEmpty() || productName.isEmpty()) { | |||
errors.add("第" + rowNum + "行:商品编号或商品名称不能为空"); | |||
continue; | |||
} | |||
if (!duplicateCheck.add(productCode)) { | |||
errors.add("第" + rowNum + "行:商品编号\"" + productCode + "\"在Excel中重复"); | |||
continue; | |||
} | |||
// 处理分类规格字段 | |||
Map<String, String> categorySpecsMap = new LinkedHashMap<>(); | |||
for (int col = extendStartCol; col < headers.size(); col++) { | |||
String key = headers.get(col); | |||
String value = rowMap.getOrDefault(col, "").trim(); | |||
categorySpecsMap.put(key, value); | |||
} | |||
BaseData data = new BaseData(); | |||
data.setProductCode(productCode); | |||
data.setProductName(productName); | |||
data.setBrand(brand); | |||
data.setCategoryId(categoryId); | |||
data.setCategoryName(categoryNameVal); | |||
data.setCategorySpecs(objectMapper.writeValueAsString(categorySpecsMap)); | |||
data.setCreatedAt(LocalDateTime.now()); | |||
data.setUpdatedAt(LocalDateTime.now()); | |||
insertList.add(data); | |||
} | |||
// 去重:数据库中已存在的商品编号 | |||
if (!insertList.isEmpty()) { | |||
List<String> existCodes = baseDataMapper.selectExistingProductCodes( | |||
insertList.stream().map(BaseData::getProductCode).collect(Collectors.toList()) | |||
); | |||
Set<String> existSet = new HashSet<>(existCodes); | |||
insertList = insertList.stream() | |||
.filter(item -> { | |||
if (existSet.contains(item.getProductCode())) { | |||
errors.add("商品编号\"" + item.getProductCode() + "\"已存在数据库中"); | |||
return false; | |||
} | |||
return true; | |||
}).collect(Collectors.toList()); | |||
} | |||
// 批量插入 | |||
if (!insertList.isEmpty()) { | |||
baseDataMapper.insertBatch(insertList); | |||
} | |||
if (!errors.isEmpty()) { | |||
throw new RuntimeException("导入完成,但存在以下错误:\n" + String.join("\n", errors)); | |||
} | |||
} catch (Exception e) { | |||
throw new RuntimeException("Excel导入失败: " + e.getMessage(), e); | |||
} | |||
} | |||
@Override | |||
public void exportExcel(Long categoryId, HttpServletResponse response) { | |||
try (Workbook workbook = new XSSFWorkbook()) { | |||
// 查询分类 | |||
Category category = categoryMapper.selectById(categoryId); | |||
if (category == null) { | |||
throw new RuntimeException("分类不存在"); | |||
} | |||
// 查询数据 | |||
List<BaseDataVO> baseDataList = baseDataMapper.findByCategoryId(categoryId); | |||
if (baseDataList.isEmpty()) { | |||
throw new RuntimeException("该分类下暂无数据"); | |||
} | |||
// 提取所有扩展字段 key(即分类规格里的字段) | |||
Set<String> specKeys = new LinkedHashSet<>(); | |||
for (BaseDataVO baseData : baseDataList) { | |||
String specJson = baseData.getCategorySpecs(); | |||
if (specJson != null && !specJson.isEmpty()) { | |||
try { | |||
Map<String, String> specMap = new ObjectMapper().readValue(specJson, new TypeReference<Map<String, String>>() {}); | |||
specKeys.addAll(specMap.keySet()); | |||
} catch (Exception e) { | |||
throw new RuntimeException("分类规格格式有误:" + specJson); | |||
} | |||
} | |||
} | |||
// 创建 Sheet | |||
Sheet sheet = workbook.createSheet("基准数据"); | |||
// 设置表头:固定字段 + 动态分类规格字段(不包含时间) | |||
String[] fixedHeaders = {"商品编号", "商品名称", "品牌", "所属分类"}; | |||
List<String> allHeaders = new ArrayList<>(Arrays.asList(fixedHeaders)); | |||
allHeaders.addAll(specKeys); | |||
Row headerRow = sheet.createRow(0); | |||
for (int i = 0; i < allHeaders.size(); i++) { | |||
headerRow.createCell(i).setCellValue(allHeaders.get(i)); | |||
} | |||
// 写入数据 | |||
for (int i = 0; i < baseDataList.size(); i++) { | |||
BaseDataVO baseData = baseDataList.get(i); | |||
Row row = sheet.createRow(i + 1); | |||
int col = 0; | |||
row.createCell(col++).setCellValue(Optional.ofNullable(baseData.getProductCode()).orElse("")); | |||
row.createCell(col++).setCellValue(Optional.ofNullable(baseData.getProductName()).orElse("")); | |||
row.createCell(col++).setCellValue(Optional.ofNullable(baseData.getBrand()).orElse("")); | |||
row.createCell(col++).setCellValue(category.getName()); | |||
// 解析分类规格 | |||
Map<String, String> specMap = new HashMap<>(); | |||
if (baseData.getCategorySpecs() != null && !baseData.getCategorySpecs().isEmpty()) { | |||
try { | |||
specMap = new ObjectMapper().readValue(baseData.getCategorySpecs(), new TypeReference<Map<String, String>>() {}); | |||
} catch (Exception e) { | |||
// 忽略解析失败的行 | |||
} | |||
} | |||
// 写入扩展字段 | |||
for (String key : specKeys) { | |||
row.createCell(col++).setCellValue(specMap.getOrDefault(key, "")); | |||
} | |||
} | |||
// 自动列宽 | |||
for (int i = 0; i < allHeaders.size(); i++) { | |||
sheet.autoSizeColumn(i); | |||
} | |||
// 设置响应头 | |||
String fileName = URLEncoder.encode("基准数据_" + category.getName() + "_" + System.currentTimeMillis() + ".xlsx", "UTF-8") | |||
.replaceAll("\\+", "%20"); | |||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); | |||
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName); | |||
// 输出到响应流 | |||
workbook.write(response.getOutputStream()); | |||
response.flushBuffer(); | |||
} catch (Exception e) { | |||
try { | |||
response.reset(); | |||
response.setContentType("application/json"); | |||
response.setCharacterEncoding("UTF-8"); | |||
response.getWriter().write("{\"success\":false,\"message\":\"" + e.getMessage() + "\"}"); | |||
} catch (IOException ex) { | |||
throw new RuntimeException("写入错误信息失败: " + ex.getMessage(), ex); | |||
} | |||
} | |||
} | |||
private List<List<String>> buildHead(List<Map<String, Object>> data) { | |||
if (data == null || data.isEmpty()) { | |||
return new ArrayList<>(); | |||
} | |||
List<List<String>> headList = new ArrayList<>(); | |||
for (String key : data.get(0).keySet()) { | |||
headList.add(Collections.singletonList(key)); | |||
} | |||
return headList; | |||
} | |||
} |
@@ -0,0 +1,80 @@ | |||
package com.ruoyi.tjfx.service.impl; | |||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |||
import com.baomidou.mybatisplus.core.metadata.IPage; | |||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.mapper.CategoryMapper; | |||
import com.ruoyi.tjfx.service.CategoryService; | |||
import com.ruoyi.tjfx.entity.Category; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
import org.springframework.util.StringUtils; | |||
import java.util.List; | |||
@Service | |||
public class CategoryServiceImpl implements CategoryService { | |||
@Autowired | |||
private CategoryMapper categoryMapper; | |||
@Override | |||
public List<Category> getAllSimple() { | |||
return categoryMapper.selectList(null); | |||
} | |||
@Override | |||
public List<Category> listAll() { | |||
return categoryMapper.selectList(null); | |||
} | |||
@Override | |||
public Category getById(Long id) { | |||
return categoryMapper.selectById(id); | |||
} | |||
@Override | |||
public void add(Category category) { | |||
categoryMapper.insert(category); | |||
} | |||
@Override | |||
public void update(Long id, Category category) { | |||
category.setId(id); | |||
categoryMapper.updateById(category); | |||
} | |||
@Override | |||
public void delete(Long id) { | |||
categoryMapper.deleteById(id); | |||
} | |||
@Override | |||
public void addBatch(List<Category> categorys) { | |||
if (categorys != null && !categorys.isEmpty()) { | |||
for (Category category : categorys) { | |||
categoryMapper.insert(category); | |||
} | |||
} | |||
} | |||
@Override | |||
public PageResponse<Category> getPage(Integer page, Integer pageSize, String name) { | |||
// 创建分页对象 | |||
Page<Category> pageParam = new Page<>(page, pageSize); | |||
// 构建查询条件 | |||
QueryWrapper<Category> queryWrapper = new QueryWrapper<>(); | |||
if (StringUtils.hasText(name)) { | |||
queryWrapper.like("name", name); | |||
} | |||
queryWrapper.orderByDesc("id"); | |||
// 执行分页查询 | |||
IPage<Category> pageResult = categoryMapper.selectPage(pageParam, queryWrapper); | |||
// 构建分页响应 | |||
PageResponse.Pagination pagination = new PageResponse.Pagination( | |||
pageResult.getTotal(), page, pageSize); | |||
return new PageResponse<>(pageResult.getRecords(), pagination); | |||
} | |||
} |
@@ -0,0 +1,274 @@ | |||
package com.ruoyi.tjfx.service.impl; | |||
import com.alibaba.fastjson.JSON; | |||
import com.ruoyi.tjfx.entity.*; | |||
import com.ruoyi.tjfx.mapper.ReportMapper; | |||
import com.ruoyi.tjfx.service.ReportService; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
import java.math.BigDecimal; | |||
import java.time.LocalDate; | |||
import java.util.*; | |||
import java.util.stream.Collectors; | |||
@Service | |||
public class ReportServiceImpl implements ReportService { | |||
@Autowired | |||
private ReportMapper reportMapper; | |||
/** | |||
* 整体销售分析 | |||
* @param startDate | |||
* @param endDate | |||
* @param category | |||
* @param categorySpecs | |||
* @param customer | |||
* @param shop | |||
* @param brand | |||
* @param productCode | |||
* @return | |||
*/ | |||
@Override | |||
public OverallAnalysisVO overallAnalysis(String startDate, String endDate, String category, String categorySpecs, String customer, String shop, String brand, String productCode) { | |||
Map<String, Object> params = new HashMap<>(); | |||
params.put("startDate", startDate); | |||
params.put("endDate", endDate); | |||
params.put("category", category); | |||
params.put("categorySpecs", categorySpecs); | |||
params.put("customer", customer); | |||
params.put("shop", shop); | |||
params.put("brand", brand); | |||
params.put("productCode", productCode); | |||
// 基础统计 | |||
BasicStatsVO basicStats = reportMapper.selectBasicStats(params); | |||
// 去年同期 | |||
String lastYearStart = LocalDate.parse(startDate).minusYears(1).toString(); | |||
String lastYearEnd = LocalDate.parse(endDate).minusYears(1).toString(); | |||
Map<String, Object> lastYearParams = new HashMap<>(params); | |||
lastYearParams.put("startDate", lastYearStart); | |||
lastYearParams.put("endDate", lastYearEnd); | |||
BasicStatsVO lastYearStats = reportMapper.selectLastYearStats(lastYearParams); | |||
// 上周期 | |||
long days = LocalDate.parse(endDate).toEpochDay() - LocalDate.parse(startDate).toEpochDay(); | |||
String prevStart = LocalDate.parse(startDate).minusDays(days + 1).toString(); | |||
String prevEnd = LocalDate.parse(startDate).minusDays(1).toString(); | |||
Map<String, Object> prevParams = new HashMap<>(params); | |||
prevParams.put("startDate", prevStart); | |||
prevParams.put("endDate", prevEnd); | |||
BasicStatsVO prevStats = reportMapper.selectPrevStats(prevParams); | |||
// 趋势 | |||
List<TrendDataVO> trendData = reportMapper.selectTrendData(params); | |||
// TOP20 | |||
List<TopProductVO> topProducts = reportMapper.selectTopProducts(params); | |||
// 品牌 | |||
List<BrandDataVO> brandData = reportMapper.selectBrandData(params); | |||
// 计算同比、环比 | |||
OverallAnalysisVO vo = new OverallAnalysisVO(); | |||
vo.setBasicStats(basicStats); | |||
vo.setGrowthRates( | |||
new GrowthRatesVO((basicStats==null||lastYearStats==null)?"0":calcRate(basicStats.getTotalAmount(), lastYearStats.getTotalAmount()), | |||
(basicStats==null||lastYearStats==null)?"0":calcRate(basicStats.getTotalQuantity(), lastYearStats.getTotalQuantity()), | |||
(basicStats==null||prevStats==null)?"0":calcRate(basicStats.getTotalAmount(), prevStats.getTotalAmount()), | |||
(basicStats==null||prevStats==null)?"0":calcRate(basicStats.getTotalQuantity(), prevStats.getTotalQuantity())) | |||
); | |||
vo.setTrendData(trendData); | |||
vo.setTopProducts(topProducts); | |||
vo.setBrandData(brandData); | |||
return vo; | |||
} | |||
private String calcRate(BigDecimal now, BigDecimal prev) { | |||
if (prev == null || prev.compareTo(BigDecimal.ZERO) == 0) return "0"; | |||
return now.subtract(prev).divide(prev, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString(); | |||
} | |||
@Override | |||
public ProductAnalysisVO productAnalysis(String startDate, String endDate, String productCode) { | |||
Map<String, Object> params = new HashMap<>(); | |||
params.put("startDate", startDate); | |||
params.put("endDate", endDate); | |||
params.put("productCode", productCode); | |||
List<RankingDataVO> rankingData = reportMapper.selectProductRanking(params); | |||
List<TrendDataVO> trendData = reportMapper.selectProductTrend(params); | |||
ProductAnalysisVO vo = new ProductAnalysisVO(); | |||
vo.setRankingData(rankingData); | |||
vo.setTrendData(trendData); | |||
return vo; | |||
} | |||
/** | |||
* 店铺销售分析 | |||
* @param startDate | |||
* @param endDate | |||
* @param shop | |||
* @return | |||
*/ | |||
@Override | |||
public ShopAnalysisVO shopAnalysis(String startDate, String endDate, String shop) { | |||
Map<String, Object> params = new HashMap<>(); | |||
params.put("startDate", startDate); | |||
params.put("endDate", endDate); | |||
params.put("shop", shop); | |||
BasicStatsVO basicStats = reportMapper.selectShopBasicStats(params); | |||
List<ShopAmountVO> shopAmountData = reportMapper.selectShopAmountData(params); | |||
List<ShopQuantityVO> shopQuantityData = reportMapper.selectShopQuantityData(params); | |||
List<CategoryTrendVO> categoryTrendData = reportMapper.selectCategoryTrendData(params); | |||
// 计算占比 | |||
BigDecimal totalAmount = shopAmountData.stream() | |||
.map(vo -> BigDecimal.valueOf(vo.getAmount())) | |||
.reduce(BigDecimal.ZERO, BigDecimal::add); | |||
BigDecimal totalQuantity = shopQuantityData.stream() | |||
.map(vo -> BigDecimal.valueOf(vo.getQuantity())) | |||
.reduce(BigDecimal.ZERO, BigDecimal::add); | |||
shopAmountData.forEach(item -> { | |||
item.setPercentage(totalAmount.compareTo(BigDecimal.ZERO) == 0 ? "0" : | |||
new BigDecimal(item.getAmount()).divide(totalAmount, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString()); | |||
}); | |||
shopQuantityData.forEach(item -> { | |||
item.setPercentage(totalQuantity.compareTo(BigDecimal.ZERO) == 0 ? "0" : | |||
new BigDecimal(item.getQuantity()).divide(totalQuantity, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString()); | |||
}); | |||
ShopAnalysisVO vo = new ShopAnalysisVO(); | |||
vo.setBasicStats(basicStats); | |||
vo.setShopAmountData(shopAmountData); | |||
vo.setShopQuantityData(shopQuantityData); | |||
vo.setCategoryTrendData(categoryTrendData); | |||
return vo; | |||
} | |||
/** | |||
* 品类销售分析 | |||
* @param startDate | |||
* @param endDate | |||
* @param category | |||
* @return | |||
*/ | |||
@Override | |||
public CategoryAnalysisVO categoryAnalysis(String startDate, String endDate, String category) { | |||
Map<String, Object> params = new HashMap<>(); | |||
params.put("startDate", startDate); | |||
params.put("endDate", endDate); | |||
params.put("category", category); | |||
List<Map<String, Object>> specsRawData = reportMapper.selectSpecsRawData(params); | |||
// 解析规格数据 | |||
Map<String, Map<String, DimensionStatsVO>> dimensionStats = new HashMap<>(); | |||
Set<String> availableDimensions = new HashSet<>(); | |||
for (Map<String, Object> item : specsRawData) { | |||
String specsStr = (String) item.get("category_specs"); | |||
BigDecimal quantity = (BigDecimal) item.get("quantity"); | |||
BigDecimal amount = (BigDecimal) item.get("amount"); | |||
try { | |||
Map<String, Object> specsObj = JSON.parseObject(specsStr, Map.class); | |||
for (Map.Entry<String, Object> entry : specsObj.entrySet()) { | |||
String dim = entry.getKey(); | |||
String val = entry.getValue() == null ? "" : entry.getValue().toString(); | |||
if (!val.trim().isEmpty()) { | |||
availableDimensions.add(dim); | |||
dimensionStats.putIfAbsent(dim, new HashMap<>()); | |||
Map<String, DimensionStatsVO> dimMap = dimensionStats.get(dim); | |||
dimMap.putIfAbsent(val, new DimensionStatsVO(val, 0d, 0d, "0",0d)); | |||
DimensionStatsVO stats = dimMap.get(val); | |||
stats.setQuantity(new BigDecimal(stats.getQuantity()).add(quantity == null ? BigDecimal.ZERO : quantity).doubleValue()); | |||
stats.setAmount(new BigDecimal(stats.getAmount()).add(amount == null ? BigDecimal.ZERO : amount).doubleValue()); | |||
} | |||
} | |||
} catch (Exception e) { | |||
// 非JSON格式跳过 | |||
} | |||
} | |||
// 计算占比 | |||
Map<String, DimensionChartVO> dimensionCharts = new HashMap<>(); | |||
for (String dim : dimensionStats.keySet()) { | |||
Map<String, DimensionStatsVO> dimMap = dimensionStats.get(dim); | |||
//BigDecimal totalAmount = dimMap.values().stream().map(DimensionStatsVO::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); | |||
//BigDecimal totalQuantity = dimMap.values().stream().map(DimensionStatsVO::getQuantity).reduce(BigDecimal.ZERO, BigDecimal::add); | |||
BigDecimal totalAmount = dimMap.values().stream() | |||
.map(vo -> BigDecimal.valueOf(vo.getAmount())) | |||
.reduce(BigDecimal.ZERO, BigDecimal::add); | |||
BigDecimal totalQuantity = dimMap.values().stream() | |||
.map(vo -> BigDecimal.valueOf(vo.getQuantity())) | |||
.reduce(BigDecimal.ZERO, BigDecimal::add); | |||
List<DimensionStatsVO> amountData = new ArrayList<>(); | |||
List<DimensionStatsVO> quantityData = new ArrayList<>(); | |||
for (DimensionStatsVO stats : dimMap.values()) { | |||
//金额集合 | |||
stats.setPercentage(totalAmount.compareTo(BigDecimal.ZERO) == 0 ? "0" : | |||
new BigDecimal(stats.getAmount()).divide(totalAmount, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString()); | |||
stats.setValue(stats.getAmount()); | |||
amountData.add(stats); | |||
//销量集合 | |||
DimensionStatsVO statsQuantity = new DimensionStatsVO( | |||
stats.getName(), | |||
stats.getQuantity(), | |||
stats.getAmount(), | |||
stats.getPercentage(), | |||
stats.getValue()); | |||
statsQuantity.setPercentage(totalQuantity.compareTo(BigDecimal.ZERO) == 0 ? "0" : | |||
new BigDecimal(statsQuantity.getQuantity()).divide(totalQuantity, 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).toString()); | |||
statsQuantity.setValue(statsQuantity.getQuantity()); | |||
quantityData.add(statsQuantity); | |||
} | |||
amountData.sort((a, b) -> b.getAmount().compareTo(a.getAmount())); | |||
quantityData.sort((a, b) -> b.getQuantity().compareTo(a.getQuantity())); | |||
dimensionCharts.put(dim, new DimensionChartVO(amountData, quantityData)); | |||
} | |||
CategoryAnalysisVO vo = new CategoryAnalysisVO(); | |||
vo.setDimensionCharts(dimensionCharts); | |||
vo.setAvailableDimensions(new ArrayList<>(availableDimensions)); | |||
vo.setCurrentCategory(category == null ? "全部分类" : category); | |||
return vo; | |||
} | |||
@Override | |||
public FilterOptionsVO filterOptions() { | |||
FilterOptionsVO vo = new FilterOptionsVO(); | |||
vo.setCategories(reportMapper.selectCategories()); | |||
vo.setCategorySpecs(reportMapper.selectCategorySpecs()); | |||
vo.setCustomers(reportMapper.selectCustomers()); | |||
vo.setShops(reportMapper.selectShops()); | |||
vo.setBrands(reportMapper.selectBrands()); | |||
return vo; | |||
} | |||
@Override | |||
public List<ProductCodeSuggestionVO> productCodeSuggestions(String keyword) { | |||
List<Map<String, Object>> suggestions = reportMapper.selectProductCodeSuggestions("%" + keyword + "%"); | |||
return suggestions.stream().map(item -> { | |||
String productName = (String) item.get("product_name"); | |||
String categorySpecs = (String) item.get("category_specs"); | |||
String categoryName = (String) item.get("category_name"); | |||
String displayText = productName == null ? "" : productName; | |||
String specsText = ""; | |||
if (categorySpecs != null) { | |||
try { | |||
Map<String, Object> specs = JSON.parseObject(categorySpecs, Map.class); | |||
specsText = specs.values().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.joining("+")); | |||
} catch (Exception e) { | |||
specsText = categorySpecs; | |||
} | |||
} | |||
if (!specsText.isEmpty()) displayText += "+" + specsText; | |||
if (categoryName != null && !categoryName.isEmpty()) displayText += "-" + categoryName; | |||
return new ProductCodeSuggestionVO((String) item.get("product_code"), displayText); | |||
}).collect(Collectors.toList()); | |||
} | |||
} |
@@ -0,0 +1,229 @@ | |||
package com.ruoyi.tjfx.service.impl; | |||
import com.alibaba.excel.EasyExcel; | |||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |||
import com.baomidou.mybatisplus.core.metadata.IPage; | |||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
import com.ruoyi.tjfx.common.PageResponse; | |||
import com.ruoyi.tjfx.entity.ShopCustomer; | |||
import com.ruoyi.tjfx.entity.ShopCustomerExcelDTO; | |||
import com.ruoyi.tjfx.mapper.ShopCustomerMapper; | |||
import com.ruoyi.tjfx.service.ShopCustomerService; | |||
import com.ruoyi.tjfx.util.ExcelUtil; | |||
import org.apache.poi.ss.usermodel.Row; | |||
import org.apache.poi.ss.usermodel.Sheet; | |||
import org.apache.poi.ss.usermodel.Workbook; | |||
import org.apache.poi.xssf.usermodel.XSSFWorkbook; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
import org.springframework.transaction.annotation.Transactional; | |||
import org.springframework.util.StringUtils; | |||
import org.springframework.web.multipart.MultipartFile; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.io.IOException; | |||
import java.net.URLEncoder; | |||
import java.util.*; | |||
import java.util.stream.Collectors; | |||
@Service | |||
public class ShopCustomerServiceImpl implements ShopCustomerService { | |||
@Autowired | |||
private ShopCustomerMapper shopCustomerMapper; | |||
@Override | |||
public List<ShopCustomer> listAll() { | |||
return shopCustomerMapper.selectList(null); | |||
} | |||
@Override | |||
public ShopCustomer getById(Long id) { | |||
return shopCustomerMapper.selectById(id); | |||
} | |||
@Override | |||
public void add(ShopCustomer shopCustomer) { | |||
shopCustomerMapper.insert(shopCustomer); | |||
} | |||
@Override | |||
public void update(Long id, ShopCustomer shopCustomer) { | |||
shopCustomer.setId(id); | |||
shopCustomerMapper.updateById(shopCustomer); | |||
} | |||
@Override | |||
public void delete(Long id) { | |||
shopCustomerMapper.deleteById(id); | |||
} | |||
@Override | |||
public void deleteBatch(List<Long> ids) { | |||
if (ids != null && !ids.isEmpty()) { | |||
shopCustomerMapper.deleteBatchIds(ids); | |||
} | |||
} | |||
@Override | |||
public PageResponse<ShopCustomer> getPage(Integer page, Integer pageSize, String shopName, String customerName) { | |||
// 创建分页对象 | |||
Page<ShopCustomer> pageParam = new Page<>(page, pageSize); | |||
// 构建查询条件 | |||
QueryWrapper<ShopCustomer> queryWrapper = new QueryWrapper<>(); | |||
if (StringUtils.hasText(shopName)) { | |||
queryWrapper.like("shop_name", shopName); | |||
} | |||
if (StringUtils.hasText(customerName)) { | |||
queryWrapper.like("customer_name", customerName); | |||
} | |||
queryWrapper.orderByDesc("id"); | |||
// 执行分页查询 | |||
IPage<ShopCustomer> pageResult = shopCustomerMapper.selectPage(pageParam, queryWrapper); | |||
// 构建分页响应 | |||
PageResponse.Pagination pagination = new PageResponse.Pagination( | |||
pageResult.getTotal(), page, pageSize); | |||
return new PageResponse<>(pageResult.getRecords(), pagination); | |||
} | |||
@Override | |||
public Map<String, Object> importExcel(MultipartFile file) { | |||
Map<String, Object> result = new HashMap<>(); | |||
List<String> errors = new ArrayList<>(); | |||
int successCount = 0; | |||
int totalRows = 0; | |||
try { | |||
// 1. 读取 Excel 数据(第一行为表头) | |||
List<ShopCustomerExcelDTO> excelDTOList = EasyExcel.read(file.getInputStream()) | |||
.head(ShopCustomerExcelDTO.class).headRowNumber(1).sheet().doReadSync(); | |||
totalRows = excelDTOList.size(); | |||
if (totalRows == 0) { | |||
result.put("total", 0); | |||
result.put("imported", 0); | |||
result.put("errors", 1); | |||
result.put("errorDetails", Collections.singletonList("Excel文件为空或格式不正确")); | |||
return result; | |||
} | |||
// 2. Excel内部去重、必填校验 | |||
Set<String> duplicateCheck = new HashSet<>(); | |||
List<ShopCustomer> validList = new ArrayList<>(); | |||
for (int i = 0; i < excelDTOList.size(); i++) { | |||
ShopCustomerExcelDTO dto = excelDTOList.get(i); | |||
int rowIndex = i + 2; // Excel行号 | |||
String shopName = dto.getShopName() != null ? dto.getShopName().trim() : ""; | |||
String customerName = dto.getCustomerName() != null ? dto.getCustomerName().trim() : ""; | |||
if (shopName.isEmpty() || customerName.isEmpty()) { | |||
errors.add("第" + rowIndex + "行: 店铺名称和客户名称不能为空"); | |||
continue; | |||
} | |||
String key = shopName + "-" + customerName; | |||
if (!duplicateCheck.add(key)) { | |||
errors.add("第" + rowIndex + "行: 店铺'" + shopName + "'与客户'" + customerName + "'的关联在Excel中重复"); | |||
continue; | |||
} | |||
ShopCustomer sc = new ShopCustomer(); | |||
sc.setShopName(shopName); | |||
sc.setCustomerName(customerName); | |||
validList.add(sc); | |||
} | |||
// 3. 数据库去重 | |||
if (!validList.isEmpty()) { | |||
List<String> dbKeys = validList.stream() | |||
.map(sc -> sc.getShopName() + "-" + sc.getCustomerName()) | |||
.collect(Collectors.toList()); | |||
QueryWrapper<ShopCustomer> queryWrapper = new QueryWrapper<>(); | |||
queryWrapper.select("shop_name", "customer_name"); | |||
queryWrapper.inSql("concat(shop_name,'-',customer_name)", | |||
dbKeys.stream() | |||
.map(k -> "'" + k.replace("'", "''") + "'") | |||
.collect(Collectors.joining(","))); | |||
List<ShopCustomer> existList = shopCustomerMapper.selectList(queryWrapper); | |||
Set<String> existKeys = existList.stream() | |||
.map(sc -> sc.getShopName() + "-" + sc.getCustomerName()) | |||
.collect(Collectors.toSet()); | |||
// 过滤掉数据库中已存在的记录 | |||
validList = validList.stream() | |||
.filter(sc -> !existKeys.contains(sc.getShopName() + "-" + sc.getCustomerName())) | |||
.collect(Collectors.toList()); | |||
for (String k : dbKeys) { | |||
if (existKeys.contains(k)) { | |||
String[] parts = k.split("-", 2); | |||
errors.add("店铺'" + parts[0] + "'与客户'" + parts[1] + "'的关联已存在于数据库中"); | |||
} | |||
} | |||
} | |||
// 4. 批量插入 | |||
for (ShopCustomer sc : validList) { | |||
try { | |||
shopCustomerMapper.insert(sc); | |||
successCount++; | |||
} catch (Exception e) { | |||
errors.add("插入失败: 店铺'" + sc.getShopName() + "', 客户'" + sc.getCustomerName() + "' - " + e.getMessage()); | |||
} | |||
} | |||
} catch (Exception e) { | |||
errors.add("导入异常: " + e.getMessage()); | |||
} | |||
result.put("total", totalRows); | |||
result.put("imported", successCount); | |||
result.put("errors", errors.size()); | |||
result.put("errorDetails", errors.size() > 0 ? errors.subList(0, Math.min(10, errors.size())) : new ArrayList<>()); | |||
return result; | |||
} | |||
@Override | |||
public void exportExcel(HttpServletResponse response) { | |||
try (Workbook workbook = new XSSFWorkbook()) { | |||
Sheet sheet = workbook.createSheet("店铺客户关联"); | |||
String[] headers = {"店铺名称", "客户名称", "创建时间", "更新时间"}; | |||
Row headerRow = sheet.createRow(0); | |||
for (int i = 0; i < headers.length; i++) { | |||
headerRow.createCell(i).setCellValue(headers[i]); | |||
} | |||
List<ShopCustomer> list = shopCustomerMapper.selectList(null); | |||
for (int i = 0; i < list.size(); i++) { | |||
ShopCustomer sc = list.get(i); | |||
Row row = sheet.createRow(i + 1); | |||
row.createCell(0).setCellValue(sc.getShopName() != null ? sc.getShopName() : ""); | |||
row.createCell(1).setCellValue(sc.getCustomerName() != null ? sc.getCustomerName() : ""); | |||
row.createCell(2).setCellValue(sc.getCreatedAt() != null ? sc.getCreatedAt().toString() : ""); | |||
row.createCell(3).setCellValue(sc.getUpdatedAt() != null ? sc.getUpdatedAt().toString() : ""); | |||
} | |||
for (int i = 0; i < headers.length; i++) { | |||
sheet.autoSizeColumn(i); | |||
} | |||
// 设置文件名 | |||
String fileName = URLEncoder.encode("shop_customer.xlsx", "UTF-8").replaceAll("\\+", "%20"); | |||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); | |||
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName); | |||
workbook.write(response.getOutputStream()); | |||
response.flushBuffer(); // 强制写出 | |||
} catch (Exception e) { | |||
throw new RuntimeException("导出Excel失败: " + e.getMessage(), e); | |||
} | |||
} | |||
} |
@@ -0,0 +1,379 @@ | |||
package com.ruoyi.tjfx.util; | |||
import com.ruoyi.tjfx.entity.AnalysisData; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.apache.poi.hssf.usermodel.HSSFWorkbook; | |||
import org.apache.poi.ss.usermodel.*; | |||
import org.apache.poi.xssf.usermodel.XSSFWorkbook; | |||
import org.jsoup.Jsoup; | |||
import org.springframework.stereotype.Component; | |||
import javax.servlet.http.HttpServletResponse; | |||
//import javax.swing.text.Document; | |||
import java.io.*; | |||
import org.jsoup.nodes.Element; | |||
import org.jsoup.select.Elements; | |||
import java.time.LocalDate; | |||
import java.time.format.DateTimeFormatter; | |||
import java.util.*; | |||
import org.apache.poi.ss.usermodel.WorkbookFactory; | |||
import org.jsoup.nodes.Document; | |||
import java.util.stream.Collectors; | |||
/** | |||
* Excel工具类 | |||
*/ | |||
@Slf4j | |||
@Component | |||
public class ExcelUtil { | |||
/** 检测文件是否为 HTML (简单检查首行) */ | |||
private boolean isHtmlFile(File file) throws IOException { | |||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) { | |||
String firstLine = reader.readLine(); | |||
return firstLine != null && firstLine.trim().startsWith("<"); | |||
} | |||
} | |||
/** 用 Jsoup 解析 HTML 表格 */ | |||
private List<Map<String, Object>> readHtmlTable(File file) throws IOException { | |||
List<Map<String, Object>> dataList = new ArrayList<>(); | |||
Document doc = Jsoup.parse(file, "UTF-8"); | |||
Element table = doc.selectFirst("table"); | |||
if (table == null) { | |||
throw new RuntimeException("HTML 中找不到 <table> 元素"); | |||
} | |||
// 读取表头 | |||
Elements headerCells = table.select("tr").get(0).select("th,td"); | |||
List<String> headers = headerCells.stream() | |||
.map(Element::text) | |||
.collect(Collectors.toList()); | |||
// 读取每一行数据 | |||
Elements rows = table.select("tr"); | |||
for (int i = 1; i < rows.size(); i++) { | |||
Elements cells = rows.get(i).select("td"); | |||
Map<String, Object> rowMap = new HashMap<>(); | |||
for (int j = 0; j < headers.size() && j < cells.size(); j++) { | |||
rowMap.put(headers.get(j), cells.get(j).text()); | |||
} | |||
dataList.add(rowMap); | |||
} | |||
return dataList; | |||
} | |||
/** 根据 Cell 类型取值的通用方法 */ | |||
private Object getCellValue2(Cell cell) { | |||
if (cell == null) return null; | |||
switch (cell.getCellType()) { | |||
case STRING: return cell.getStringCellValue(); | |||
case NUMERIC: return DateUtil.isCellDateFormatted(cell) | |||
? cell.getDateCellValue() | |||
: cell.getNumericCellValue(); | |||
case BOOLEAN: return cell.getBooleanCellValue(); | |||
case FORMULA: return cell.getCellFormula(); | |||
case BLANK: return ""; | |||
default: return cell.toString(); | |||
} | |||
} | |||
/** | |||
* 读取Excel文件 | |||
*/ | |||
public List<Map<String, Object>> readExcel(String filePath) { | |||
File file = new File(filePath); | |||
List<Map<String, Object>> dataList = new ArrayList<>(); | |||
try { | |||
// 1. 检测是否为 HTML 文件(第一行以 '<' 开头) | |||
if (isHtmlFile(file)) { | |||
return readHtmlTable(file); | |||
} | |||
// 2. 如果不是 HTML,就用 POI 自动识别 xls/xlsx | |||
try (FileInputStream fis = new FileInputStream(file); | |||
Workbook workbook = WorkbookFactory.create(fis)) { | |||
Sheet sheet = workbook.getSheetAt(0); | |||
Row headerRow = sheet.getRow(0); | |||
if (headerRow == null) { | |||
return dataList; | |||
} | |||
// 3. 读取表头 | |||
List<String> headers = new ArrayList<>(); | |||
for (int i = 0; i < headerRow.getLastCellNum(); i++) { | |||
Cell cell = headerRow.getCell(i); | |||
headers.add(cell != null ? cell.toString() : ""); | |||
} | |||
// 4. 读取数据行 | |||
for (int i = 1; i <= sheet.getLastRowNum(); i++) { | |||
Row row = sheet.getRow(i); | |||
if (row == null) continue; | |||
Map<String, Object> rowData = new HashMap<>(); | |||
for (int j = 0; j < headers.size(); j++) { | |||
Cell cell = row.getCell(j); | |||
rowData.put(headers.get(j), getCellValue2(cell)); | |||
} | |||
dataList.add(rowData); | |||
} | |||
} | |||
} catch (Exception e) { | |||
log.error("读取Excel文件失败: {}", filePath, e); | |||
throw new RuntimeException("读取Excel失败:" + e.getMessage()); | |||
} | |||
return dataList; | |||
} | |||
/* public List<Map<String, Object>> readExcel(String filePath) { | |||
List<Map<String, Object>> dataList = new ArrayList<>(); | |||
try (FileInputStream fis = new FileInputStream(filePath); | |||
Workbook workbook = WorkbookFactory.create(fis)) { // 自动识别xls或xlsx | |||
Sheet sheet = workbook.getSheetAt(0); | |||
Row headerRow = sheet.getRow(0); | |||
if (headerRow == null) { | |||
return dataList; | |||
} | |||
List<String> headers = new ArrayList<>(); | |||
for (int i = 0; i < headerRow.getLastCellNum(); i++) { | |||
Cell cell = headerRow.getCell(i); | |||
headers.add(cell != null ? cell.toString() : ""); | |||
} | |||
for (int i = 1; i <= sheet.getLastRowNum(); i++) { | |||
Row row = sheet.getRow(i); | |||
if (row == null) continue; | |||
Map<String, Object> rowData = new HashMap<>(); | |||
for (int j = 0; j < headers.size(); j++) { | |||
Cell cell = row.getCell(j); | |||
String header = headers.get(j); | |||
Object value = getCellValue(cell); | |||
rowData.put(header, value); | |||
} | |||
dataList.add(rowData); | |||
} | |||
} catch (Exception e) { | |||
log.error("读取Excel文件失败: {}", filePath, e); | |||
} | |||
return dataList; | |||
}*/ | |||
/** | |||
* 读取FBA Excel文件 | |||
*/ | |||
public List<Map<String, Object>> readFbaExcel(String filePath) { | |||
return readExcel(filePath); // 使用相同的读取方法 | |||
} | |||
/** | |||
* 导出分析数据到Excel | |||
*/ | |||
public void exportAnalysisData(HttpServletResponse response, Map<String, List<AnalysisData>> groupedData) { | |||
try (Workbook workbook = new XSSFWorkbook()) { | |||
for (Map.Entry<String, List<AnalysisData>> entry : groupedData.entrySet()) { | |||
String categoryName = entry.getKey(); | |||
List<AnalysisData> dataList = entry.getValue(); | |||
// 清理工作表名称中的非法字符 | |||
String safeSheetName = categoryName.replaceAll("[\\\\/*?:\"<>|]", "_").substring(0, Math.min(31, categoryName.length())); | |||
Sheet sheet = workbook.createSheet(safeSheetName); | |||
// 创建表头 | |||
Row headerRow = sheet.createRow(0); | |||
String[] headers = { | |||
"日期", "店铺", "商品编号", "商品名称", "客户", "品牌", "数量", "合计", | |||
"来源", "状态", "出库类型", "目的地", "备注", "订单编号", "行号" | |||
}; | |||
for (int i = 0; i < headers.length; i++) { | |||
Cell cell = headerRow.createCell(i); | |||
cell.setCellValue(headers[i]); | |||
} | |||
// 填充数据 | |||
for (int i = 0; i < dataList.size(); i++) { | |||
AnalysisData data = dataList.get(i); | |||
Row row = sheet.createRow(i + 1); | |||
row.createCell(0).setCellValue(data.getDate().toString()); | |||
row.createCell(1).setCellValue(data.getShopName()); | |||
row.createCell(2).setCellValue(data.getProductCode()); | |||
row.createCell(3).setCellValue(data.getProductName()); | |||
row.createCell(4).setCellValue(data.getCustomerName()); | |||
row.createCell(5).setCellValue(data.getBrand()); | |||
row.createCell(6).setCellValue(data.getQuantity()); | |||
row.createCell(7).setCellValue(data.getTotalAmount().doubleValue()); | |||
row.createCell(8).setCellValue(data.getSource()); | |||
row.createCell(9).setCellValue(getStatusText(data.getStatus())); | |||
row.createCell(10).setCellValue(data.getDeliveryType()); | |||
row.createCell(11).setCellValue(data.getDestination()); | |||
row.createCell(12).setCellValue(data.getRemarks()); | |||
row.createCell(13).setCellValue(data.getOrderNumber()); | |||
row.createCell(14).setCellValue(data.getRowNumber()); | |||
} | |||
// 自动调整列宽 | |||
for (int i = 0; i < headers.length; i++) { | |||
sheet.autoSizeColumn(i); | |||
} | |||
} | |||
// 设置响应头 | |||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); | |||
response.setHeader("Content-Disposition", "attachment; filename=sales_analysis_data.xlsx"); | |||
// 写入响应流 | |||
workbook.write(response.getOutputStream()); | |||
} catch (IOException e) { | |||
log.error("导出Excel失败", e); | |||
throw new RuntimeException("导出Excel失败"); | |||
} | |||
} | |||
/** | |||
* 处理Excel日期 | |||
*/ | |||
public LocalDate processExcelDate(Object dateValue) { | |||
if (dateValue == null) { | |||
return null; | |||
} | |||
try { | |||
if (dateValue instanceof Date) { | |||
return ((Date) dateValue).toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); | |||
} else if (dateValue instanceof String) { | |||
String dateStr = (String) dateValue; | |||
// 尝试多种日期格式 | |||
String[] formats = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyy.MM.dd","yyyy/M/d", "yyyy年MM月dd日"}; | |||
for (String format : formats) { | |||
try { | |||
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(format)); | |||
} catch (Exception ignored) { | |||
// 继续尝试下一个格式 | |||
} | |||
} | |||
} else if (dateValue instanceof Number) { | |||
// Excel日期数字格式 | |||
double excelDate = ((Number) dateValue).doubleValue(); | |||
return DateUtil.getJavaDate(excelDate).toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); | |||
} | |||
} catch (Exception e) { | |||
log.error("日期处理失败: {}", dateValue, e); | |||
} | |||
return null; | |||
} | |||
/** | |||
* 处理FBA日期 | |||
*/ | |||
public LocalDate processFbaDate(Object dateValue) { | |||
if (dateValue == null) { | |||
return null; | |||
} | |||
try { | |||
if (dateValue instanceof String) { | |||
String dateStr = (String) dateValue; | |||
// 处理ISO格式日期 | |||
if (dateStr.contains("T")) { | |||
return LocalDate.parse(dateStr.substring(0, 10)); | |||
} | |||
} | |||
// 使用通用的Excel日期处理 | |||
return processExcelDate(dateValue); | |||
} catch (Exception e) { | |||
log.error("FBA日期处理失败: {}", dateValue, e); | |||
} | |||
return null; | |||
} | |||
/** | |||
* 处理FBA SKU | |||
*/ | |||
public String processFbaSku(String sku) { | |||
if (sku == null || sku.trim().isEmpty()) { | |||
return ""; | |||
} | |||
String trimmedSku = sku.trim(); | |||
// 具体的后缀模式 | |||
String[] specificSuffixes = {"-F", "-SLP", "-SF", "-F1", "-2K", "-5F", "-D", "-E"}; | |||
// 首先尝试匹配具体的后缀 | |||
for (String suffix : specificSuffixes) { | |||
if (trimmedSku.endsWith(suffix)) { | |||
return trimmedSku.substring(0, trimmedSku.length() - suffix.length()); | |||
} | |||
} | |||
// 如果没有匹配到具体后缀,尝试通用模式 | |||
String result = trimmedSku.replaceAll("^(.+?)[-_][A-Z]{1,4}$", "$1"); | |||
return result.equals(trimmedSku) ? trimmedSku : result; | |||
} | |||
/** | |||
* 获取单元格值 | |||
*/ | |||
private Object getCellValue(Cell cell) { | |||
if (cell == null) { | |||
return ""; | |||
} | |||
switch (cell.getCellType()) { | |||
case STRING: | |||
return cell.getStringCellValue(); | |||
case NUMERIC: | |||
if (DateUtil.isCellDateFormatted(cell)) { | |||
return cell.getDateCellValue(); | |||
} else { | |||
return cell.getNumericCellValue(); | |||
} | |||
case BOOLEAN: | |||
return cell.getBooleanCellValue(); | |||
case FORMULA: | |||
return cell.getCellFormula(); | |||
default: | |||
return ""; | |||
} | |||
} | |||
/** | |||
* 获取状态文本 | |||
*/ | |||
private String getStatusText(Integer status) { | |||
if (status == null) return ""; | |||
switch (status) { | |||
case 1: return "正常"; | |||
case 2: return "客户名称未匹配"; | |||
case 3: return "分类规格未匹配"; | |||
default: return "未知"; | |||
} | |||
} | |||
} |
@@ -0,0 +1,91 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |||
<mapper namespace="com.ruoyi.tjfx.mapper.AnalysisDataMapper"> | |||
<!-- 分页查询分析数据 --> | |||
<select id="selectPageWithConditions" resultType="com.ruoyi.tjfx.entity.AnalysisData"> | |||
SELECT * FROM zs_tjfx_analysis_data | |||
<where> | |||
<if test="productCode != null and productCode != ''"> | |||
AND product_code LIKE CONCAT('%', #{productCode}, '%') | |||
</if> | |||
<if test="productName != null and productName != ''"> | |||
AND product_name LIKE CONCAT('%', #{productName}, '%') | |||
</if> | |||
<if test="customerName != null and customerName != ''"> | |||
AND customer_name LIKE CONCAT('%', #{customerName}, '%') | |||
</if> | |||
<if test="shopName != null and shopName != ''"> | |||
AND shop_name LIKE CONCAT('%', #{shopName}, '%') | |||
</if> | |||
<if test="status != null"> | |||
AND status = #{status} | |||
</if> | |||
<if test="startDate != null"> | |||
AND date >= #{startDate} | |||
</if> | |||
<if test="endDate != null"> | |||
AND date <= #{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> |
@@ -0,0 +1,80 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |||
<mapper namespace="com.ruoyi.tjfx.mapper.BaseDataMapper"> | |||
<!-- 根据商品编号和分类ID查询基准数据 --> | |||
<select id="selectByProductCodeAndCategoryId" resultType="com.ruoyi.tjfx.entity.BaseData"> | |||
SELECT bd.*, c.name as category_name | |||
FROM zs_tjfx_base_data bd | |||
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id | |||
WHERE bd.product_code = #{productCode} | |||
<if test="categoryId != null"> | |||
AND bd.category_id = #{categoryId} | |||
</if> | |||
LIMIT 1 | |||
</select> | |||
<select id="findByCategoryId" resultType="com.ruoyi.tjfx.entity.BaseDataVO"> | |||
SELECT bd.*, c.name as category_name | |||
FROM zs_tjfx_base_data bd | |||
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id | |||
WHERE 1=1 | |||
<if test="categoryId != null"> | |||
AND bd.category_id = #{categoryId} | |||
</if> | |||
</select> | |||
<select id="selectByProductCode" resultType="com.ruoyi.tjfx.entity.BaseDataVO"> | |||
SELECT bd.*, c.name as category_name | |||
FROM zs_tjfx_base_data bd | |||
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id | |||
WHERE bd.product_code = #{productCode} | |||
LIMIT 1 | |||
</select> | |||
<select id="selectExistingProductCodes" resultType="string"> | |||
SELECT product_code | |||
FROM zs_tjfx_base_data | |||
WHERE product_code IN | |||
<foreach collection="codes" item="code" open="(" separator="," close=")"> | |||
#{code} | |||
</foreach> | |||
</select> | |||
<select id="selectPageWithJoin" resultType="com.ruoyi.tjfx.entity.BaseDataVO"> | |||
SELECT | |||
bd.id, bd.product_code, bd.product_name, bd.brand, | |||
bd.category_id, c.name AS category_name, | |||
bd.category_specs, bd.created_at, bd.updated_at | |||
FROM zs_tjfx_base_data bd | |||
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id | |||
<where> | |||
<if test="productCode != null and productCode != ''"> | |||
AND bd.product_code LIKE CONCAT('%', #{productCode}, '%') | |||
</if> | |||
<if test="productName != null and productName != ''"> | |||
AND bd.product_name LIKE CONCAT('%', #{productName}, '%') | |||
</if> | |||
<if test="categoryId != null"> | |||
AND bd.category_id = #{categoryId} | |||
</if> | |||
</where> | |||
ORDER BY bd.created_at DESC | |||
</select> | |||
<insert id="insertBatch"> | |||
INSERT INTO zs_tjfx_base_data ( | |||
product_code, product_name, brand, | |||
category_id, category_specs, | |||
created_at, updated_at | |||
) | |||
VALUES | |||
<foreach collection="list" item="item" separator=","> | |||
( | |||
#{item.productCode}, | |||
#{item.productName}, | |||
#{item.brand}, | |||
#{item.categoryId}, | |||
#{item.categorySpecs}, | |||
#{item.createdAt}, | |||
#{item.updatedAt} | |||
) | |||
</foreach> | |||
</insert> | |||
</mapper> |
@@ -0,0 +1,504 @@ | |||
<?xml version="1.0" encoding="UTF-8" ?> | |||
<!DOCTYPE mapper | |||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" | |||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |||
<mapper namespace="com.ruoyi.tjfx.mapper.ReportMapper"> | |||
<!-- 1. 整体销售分析 基础统计 --> | |||
<select id="selectBasicStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO"> | |||
SELECT | |||
COUNT(*) AS totalRecords, | |||
SUM(quantity) AS totalQuantity, | |||
SUM(total_amount) AS totalAmount, | |||
COUNT(DISTINCT product_code) AS uniqueProducts, | |||
COUNT(DISTINCT customer_name) AS uniqueCustomers | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
<if test="category != null and category != ''">AND category = #{category}</if> | |||
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if> | |||
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if> | |||
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if> | |||
<if test="brand != null and brand != ''">AND brand = #{brand}</if> | |||
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if> | |||
AND status = 1 | |||
</select> | |||
<!-- 2. 去年同期统计 --> | |||
<select id="selectLastYearStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO"> | |||
SELECT | |||
SUM(quantity) AS totalQuantity, | |||
SUM(total_amount) AS totalAmount | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
AND status = 1 | |||
</select> | |||
<!-- 3. 上周期统计 --> | |||
<select id="selectPrevStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO"> | |||
SELECT | |||
SUM(quantity) AS totalQuantity, | |||
SUM(total_amount) AS totalAmount | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
AND status = 1 | |||
</select> | |||
<!-- 4. 销售趋势数据 --> | |||
<select id="selectTrendData" resultType="com.ruoyi.tjfx.entity.TrendDataVO"> | |||
SELECT | |||
date, | |||
SUM(quantity) AS quantity, | |||
SUM(total_amount) AS amount | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
<if test="category != null and category != ''">AND category = #{category}</if> | |||
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if> | |||
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if> | |||
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if> | |||
<if test="brand != null and brand != ''">AND brand = #{brand}</if> | |||
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if> | |||
AND status = 1 | |||
GROUP BY date | |||
ORDER BY date | |||
</select> | |||
<!-- 5. TOP20商品销售 --> | |||
<select id="selectTopProducts" resultType="com.ruoyi.tjfx.entity.TopProductVO"> | |||
SELECT | |||
product_code AS productCode, | |||
product_name AS productName, | |||
SUM(quantity) AS quantity, | |||
SUM(total_amount) AS amount | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
<if test="category != null and category != ''">AND category = #{category}</if> | |||
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if> | |||
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if> | |||
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if> | |||
<if test="brand != null and brand != ''">AND brand = #{brand}</if> | |||
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if> | |||
AND status = 1 | |||
GROUP BY product_code, product_name | |||
ORDER BY amount DESC | |||
LIMIT 20 | |||
</select> | |||
<!-- 6. 品牌销售数据 --> | |||
<select id="selectBrandData" resultType="com.ruoyi.tjfx.entity.BrandDataVO"> | |||
SELECT | |||
bd.brand AS brand, | |||
SUM(ad.quantity) AS quantity, | |||
SUM(ad.total_amount) AS amount | |||
FROM zs_tjfx_analysis_data ad | |||
JOIN zs_tjfx_base_data bd ON ad.product_code = bd.product_code | |||
WHERE ad.date BETWEEN #{startDate} AND #{endDate} | |||
<if test="category != null and category != ''">AND ad.category = #{category}</if> | |||
<if test="categorySpecs != null and categorySpecs != ''">AND ad.category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if> | |||
<if test="customer != null and customer != ''">AND ad.customer_name = #{customer}</if> | |||
<if test="shop != null and shop != ''">AND ad.shop_name = #{shop}</if> | |||
<if test="brand != null and brand != ''">AND ad.brand = #{brand}</if> | |||
<if test="productCode != null and productCode != ''">AND ad.product_code LIKE CONCAT('%', #{productCode}, '%')</if> | |||
AND ad.status = 1 | |||
AND bd.brand IS NOT NULL | |||
AND bd.brand != '' | |||
GROUP BY bd.brand | |||
ORDER BY amount DESC | |||
</select> | |||
<!-- 7. 单品销售排行 --> | |||
<select id="selectProductRanking" resultType="com.ruoyi.tjfx.entity.RankingDataVO"> | |||
SELECT | |||
date, | |||
SUM(total_amount) AS totalAmount, | |||
SUM(quantity) AS totalQuantity | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
AND status = 1 | |||
AND product_code = #{productCode} | |||
GROUP BY date | |||
ORDER BY totalAmount DESC | |||
LIMIT 20 | |||
</select> | |||
<!-- 8. 单品销售趋势 --> | |||
<select id="selectProductTrend" resultType="com.ruoyi.tjfx.entity.TrendDataVO"> | |||
SELECT | |||
date, | |||
SUM(total_amount) AS amount, | |||
SUM(quantity) AS quantity | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
AND status = 1 | |||
AND product_code = #{productCode} | |||
GROUP BY date | |||
ORDER BY date ASC | |||
</select> | |||
<!-- 9. 店铺销售基础统计 --> | |||
<select id="selectShopBasicStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO"> | |||
SELECT | |||
SUM(quantity) AS totalQuantity, | |||
SUM(total_amount) AS totalAmount, | |||
COUNT(DISTINCT shop_name) AS totalShops, | |||
COUNT(DISTINCT category) AS totalCategories | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if> | |||
AND status = 1 | |||
</select> | |||
<!-- 10. 各店铺销售金额 --> | |||
<select id="selectShopAmountData" resultType="com.ruoyi.tjfx.entity.ShopAmountVO"> | |||
SELECT | |||
shop_name AS shopName, | |||
SUM(total_amount) AS amount | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if> | |||
AND status = 1 | |||
GROUP BY shop_name | |||
ORDER BY amount DESC | |||
</select> | |||
<!-- 11. 各店铺销售数量 --> | |||
<select id="selectShopQuantityData" resultType="com.ruoyi.tjfx.entity.ShopQuantityVO"> | |||
SELECT | |||
shop_name AS shopName, | |||
SUM(quantity) AS quantity | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if> | |||
AND status = 1 | |||
GROUP BY shop_name | |||
ORDER BY quantity DESC | |||
</select> | |||
<!-- 12. 各品类销售趋势 --> | |||
<select id="selectCategoryTrendData" resultType="com.ruoyi.tjfx.entity.CategoryTrendVO"> | |||
SELECT | |||
category, | |||
SUM(quantity) AS quantity, | |||
SUM(total_amount) AS amount | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if> | |||
AND status = 1 | |||
AND category IS NOT NULL | |||
AND category != '' | |||
GROUP BY category | |||
ORDER BY amount DESC | |||
</select> | |||
<!-- 13. 分类规格原始数据 --> | |||
<select id="selectSpecsRawData" resultType="map"> | |||
SELECT | |||
category_specs, | |||
SUM(quantity) AS quantity, | |||
SUM(total_amount) AS amount | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
<if test="category != null and category != ''">AND category = #{category}</if> | |||
AND status = 1 | |||
AND category_specs IS NOT NULL | |||
AND category_specs != '' | |||
GROUP BY category_specs | |||
ORDER BY amount DESC | |||
</select> | |||
<!-- 14. 筛选选项 --> | |||
<select id="selectCategories" resultType="string"> | |||
SELECT DISTINCT category | |||
FROM zs_tjfx_analysis_data | |||
WHERE category IS NOT NULL AND category != '' | |||
ORDER BY category | |||
</select> | |||
<select id="selectCategorySpecs" resultType="string"> | |||
SELECT DISTINCT category_specs | |||
FROM zs_tjfx_analysis_data | |||
WHERE category_specs IS NOT NULL AND category_specs != '' | |||
ORDER BY category_specs | |||
</select> | |||
<select id="selectCustomers" resultType="string"> | |||
SELECT DISTINCT customer_name | |||
FROM zs_tjfx_analysis_data | |||
WHERE customer_name IS NOT NULL AND customer_name != '' | |||
ORDER BY customer_name | |||
</select> | |||
<select id="selectShops" resultType="string"> | |||
SELECT DISTINCT shop_name | |||
FROM zs_tjfx_analysis_data | |||
WHERE shop_name IS NOT NULL AND shop_name != '' | |||
ORDER BY shop_name | |||
</select> | |||
<select id="selectBrands" resultType="string"> | |||
SELECT DISTINCT brand | |||
FROM zs_tjfx_base_data | |||
WHERE brand IS NOT NULL AND brand != '' | |||
ORDER BY brand | |||
</select> | |||
<!-- 15. 商品编码联想 --> | |||
<select id="selectProductCodeSuggestions" resultType="map"> | |||
SELECT | |||
bd.product_code AS product_code, | |||
bd.product_name AS product_name, | |||
bd.category_specs AS category_specs, | |||
c.name AS category_name | |||
FROM zs_tjfx_base_data bd | |||
LEFT JOIN zs_tjfx_categories c ON bd.category_id = c.id | |||
WHERE bd.product_code LIKE #{keyword} | |||
ORDER BY bd.product_code | |||
LIMIT 10 | |||
</select> | |||
<!-- 1. 整体销售分析 基础统计 --> | |||
<!-- <select id="selectBasicStats" resultType="com.ruoyi.tjfx.entity.BasicStatsVO"> | |||
SELECT | |||
COUNT(*) as totalRecords, | |||
SUM(quantity) as totalQuantity, | |||
SUM(total_amount) as totalAmount, | |||
COUNT(DISTINCT product_code) as uniqueProducts, | |||
COUNT(DISTINCT customer_name) as uniqueCustomers | |||
FROM zs_tjfx_analysis_data | |||
WHERE date BETWEEN #{startDate} AND #{endDate} | |||
<if test="category != null and category != ''">AND category = #{category}</if> | |||
<if test="categorySpecs != null and categorySpecs != ''">AND category_specs LIKE CONCAT('%', #{categorySpecs}, '%')</if> | |||
<if test="customer != null and customer != ''">AND customer_name = #{customer}</if> | |||
<if test="shop != null and shop != ''">AND shop_name = #{shop}</if> | |||
<if test="brand != null and brand != ''">AND brand = #{brand}</if> | |||
<if test="productCode != null and productCode != ''">AND product_code LIKE CONCAT('%', #{productCode}, '%')</if> | |||
AND status = 1 | |||
</select> | |||
<!– 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> |
@@ -0,0 +1,12 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |||
<mapper namespace="com.ruoyi.tjfx.mapper.ShopCustomerMapper"> | |||
<!-- 根据店铺名称查询客户名称 --> | |||
<select id="selectByShopName" resultType="com.ruoyi.tjfx.entity.ShopCustomer"> | |||
SELECT * FROM zs_tjfx_shop_customer | |||
WHERE shop_name = #{shopName} | |||
LIMIT 1 | |||
</select> | |||
</mapper> |