feat: 添加账单并上传文件

This commit is contained in:
Bunny 2025-01-02 22:51:39 +08:00
parent 357f466978
commit a7e481ca4c
4 changed files with 156 additions and 95 deletions

180
ReadMe.md
View File

@ -262,71 +262,11 @@ db.createUser({ user: 'admin', pwd: '02120212', roles: [ { role: "root", db: "ad
# 项目特点
### 按钮权限显示
### RBAC
如果当前用户在这个页面中只有【添加】和【删除】那么页面按钮中只会显示出【添加按钮】和【删除按钮】
采用RBAC设计原设计数据表如下
### 去除前后空格
后端配置了自动去除前端传递的空字符串,如果传递的内容前后有空格会自动去除前后的空格
![image-20241105215241811](http://116.196.101.14:9000/docs/image-20241105215241811.png)
代码内容
```java
@ControllerAdvice
public class ControllerStringParamTrimConfig {
/**
* 创建 String trim 编辑器
* 构造方法中 boolean 参数含义为如果是空白字符串,是否转换为null
* 即如果为true,那么 " " 会被转换为 null,否者为 ""
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
StringTrimmerEditor propertyEditor = new StringTrimmerEditor(false);
// 为 String 类对象注册编辑器
binder.registerCustomEditor(String.class, propertyEditor);
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return jacksonObjectMapperBuilder -> {
// 为 String 类型自定义反序列化操作
jacksonObjectMapperBuilder
.deserializerByType(String.class, new StdScalarDeserializer<String>(String.class) {
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
// 去除全部空格
// return StringUtils.trimAllWhitespace(jsonParser.getValueAsString());
// 仅去除前后空格
return jsonParser.getValueAsString().trim();
}
});
};
}
}
```
### 项目接口和页面
接口地址有两个:
1. knife4j
2. swagger
接口地址://localhost:7070/doc.html#/home
![image-20241105213953503](http://116.196.101.14:9000/docs/image-20241105213953503.png)
swagger接口地址http://localhost:7070/swagger-ui/index.html
![image-20241105214100720](http://116.196.101.14:9000/docs/image-20241105214100720.png)
前端接口地址http://localhost:7000/#/welcome
![image-20241105214230389](http://116.196.101.14:9000/docs/image-20241105214230389.png)
![img](http://116.196.101.14:9000/docs/2174ea444bb9ad1906918db9e89e27fd.jpg)
## 登录功能
@ -413,47 +353,123 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
}
```
## 首页功能
## 功能介绍
![image-20241105212403630](http://116.196.101.14:9000/docs/image-20241105212403630.png)
### 首页功能
功能菜单,首页图表展示部分功能已经由这个模板作者设计好,其中需要注意的是,如果要查看历史消息或者是进入消息页面可以双击![image-20241105213346408](http://116.196.101.14:9000/docs/image-20241105213346408.png)既可进入消息页面
#### 业务需求
1. 要求统计当前数据已消费金额、收入 金额、支出占比、盈利金额
2. 根据收入支出数据展示双轴图数据
3. 根据收入支出数据进行排行榜
4. 展示收入支出表格数据
#### 实现
计算当前月份总收入、总支出、盈利金额、支出占比
分析概览中展示当前的支出为红色柱状图;绿色折线图为收入
排行榜展示当前月份收入或支出最高的金额
![image-20250101164041383](http://116.196.101.14:9000/docs/image-20250101164041383.png)
日期选择器可以筛选当前年的月份和本周数据,选择后展示数据也会发生变化
![image-20250101164422293](http://116.196.101.14:9000/docs/image-20250101164422293.png)
选择日期为`12月1日~12月7日`
![image-20250101164613379](http://116.196.101.14:9000/docs/image-20250101164613379.png)
### 消息功能
![image-20241105213539594](http://116.196.101.14:9000/docs/image-20241105213539594-1730813844820-2.png)
#### 业务需求
1. 消息页面的展示,包含删除、批量删除、选中已读和当前页面所有消息都标为已读
2. 当用户对左侧菜单点击时可以过滤出消息内容,展示不同的消息类型
![image-20241105213720011](http://116.196.101.14:9000/docs/image-20241105213720011.png)
3. 可以点击已读和全部进行筛选消息
![image-20241105214342220](http://116.196.101.14:9000/docs/image-20241105214342220.png)
3. 可以根据标题进行搜搜
4. 包含分页
#### 实现思路
#### 实现
1. 显示当前消息类型,用户点击时带参数请求,只带当前消息类型,不默认携带已读状态查询,然后从数据库筛选并返回结果。
整个页面刷新会第一次进入都会获取消息数据,获取之后计算有多少消息,包含所有消息分类下的所有消息数,在页面的顶部展示
2. 点击"已读"选项时,若选择"全部"之前是设置为undefined这样就不会携带参数了但是底下会有警告现在改为空字符串后端只需过滤掉空字符串即可。
![image-20250101164852021](http://116.196.101.14:9000/docs/image-20250101164852021.png)
3. 删除选定数据若用户选择列表并筛选出所有ID将数据传递给后端用户删除为逻辑删除
第一次刷新页面后会获取消息,之后还会在下方展示,展示消息大概信息内容
4. 全部标为已读![image-20241106131949217](http://116.196.101.14:9000/docs/image-20241106131949217.png)类似删除操作筛选出选中数据的ID然后传递给后端以标记为已读。
![image-20250101164931942](http://116.196.101.14:9000/docs/image-20250101164931942.png)
5. 将所有数据标记为已读当前页面前端使用map提取所有ID整合成ID列表传递给后端表示页面上所有数据已读。
功能菜单,首页图表展示部分功能已经由这个模板作者设计好,其中需要注意的是,如果要查看历史消息或者是进入消息页面可以双击![image-20241105213346408](http://116.196.101.14:9000/docs/image-20241105213346408.png)既可进入消息页面,消息页面中会有消息的阅读情况,是否已读或维度
6. 输入标题后,随输入变化进行搜索。
![image-20250101164958864](http://116.196.101.14:9000/docs/image-20250101164958864.png)
**后端代码位置**
消息未读时,会显示未拆封信件图标
![image-20241105213922824](http://116.196.101.14:9000/docs/image-20241105213922824.png)
![image-20250101165123316](http://116.196.101.14:9000/docs/image-20250101165123316.png)
点击这个消息进入详情页,针对不同的编辑器会显示不同的效果,当前是使用富文本编辑器,效果图如下
![image-20250101165230016](http://116.196.101.14:9000/docs/image-20250101165230016.png)
阅读完成后不会再提示该消息,刷新页面后也不会再出现已读消息,阅读完成后图标也会发生变化,出现拆封信件图标样式
![image-20250101165352744](http://116.196.101.14:9000/docs/image-20250101165352744.png)
> 消息已读并不会影响其他用户消息,不会出现当前用户读取消息后其他用户消息也已读
### 财务管理
#### 业务需求
1. 对账单表CURD查询
2. 导出用户数据表
3. 添加账单方式
1. 手动添加
2. Excel表添加
4. 首页导航栏出也要有入口可以添加
#### 实现
首页导航栏点击也可以弹出记账功能弹窗
![image-20250101171718156](http://116.196.101.14:9000/docs/image-20250101171718156.png)
##### 手动导入
记账功能弹窗
![image-20250101171751011](http://116.196.101.14:9000/docs/image-20250101171751011.png)
##### 文件导入
使用文件导入,因为每个用户类别不一样,详见**账单分类管理**,要根据这些分类生成对应的枚举 ,否则用户不知道自己有哪些类别,类别不一致导入数据时会找不到字段。
后端需要查询当前用户的数据库找到属于这个用户的字段填充到下面的Excel表中
![image-20250101172238607](http://116.196.101.14:9000/docs/image-20250101172238607.png)
> 效果示例
>
> ![image-20250101172352120](http://116.196.101.14:9000/docs/image-20250101172352120.png)
>
> 之后用户添加时表格中会有数据校验,如果这个字段不存在会报警告
>
> ![image-20250101172447687](http://116.196.101.14:9000/docs/image-20250101172447687.png)
点击文件导入,弹出上传文件的弹窗,选择要上传文件或者直接拖动文件到窗口之后点击确认就可以导入。
![image-20250101171821403](http://116.196.101.14:9000/docs/image-20250101171821403.png)
![image-20250101171833551](http://116.196.101.14:9000/docs/image-20250101171833551.png)
> 如果数据导入失败会显示导入失败的数据条目到消息中,会在消息中显示
>
> ![image-20250101172639305](http://116.196.101.14:9000/docs/image-20250101172639305.png)
### 用户管理
@ -889,6 +905,8 @@ public class MenuIconVo extends BaseUserVo {
![image-20241106144317746](http://116.196.101.14:9000/docs/image-20241106144317746.png)
# 环境部署
使用Docker进行部署后端接口地址以`/admin`开头,但前端默认请求前缀为`/api`,因此在请求时需要进行替换。详细内容请参考以下【项目部署】说明。

View File

@ -13,6 +13,7 @@ public class MinioConstant {
public static final String carousel = "carousel";
public static final String feedback = "feedback";
public static final String backup = "backup";
public static final String billExcel = "billExcel";
public static final Map<String, String> typeMap = new HashMap<>();
static {
@ -22,6 +23,7 @@ public class MinioConstant {
typeMap.put(carousel, "/carousel/");
typeMap.put(feedback, "/feedback/");
typeMap.put(backup, "/backup/");
typeMap.put(billExcel, "/billExcel/");
typeMap.put("images", "/images/");
typeMap.put("video", "/video/");
typeMap.put("default", "/default/");

View File

@ -1,17 +1,23 @@
package cn.bunny.services.factory;
import cn.bunny.common.service.utils.minio.MinioProperties;
import cn.bunny.common.service.utils.minio.MinioUtil;
import cn.bunny.dao.constant.LocalDateTimeConstant;
import cn.bunny.dao.constant.MinioConstant;
import cn.bunny.dao.dto.financial.bill.BillDto;
import cn.bunny.dao.dto.financial.bill.IncomeExpenseQueryDto;
import cn.bunny.dao.dto.financial.bill.excel.BillExportDto;
import cn.bunny.dao.constant.LocalDateTimeConstant;
import cn.bunny.dao.enums.EmailTemplateEnums;
import cn.bunny.dao.model.excel.BillExportExcelByUser;
import cn.bunny.dao.entity.mysql.system.AdminUser;
import cn.bunny.dao.entity.mysql.system.EmailTemplate;
import cn.bunny.dao.entity.mysql.system.Files;
import cn.bunny.dao.enums.EmailTemplateEnums;
import cn.bunny.dao.model.excel.BillExportExcelByUser;
import cn.bunny.dao.model.file.MinioFilePath;
import cn.bunny.dao.vo.financial.admin.BillVo;
import cn.bunny.dao.vo.financial.user.expendAndIncome.ExpendWithIncome;
import cn.bunny.services.mapper.email.EmailTemplateMapper;
import cn.bunny.services.mapper.financial.BillMapper;
import cn.bunny.services.mapper.system.FilesMapper;
import cn.bunny.services.mapper.system.UserMapper;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
@ -21,8 +27,11 @@ import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
@ -42,15 +51,24 @@ public class BillFactory {
@Autowired
private BillMapper billMapper;
@Autowired
private EmailFactory emailFactory;
@Autowired
private EmailTemplateMapper emailTemplateMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private FilesMapper filesMapper;
@Autowired
private EmailFactory emailFactory;
@Autowired
private MinioUtil minioUtil;
@Autowired
private MinioProperties minioProperties;
public void exportBill(BillExportDto dto, HttpServletResponse response) {
LocalDate startDate = dto.getStartDate();
LocalDate endDate = dto.getEndDate();
@ -210,4 +228,26 @@ public class BillFactory {
emailFactory.sendEmailTemplate(adminUser.getEmail(), emailTemplate, hashMap);
}
/**
* 异步上传账单导入文件
*
* @param file 账单Excel导入文件
* @throws IOException 上窜错误异常
*/
@Async
public void uploadBillExcelWithAsync(MultipartFile file) throws IOException {
// 上传文件到Minio
MinioFilePath minioFilePath = minioUtil.uploadObjectReturnFilePath(file, MinioConstant.billExcel);
// 将文件信息存入数据库
Files files = new Files();
files.setFileType(file.getContentType());
files.setFileSize(file.getSize());
files.setFilepath("/" + minioProperties.getBucketName() + minioFilePath.getFilepath());
files.setFilename(minioFilePath.getFilename());
files.setDownloadCount(0);
filesMapper.insert(files);
}
}

View File

@ -14,13 +14,13 @@ import cn.bunny.dao.entity.mysql.financial.Bill;
import cn.bunny.dao.entity.mysql.system.AdminUser;
import cn.bunny.dao.entity.mysql.system.EmailTemplate;
import cn.bunny.dao.enums.EmailTemplateEnums;
import cn.bunny.dao.vo.result.PageResult;
import cn.bunny.dao.vo.result.ResultCodeEnum;
import cn.bunny.dao.vo.financial.admin.BillVo;
import cn.bunny.dao.vo.financial.user.BillUserVo;
import cn.bunny.dao.vo.financial.user.expendAndIncome.CategoryAmount;
import cn.bunny.dao.vo.financial.user.expendAndIncome.ExpendWithIncome;
import cn.bunny.dao.vo.financial.user.expendAndIncome.ExpendWithIncomeListVo;
import cn.bunny.dao.vo.result.PageResult;
import cn.bunny.dao.vo.result.ResultCodeEnum;
import cn.bunny.services.excel.BillAddUserListener;
import cn.bunny.services.factory.BillFactory;
import cn.bunny.services.factory.HomeFactory;
@ -38,6 +38,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -45,7 +46,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
@ -85,6 +85,7 @@ public class BillServiceImpl extends ServiceImpl<BillMapper, Bill> implements Bi
@Autowired
private EmailTemplateMapper emailTemplateMapper;
/**
* * 账单信息 服务实现类
*
@ -277,13 +278,13 @@ public class BillServiceImpl extends ServiceImpl<BillMapper, Bill> implements Bi
*
* @param file 文件
*/
@SneakyThrows
@Override
public void importBill(MultipartFile file) {
try {
EasyExcel.read(file.getInputStream(), BillImportByUserDto.class, new BillAddUserListener(categoryMapper, messageMapper, messageReceivedMapper, this)).sheet().doRead();
} catch (IOException e) {
throw new AuthCustomerException(ResultCodeEnum.UPLOAD_ERROR);
}
EasyExcel.read(file.getInputStream(), BillImportByUserDto.class, new BillAddUserListener(categoryMapper, messageMapper, messageReceivedMapper, this)).sheet().doRead();
// 上传文件
billFactory.uploadBillExcelWithAsync(file);
}
/**