diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/VisitorsServiceImpl.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/VisitorsServiceImpl.java index 62e8a705b..89936886e 100644 --- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/VisitorsServiceImpl.java +++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/VisitorsServiceImpl.java @@ -320,6 +320,7 @@ public class VisitorsServiceImpl extends ServiceImpl smErpTasteStockEntityList; -} -``` - -## 使用方法 - -### 1. 新增商品SPU(包含口味) - -```java -@PostMapping -public R save(@RequestBody SmErpSpuEntity supermarketSpu) { - supermarketSpu.setDeptId(SecurityUtils.getUser().getDeptId()); - return smErpSpuService.saveSpu(supermarketSpu); -} -``` - -**请求示例:** -```json -{ - "productCode": "SPU001", - "productName": "果汁饮料", - "spec": "500ml", - "status": "0", - "stock": 100.0, - "smErpTasteStockEntityList": [ - { - "tasteName": "原味", - "stock": 50.0 - }, - { - "tasteName": "草莓味", - "stock": 30.0 - }, - { - "tasteName": "橙子味", - "stock": 20.0 - } - ], - "smErpUnitEntityList": [ - { - "unit": "瓶", - "packageNum": 1, - "isBase": true - } - ] -} -``` - -### 2. 编辑商品SPU(修改口味) - -```java -@PutMapping -public R updateById(@RequestBody SmErpSpuEntity supermarketSpu) { - return smErpSpuService.updateSpuById(supermarketSpu); -} -``` - -**请求示例(更新现有口味,新增口味,删除口味):** -```json -{ - "id": 1, - "productCode": "SPU001", - "productName": "果汁饮料", - "spec": "500ml", - "status": "0", - "stock": 120.0, - "smErpTasteStockEntityList": [ - { - "id": 1, // 有ID表示更新 - "tasteName": "原味", - "stock": 60.0 // 更新库存 - }, - { - "id": 2, // 有ID表示更新 - "tasteName": "草莓味", - "stock": 40.0 // 更新库存 - }, - { - "tasteName": "葡萄味", // 无ID表示新增 - "stock": 20.0 - } - // 不包含巧克力味,会被自动删除 - ], - "smErpUnitEntityList": [ - { - "id": 1, - "unit": "瓶", - "packageNum": 1, - "isBase": true - } - ] -} -``` - -## 业务逻辑 - -### 新增逻辑 -1. 验证商品基本信息 -2. 保存商品SPU主表 -3. 创建库存记录 -4. **处理口味集合**: - - 遍历口味列表 - - 设置SPU ID关联 - - 设置默认库存(如果未指定) - - 批量保存口味数据 -5. 处理包装单位 -6. 处理辅助条码 - -### 编辑逻辑 -1. 更新商品SPU主表 -2. 更新库存记录 -3. 处理包装单位(新增/更新/删除) -4. 处理辅助条码(新增/更新/删除) -5. **处理口味集合**: - - 分离新增和更新的口味 - - 新增口味:设置SPU ID,设置默认库存 - - 更新口味:保持ID,更新数据 - - 删除口味:计算需要删除的口味ID,批量删除 - -## 注意事项 - -### 1. 数据一致性 -- 所有操作都在事务中执行 -- 删除操作会级联删除相关数据 -- 口味ID必须与SPU ID匹配 - -### 2. 性能考虑 -- 使用批量操作提高性能 -- 避免N+1查询问题 -- 合理设置批量大小 - -### 3. 业务规则 -- 口味名称在同一SPU下必须唯一 -- 库存数量不能为负数 -- 删除SPU时自动清理相关口味 - -### 4. 错误处理 -- 提供详细的错误信息 -- 支持事务回滚 -- 记录操作日志 - -## 测试用例 - -项目包含完整的测试用例,覆盖以下场景: -- 新增商品SPU(包含口味) -- 编辑商品SPU(更新口味) -- 编辑商品SPU(删除口味) -- 混合操作(新增+更新+删除) - -运行测试: -```bash -mvn test -Dtest=SmErpSpuServiceTest -``` - -## 扩展功能 - -### 1. 口味库存管理 -- 支持口味级别的库存调整 -- 库存预警功能 -- 库存盘点功能 - -### 2. 口味统计分析 -- 口味销售统计 -- 口味库存分析 -- 口味偏好分析 - -### 3. 口味权限控制 -- 基于角色的口味管理权限 -- 口味数据访问控制 -- 操作审计日志 - -## 相关文件 - -- `SmErpSpuServiceImpl.java` - 服务实现类 -- `SmErpTasteStockEntity.java` - 口味实体类 -- `SmErpSpuEntity.java` - 商品SPU实体类 -- `SmErpSpuServiceTest.java` - 测试类 -- `README_TASTE_FEATURE.md` - 本文档 - -## 技术支持 - -如有问题或建议,请联系开发团队或提交Issue。 - diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/SELECT_OPTION_API.md b/pjcloud-supermarket/pjcloud-supermarket-biz/SELECT_OPTION_API.md deleted file mode 100644 index e36743241..000000000 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/SELECT_OPTION_API.md +++ /dev/null @@ -1,308 +0,0 @@ -# 下拉选择接口使用说明 - -## 概述 - -为了方便后台管理系统使用下拉选择组件,我们提供了一系列统一的下拉选择接口。这些接口返回标准化的选项数据格式,便于前端组件使用。 - -## 统一返回格式 - -所有下拉选择接口都返回以下格式的数据: - -```json -{ - "code": 0, - "msg": "操作成功", - "data": [ - { - "value": 1, - "label": "选项名称", - "disabled": false, - "extra": "扩展信息" - } - ] -} -``` - -### 字段说明 -- `value`: 选项值,可以是数字、字符串等类型 -- `label`: 选项显示名称 -- `disabled`: 是否禁用,默认为false -- `extra`: 扩展信息,可以包含额外的数据 - -## 接口列表 - -### 1. 商户下拉选择 - -#### 接口地址 -``` -GET /selectOption/merchant?keyWord=关键字 -``` - -#### 请求参数 -- `keyWord` (可选): 关键字,支持商户名称和手机号模糊查询 - -#### 权限要求 -需要 `supermarket_mallMerchant_view` 权限 - -#### 使用示例 -``` -# 获取所有商户 -GET /selectOption/merchant - -# 按商户名称搜索 -GET /selectOption/merchant?keyWord=农场 - -# 按手机号搜索 -GET /selectOption/merchant?keyWord=138 - -# 按关键字搜索(商户名称或手机号) -GET /selectOption/merchant?keyWord=张三 -``` - -#### 响应示例 -```json -{ - "code": 0, - "msg": "操作成功", - "data": [ - { - "value": 1, - "label": "张三农场", - "disabled": false, - "extra": "13800138000" - }, - { - "value": 2, - "label": "李四果园", - "disabled": false, - "extra": "13900139000" - } - ] -} -``` - -### 2. 分类下拉选择 - -#### 接口地址 -``` -GET /selectOption/category -``` - -#### 权限要求 -需要 `supermarket_mallCategory_view` 权限 - -#### 响应示例 -```json -{ - "code": 0, - "msg": "操作成功", - "data": [ - { - "value": 1, - "label": "水果", - "disabled": false, - "extra": 0 - }, - { - "value": 2, - "label": "蔬菜", - "disabled": false, - "extra": 0 - }, - { - "value": 3, - "label": "苹果", - "disabled": false, - "extra": 1 - } - ] -} -``` - -### 3. 分类下拉选择(树形结构) - -#### 接口地址 -``` -GET /selectOption/category/tree -``` - -#### 权限要求 -需要 `supermarket_mallCategory_view` 权限 - -#### 响应示例 -```json -{ - "code": 0, - "msg": "操作成功", - "data": [ - { - "value": 1, - "label": "水果", - "disabled": false, - "extra": 0 - }, - { - "value": 3, - "label": " └─ 苹果", - "disabled": false, - "extra": 1 - }, - { - "value": 2, - "label": "蔬菜", - "disabled": false, - "extra": 0 - } - ] -} -``` - -## 传统接口 - -除了统一的下拉选择接口,原有的控制器也提供了下拉选择接口: - -### 商户下拉选择(传统接口) -``` -GET /mallMerchant/selectList -``` - -### 分类下拉选择(传统接口) -``` -GET /mallCategory/selectList -``` - -## 前端使用示例 - -### Vue.js + Element Plus - -```javascript -// 获取商户下拉选择 -async getMerchantOptions(keyword = '') { - try { - const params = keyword ? { keyWord: keyword } : {}; - const response = await this.$http.get('/selectOption/merchant', { params }); - this.merchantOptions = response.data.data.map(item => ({ - value: item.value, - label: item.label, - disabled: item.disabled - })); - } catch (error) { - console.error('获取商户选项失败:', error); - } -} - -// 搜索商户 -async searchMerchants(keyword) { - await this.getMerchantOptions(keyword); -} - -// 获取分类下拉选择 -async getCategoryOptions() { - try { - const response = await this.$http.get('/selectOption/category'); - this.categoryOptions = response.data.data.map(item => ({ - value: item.value, - label: item.label, - disabled: item.disabled - })); - } catch (error) { - console.error('获取分类选项失败:', error); - } -} -``` - -### HTML模板 - -```html - - - -``` - -## 扩展说明 - -### 添加新的下拉选择类型 - -如果需要添加新的下拉选择类型,可以: - -1. 在 `SelectOptionService` 中添加新的方法 -2. 在 `SelectOptionController` 中添加对应的接口 -3. 按照统一的格式返回数据 - -### 自定义扩展信息 - -`extra` 字段可以包含任何额外的信息,例如: -- 联系电话 -- 父级ID -- 状态信息 -- 其他业务相关数据 - -前端可以根据需要解析和使用这些扩展信息。 - -## 注意事项 - -1. 所有接口都需要相应的权限验证 -2. 只返回启用状态的数据 -3. 数据按照指定的排序规则返回 -4. 建议前端对接口返回的数据进行缓存,避免频繁请求 diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/SQL_FIX_SCRIPT_GUIDE.md b/pjcloud-supermarket/pjcloud-supermarket-biz/SQL_FIX_SCRIPT_GUIDE.md new file mode 100644 index 000000000..7d9a96292 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/SQL_FIX_SCRIPT_GUIDE.md @@ -0,0 +1,226 @@ +# 出入库记录totalAmount字段SQL修复脚本使用指南 + +## 问题描述 + +由于历史数据问题,线上存在大量出入库记录的主表 `totalAmount` 字段与明细表数据不一致的脏数据。这些数据在编辑时会导致总金额显示不准确。 + +## 解决方案 + +使用SQL脚本在数据库层面直接修复数据不一致问题。 + +## SQL修复脚本 + +### 执行文件 +- **脚本文件**: `fix_inventory_total_amount.sql` +- **说明文档**: `SQL_FIX_SCRIPT_GUIDE.md` + +### 执行步骤 + +#### 1. 查看问题数据 + +```sql +-- 查看数据不一致的记录数量 +SELECT + '数据不一致记录统计' as description, + COUNT(*) as inconsistent_count +FROM sm_erp_inventory_records r +WHERE r.total_amount != ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +); +``` + +#### 2. 查看具体的不一致记录详情(前10条) + +```sql +SELECT + r.id, + r.inventory_code, + r.inventory_date, + r.total_amount as main_table_amount, + COALESCE(SUM(i.total_amount), 0) as calculated_amount, + (r.total_amount - COALESCE(SUM(i.total_amount), 0)) as difference +FROM sm_erp_inventory_records r +LEFT JOIN sm_erp_inventory_records_item i ON i.inventory_id = r.id +GROUP BY r.id, r.inventory_code, r.inventory_date, r.total_amount +HAVING r.total_amount != COALESCE(SUM(i.total_amount), 0) +ORDER BY r.id +LIMIT 10; +``` + +#### 3. 创建备份表 + +```sql +-- 备份原始数据 +CREATE TABLE IF NOT EXISTS sm_erp_inventory_records_backup_20250127 AS +SELECT * FROM sm_erp_inventory_records; + +-- 创建备份表的索引 +CREATE INDEX IF NOT EXISTS idx_backup_inventory_id ON sm_erp_inventory_records_backup_20250127(id); +``` + +#### 4. 执行修复 + +```sql +-- 修复数据不一致的记录 +UPDATE sm_erp_inventory_records r +SET total_amount = ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +) +WHERE r.total_amount != ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +); +``` + +#### 5. 验证修复结果 + +```sql +-- 检查是否还有不一致的记录 +SELECT + '修复后数据检查' as description, + COUNT(*) as remaining_inconsistent_count +FROM sm_erp_inventory_records r +WHERE r.total_amount != ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +); +``` + +#### 6. 生成修复报告 + +```sql +SELECT + '修复报告' as report_type, + COUNT(*) as total_records, + SUM(CASE + WHEN r.total_amount = COALESCE(SUM(i.total_amount), 0) THEN 1 + ELSE 0 + END) as consistent_records, + SUM(CASE + WHEN r.total_amount != COALESCE(SUM(i.total_amount), 0) THEN 1 + ELSE 0 + END) as inconsistent_records, + ROUND( + SUM(CASE + WHEN r.total_amount = COALESCE(SUM(i.total_amount), 0) THEN 1 + ELSE 0 + END) * 100.0 / COUNT(*), 2 + ) as consistency_rate +FROM sm_erp_inventory_records r +LEFT JOIN sm_erp_inventory_records_item i ON i.inventory_id = r.id +GROUP BY r.id, r.total_amount; +``` + +## 大数据量分批执行 + +如果数据量很大,建议分批执行避免锁表时间过长: + +```sql +-- 分批修复(每次1000条) +UPDATE sm_erp_inventory_records r +SET total_amount = ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +) +WHERE r.id BETWEEN 1 AND 1000 +AND r.total_amount != ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +); +``` + +## 数据一致性检查视图 + +创建视图方便后续检查数据一致性: + +```sql +-- 创建数据一致性检查视图 +CREATE OR REPLACE VIEW v_inventory_data_consistency AS +SELECT + r.id, + r.inventory_code, + r.inventory_date, + r.total_amount as main_table_amount, + COALESCE(SUM(i.total_amount), 0) as calculated_amount, + (r.total_amount - COALESCE(SUM(i.total_amount), 0)) as difference, + CASE + WHEN r.total_amount = COALESCE(SUM(i.total_amount), 0) THEN 'CONSISTENT' + ELSE 'INCONSISTENT' + END as status +FROM sm_erp_inventory_records r +LEFT JOIN sm_erp_inventory_records_item i ON i.inventory_id = r.id +GROUP BY r.id, r.inventory_code, r.inventory_date, r.total_amount; + +-- 查询不一致的记录 +SELECT * FROM v_inventory_data_consistency WHERE status = 'INCONSISTENT' LIMIT 10; +``` + +## 执行建议 + +### 推荐执行顺序 + +1. **测试环境验证** + - 先在测试环境执行脚本 + - 验证修复逻辑正确性 + +2. **生产环境执行** + - 选择业务低峰期执行 + - 建议分批执行大数据量 + +3. **验证结果** + - 检查修复后的数据一致性 + - 验证业务功能正常 + +### 注意事项 + +1. **备份数据** + - 执行前务必备份相关数据表 + - 保留备份直到确认修复成功 + +2. **业务影响** + - 建议在业务低峰期执行 + - 避免在执行过程中进行数据编辑操作 + +3. **性能考虑** + - 大数据量建议分批执行 + - 监控数据库性能指标 + +4. **回滚方案** + - 如果出现问题,可以从备份表恢复数据 + ```sql + -- 回滚示例 + UPDATE sm_erp_inventory_records r + SET total_amount = b.total_amount + FROM sm_erp_inventory_records_backup_20250127 b + WHERE r.id = b.id; + ``` + +## 清理备份表 + +确认修复成功后,可以清理备份表: + +```sql +-- 注意:请确认修复成功后再执行删除操作 +DROP TABLE IF EXISTS sm_erp_inventory_records_backup_20250127; +``` + +## 后续维护 + +建议定期检查数据一致性: + +```sql +-- 定期检查脚本 +SELECT COUNT(*) as inconsistent_count +FROM v_inventory_data_consistency +WHERE status = 'INCONSISTENT'; +``` + +如果发现新的不一致数据,可以重新执行修复脚本。 diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/fix_inventory_total_amount.sql b/pjcloud-supermarket/pjcloud-supermarket-biz/fix_inventory_total_amount.sql new file mode 100644 index 000000000..2d648d35f --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/fix_inventory_total_amount.sql @@ -0,0 +1,161 @@ +-- 出入库记录totalAmount字段修复SQL脚本 +-- 用途:修复由于历史数据问题导致的主表totalAmount字段与明细表数据不一致的问题 +-- 作者:pjcloud +-- 日期:2025-01-27 + +-- ============================================ +-- 1. 查看数据不一致的记录数量 +-- ============================================ +SELECT + '数据不一致记录统计' as description, + COUNT(*) as inconsistent_count +FROM sm_erp_inventory_records r +WHERE r.total_amount != ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +); + +-- ============================================ +-- 2. 查看具体的不一致记录详情(前10条) +-- ============================================ +SELECT + r.id, + r.inventory_code, + r.inventory_date, + r.total_amount as main_table_amount, + COALESCE(SUM(i.total_amount), 0) as calculated_amount, + (r.total_amount - COALESCE(SUM(i.total_amount), 0)) as difference +FROM sm_erp_inventory_records r +LEFT JOIN sm_erp_inventory_records_item i ON i.inventory_id = r.id +GROUP BY r.id, r.inventory_code, r.inventory_date, r.total_amount +HAVING r.total_amount != COALESCE(SUM(i.total_amount), 0) +ORDER BY r.id +LIMIT 10; + +-- ============================================ +-- 3. 备份原始数据(可选,建议执行) +-- ============================================ +-- 创建备份表 +CREATE TABLE IF NOT EXISTS sm_erp_inventory_records_backup_20250127 AS +SELECT * FROM sm_erp_inventory_records; + +-- 创建备份表的索引 +CREATE INDEX IF NOT EXISTS idx_backup_inventory_id ON sm_erp_inventory_records_backup_20250127(id); + +-- ============================================ +-- 4. 修复数据不一致的记录 +-- ============================================ +-- 方法1:使用UPDATE JOIN(推荐,性能较好) +UPDATE sm_erp_inventory_records r +SET total_amount = ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +) +WHERE r.total_amount != ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +); + +-- ============================================ +-- 5. 验证修复结果 +-- ============================================ +-- 检查是否还有不一致的记录 +SELECT + '修复后数据检查' as description, + COUNT(*) as remaining_inconsistent_count +FROM sm_erp_inventory_records r +WHERE r.total_amount != ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id +); + +-- ============================================ +-- 6. 生成修复报告 +-- ============================================ +SELECT + '修复报告' as report_type, + COUNT(*) as total_records, + SUM(CASE + WHEN r.total_amount = COALESCE(SUM(i.total_amount), 0) THEN 1 + ELSE 0 + END) as consistent_records, + SUM(CASE + WHEN r.total_amount != COALESCE(SUM(i.total_amount), 0) THEN 1 + ELSE 0 + END) as inconsistent_records, + ROUND( + SUM(CASE + WHEN r.total_amount = COALESCE(SUM(i.total_amount), 0) THEN 1 + ELSE 0 + END) * 100.0 / COUNT(*), 2 + ) as consistency_rate +FROM sm_erp_inventory_records r +LEFT JOIN sm_erp_inventory_records_item i ON i.inventory_id = r.id +GROUP BY r.id, r.total_amount; + +-- ============================================ +-- 7. 清理备份表(可选,确认修复成功后执行) +-- ============================================ +-- 注意:请确认修复成功后再执行删除操作 +-- DROP TABLE IF EXISTS sm_erp_inventory_records_backup_20250127; + +-- ============================================ +-- 8. 创建数据一致性检查视图(可选) +-- ============================================ +CREATE OR REPLACE VIEW v_inventory_data_consistency AS +SELECT + r.id, + r.inventory_code, + r.inventory_date, + r.total_amount as main_table_amount, + COALESCE(SUM(i.total_amount), 0) as calculated_amount, + (r.total_amount - COALESCE(SUM(i.total_amount), 0)) as difference, + CASE + WHEN r.total_amount = COALESCE(SUM(i.total_amount), 0) THEN 'CONSISTENT' + ELSE 'INCONSISTENT' + END as status +FROM sm_erp_inventory_records r +LEFT JOIN sm_erp_inventory_records_item i ON i.inventory_id = r.id +GROUP BY r.id, r.inventory_code, r.inventory_date, r.total_amount; + +-- 查询数据一致性视图 +-- SELECT * FROM v_inventory_data_consistency WHERE status = 'INCONSISTENT' LIMIT 10; + +-- ============================================ +-- 使用说明: +-- ============================================ +/* +1. 执行前建议: + - 在测试环境先验证脚本 + - 备份相关数据表 + - 在业务低峰期执行 + +2. 执行步骤: + - 先执行步骤1-2查看问题数据 + - 执行步骤3创建备份 + - 执行步骤4进行修复 + - 执行步骤5-6验证结果 + +3. 注意事项: + - 脚本会修改数据,请谨慎执行 + - 建议分批次执行,避免锁表时间过长 + - 执行后请验证业务功能正常 + +4. 如果数据量很大,可以考虑分批执行: + UPDATE sm_erp_inventory_records r + SET total_amount = ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id + ) + WHERE r.id BETWEEN 1 AND 1000 + AND r.total_amount != ( + SELECT COALESCE(SUM(i.total_amount), 0) + FROM sm_erp_inventory_records_item i + WHERE i.inventory_id = r.id + ); +*/ diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/RealtimeSalesReportController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/RealtimeSalesReportController.java new file mode 100644 index 000000000..5c9d52e37 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/RealtimeSalesReportController.java @@ -0,0 +1,262 @@ +package com.bsd.pjcloud.supermarket.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.log.annotation.SysLog; +import com.bsd.pjcloud.supermarket.dto.realtime.RealtimeSalesReportQueryDTO; +import com.bsd.pjcloud.supermarket.service.RealtimeSalesReportService; +import com.bsd.pjcloud.supermarket.vo.realtime.RealtimeSalesReportVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +/** + * 即时销售报表控制器 + * + * @author pjcloud + * @date 2025-01-27 10:00:00 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/realtimeSalesReport") +@Tag(description = "即时销售报表", name = "即时销售报表管理") +@SecurityRequirement(name = HttpHeaders.AUTHORIZATION) +public class RealtimeSalesReportController { + + private final RealtimeSalesReportService realtimeSalesReportService; + + /** + * 获取即时销售报表 + * + * @param queryDTO 查询条件 + * @return 即时销售报表数据 + */ + @Operation(summary = "获取即时销售报表", description = "获取完整的即时销售报表,包括销售汇总、畅销品排名、滞销品排名、客单价分析等") + @GetMapping("/report") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R getRealtimeSalesReport( + @Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) { + RealtimeSalesReportVO report = realtimeSalesReportService.getRealtimeSalesReport(queryDTO); + return R.ok(report); + } + + /** + * 获取实时销售数据 + * + * @param queryDTO 查询条件 + * @return 实时销售数据 + */ + @Operation(summary = "获取实时销售数据", description = "获取实时销售数据,通常查询最近1小时的数据,按分钟更新") + @GetMapping("/realtime") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R getRealtimeSalesData( + @Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) { + RealtimeSalesReportVO report = realtimeSalesReportService.getRealtimeSalesData(queryDTO); + return R.ok(report); + } + + /** + * 获取畅销品排名 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 畅销品排名列表 + */ + @Operation(summary = "获取畅销品排名", description = "获取畅销品排名,按销售额降序排列") + @GetMapping("/topSellingProducts") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R> getTopSellingProducts( + @Parameter(description = "分页参数") Page page, + @Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) { + Page result = + realtimeSalesReportService.getTopSellingProducts(page, queryDTO); + return R.ok(result); + } + + /** + * 获取滞销品排名 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 滞销品排名列表 + */ + @Operation(summary = "获取滞销品排名", description = "获取滞销品排名,按销售额升序排列") + @GetMapping("/slowSellingProducts") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R> getSlowSellingProducts( + @Parameter(description = "分页参数") Page page, + @Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) { + Page result = + realtimeSalesReportService.getSlowSellingProducts(page, queryDTO); + return R.ok(result); + } + + /** + * 获取时段销售数据 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 时段销售数据列表 + */ + @Operation(summary = "获取时段销售数据", description = "获取时段销售数据,支持按小时/天/月查询") + @GetMapping("/timePeriodSales") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R> getTimePeriodSales( + @Parameter(description = "分页参数") Page page, + @Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) { + Page result = + realtimeSalesReportService.getTimePeriodSales(page, queryDTO); + return R.ok(result); + } + + /** + * 获取客单价分析数据 + * + * @param queryDTO 查询条件 + * @return 客单价分析数据 + */ + @Operation(summary = "获取客单价分析数据", description = "获取客单价分析数据,包括平均客单价、最高最低客单价、分布区间等") + @GetMapping("/customerAnalysis") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R getCustomerAnalysis( + @Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) { + RealtimeSalesReportVO.CustomerAnalysisVO analysis = + realtimeSalesReportService.getCustomerAnalysis(queryDTO); + return R.ok(analysis); + } + + /** + * 获取销售趋势数据 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 销售趋势数据列表 + */ + @Operation(summary = "获取销售趋势数据", description = "获取销售趋势数据,用于图表展示") + @GetMapping("/salesTrend") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R> getSalesTrend( + @Parameter(description = "分页参数") Page page, + @Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) { + Page result = + realtimeSalesReportService.getSalesTrend(page, queryDTO); + return R.ok(result); + } + + /** + * 刷新实时销售数据缓存 + * + * @return 刷新结果 + */ + @Operation(summary = "刷新实时销售数据缓存", description = "手动刷新实时销售数据缓存") + @SysLog("刷新实时销售数据缓存") + @PostMapping("/refresh") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R refreshRealtimeData() { + Boolean result = realtimeSalesReportService.refreshRealtimeData(); + return R.ok(result); + } + + /** + * 获取今日销售汇总 + * + * @return 今日销售汇总数据 + */ + @Operation(summary = "获取今日销售汇总", description = "获取今日销售汇总数据,包括销售额、销售笔数、客单价等") + @GetMapping("/todaySummary") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R getTodaySummary() { + RealtimeSalesReportQueryDTO queryDTO = new RealtimeSalesReportQueryDTO(); + queryDTO.setQueryType("day"); + queryDTO.setStartTime(java.time.LocalDateTime.now().withHour(0).withMinute(0).withSecond(0)); + queryDTO.setEndTime(java.time.LocalDateTime.now()); + + RealtimeSalesReportVO report = realtimeSalesReportService.getRealtimeSalesReport(queryDTO); + return R.ok(report.getSalesSummary()); + } + + /** + * 获取本月销售汇总 + * + * @return 本月销售汇总数据 + */ + @Operation(summary = "获取本月销售汇总", description = "获取本月销售汇总数据") + @GetMapping("/monthSummary") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R getMonthSummary() { + RealtimeSalesReportQueryDTO queryDTO = new RealtimeSalesReportQueryDTO(); + queryDTO.setQueryType("month"); + queryDTO.setStartTime(java.time.LocalDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0)); + queryDTO.setEndTime(java.time.LocalDateTime.now()); + + RealtimeSalesReportVO report = realtimeSalesReportService.getRealtimeSalesReport(queryDTO); + return R.ok(report.getSalesSummary()); + } + + /** + * 获取实时畅销品TOP10 + * + * @return 实时畅销品TOP10 + */ + @Operation(summary = "获取实时畅销品TOP10", description = "获取实时畅销品TOP10排名") + @GetMapping("/top10Products") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R> getTop10Products() { + RealtimeSalesReportQueryDTO queryDTO = new RealtimeSalesReportQueryDTO(); + queryDTO.setQueryType("day"); + queryDTO.setStartTime(java.time.LocalDateTime.now().withHour(0).withMinute(0).withSecond(0)); + queryDTO.setEndTime(java.time.LocalDateTime.now()); + queryDTO.setTopLimit(10); + + Page page = new Page<>(1, 10); + Page result = + realtimeSalesReportService.getTopSellingProducts(page, queryDTO); + return R.ok(result); + } + + /** + * 获取实时滞销品TOP10 + * + * @return 实时滞销品TOP10 + */ + @Operation(summary = "获取实时滞销品TOP10", description = "获取实时滞销品TOP10排名") + @GetMapping("/bottom10Products") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R> getBottom10Products() { + RealtimeSalesReportQueryDTO queryDTO = new RealtimeSalesReportQueryDTO(); + queryDTO.setQueryType("day"); + queryDTO.setStartTime(java.time.LocalDateTime.now().withHour(0).withMinute(0).withSecond(0)); + queryDTO.setEndTime(java.time.LocalDateTime.now()); + queryDTO.setTopLimit(10); + + Page page = new Page<>(1, 10); + Page result = + realtimeSalesReportService.getSlowSellingProducts(page, queryDTO); + return R.ok(result); + } + + /** + * 获取小时销售趋势(今日) + * + * @return 今日小时销售趋势数据 + */ + @Operation(summary = "获取小时销售趋势", description = "获取今日小时销售趋势数据") + @GetMapping("/hourlyTrend") + @PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')") + public R> getHourlyTrend() { + RealtimeSalesReportQueryDTO queryDTO = new RealtimeSalesReportQueryDTO(); + queryDTO.setQueryType("hour"); + queryDTO.setStartTime(java.time.LocalDateTime.now().withHour(0).withMinute(0).withSecond(0)); + queryDTO.setEndTime(java.time.LocalDateTime.now()); + + Page page = new Page<>(1, 24); + Page result = + realtimeSalesReportService.getTimePeriodSales(page, queryDTO); + return R.ok(result); + } +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/RealtimeSalesReportService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/RealtimeSalesReportService.java new file mode 100644 index 000000000..e4b5b4b78 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/RealtimeSalesReportService.java @@ -0,0 +1,81 @@ +package com.bsd.pjcloud.supermarket.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.supermarket.dto.realtime.RealtimeSalesReportQueryDTO; +import com.bsd.pjcloud.supermarket.vo.realtime.RealtimeSalesReportVO; + +/** + * 即时销售报表服务接口 + * + * @author pjcloud + * @date 2025-01-27 10:00:00 + */ +public interface RealtimeSalesReportService { + + /** + * 获取即时销售报表 + * + * @param queryDTO 查询条件 + * @return 即时销售报表数据 + */ + RealtimeSalesReportVO getRealtimeSalesReport(RealtimeSalesReportQueryDTO queryDTO); + + /** + * 获取实时销售数据(按分钟更新) + * + * @param queryDTO 查询条件 + * @return 实时销售数据 + */ + RealtimeSalesReportVO getRealtimeSalesData(RealtimeSalesReportQueryDTO queryDTO); + + /** + * 获取畅销品排名 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 畅销品排名列表 + */ + Page getTopSellingProducts(Page page, RealtimeSalesReportQueryDTO queryDTO); + + /** + * 获取滞销品排名 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 滞销品排名列表 + */ + Page getSlowSellingProducts(Page page, RealtimeSalesReportQueryDTO queryDTO); + + /** + * 获取时段销售数据 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 时段销售数据列表 + */ + Page getTimePeriodSales(Page page, RealtimeSalesReportQueryDTO queryDTO); + + /** + * 获取客单价分析数据 + * + * @param queryDTO 查询条件 + * @return 客单价分析数据 + */ + RealtimeSalesReportVO.CustomerAnalysisVO getCustomerAnalysis(RealtimeSalesReportQueryDTO queryDTO); + + /** + * 获取销售趋势数据 + * + * @param page 分页参数 + * @param queryDTO 查询条件 + * @return 销售趋势数据列表 + */ + Page getSalesTrend(Page page, RealtimeSalesReportQueryDTO queryDTO); + + /** + * 刷新实时销售数据缓存 + * + * @return 是否刷新成功 + */ + Boolean refreshRealtimeData(); +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/RealtimeSalesReportServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/RealtimeSalesReportServiceImpl.java new file mode 100644 index 000000000..56d9f99a2 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/RealtimeSalesReportServiceImpl.java @@ -0,0 +1,1268 @@ +package com.bsd.pjcloud.supermarket.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.common.data.datascope.DataScope; +import com.bsd.pjcloud.supermarket.dto.realtime.RealtimeSalesReportQueryDTO; +import com.bsd.pjcloud.supermarket.entity.SmErpInventoryRecordsEntity; +import com.bsd.pjcloud.supermarket.entity.SmErpInventoryRecordsItemEntity; +import com.bsd.pjcloud.supermarket.entity.SmErpSpuEntity; +import com.bsd.pjcloud.supermarket.entity.SmErpStockEntity; +import com.bsd.pjcloud.supermarket.entity.SmErpUnitEntity; +import com.bsd.pjcloud.supermarket.entity.SmErpCategoryEntity; +import com.bsd.pjcloud.supermarket.mapper.SmErpInventoryRecordsItemMapper; +import com.bsd.pjcloud.supermarket.mapper.SmErpInventoryRecordsMapper; +import com.bsd.pjcloud.supermarket.mapper.SmErpSpuMapper; +import com.bsd.pjcloud.supermarket.mapper.SmErpStockMapper; +import com.bsd.pjcloud.supermarket.mapper.SmErpUnitMapper; +import com.bsd.pjcloud.supermarket.mapper.SmErpCategoryMapper; +import com.bsd.pjcloud.supermarket.service.RealtimeSalesReportService; +import com.bsd.pjcloud.supermarket.vo.realtime.RealtimeSalesReportVO; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 即时销售报表服务实现类 + * + * @author pjcloud + * @date 2025-01-27 10:00:00 + */ +@Slf4j +@Service +@AllArgsConstructor +public class RealtimeSalesReportServiceImpl implements RealtimeSalesReportService { + + private final SmErpInventoryRecordsMapper inventoryRecordsMapper; + private final SmErpInventoryRecordsItemMapper smErpInventoryRecordsItemMapper; + private final SmErpSpuMapper smErpSpuMapper; + private final SmErpStockMapper stockMapper; + private final SmErpUnitMapper smErpUnitMapper; + private final SmErpCategoryMapper smErpCategoryMapper; + + @Override + public RealtimeSalesReportVO getRealtimeSalesReport(RealtimeSalesReportQueryDTO queryDTO) { + RealtimeSalesReportVO reportVO = new RealtimeSalesReportVO(); + reportVO.setReportTime(LocalDateTime.now()); + + // 设置查询时间段 + String timeRange = buildTimeRange(queryDTO); + reportVO.setTimeRange(timeRange); + + // 获取销售汇总信息 + RealtimeSalesReportVO.SalesSummaryVO salesSummary = getSalesSummary(queryDTO); + reportVO.setSalesSummary(salesSummary); + + // 获取时段销售数据 + List timePeriodSales = getTimePeriodSalesData(queryDTO); + reportVO.setTimePeriodSales(timePeriodSales); + + // 获取畅销品排名 + List topSellingProducts = getTopSellingProductsData(queryDTO); + reportVO.setTopSellingProducts(topSellingProducts); + + // 获取滞销品排名 + List slowSellingProducts = getSlowSellingProductsData(queryDTO); + reportVO.setSlowSellingProducts(slowSellingProducts); + + // 获取客单价分析 + RealtimeSalesReportVO.CustomerAnalysisVO customerAnalysis = getCustomerAnalysisData(queryDTO); + reportVO.setCustomerAnalysis(customerAnalysis); + + // 获取销售趋势数据 + List salesTrend = getSalesTrendData(queryDTO); + reportVO.setSalesTrend(salesTrend); + + return reportVO; + } + + @Override + public RealtimeSalesReportVO getRealtimeSalesData(RealtimeSalesReportQueryDTO queryDTO) { + // 实时数据查询,通常查询最近1小时的数据 + RealtimeSalesReportQueryDTO realtimeQuery = new RealtimeSalesReportQueryDTO(); + realtimeQuery.setQueryType("hour"); + realtimeQuery.setStartTime(LocalDateTime.now().minusHours(1)); + realtimeQuery.setEndTime(LocalDateTime.now()); + realtimeQuery.setIncludeRealtime(true); + realtimeQuery.setUpdateByMinute(true); + + return getRealtimeSalesReport(realtimeQuery); + } + + @Override + public Page getTopSellingProducts(Page page, RealtimeSalesReportQueryDTO queryDTO) { + List topProducts = getTopSellingProductsData(queryDTO); + + // 分页处理 + int start = (int) ((page.getCurrent() - 1) * page.getSize()); + int end = Math.min(start + (int) page.getSize(), topProducts.size()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize()); + if (start < topProducts.size()) { + resultPage.setRecords(topProducts.subList(start, end)); + resultPage.setTotal(topProducts.size()); + } else { + resultPage.setRecords(new ArrayList<>()); + resultPage.setTotal(0); + } + + return resultPage; + } + + @Override + public Page getSlowSellingProducts(Page page, RealtimeSalesReportQueryDTO queryDTO) { + List slowProducts = getSlowSellingProductsData(queryDTO); + + // 分页处理 + int start = (int) ((page.getCurrent() - 1) * page.getSize()); + int end = Math.min(start + (int) page.getSize(), slowProducts.size()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize()); + if (start < slowProducts.size()) { + resultPage.setRecords(slowProducts.subList(start, end)); + resultPage.setTotal(slowProducts.size()); + } else { + resultPage.setRecords(new ArrayList<>()); + resultPage.setTotal(0); + } + + return resultPage; + } + + @Override + public Page getTimePeriodSales(Page page, RealtimeSalesReportQueryDTO queryDTO) { + List timePeriodData = getTimePeriodSalesData(queryDTO); + + // 分页处理 + int start = (int) ((page.getCurrent() - 1) * page.getSize()); + int end = Math.min(start + (int) page.getSize(), timePeriodData.size()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize()); + if (start < timePeriodData.size()) { + resultPage.setRecords(timePeriodData.subList(start, end)); + resultPage.setTotal(timePeriodData.size()); + } else { + resultPage.setRecords(new ArrayList<>()); + resultPage.setTotal(0); + } + + return resultPage; + } + + @Override + public RealtimeSalesReportVO.CustomerAnalysisVO getCustomerAnalysis(RealtimeSalesReportQueryDTO queryDTO) { + return getCustomerAnalysisData(queryDTO); + } + + @Override + public Page getSalesTrend(Page page, RealtimeSalesReportQueryDTO queryDTO) { + List trendData = getSalesTrendData(queryDTO); + + // 分页处理 + int start = (int) ((page.getCurrent() - 1) * page.getSize()); + int end = Math.min(start + (int) page.getSize(), trendData.size()); + + Page resultPage = new Page<>(page.getCurrent(), page.getSize()); + if (start < trendData.size()) { + resultPage.setRecords(trendData.subList(start, end)); + resultPage.setTotal(trendData.size()); + } else { + resultPage.setRecords(new ArrayList<>()); + resultPage.setTotal(0); + } + + return resultPage; + } + + @Override + public Boolean refreshRealtimeData() { + try { + // 这里可以实现缓存刷新逻辑 + // 例如:清除Redis缓存,重新计算实时数据等 + log.info("刷新实时销售数据缓存成功"); + return true; + } catch (Exception e) { + log.error("刷新实时销售数据缓存失败", e); + return false; + } + } + + /** + * 构建查询时间段描述 + */ + private String buildTimeRange(RealtimeSalesReportQueryDTO queryDTO) { + if (queryDTO.getStartTime() != null && queryDTO.getEndTime() != null) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + return queryDTO.getStartTime().format(formatter) + " ~ " + queryDTO.getEndTime().format(formatter); + } else if (queryDTO.getStartDate() != null && queryDTO.getEndDate() != null) { + return queryDTO.getStartDate() + " ~ " + queryDTO.getEndDate(); + } + return "实时数据"; + } + + /** + * 获取销售汇总信息 + */ + private RealtimeSalesReportVO.SalesSummaryVO getSalesSummary(RealtimeSalesReportQueryDTO queryDTO) { + // 查询销售记录 + List salesRecords = getSalesRecords(queryDTO); + + RealtimeSalesReportVO.SalesSummaryVO summary = new RealtimeSalesReportVO.SalesSummaryVO(); + + if (CollectionUtils.isEmpty(salesRecords)) { + summary.setTotalSalesAmount(BigDecimal.ZERO); + summary.setTotalSalesCount(0); + summary.setAverageCustomerValue(BigDecimal.ZERO); + summary.setProductVarietyCount(0); + summary.setTotalSalesQuantity(0.0); + summary.setSalesGrowthRate(BigDecimal.ZERO); + summary.setSalesGrowthRateMonth(BigDecimal.ZERO); + summary.setRealtimeStatus("LOW"); + return summary; + } + + // 计算总销售额 + BigDecimal totalSalesAmount = salesRecords.stream() + .map(record -> new BigDecimal(record.getTotalAmount() != null ? record.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + // 计算总销售笔数 + int totalSalesCount = salesRecords.size(); + + // 计算平均客单价 + BigDecimal averageCustomerValue = totalSalesCount > 0 ? + totalSalesAmount.divide(new BigDecimal(totalSalesCount), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 获取销售商品种类数 + Set productIds = getSoldProductIds(queryDTO); + int productVarietyCount = productIds.size(); + + // 计算总销售数量 + double totalSalesQuantity = getTotalSalesQuantity(queryDTO); + + // 计算增长率(这里简化处理,实际应该对比历史数据) + BigDecimal salesGrowthRate = calculateGrowthRate(queryDTO, "year"); + BigDecimal salesGrowthRateMonth = calculateGrowthRate(queryDTO, "month"); + + // 判断实时销售状态 + String realtimeStatus = determineRealtimeStatus(totalSalesAmount, queryDTO); + + summary.setTotalSalesAmount(totalSalesAmount); + summary.setTotalSalesCount(totalSalesCount); + summary.setAverageCustomerValue(averageCustomerValue); + summary.setProductVarietyCount(productVarietyCount); + summary.setTotalSalesQuantity(totalSalesQuantity); + summary.setSalesGrowthRate(salesGrowthRate); + summary.setSalesGrowthRateMonth(salesGrowthRateMonth); + summary.setRealtimeStatus(realtimeStatus); + + return summary; + } + + /** + * 获取时段销售数据 + */ + private List getTimePeriodSalesData(RealtimeSalesReportQueryDTO queryDTO) { + List timePeriodList = new ArrayList<>(); + + String queryType = queryDTO.getQueryType(); + if (StrUtil.isBlank(queryType)) { + queryType = "day"; + } + + switch (queryType) { + case "hour": + timePeriodList = getHourlySalesData(queryDTO); + break; + case "day": + timePeriodList = getDailySalesData(queryDTO); + break; + case "month": + timePeriodList = getMonthlySalesData(queryDTO); + break; + default: + timePeriodList = getDailySalesData(queryDTO); + break; + } + + return timePeriodList; + } + + /** + * 获取畅销品排名数据 + */ + private List getTopSellingProductsData(RealtimeSalesReportQueryDTO queryDTO) { + List rankingList = new ArrayList<>(); + + // 查询销售明细数据 + List salesItems = getSalesItems(queryDTO); + + if (CollectionUtils.isEmpty(salesItems)) { + // 没有销售数据时返回空列表 + return rankingList; + } + + // 按商品ID分组统计 + Map> productGroups = salesItems.stream() + .collect(Collectors.groupingBy(SmErpInventoryRecordsItemEntity::getErpSpuId)); + + // 获取商品信息 + Set productIds = productGroups.keySet(); + List products = smErpSpuMapper.selectByIds(productIds); + Map productMap = products.stream() + .collect(Collectors.toMap(SmErpSpuEntity::getId, product -> product)); + + // 获取库存信息 + List stocks = stockMapper.selectList( + new LambdaQueryWrapper() + .in(SmErpStockEntity::getErpSpuId, productIds) + ); + Map stockMap = stocks.stream() + .collect(Collectors.toMap(SmErpStockEntity::getErpSpuId, stock -> stock)); + + // 计算每个商品的销售数据 + int ranking = 1; + for (Map.Entry> entry : productGroups.entrySet()) { + Long productId = entry.getKey(); + List items = entry.getValue(); + + // 计算销售额、数量、笔数 + BigDecimal salesAmount = items.stream() + .map(item -> new BigDecimal(item.getTotalAmount() != null ? item.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + double salesQuantity = items.stream() + .mapToDouble(item -> item.getQuantity() != null ? item.getQuantity() : 0) + .sum(); + + int salesCount = items.size(); + + BigDecimal averagePrice = salesQuantity > 0 ? + salesAmount.divide(new BigDecimal(salesQuantity), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 获取库存信息 + SmErpStockEntity stock = stockMap.get(productId); + double stockQuantity = stock != null ? stock.getCurrentStock() : 0; + String stockStatus = determineStockStatus(stockQuantity); + + RealtimeSalesReportVO.ProductRankingVO rankingVO = new RealtimeSalesReportVO.ProductRankingVO(); + rankingVO.setRanking(ranking++); + rankingVO.setProductId(productId); + + SmErpSpuEntity product = productMap.get(productId); + if (product != null) { + rankingVO.setProductName(product.getProductName()); + rankingVO.setProductCode(product.getProductCode()); + } + + rankingVO.setSalesAmount(salesAmount); + rankingVO.setSalesQuantity(salesQuantity); + rankingVO.setSalesCount(salesCount); + rankingVO.setAveragePrice(averagePrice); + rankingVO.setStockQuantity(stockQuantity); + rankingVO.setStockStatus(stockStatus); + rankingVO.setGrowthRate(BigDecimal.ZERO); // 简化处理 + + rankingList.add(rankingVO); + } + + // 按销售额降序排序 + rankingList.sort((a, b) -> b.getSalesAmount().compareTo(a.getSalesAmount())); + + // 重新设置排名 + for (int i = 0; i < rankingList.size(); i++) { + rankingList.get(i).setRanking(i + 1); + } + + // 限制返回数量 + Integer topLimit = queryDTO.getTopLimit(); + if (topLimit != null && topLimit > 0) { + rankingList = rankingList.stream().limit(topLimit).collect(Collectors.toList()); + } + + return rankingList; + } + + /** + * 获取滞销品排名数据 + * 滞销品是指没有销售记录的商品(零销售商品) + */ + private List getSlowSellingProductsData(RealtimeSalesReportQueryDTO queryDTO) { + List rankingList = new ArrayList<>(); + + // 查询所有有库存的商品 + LambdaQueryWrapper stockWrapper = new LambdaQueryWrapper<>(); + stockWrapper.gt(SmErpStockEntity::getCurrentStock, 0); // 只查询有库存的商品 + List stocks = stockMapper.selectList(stockWrapper); + + if (CollectionUtils.isEmpty(stocks)) { + return rankingList; + } + + // 获取所有有库存的商品ID + Set stockProductIds = stocks.stream() + .map(SmErpStockEntity::getErpSpuId) + .collect(Collectors.toSet()); + + // 查询这些商品的销售记录 + LambdaQueryWrapper salesWrapper = new LambdaQueryWrapper<>(); + salesWrapper.in(SmErpInventoryRecordsItemEntity::getErpSpuId, stockProductIds); + salesWrapper.eq(SmErpInventoryRecordsItemEntity::getInventoryType, "5"); // 即时销售 + salesWrapper.eq(SmErpInventoryRecordsItemEntity::getOrderType, "2"); // 出库 + + // 时间条件 + if (queryDTO.getStartTime() != null && queryDTO.getEndTime() != null) { + // 通过主表查询时间范围 + LambdaQueryWrapper mainWrapper = new LambdaQueryWrapper<>(); + mainWrapper.ge(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getStartTime()); + mainWrapper.le(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getEndTime()); + mainWrapper.eq(SmErpInventoryRecordsEntity::getInventoryType, "5"); + mainWrapper.eq(SmErpInventoryRecordsEntity::getOrderType, "2"); + + List salesRecords = inventoryRecordsMapper.selectList(mainWrapper); + if (!CollectionUtils.isEmpty(salesRecords)) { + List salesRecordIds = salesRecords.stream() + .map(SmErpInventoryRecordsEntity::getId) + .collect(Collectors.toList()); + salesWrapper.in(SmErpInventoryRecordsItemEntity::getInventoryId, salesRecordIds); + } else { + // 如果没有销售记录,说明所有商品都是滞销品 + salesWrapper.eq(SmErpInventoryRecordsItemEntity::getId, -1); // 查询不到任何记录 + } + } + + List salesItems = smErpInventoryRecordsItemMapper.selectList(salesWrapper); + + // 获取已销售的商品ID + Set soldProductIds = salesItems.stream() + .map(SmErpInventoryRecordsItemEntity::getErpSpuId) + .collect(Collectors.toSet()); + + // 找出滞销品(有库存但没有销售记录的商品) + Set slowSellingProductIds = stockProductIds.stream() + .filter(productId -> !soldProductIds.contains(productId)) + .collect(Collectors.toSet()); + + if (slowSellingProductIds.isEmpty()) { + return rankingList; + } + + // 查询滞销品的商品信息 + LambdaQueryWrapper productWrapper = new LambdaQueryWrapper<>(); + productWrapper.in(SmErpSpuEntity::getId, slowSellingProductIds); + + // 商品名称筛选 + if (StrUtil.isNotBlank(queryDTO.getProductName())) { + productWrapper.like(SmErpSpuEntity::getProductName, queryDTO.getProductName()); + } + + // 商品分类筛选 + if (queryDTO.getCategoryId() != null) { + productWrapper.eq(SmErpSpuEntity::getCategoryId, queryDTO.getCategoryId()); + } + + List products = smErpSpuMapper.selectList(productWrapper); + + if (CollectionUtils.isEmpty(products)) { + return rankingList; + } + + // 重新查询库存信息(只查询滞销品的库存) + Set finalProductIds = products.stream() + .map(SmErpSpuEntity::getId) + .collect(Collectors.toSet()); + + List finalStocks = stockMapper.selectList( + new LambdaQueryWrapper() + .in(SmErpStockEntity::getErpSpuId, finalProductIds) + ); + Map stockMap = finalStocks.stream() + .collect(Collectors.toMap(SmErpStockEntity::getErpSpuId, stock -> stock)); + + // 构建滞销品排名列表 + int ranking = 1; + for (SmErpSpuEntity product : products) { + Long productId = product.getId(); + SmErpStockEntity stock = stockMap.get(productId); + double stockQuantity = stock != null ? stock.getCurrentStock() : 0; + String stockStatus = determineStockStatus(stockQuantity); + + RealtimeSalesReportVO.ProductRankingVO rankingVO = new RealtimeSalesReportVO.ProductRankingVO(); + rankingVO.setRanking(ranking++); + rankingVO.setProductId(productId); + rankingVO.setProductName(product.getProductName()); + rankingVO.setProductCode(product.getProductCode()); + + // 滞销品销售数据都为0 + rankingVO.setSalesAmount(BigDecimal.ZERO); + rankingVO.setSalesQuantity(0.0); + rankingVO.setSalesCount(0); + rankingVO.setAveragePrice(BigDecimal.ZERO); + rankingVO.setStockQuantity(stockQuantity); + rankingVO.setStockStatus(stockStatus); + rankingVO.setGrowthRate(BigDecimal.ZERO); + + rankingList.add(rankingVO); + } + + // 按库存数量降序排序(库存多的滞销品排在前面,更值得关注) + rankingList.sort((a, b) -> Double.compare(b.getStockQuantity(), a.getStockQuantity())); + + // 重新设置排名 + for (int i = 0; i < rankingList.size(); i++) { + rankingList.get(i).setRanking(i + 1); + } + + // 限制返回数量 + Integer topLimit = queryDTO.getTopLimit(); + if (topLimit != null && topLimit > 0) { + rankingList = rankingList.stream().limit(topLimit).collect(Collectors.toList()); + } + + return rankingList; + } + + /** + * 获取客单价分析数据 + */ + private RealtimeSalesReportVO.CustomerAnalysisVO getCustomerAnalysisData(RealtimeSalesReportQueryDTO queryDTO) { + RealtimeSalesReportVO.CustomerAnalysisVO analysis = new RealtimeSalesReportVO.CustomerAnalysisVO(); + + // 查询销售记录 + List salesRecords = getSalesRecords(queryDTO); + + if (CollectionUtils.isEmpty(salesRecords)) { + analysis.setAverageCustomerValue(BigDecimal.ZERO); + analysis.setMaxCustomerValue(BigDecimal.ZERO); + analysis.setMinCustomerValue(BigDecimal.ZERO); + analysis.setMedianCustomerValue(BigDecimal.ZERO); + analysis.setHighValueOrderRatio(BigDecimal.ZERO); + analysis.setValueRanges(new ArrayList<>()); + return analysis; + } + + // 计算客单价列表 + List customerValues = salesRecords.stream() + .map(record -> new BigDecimal(record.getTotalAmount() != null ? record.getTotalAmount() : 0) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)) + .sorted() + .collect(Collectors.toList()); + + // 计算统计数据 + BigDecimal averageCustomerValue = customerValues.stream() + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal(customerValues.size()), 2, RoundingMode.HALF_UP); + + BigDecimal maxCustomerValue = customerValues.isEmpty() ? BigDecimal.ZERO : customerValues.get(customerValues.size() - 1); + BigDecimal minCustomerValue = customerValues.isEmpty() ? BigDecimal.ZERO : customerValues.get(0); + + // 计算中位数 + BigDecimal medianCustomerValue = calculateMedian(customerValues); + + // 计算高客单价订单占比(假设超过平均客单价1.5倍的为高客单价) + BigDecimal threshold = averageCustomerValue.multiply(new BigDecimal("1.5")); + long highValueOrderCount = customerValues.stream() + .filter(value -> value.compareTo(threshold) > 0) + .count(); + BigDecimal highValueOrderRatio = customerValues.isEmpty() ? BigDecimal.ZERO : + new BigDecimal(highValueOrderCount) + .multiply(new BigDecimal("100")) + .divide(new BigDecimal(customerValues.size()), 2, RoundingMode.HALF_UP); + + // 计算客单价分布区间 + List valueRanges = calculateValueRanges(customerValues); + + analysis.setAverageCustomerValue(averageCustomerValue); + analysis.setMaxCustomerValue(maxCustomerValue); + analysis.setMinCustomerValue(minCustomerValue); + analysis.setMedianCustomerValue(medianCustomerValue); + analysis.setHighValueOrderRatio(highValueOrderRatio); + analysis.setValueRanges(valueRanges); + + return analysis; + } + + /** + * 获取销售趋势数据 + */ + private List getSalesTrendData(RealtimeSalesReportQueryDTO queryDTO) { + List trendList = new ArrayList<>(); + + // 先查询是否有销售数据 + List salesRecords = getSalesRecords(queryDTO); + + if (CollectionUtils.isEmpty(salesRecords)) { + // 没有销售数据时返回空列表 + return trendList; + } + + // 根据查询类型生成趋势数据点 + String queryType = queryDTO.getQueryType(); + if (StrUtil.isBlank(queryType)) { + queryType = "day"; + } + + // 根据查询类型和时间范围生成趋势数据点 + switch (queryType) { + case "hour": + trendList = getHourlyTrendData(queryDTO, salesRecords); + break; + case "day": + trendList = getDailyTrendData(queryDTO, salesRecords); + break; + case "month": + trendList = getMonthlyTrendData(queryDTO, salesRecords); + break; + default: + trendList = getDailyTrendData(queryDTO, salesRecords); + break; + } + + return trendList; + } + + // 辅助方法 + + /** + * 查询销售记录 + */ + private List getSalesRecords(RealtimeSalesReportQueryDTO queryDTO) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 固定条件:即时销售类型和出库 + wrapper.eq(SmErpInventoryRecordsEntity::getInventoryType, "5"); // 即时销售 + wrapper.eq(SmErpInventoryRecordsEntity::getOrderType, "2"); // 出库 + + // 时间条件 - 支持多种时间筛选方式 + if (queryDTO.getStartTime() != null && queryDTO.getEndTime() != null) { + // 使用精确的时间范围 + wrapper.ge(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getStartTime()); + wrapper.le(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getEndTime()); + } else if (queryDTO.getStartDate() != null && queryDTO.getEndDate() != null) { + // 使用日期范围 + wrapper.ge(SmErpInventoryRecordsEntity::getInventoryDate, queryDTO.getStartDate()); + wrapper.le(SmErpInventoryRecordsEntity::getInventoryDate, queryDTO.getEndDate()); + } else if (queryDTO.getStartTime() != null) { + // 只有开始时间 + wrapper.ge(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getStartTime()); + } else if (queryDTO.getEndTime() != null) { + // 只有结束时间 + wrapper.le(SmErpInventoryRecordsEntity::getCreateTime, queryDTO.getEndTime()); + } else if (queryDTO.getStartDate() != null) { + // 只有开始日期 + wrapper.ge(SmErpInventoryRecordsEntity::getInventoryDate, queryDTO.getStartDate()); + } else if (queryDTO.getEndDate() != null) { + // 只有结束日期 + wrapper.le(SmErpInventoryRecordsEntity::getInventoryDate, queryDTO.getEndDate()); + } + + // 其他条件 + if (queryDTO.getSalespersonId() != null) { + wrapper.eq(SmErpInventoryRecordsEntity::getHandlerUserId, queryDTO.getSalespersonId()); + } + if (StrUtil.isNotBlank(queryDTO.getWarehouseId())) { + wrapper.eq(SmErpInventoryRecordsEntity::getWarehouseId, queryDTO.getWarehouseId()); + } + + return inventoryRecordsMapper.selectListByScope(wrapper, DataScope.of()); + } + + /** + * 查询销售明细 + */ + private List getSalesItems(RealtimeSalesReportQueryDTO queryDTO) { + List salesRecords = getSalesRecords(queryDTO); + + if (CollectionUtils.isEmpty(salesRecords)) { + return new ArrayList<>(); + } + + Set recordIds = salesRecords.stream() + .map(SmErpInventoryRecordsEntity::getId) + .collect(Collectors.toSet()); + + LambdaQueryWrapper itemWrapper = new LambdaQueryWrapper<>(); + itemWrapper.in(SmErpInventoryRecordsItemEntity::getInventoryId, recordIds); + + if (queryDTO.getCategoryId() != null) { + // 这里需要关联商品分类查询,简化处理 + } + + if (StrUtil.isNotBlank(queryDTO.getProductName())) { + // 这里需要关联商品名称查询,简化处理 + } + + return smErpInventoryRecordsItemMapper.selectList(itemWrapper); + } + + /** + * 获取已销售商品ID集合 + */ + private Set getSoldProductIds(RealtimeSalesReportQueryDTO queryDTO) { + List salesItems = getSalesItems(queryDTO); + return salesItems.stream() + .map(SmErpInventoryRecordsItemEntity::getErpSpuId) + .collect(Collectors.toSet()); + } + + /** + * 计算总销售数量 + */ + private double getTotalSalesQuantity(RealtimeSalesReportQueryDTO queryDTO) { + List salesItems = getSalesItems(queryDTO); + return salesItems.stream() + .mapToDouble(item -> item.getQuantity() != null ? item.getQuantity() : 0) + .sum(); + } + + /** + * 计算增长率 + */ + private BigDecimal calculateGrowthRate(RealtimeSalesReportQueryDTO queryDTO, String period) { + // 简化处理,实际应该对比历史同期数据 + return BigDecimal.ZERO; + } + + /** + * 判断实时销售状态 + */ + private String determineRealtimeStatus(BigDecimal totalSalesAmount, RealtimeSalesReportQueryDTO queryDTO) { + // 简化判断逻辑,实际应该根据历史数据设定阈值 + if (totalSalesAmount.compareTo(new BigDecimal("1000")) > 0) { + return "HIGH"; + } else if (totalSalesAmount.compareTo(new BigDecimal("500")) > 0) { + return "MEDIUM"; + } else { + return "LOW"; + } + } + + /** + * 判断库存状态 + */ + private String determineStockStatus(double stockQuantity) { + if (stockQuantity <= 0) { + return "OUT"; + } else if (stockQuantity < 10) { + return "LOW"; + } else { + return "SUFFICIENT"; + } + } + + /** + * 计算中位数 + */ + private BigDecimal calculateMedian(List values) { + if (values.isEmpty()) { + return BigDecimal.ZERO; + } + + int size = values.size(); + if (size % 2 == 0) { + return values.get(size / 2 - 1).add(values.get(size / 2)).divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); + } else { + return values.get(size / 2); + } + } + + /** + * 计算客单价分布区间 + */ + private List calculateValueRanges(List customerValues) { + List ranges = new ArrayList<>(); + + // 定义区间:0-50, 50-100, 100-200, 200-500, 500+ + BigDecimal[] thresholds = { + BigDecimal.ZERO, new BigDecimal("50"), new BigDecimal("100"), + new BigDecimal("200"), new BigDecimal("500"), new BigDecimal("999999") + }; + + for (int i = 0; i < thresholds.length - 1; i++) { + BigDecimal min = thresholds[i]; + BigDecimal max = thresholds[i + 1]; + + long count = customerValues.stream() + .filter(value -> value.compareTo(min) >= 0 && value.compareTo(max) < 0) + .count(); + + RealtimeSalesReportVO.CustomerValueRangeVO range = new RealtimeSalesReportVO.CustomerValueRangeVO(); + if (max.equals(new BigDecimal("999999"))) { + range.setRangeLabel(min + "+元"); + } else { + range.setRangeLabel(min + "-" + max + "元"); + } + range.setOrderCount((int) count); + + if (!customerValues.isEmpty()) { + BigDecimal percentage = new BigDecimal(count) + .multiply(new BigDecimal("100")) + .divide(new BigDecimal(customerValues.size()), 2, RoundingMode.HALF_UP); + range.setPercentage(percentage); + } else { + range.setPercentage(BigDecimal.ZERO); + } + + ranges.add(range); + } + + return ranges; + } + + /** + * 获取小时销售趋势数据 + */ + private List getHourlyTrendData(RealtimeSalesReportQueryDTO queryDTO, List salesRecords) { + List trendList = new ArrayList<>(); + + // 按小时分组统计销售数据 + Map> hourlyGroups = salesRecords.stream() + .collect(Collectors.groupingBy(record -> record.getCreateTime().getHour())); + + // 生成24小时的数据点 + for (int hour = 0; hour < 24; hour++) { + RealtimeSalesReportVO.SalesTrendVO trendVO = new RealtimeSalesReportVO.SalesTrendVO(); + trendVO.setTimePoint(String.format("%02d:00", hour)); + + List hourRecords = hourlyGroups.getOrDefault(hour, new ArrayList<>()); + + if (hourRecords.isEmpty()) { + trendVO.setSalesAmount(BigDecimal.ZERO); + trendVO.setSalesCount(0); + trendVO.setCustomerValue(BigDecimal.ZERO); + } else { + // 计算该小时的销售数据 + BigDecimal totalAmount = hourRecords.stream() + .map(record -> new BigDecimal(record.getTotalAmount() != null ? record.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + int salesCount = hourRecords.size(); + BigDecimal customerValue = salesCount > 0 ? + totalAmount.divide(new BigDecimal(salesCount), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + trendVO.setSalesAmount(totalAmount); + trendVO.setSalesCount(salesCount); + trendVO.setCustomerValue(customerValue); + } + + trendList.add(trendVO); + } + + return trendList; + } + + /** + * 获取日销售趋势数据 + */ + private List getDailyTrendData(RealtimeSalesReportQueryDTO queryDTO, List salesRecords) { + List trendList = new ArrayList<>(); + + // 按日期分组统计销售数据 + Map> dailyGroups = salesRecords.stream() + .collect(Collectors.groupingBy(record -> + record.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + + // 按日期排序 + List sortedDates = dailyGroups.keySet().stream() + .sorted() + .collect(Collectors.toList()); + + for (String date : sortedDates) { + RealtimeSalesReportVO.SalesTrendVO trendVO = new RealtimeSalesReportVO.SalesTrendVO(); + trendVO.setTimePoint(date); + + List dayRecords = dailyGroups.get(date); + + // 计算该日的销售数据 + BigDecimal totalAmount = dayRecords.stream() + .map(record -> new BigDecimal(record.getTotalAmount() != null ? record.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + int salesCount = dayRecords.size(); + BigDecimal customerValue = salesCount > 0 ? + totalAmount.divide(new BigDecimal(salesCount), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + trendVO.setSalesAmount(totalAmount); + trendVO.setSalesCount(salesCount); + trendVO.setCustomerValue(customerValue); + + trendList.add(trendVO); + } + + return trendList; + } + + /** + * 获取月销售趋势数据 + */ + private List getMonthlyTrendData(RealtimeSalesReportQueryDTO queryDTO, List salesRecords) { + List trendList = new ArrayList<>(); + + // 按月份分组统计销售数据 + Map> monthlyGroups = salesRecords.stream() + .collect(Collectors.groupingBy(record -> + record.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM")))); + + // 按月份排序 + List sortedMonths = monthlyGroups.keySet().stream() + .sorted() + .collect(Collectors.toList()); + + for (String month : sortedMonths) { + RealtimeSalesReportVO.SalesTrendVO trendVO = new RealtimeSalesReportVO.SalesTrendVO(); + trendVO.setTimePoint(month); + + List monthRecords = monthlyGroups.get(month); + + // 计算该月的销售数据 + BigDecimal totalAmount = monthRecords.stream() + .map(record -> new BigDecimal(record.getTotalAmount() != null ? record.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + int salesCount = monthRecords.size(); + BigDecimal customerValue = salesCount > 0 ? + totalAmount.divide(new BigDecimal(salesCount), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + trendVO.setSalesAmount(totalAmount); + trendVO.setSalesCount(salesCount); + trendVO.setCustomerValue(customerValue); + + trendList.add(trendVO); + } + + return trendList; + } + + /** + * 获取小时销售数据 + */ + private List getHourlySalesData(RealtimeSalesReportQueryDTO queryDTO) { + List timePeriodList = new ArrayList<>(); + + // 查询销售记录 + List salesRecords = getSalesRecords(queryDTO); + + if (CollectionUtils.isEmpty(salesRecords)) { + return timePeriodList; + } + + // 按小时分组统计销售数据 + Map> hourlyGroups = salesRecords.stream() + .collect(Collectors.groupingBy(record -> record.getCreateTime().getHour())); + + // 生成24小时的数据点 + for (int hour = 0; hour < 24; hour++) { + RealtimeSalesReportVO.TimePeriodSalesVO timePeriodVO = new RealtimeSalesReportVO.TimePeriodSalesVO(); + timePeriodVO.setTimeLabel(String.format("%02d:00", hour)); + + List hourRecords = hourlyGroups.getOrDefault(hour, new ArrayList<>()); + + if (hourRecords.isEmpty()) { + timePeriodVO.setSalesAmount(BigDecimal.ZERO); + timePeriodVO.setSalesCount(0); + timePeriodVO.setCustomerValue(BigDecimal.ZERO); + timePeriodVO.setSalesQuantity(0.0); + timePeriodVO.setGrowthRate(BigDecimal.ZERO); + timePeriodVO.setProductDetails(new ArrayList<>()); + } else { + // 计算该小时的销售数据 + BigDecimal totalAmount = hourRecords.stream() + .map(record -> new BigDecimal(record.getTotalAmount() != null ? record.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + int salesCount = hourRecords.size(); + BigDecimal customerValue = salesCount > 0 ? + totalAmount.divide(new BigDecimal(salesCount), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 计算销售数量(这里简化处理,实际应该从明细表计算) + double salesQuantity = salesCount * 1.0; // 简化处理 + + timePeriodVO.setSalesAmount(totalAmount); + timePeriodVO.setSalesCount(salesCount); + timePeriodVO.setCustomerValue(customerValue); + timePeriodVO.setSalesQuantity(salesQuantity); + timePeriodVO.setGrowthRate(BigDecimal.ZERO); // 简化处理 + + // 获取该时段的商品详情 + List productDetails = + getProductDetailsForRecords(hourRecords); + timePeriodVO.setProductDetails(productDetails); + } + + timePeriodList.add(timePeriodVO); + } + + return timePeriodList; + } + + /** + * 获取日销售数据 + */ + private List getDailySalesData(RealtimeSalesReportQueryDTO queryDTO) { + List timePeriodList = new ArrayList<>(); + + // 查询销售记录 + List salesRecords = getSalesRecords(queryDTO); + + if (CollectionUtils.isEmpty(salesRecords)) { + return timePeriodList; + } + + // 按日期分组统计销售数据 + Map> dailyGroups = salesRecords.stream() + .collect(Collectors.groupingBy(record -> + record.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + + // 按日期排序 + List sortedDates = dailyGroups.keySet().stream() + .sorted() + .collect(Collectors.toList()); + + for (String date : sortedDates) { + RealtimeSalesReportVO.TimePeriodSalesVO timePeriodVO = new RealtimeSalesReportVO.TimePeriodSalesVO(); + timePeriodVO.setTimeLabel(date); + + List dayRecords = dailyGroups.get(date); + + // 计算该日的销售数据 + BigDecimal totalAmount = dayRecords.stream() + .map(record -> new BigDecimal(record.getTotalAmount() != null ? record.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + int salesCount = dayRecords.size(); + BigDecimal customerValue = salesCount > 0 ? + totalAmount.divide(new BigDecimal(salesCount), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 计算销售数量(这里简化处理,实际应该从明细表计算) + double salesQuantity = salesCount * 1.0; // 简化处理 + + timePeriodVO.setSalesAmount(totalAmount); + timePeriodVO.setSalesCount(salesCount); + timePeriodVO.setCustomerValue(customerValue); + timePeriodVO.setSalesQuantity(salesQuantity); + timePeriodVO.setGrowthRate(BigDecimal.ZERO); // 简化处理 + + // 获取该时段的商品详情 + List productDetails = + getProductDetailsForRecords(dayRecords); + timePeriodVO.setProductDetails(productDetails); + + timePeriodList.add(timePeriodVO); + } + + return timePeriodList; + } + + /** + * 获取月销售数据 + */ + private List getMonthlySalesData(RealtimeSalesReportQueryDTO queryDTO) { + List timePeriodList = new ArrayList<>(); + + // 查询销售记录 + List salesRecords = getSalesRecords(queryDTO); + + if (CollectionUtils.isEmpty(salesRecords)) { + return timePeriodList; + } + + // 按月份分组统计销售数据 + Map> monthlyGroups = salesRecords.stream() + .collect(Collectors.groupingBy(record -> + record.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM")))); + + // 按月份排序 + List sortedMonths = monthlyGroups.keySet().stream() + .sorted() + .collect(Collectors.toList()); + + for (String month : sortedMonths) { + RealtimeSalesReportVO.TimePeriodSalesVO timePeriodVO = new RealtimeSalesReportVO.TimePeriodSalesVO(); + timePeriodVO.setTimeLabel(month); + + List monthRecords = monthlyGroups.get(month); + + // 计算该月的销售数据 + BigDecimal totalAmount = monthRecords.stream() + .map(record -> new BigDecimal(record.getTotalAmount() != null ? record.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + int salesCount = monthRecords.size(); + BigDecimal customerValue = salesCount > 0 ? + totalAmount.divide(new BigDecimal(salesCount), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 计算销售数量(这里简化处理,实际应该从明细表计算) + double salesQuantity = salesCount * 1.0; // 简化处理 + + timePeriodVO.setSalesAmount(totalAmount); + timePeriodVO.setSalesCount(salesCount); + timePeriodVO.setCustomerValue(customerValue); + timePeriodVO.setSalesQuantity(salesQuantity); + timePeriodVO.setGrowthRate(BigDecimal.ZERO); // 简化处理 + + // 获取该时段的商品详情 + List productDetails = + getProductDetailsForRecords(monthRecords); + timePeriodVO.setProductDetails(productDetails); + + timePeriodList.add(timePeriodVO); + } + + return timePeriodList; + } + + /** + * 获取销售记录的商品详情 + */ + private List getProductDetailsForRecords( + List records) { + + if (CollectionUtils.isEmpty(records)) { + return new ArrayList<>(); + } + + // 获取所有记录ID + List recordIds = records.stream() + .map(SmErpInventoryRecordsEntity::getId) + .collect(Collectors.toList()); + + // 查询所有明细记录 + LambdaQueryWrapper itemWrapper = new LambdaQueryWrapper<>(); + itemWrapper.in(SmErpInventoryRecordsItemEntity::getInventoryId, recordIds); + List itemRecords = smErpInventoryRecordsItemMapper.selectList(itemWrapper); + + if (CollectionUtils.isEmpty(itemRecords)) { + return new ArrayList<>(); + } + + // 按商品ID分组统计 + Map> productGroups = itemRecords.stream() + .collect(Collectors.groupingBy(SmErpInventoryRecordsItemEntity::getErpSpuId)); + + // 获取所有商品ID和单位ID + Set productIds = productGroups.keySet(); + Set unitIds = itemRecords.stream() + .map(SmErpInventoryRecordsItemEntity::getUnitId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + // 查询商品信息 + Map productMap = new HashMap<>(); + if (!productIds.isEmpty()) { + LambdaQueryWrapper productWrapper = new LambdaQueryWrapper<>(); + productWrapper.in(SmErpSpuEntity::getId, productIds); + List products = smErpSpuMapper.selectList(productWrapper); + productMap = products.stream() + .collect(Collectors.toMap(SmErpSpuEntity::getId, product -> product)); + } + + // 查询单位信息 + Map unitMap = new HashMap<>(); + if (!unitIds.isEmpty()) { + LambdaQueryWrapper unitWrapper = new LambdaQueryWrapper<>(); + unitWrapper.in(SmErpUnitEntity::getId, unitIds); + List units = smErpUnitMapper.selectList(unitWrapper); + unitMap = units.stream() + .collect(Collectors.toMap(SmErpUnitEntity::getId, unit -> unit)); + } + + // 查询分类信息 + Map categoryMap = new HashMap<>(); + Set categoryIds = productMap.values().stream() + .map(SmErpSpuEntity::getCategoryId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (!categoryIds.isEmpty()) { + LambdaQueryWrapper categoryWrapper = new LambdaQueryWrapper<>(); + categoryWrapper.in(SmErpCategoryEntity::getId, categoryIds); + List categories = smErpCategoryMapper.selectList(categoryWrapper); + categoryMap = categories.stream() + .collect(Collectors.toMap(SmErpCategoryEntity::getId, category -> category)); + } + + // 构建商品详情列表 + List productDetails = new ArrayList<>(); + + for (Map.Entry> entry : productGroups.entrySet()) { + Long productId = entry.getKey(); + List productItems = entry.getValue(); + + SmErpSpuEntity product = productMap.get(productId); + if (product == null) continue; + + // 计算该商品的总销售数据 + BigDecimal totalAmount = productItems.stream() + .map(item -> new BigDecimal(item.getTotalAmount() != null ? item.getTotalAmount() : 0)) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); + + double totalQuantity = productItems.stream() + .mapToDouble(item -> item.getQuantity() != null ? item.getQuantity() : 0.0) + .sum(); + + int salesCount = productItems.size(); + BigDecimal unitPrice = salesCount > 0 ? + totalAmount.divide(new BigDecimal(totalQuantity), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + // 获取单位信息 + SmErpUnitEntity unit = null; + if (!productItems.isEmpty() && productItems.get(0).getUnitId() != null) { + unit = unitMap.get(productItems.get(0).getUnitId()); + } + + // 获取分类信息 + SmErpCategoryEntity category = null; + if (product.getCategoryId() != null) { + category = categoryMap.get(product.getCategoryId()); + } + + RealtimeSalesReportVO.TimePeriodSalesVO.TimePeriodProductDetailVO productDetail = + new RealtimeSalesReportVO.TimePeriodSalesVO.TimePeriodProductDetailVO(); + + productDetail.setProductId(productId); + productDetail.setProductName(product.getProductName()); + productDetail.setProductCode(product.getProductCode()); + productDetail.setCategoryId(product.getCategoryId()); + productDetail.setCategoryName(category != null ? category.getName() : null); + productDetail.setUnit(unit != null ? unit.getUnit() : null); + productDetail.setUnitName(unit != null ? unit.getUnit() : null); // 单位名称与单位相同 + productDetail.setQuantity(totalQuantity); + productDetail.setUnitPrice(unitPrice); + productDetail.setTotalAmount(totalAmount); + productDetail.setSalesCount(salesCount); + + productDetails.add(productDetail); + } + + // 按销售额降序排序 + productDetails.sort((a, b) -> b.getTotalAmount().compareTo(a.getTotalAmount())); + + return productDetails; + } +}