diff --git a/ReadMe.md b/ReadMe.md index d432f5c..f25d62c 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -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.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`,因此在请求时需要进行替换。详细内容请参考以下【项目部署】说明。 diff --git a/dao/src/main/java/cn/bunny/dao/constant/MinioConstant.java b/dao/src/main/java/cn/bunny/dao/constant/MinioConstant.java index 21dd2c5..079481f 100644 --- a/dao/src/main/java/cn/bunny/dao/constant/MinioConstant.java +++ b/dao/src/main/java/cn/bunny/dao/constant/MinioConstant.java @@ -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 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/"); diff --git a/service/src/main/java/cn/bunny/services/factory/BillFactory.java b/service/src/main/java/cn/bunny/services/factory/BillFactory.java index ac9dba9..160e8ff 100644 --- a/service/src/main/java/cn/bunny/services/factory/BillFactory.java +++ b/service/src/main/java/cn/bunny/services/factory/BillFactory.java @@ -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); + } } diff --git a/service/src/main/java/cn/bunny/services/service/financial/impl/BillServiceImpl.java b/service/src/main/java/cn/bunny/services/service/financial/impl/BillServiceImpl.java index eb119d6..4a54eff 100644 --- a/service/src/main/java/cn/bunny/services/service/financial/impl/BillServiceImpl.java +++ b/service/src/main/java/cn/bunny/services/service/financial/impl/BillServiceImpl.java @@ -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 implements Bi @Autowired private EmailTemplateMapper emailTemplateMapper; + /** * * 账单信息 服务实现类 * @@ -277,13 +278,13 @@ public class BillServiceImpl extends ServiceImpl 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); } /**