diff --git a/README.md b/README.md index c8d915a..fd47082 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ ## 主要功能 -- 监控个人资产负债。 -- 记录个人支出和收入。 +- 监控个人资产负债 +- 记录个人支出和收入 +- 支持账单添加多个附件 - 支持多个账本记账 - 支持多币种 - 支持多种账本模板 diff --git a/moneynote-api-base/src/main/java/cn/biq/mn/base/validation/FileValidator.java b/moneynote-api-base/src/main/java/cn/biq/mn/base/validation/FileValidator.java new file mode 100644 index 0000000..a90705f --- /dev/null +++ b/moneynote-api-base/src/main/java/cn/biq/mn/base/validation/FileValidator.java @@ -0,0 +1,30 @@ +package cn.biq.mn.base.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.springframework.web.multipart.MultipartFile; + +public class FileValidator implements ConstraintValidator { + + @Override + public void initialize(ValidFile constraintAnnotation) { + + } + + @Override + public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) { + boolean result = true; + String contentType = multipartFile.getContentType(); + if (!isSupportedContentType(contentType)) { + result = false; + } + return result; + } + + private boolean isSupportedContentType(String contentType) { + return contentType.equals("application/pdf") + || contentType.equals("image/png") + || contentType.equals("image/jpg") + || contentType.equals("image/jpeg"); + } +} diff --git a/moneynote-api-base/src/main/java/cn/biq/mn/base/validation/ValidFile.java b/moneynote-api-base/src/main/java/cn/biq/mn/base/validation/ValidFile.java new file mode 100644 index 0000000..d9716d6 --- /dev/null +++ b/moneynote-api-base/src/main/java/cn/biq/mn/base/validation/ValidFile.java @@ -0,0 +1,18 @@ +package cn.biq.mn.base.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {FileValidator.class}) +public @interface ValidFile { + String message() default "Only PDF,XML,PNG or JPG images are allowed"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/moneynote-api-base/src/main/resources/i18n/messages-base.properties b/moneynote-api-base/src/main/resources/i18n/messages-base.properties index 5df4256..03e57e0 100644 --- a/moneynote-api-base/src/main/resources/i18n/messages-base.properties +++ b/moneynote-api-base/src/main/resources/i18n/messages-base.properties @@ -15,4 +15,6 @@ item.exists.exception=名称已存在 item.not.found.exception=记录不存在 error.parent.itself=父类不能是自己 -response.message.success=操作成功 \ No newline at end of file +response.message.success=操作成功 + +auth.error=没有权限 \ No newline at end of file diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlow.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlow.java index 1aed931..1809961 100644 --- a/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlow.java +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlow.java @@ -2,6 +2,7 @@ import cn.biq.mn.user.account.Account; import cn.biq.mn.user.book.Book; +import cn.biq.mn.user.flowfile.FlowFile; import cn.biq.mn.user.group.Group; import cn.biq.mn.user.payee.Payee; import cn.biq.mn.user.user.User; @@ -90,4 +91,7 @@ public class BalanceFlow extends BaseEntity { @Column(nullable = false, updatable = false) private Long insertAt = System.currentTimeMillis(); + @OneToMany(mappedBy = "flow", fetch = FetchType.LAZY) + private Set files = new HashSet<>(); + } diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlowController.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlowController.java index 747bbed..44659c5 100644 --- a/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlowController.java +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlowController.java @@ -1,14 +1,17 @@ package cn.biq.mn.user.balanceflow; +import cn.biq.mn.base.validation.ValidFile; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import cn.biq.mn.base.response.BaseResponse; import cn.biq.mn.base.response.PageResponse; import cn.biq.mn.base.response.DataResponse; +import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/balance-flows") @@ -52,4 +55,14 @@ public BaseResponse handleConfirm(@PathVariable("id") Integer id) { return new BaseResponse(balanceFlowService.confirm(id)); } + @RequestMapping(method = RequestMethod.POST, value = "/{id}/addFile") + public BaseResponse handleAddFile(@PathVariable("id") Integer id, @Validated @ValidFile @RequestParam("file") MultipartFile file) { + return new DataResponse<>(balanceFlowService.addFile(id, file)); + } + + @RequestMapping(method = RequestMethod.GET, value = "/{id}/files") + public BaseResponse handleFiles(@PathVariable("id") Integer id) { + return new DataResponse<>(balanceFlowService.getFiles(id)); + } + } diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlowService.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlowService.java index 4f086e6..6e771c0 100644 --- a/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlowService.java +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/balanceflow/BalanceFlowService.java @@ -4,6 +4,10 @@ import cn.biq.mn.user.account.AccountRepository; import cn.biq.mn.user.base.BaseService; import cn.biq.mn.user.book.Book; +import cn.biq.mn.user.flowfile.FlowFile; +import cn.biq.mn.user.flowfile.FlowFileDetails; +import cn.biq.mn.user.flowfile.FlowFileMapper; +import cn.biq.mn.user.flowfile.FlowFileRepository; import cn.biq.mn.user.group.Group; import cn.biq.mn.user.payee.Payee; import cn.biq.mn.user.payee.PayeeRepository; @@ -25,7 +29,9 @@ import cn.biq.mn.user.categoryrelation.CategoryRelationService; import cn.biq.mn.user.tagrelation.TagRelation; import cn.biq.mn.user.tagrelation.TagRelationService; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.math.BigDecimal; import java.util.List; import java.util.Objects; @@ -44,6 +50,7 @@ public class BalanceFlowService { private final CategoryRelationService categoryRelationService; private final TagRelationService tagRelationService; private final BalanceFlowMapper balanceFlowMapper; + private final FlowFileRepository flowFileRepository; private void checkBeforeAdd(BalanceFlowAddForm form, Book book, User user) { // 对用户添加账单的操作进行限流。 @@ -389,4 +396,27 @@ public boolean confirm(Integer id) { return true; } + public FlowFileDetails addFile(Integer id, MultipartFile file) { + FlowFile flowFile = new FlowFile(); + try { + flowFile.setData(file.getBytes()); + } catch (IOException e) { + throw new FailureMessageException("add.flow.file.fail"); + } + flowFile.setCreator(sessionUtil.getCurrentUser()); + flowFile.setFlow(baseService.findFlowById(id)); + flowFile.setCreateTime(System.currentTimeMillis()); + flowFile.setSize(file.getSize()); + flowFile.setContentType(file.getContentType()); + flowFile.setOriginalName(file.getOriginalFilename()); + flowFileRepository.save(flowFile); + return FlowFileMapper.toDetails(flowFile); + } + + public List getFiles(Integer id) { + BalanceFlow flow = baseService.findFlowById(id); + List flowFiles = flowFileRepository.findByFlow(flow); + return flowFiles.stream().map(FlowFileMapper::toDetails).toList(); + } + } diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFile.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFile.java new file mode 100644 index 0000000..48ff960 --- /dev/null +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFile.java @@ -0,0 +1,41 @@ +package cn.biq.mn.user.flowfile; + +import cn.biq.mn.base.base.BaseEntity; +import cn.biq.mn.user.balanceflow.BalanceFlow; +import cn.biq.mn.user.user.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "t_flow_file") +@Getter +@Setter +public class FlowFile extends BaseEntity { + + @Lob + @Basic(fetch = FetchType.LAZY) + @Column(columnDefinition = "LONGBLOB", nullable = false) + private byte[] data; + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User creator; //上传人 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "flow_id") + private BalanceFlow flow; + + @Column(nullable = false) + private Long createTime; + + @Column(length = 32, nullable = false) + private String contentType; + + @Column(nullable = false) + private Long size; + + @Column(length = 512, nullable = false) + private String originalName; + +} diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileController.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileController.java new file mode 100644 index 0000000..abb06fa --- /dev/null +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileController.java @@ -0,0 +1,31 @@ +package cn.biq.mn.user.flowfile; + +import cn.biq.mn.base.base.BaseController; +import cn.biq.mn.base.response.BaseResponse; +import cn.biq.mn.user.utils.SessionUtil; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("/flow-files") +@RequiredArgsConstructor +public class FlowFileController extends BaseController { + + private final FlowFileService flowFileService; + + @RequestMapping(method = RequestMethod.GET, value = "/view") + public ResponseEntity handleView(@Valid FlowFileViewForm form) { + FlowFile flowFile = flowFileService.getFile(form); + return ResponseEntity.ok().contentType(MediaType.parseMediaType(flowFile.getContentType())).body(flowFile.getData()); + } + + @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") + public BaseResponse handleDelete(@PathVariable("id") Integer id) { + return new BaseResponse(flowFileService.remove(id)); + } + +} \ No newline at end of file diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileDetails.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileDetails.java new file mode 100644 index 0000000..5684cfd --- /dev/null +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileDetails.java @@ -0,0 +1,20 @@ +package cn.biq.mn.user.flowfile; + +import cn.biq.mn.base.base.BaseDetails; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class FlowFileDetails extends BaseDetails { + + private Long createTime; + private String contentType; + private Long size; + private String originalName; + + public boolean isImage() { + return contentType.equals("image/jpeg") || contentType.equals("image/png"); + } + +} diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileMapper.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileMapper.java new file mode 100644 index 0000000..3c3b378 --- /dev/null +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileMapper.java @@ -0,0 +1,17 @@ +package cn.biq.mn.user.flowfile; + + +public class FlowFileMapper { + + public static FlowFileDetails toDetails(FlowFile entity) { + if (entity == null) return null; + var details = new FlowFileDetails(); + details.setId(entity.getId()); + details.setCreateTime(entity.getCreateTime()); + details.setContentType(entity.getContentType()); + details.setSize(entity.getSize()); + details.setOriginalName(entity.getOriginalName()); + return details; + } + +} diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileRepository.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileRepository.java new file mode 100644 index 0000000..9a08f37 --- /dev/null +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileRepository.java @@ -0,0 +1,15 @@ +package cn.biq.mn.user.flowfile; + +import cn.biq.mn.base.base.BaseRepository; +import cn.biq.mn.user.balanceflow.BalanceFlow; +import org.springframework.stereotype.Repository; + +import java.util.List; + + +@Repository +public interface FlowFileRepository extends BaseRepository { + + List findByFlow(BalanceFlow flow); + +} diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileService.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileService.java new file mode 100644 index 0000000..0733594 --- /dev/null +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileService.java @@ -0,0 +1,42 @@ +package cn.biq.mn.user.flowfile; + +import cn.biq.mn.base.exception.FailureMessageException; +import cn.biq.mn.user.balanceflow.BalanceFlow; +import cn.biq.mn.user.base.BaseService; +import cn.biq.mn.user.book.Book; +import cn.biq.mn.user.group.Group; +import cn.biq.mn.user.user.User; +import cn.biq.mn.user.utils.SessionUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class FlowFileService { + + private final FlowFileRepository flowFileRepository; + private final SessionUtil sessionUtil; + private final BaseService baseService; + + @Transactional(readOnly = true) + public FlowFile getFile(FlowFileViewForm form) { + FlowFile flowFile = flowFileRepository.getReferenceById(form.getId()); + if (!flowFile.getCreateTime().equals(form.getCreateTime())) { + throw new FailureMessageException(); + } + return flowFile; + } + + public boolean remove(Integer id) { + FlowFile flowFile = flowFileRepository.getReferenceById(id); + BalanceFlow flow = baseService.findFlowById(flowFile.getFlow().getId()); + if (flow == null) { + throw new FailureMessageException("auth.error"); + } + flowFileRepository.delete(flowFile); + return true; + } + +} diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileViewForm.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileViewForm.java new file mode 100644 index 0000000..0c64960 --- /dev/null +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/flowfile/FlowFileViewForm.java @@ -0,0 +1,18 @@ +package cn.biq.mn.user.flowfile; + +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + + +@Getter @Setter +public class FlowFileViewForm { + + @NonNull + private Integer id; + + // 时间是为了增加地址的安全性 + @NonNull + private Long createTime; + +} diff --git a/moneynote-api-user/src/main/java/cn/biq/mn/user/interceptor/MvcInterceptorConfig.java b/moneynote-api-user/src/main/java/cn/biq/mn/user/interceptor/MvcInterceptorConfig.java index fad8830..d8a71b2 100644 --- a/moneynote-api-user/src/main/java/cn/biq/mn/user/interceptor/MvcInterceptorConfig.java +++ b/moneynote-api-user/src/main/java/cn/biq/mn/user/interceptor/MvcInterceptorConfig.java @@ -15,7 +15,11 @@ public class MvcInterceptorConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/**") - .excludePathPatterns("/login", "/register", "/loginWechat/**", "/loginGoogle/**", "/version", "/test*"); + .excludePathPatterns("/login", "/register") + .excludePathPatterns("/loginWechat/**", "/loginGoogle/**") + .excludePathPatterns("/flow-files/view") + .excludePathPatterns("/version") + .excludePathPatterns("/test*"); WebMvcConfigurer.super.addInterceptors(registry); } diff --git a/moneynote-api-user/src/main/resources/application.properties b/moneynote-api-user/src/main/resources/application.properties index 7bf364d..cf6953b 100644 --- a/moneynote-api-user/src/main/resources/application.properties +++ b/moneynote-api-user/src/main/resources/application.properties @@ -43,4 +43,13 @@ sentry.dsn=https://7ef801c07e2d40fe81baa8dbc6a71f2f@o506813.ingest.sentry.io/450 sentry.traces-sample-rate=1.0 #sentry.debug=true -user_api_base_url=https://api.moneywhere.com/api/v1/user-api/ \ No newline at end of file +user_api_base_url=https://api.moneywhere.com/api/v1/user-api/ + + +# 文件上传 +# 设置内置Tomcat请求大小 +server.tomcat.max-http-form-post-size=100MB +# 设置请求最大大小 +spring.servlet.multipart.max-request-size=100MB +# 设置文件上传最大大小 +spring.servlet.multipart.max-file-size=100MB \ No newline at end of file diff --git a/moneynote-api-user/src/main/resources/i18n/messages.properties b/moneynote-api-user/src/main/resources/i18n/messages.properties index 114160f..53dd41c 100644 --- a/moneynote-api-user/src/main/resources/i18n/messages.properties +++ b/moneynote-api-user/src/main/resources/i18n/messages.properties @@ -37,6 +37,7 @@ add.flow.category.duplicated=分类不能重复 add.flow.category.empty=分类不能为空 add.flow.category.overflow=超过分类数最大限制 add.flow.daily.overflow=操作太频繁,请稍后重试。 +add.flow.file.fail=上传文件出错。 label.expense=支出 label.income=收入 label.transfer=转账 diff --git a/moneynote-api-user/src/main/resources/i18n/messages_en_US.properties b/moneynote-api-user/src/main/resources/i18n/messages_en_US.properties index 7c86ed5..2820213 100644 --- a/moneynote-api-user/src/main/resources/i18n/messages_en_US.properties +++ b/moneynote-api-user/src/main/resources/i18n/messages_en_US.properties @@ -37,7 +37,7 @@ add.flow.category.duplicated=category duplicated add.flow.category.empty=category can't be empty add.flow.category.overflow=Maximum number of category exceeded add.flow.daily.overflow=Maximum daily number of flow exceeded - +add.flow.file.fail=Add file failed label.expense=Expense label.income=Income