即时销售报表

This commit is contained in:
zjf committed 2025-09-22 19:40:28 +08:00
1 parent daa318f320
commit aadefd2981
8 files changed
+1999 -537

No files matched your search

@@ -320,6 +320,7 @@ public class VisitorsServiceImpl extends ServiceImpl<VisitorsMapper, VisitorsEnt
@Override
@Transactional
public Object updateCheckStatus(Long id, String checkStatus) {
//todo 需要判断数据权限,防止水平越权
VisitorsEntity dataVisitors = baseMapper.selectById(id);
if (dataVisitors == null) {
throw new ServiceException("数据不存在");
@@ -1,229 +0,0 @@
# 商品SPU口味功能说明
## 概述
本功能为商品SPU添加了口味集合管理能力,支持在新增和编辑商品时管理多种口味,每种口味可以独立设置库存数量。
## 功能特性
### 1. 新增商品SPU时的口味处理
- 支持同时添加多个口味
- 每个口味自动关联到对应的SPU
- 自动设置默认库存为0(如果未指定)
- 口味数据与商品SPU在同一事务中保存
### 2. 编辑商品SPU时的口味处理
- **新增口味**:ID为null的口味会被识别为新增
- **更新口味**:ID不为null的口味会被识别为更新
- **删除口味**:提交数据中不包含的口味会被自动删除
- 支持混合操作(同时新增、更新、删除)
### 3. 数据完整性
- 自动维护口味与SPU的关联关系
- 支持级联删除(删除SPU时自动删除相关口味)
- 事务保证数据一致性
## 数据结构
### SmErpTasteStockEntity(口味库存实体)
```java
public class SmErpTasteStockEntity {
private Long id; // 主键ID
private Long spuId; // 关联的SPU ID
private String tasteName; // 口味名称
private Double stock; // 该口味的库存数量
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
}
```
### SmErpSpuEntity(商品SPU实体)
```java
public class SmErpSpuEntity {
// ... 其他字段 ...
@Schema(description="口味集合")
@TableField(exist = false)
private List<SmErpTasteStockEntity> 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。
@@ -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
<template>
<div>
<!-- 商户选择(支持搜索) -->
<el-select
v-model="form.merchantId"
placeholder="请选择商户"
filterable
remote
:remote-method="searchMerchants"
:loading="merchantLoading">
<el-option
v-for="item in merchantOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled">
<span style="float: left">{{ item.label }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.extra }}</span>
</el-option>
</el-select>
<!-- 分类选择 -->
<el-select v-model="form.categoryId" placeholder="请选择分类">
<el-option
v-for="item in categoryOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled">
</el-option>
</el-select>
</div>
</template>
<script>
export default {
data() {
return {
form: {
merchantId: null,
categoryId: null
},
merchantOptions: [],
categoryOptions: [],
merchantLoading: false
}
},
mounted() {
this.getMerchantOptions();
this.getCategoryOptions();
},
methods: {
// 搜索商户
async searchMerchants(keyword) {
this.merchantLoading = true;
try {
await this.getMerchantOptions(keyword);
} finally {
this.merchantLoading = false;
}
}
}
}
</script>
```
## 扩展说明
### 添加新的下拉选择类型
如果需要添加新的下拉选择类型,可以:
1. 在 `SelectOptionService` 中添加新的方法
2. 在 `SelectOptionController` 中添加对应的接口
3. 按照统一的格式返回数据
### 自定义扩展信息
`extra` 字段可以包含任何额外的信息,例如:
- 联系电话
- 父级ID
- 状态信息
- 其他业务相关数据
前端可以根据需要解析和使用这些扩展信息。
## 注意事项
1. 所有接口都需要相应的权限验证
2. 只返回启用状态的数据
3. 数据按照指定的排序规则返回
4. 建议前端对接口返回的数据进行缓存,避免频繁请求
@@ -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';
```
如果发现新的不一致数据,可以重新执行修复脚本。
@@ -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
);
*/
@@ -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<RealtimeSalesReportVO> 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<RealtimeSalesReportVO> 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<Page<RealtimeSalesReportVO.ProductRankingVO>> getTopSellingProducts(
@Parameter(description = "分页参数") Page<RealtimeSalesReportVO.ProductRankingVO> page,
@Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) {
Page<RealtimeSalesReportVO.ProductRankingVO> 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<Page<RealtimeSalesReportVO.ProductRankingVO>> getSlowSellingProducts(
@Parameter(description = "分页参数") Page<RealtimeSalesReportVO.ProductRankingVO> page,
@Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) {
Page<RealtimeSalesReportVO.ProductRankingVO> 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<Page<RealtimeSalesReportVO.TimePeriodSalesVO>> getTimePeriodSales(
@Parameter(description = "分页参数") Page<RealtimeSalesReportVO.TimePeriodSalesVO> page,
@Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) {
Page<RealtimeSalesReportVO.TimePeriodSalesVO> 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<RealtimeSalesReportVO.CustomerAnalysisVO> 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<Page<RealtimeSalesReportVO.SalesTrendVO>> getSalesTrend(
@Parameter(description = "分页参数") Page<RealtimeSalesReportVO.SalesTrendVO> page,
@Parameter(description = "查询条件") RealtimeSalesReportQueryDTO queryDTO) {
Page<RealtimeSalesReportVO.SalesTrendVO> result =
realtimeSalesReportService.getSalesTrend(page, queryDTO);
return R.ok(result);
}
/**
* 刷新实时销售数据缓存
*
* @return 刷新结果
*/
@Operation(summary = "刷新实时销售数据缓存", description = "手动刷新实时销售数据缓存")
@SysLog("刷新实时销售数据缓存")
@PostMapping("/refresh")
@PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')")
public R<Boolean> refreshRealtimeData() {
Boolean result = realtimeSalesReportService.refreshRealtimeData();
return R.ok(result);
}
/**
* 获取今日销售汇总
*
* @return 今日销售汇总数据
*/
@Operation(summary = "获取今日销售汇总", description = "获取今日销售汇总数据,包括销售额、销售笔数、客单价等")
@GetMapping("/todaySummary")
@PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')")
public R<RealtimeSalesReportVO.SalesSummaryVO> 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<RealtimeSalesReportVO.SalesSummaryVO> 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<Page<RealtimeSalesReportVO.ProductRankingVO>> 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<RealtimeSalesReportVO.ProductRankingVO> page = new Page<>(1, 10);
Page<RealtimeSalesReportVO.ProductRankingVO> 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<Page<RealtimeSalesReportVO.ProductRankingVO>> 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<RealtimeSalesReportVO.ProductRankingVO> page = new Page<>(1, 10);
Page<RealtimeSalesReportVO.ProductRankingVO> result =
realtimeSalesReportService.getSlowSellingProducts(page, queryDTO);
return R.ok(result);
}
/**
* 获取小时销售趋势(今日)
*
* @return 今日小时销售趋势数据
*/
@Operation(summary = "获取小时销售趋势", description = "获取今日小时销售趋势数据")
@GetMapping("/hourlyTrend")
@PreAuthorize("@pms.hasPermission('supermarket_realtimeSalesReport_view')")
public R<Page<RealtimeSalesReportVO.TimePeriodSalesVO>> 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<RealtimeSalesReportVO.TimePeriodSalesVO> page = new Page<>(1, 24);
Page<RealtimeSalesReportVO.TimePeriodSalesVO> result =
realtimeSalesReportService.getTimePeriodSales(page, queryDTO);
return R.ok(result);
}
}
@@ -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<RealtimeSalesReportVO.ProductRankingVO> getTopSellingProducts(Page<RealtimeSalesReportVO.ProductRankingVO> page, RealtimeSalesReportQueryDTO queryDTO);
/**
* 获取滞销品排名
*
* @param page 分页参数
* @param queryDTO 查询条件
* @return 滞销品排名列表
*/
Page<RealtimeSalesReportVO.ProductRankingVO> getSlowSellingProducts(Page<RealtimeSalesReportVO.ProductRankingVO> page, RealtimeSalesReportQueryDTO queryDTO);
/**
* 获取时段销售数据
*
* @param page 分页参数
* @param queryDTO 查询条件
* @return 时段销售数据列表
*/
Page<RealtimeSalesReportVO.TimePeriodSalesVO> getTimePeriodSales(Page<RealtimeSalesReportVO.TimePeriodSalesVO> page, RealtimeSalesReportQueryDTO queryDTO);
/**
* 获取客单价分析数据
*
* @param queryDTO 查询条件
* @return 客单价分析数据
*/
RealtimeSalesReportVO.CustomerAnalysisVO getCustomerAnalysis(RealtimeSalesReportQueryDTO queryDTO);
/**
* 获取销售趋势数据
*
* @param page 分页参数
* @param queryDTO 查询条件
* @return 销售趋势数据列表
*/
Page<RealtimeSalesReportVO.SalesTrendVO> getSalesTrend(Page<RealtimeSalesReportVO.SalesTrendVO> page, RealtimeSalesReportQueryDTO queryDTO);
/**
* 刷新实时销售数据缓存
*
* @return 是否刷新成功
*/
Boolean refreshRealtimeData();
}