Repository: markliu2013/bookkeeping
Branch: main
Commit: 10dbfc0ff078
Files: 1544
Total size: 3.2 MB
Directory structure:
gitextract_uxeyvckg/
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── bookkeeping-user-api/
│ ├── .mvn/
│ │ └── wrapper/
│ │ ├── MavenWrapperDownloader.java
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
│ ├── Dockerfile
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── pom.xml
│ ├── restart.sh
│ ├── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── jiukuaitech/
│ │ │ └── bookkeeping/
│ │ │ └── user/
│ │ │ ├── Application.java
│ │ │ ├── RegexTest.java
│ │ │ ├── account/
│ │ │ │ ├── Account.java
│ │ │ │ ├── AccountAddRequest.java
│ │ │ │ ├── AccountAdjustBalanceNotValidException.java
│ │ │ │ ├── AccountController.java
│ │ │ │ ├── AccountExceptionHandler.java
│ │ │ │ ├── AccountHasTransactionException.java
│ │ │ │ ├── AccountMaxCountException.java
│ │ │ │ ├── AccountNameExistsException.java
│ │ │ │ ├── AccountQueryRequest.java
│ │ │ │ ├── AccountRepository.java
│ │ │ │ ├── AccountService.java
│ │ │ │ ├── AccountSpec.java
│ │ │ │ ├── AccountSumVO.java
│ │ │ │ ├── AccountUpdateRequest.java
│ │ │ │ ├── AccountVOForExtend.java
│ │ │ │ ├── AccountVOForList.java
│ │ │ │ ├── DefaultExpenseAccountException.java
│ │ │ │ ├── DefaultIncomeAccountException.java
│ │ │ │ ├── DefaultTransferFromAccountException.java
│ │ │ │ └── DefaultTransferToAccountException.java
│ │ │ ├── adjust_balance/
│ │ │ │ ├── AdjustBalance.java
│ │ │ │ ├── AdjustBalanceAddRequest.java
│ │ │ │ ├── AdjustBalanceController.java
│ │ │ │ ├── AdjustBalanceRepository.java
│ │ │ │ ├── AdjustBalanceService.java
│ │ │ │ └── AdjustBalanceVOForList.java
│ │ │ ├── aop/
│ │ │ │ └── TestAspect.java
│ │ │ ├── asset_account/
│ │ │ │ ├── AssetAccount.java
│ │ │ │ ├── AssetAccountController.java
│ │ │ │ ├── AssetAccountRepository.java
│ │ │ │ ├── AssetAccountService.java
│ │ │ │ └── AssetAccountVOForList.java
│ │ │ ├── balance_flow/
│ │ │ │ ├── AccountInvalidateException.java
│ │ │ │ ├── AmountInvalidateException.java
│ │ │ │ ├── BalanceFlow.java
│ │ │ │ ├── BalanceFlowAddRequest.java
│ │ │ │ ├── BalanceFlowController.java
│ │ │ │ ├── BalanceFlowExceptionHandler.java
│ │ │ │ ├── BalanceFlowQueryRequest.java
│ │ │ │ ├── BalanceFlowQueryResultVO.java
│ │ │ │ ├── BalanceFlowRepository.java
│ │ │ │ ├── BalanceFlowService.java
│ │ │ │ ├── BalanceFlowSpec.java
│ │ │ │ ├── BalanceFlowUpdateRequest.java
│ │ │ │ ├── BalanceFlowVOForExtend.java
│ │ │ │ ├── BalanceFlowVOForList.java
│ │ │ │ └── StatusNotValidateException.java
│ │ │ ├── balance_log/
│ │ │ │ ├── BalanceLog.java
│ │ │ │ ├── BalanceLogAddRequest.java
│ │ │ │ ├── BalanceLogController.java
│ │ │ │ ├── BalanceLogRepository.java
│ │ │ │ ├── BalanceLogService.java
│ │ │ │ └── BalanceLogVOForList.java
│ │ │ ├── base/
│ │ │ │ ├── BaseController.java
│ │ │ │ ├── BaseEntity.java
│ │ │ │ ├── BaseRepository.java
│ │ │ │ ├── BaseRepositoryFactoryBean.java
│ │ │ │ ├── BaseRepositoryImpl.java
│ │ │ │ ├── BookNameNotesEnableEntity.java
│ │ │ │ ├── BookNameNotesEnableSpec.java
│ │ │ │ ├── HasBookEntity.java
│ │ │ │ ├── HasBookRepository.java
│ │ │ │ ├── JpaDataConfig.java
│ │ │ │ ├── NameNotesEnableEntity.java
│ │ │ │ └── TestController.java
│ │ │ ├── book/
│ │ │ │ ├── Book.java
│ │ │ │ ├── BookAddRequest.java
│ │ │ │ ├── BookController.java
│ │ │ │ ├── BookExceptionHandler.java
│ │ │ │ ├── BookMaxCountException.java
│ │ │ │ ├── BookRepository.java
│ │ │ │ ├── BookService.java
│ │ │ │ ├── BookUpdateRequest.java
│ │ │ │ └── BookVOForList.java
│ │ │ ├── category/
│ │ │ │ ├── Category.java
│ │ │ │ ├── CategoryAddRequest.java
│ │ │ │ ├── CategoryController.java
│ │ │ │ ├── CategoryExceptionHandler.java
│ │ │ │ ├── CategoryHasDealException.java
│ │ │ │ ├── CategoryIsDefaultExpenseException.java
│ │ │ │ ├── CategoryIsDefaultIncomeException.java
│ │ │ │ ├── CategoryLevelException.java
│ │ │ │ ├── CategoryMaxCountException.java
│ │ │ │ ├── CategoryNameExistsException.java
│ │ │ │ ├── CategoryQueryRequest.java
│ │ │ │ ├── CategoryRepository.java
│ │ │ │ ├── CategoryService.java
│ │ │ │ ├── CategorySimpleVO.java
│ │ │ │ ├── CategorySpec.java
│ │ │ │ ├── CategoryTreeVO.java
│ │ │ │ ├── CategoryUpdateRequest.java
│ │ │ │ ├── DefaultExpenseCategoryException.java
│ │ │ │ ├── DefaultIncomeCategoryException.java
│ │ │ │ └── ParentCategoryNotEnableException.java
│ │ │ ├── category_relation/
│ │ │ │ ├── CategoryRelation.java
│ │ │ │ ├── CategoryRelationAddRequest.java
│ │ │ │ ├── CategoryRelationRepository.java
│ │ │ │ └── CategoryRelationVOForList.java
│ │ │ ├── checking_account/
│ │ │ │ ├── CheckingAccount.java
│ │ │ │ ├── CheckingAccountController.java
│ │ │ │ ├── CheckingAccountRepository.java
│ │ │ │ └── CheckingAccountService.java
│ │ │ ├── credit_account/
│ │ │ │ ├── CreditAccount.java
│ │ │ │ ├── CreditAccountAddRequest.java
│ │ │ │ ├── CreditAccountController.java
│ │ │ │ ├── CreditAccountRepository.java
│ │ │ │ ├── CreditAccountService.java
│ │ │ │ ├── CreditAccountSumVO.java
│ │ │ │ └── CreditAccountVOForList.java
│ │ │ ├── currency/
│ │ │ │ ├── Currency.java
│ │ │ │ ├── CurrencyController.java
│ │ │ │ ├── CurrencyRepository.java
│ │ │ │ └── CurrencyService.java
│ │ │ ├── dashboard/
│ │ │ │ ├── AssetOverviewVO.java
│ │ │ │ ├── DashboardController.java
│ │ │ │ └── DashboardService.java
│ │ │ ├── deal/
│ │ │ │ ├── CategoryConflictException.java
│ │ │ │ ├── Deal.java
│ │ │ │ ├── DealAddRequest.java
│ │ │ │ ├── DealController.java
│ │ │ │ ├── DealExceptionHandler.java
│ │ │ │ ├── DealQueryResultVO.java
│ │ │ │ ├── DealRepository.java
│ │ │ │ ├── DealService.java
│ │ │ │ ├── DealSpec.java
│ │ │ │ ├── DealUpdateRequest.java
│ │ │ │ └── DealVOForList.java
│ │ │ ├── debt_account/
│ │ │ │ ├── DebtAccount.java
│ │ │ │ ├── DebtAccountAddRequest.java
│ │ │ │ ├── DebtAccountController.java
│ │ │ │ ├── DebtAccountRepository.java
│ │ │ │ ├── DebtAccountService.java
│ │ │ │ ├── DebtAccountSumVO.java
│ │ │ │ └── DebtAccountVOForList.java
│ │ │ ├── exception/
│ │ │ │ ├── GlobalExceptionHandler.java
│ │ │ │ ├── InputNotValidException.java
│ │ │ │ ├── ItemNotFoundException.java
│ │ │ │ ├── NameExistsException.java
│ │ │ │ ├── PermissionException.java
│ │ │ │ ├── TokenEmptyException.java
│ │ │ │ └── TokenNotValidException.java
│ │ │ ├── expense/
│ │ │ │ ├── Expense.java
│ │ │ │ ├── ExpenseController.java
│ │ │ │ ├── ExpenseRepository.java
│ │ │ │ └── ExpenseService.java
│ │ │ ├── expense_category/
│ │ │ │ ├── ExpenseCategory.java
│ │ │ │ └── ExpenseCategoryController.java
│ │ │ ├── flow_images/
│ │ │ │ ├── FlowImage.java
│ │ │ │ ├── FlowImageController.java
│ │ │ │ ├── FlowImageExceptionHandler.java
│ │ │ │ ├── FlowImageRepository.java
│ │ │ │ ├── FlowImageService.java
│ │ │ │ ├── FlowImageVOForList.java
│ │ │ │ ├── ImageExistsException.java
│ │ │ │ ├── UploadCallbackRequest.java
│ │ │ │ └── UploadKeyEmptyException.java
│ │ │ ├── group/
│ │ │ │ ├── Group.java
│ │ │ │ ├── GroupAddRequest.java
│ │ │ │ ├── GroupController.java
│ │ │ │ ├── GroupExceptionHandler.java
│ │ │ │ ├── GroupHasBookException.java
│ │ │ │ ├── GroupMaxCountException.java
│ │ │ │ ├── GroupRepository.java
│ │ │ │ ├── GroupService.java
│ │ │ │ ├── GroupUpdateRequest.java
│ │ │ │ └── GroupVOForList.java
│ │ │ ├── income/
│ │ │ │ ├── Income.java
│ │ │ │ ├── IncomeController.java
│ │ │ │ ├── IncomeRepository.java
│ │ │ │ └── IncomeService.java
│ │ │ ├── income_category/
│ │ │ │ ├── IncomeCategory.java
│ │ │ │ └── IncomeCategoryController.java
│ │ │ ├── interceptor/
│ │ │ │ ├── AuthInterceptor.java
│ │ │ │ ├── MvcInterceptorConfig.java
│ │ │ │ └── StringTrimModule.java
│ │ │ ├── item/
│ │ │ │ ├── Item.java
│ │ │ │ ├── ItemAddRequest.java
│ │ │ │ ├── ItemController.java
│ │ │ │ ├── ItemCountException.java
│ │ │ │ ├── ItemExceptionHandler.java
│ │ │ │ ├── ItemQueryRequest.java
│ │ │ │ ├── ItemRepository.java
│ │ │ │ ├── ItemService.java
│ │ │ │ ├── ItemSpec.java
│ │ │ │ ├── ItemUpdateRequest.java
│ │ │ │ └── ItemVOForList.java
│ │ │ ├── payee/
│ │ │ │ ├── Payee.java
│ │ │ │ ├── PayeeAddRequest.java
│ │ │ │ ├── PayeeController.java
│ │ │ │ ├── PayeeExceptionHandler.java
│ │ │ │ ├── PayeeHasDealException.java
│ │ │ │ ├── PayeeMaxCountException.java
│ │ │ │ ├── PayeeQueryRequest.java
│ │ │ │ ├── PayeeRepository.java
│ │ │ │ ├── PayeeService.java
│ │ │ │ ├── PayeeSpec.java
│ │ │ │ ├── PayeeUpdateRequest.java
│ │ │ │ └── PayeeVOForList.java
│ │ │ ├── permission/
│ │ │ │ ├── PermissionCheck.java
│ │ │ │ └── PermissionCheckAspect.java
│ │ │ ├── refund/
│ │ │ │ ├── Refund.java
│ │ │ │ ├── RefundRepository.java
│ │ │ │ └── RefundService.java
│ │ │ ├── reports/
│ │ │ │ ├── BreakOutOfMaxException.java
│ │ │ │ ├── ChartVO.java
│ │ │ │ ├── ChartVO2.java
│ │ │ │ ├── ExpenseIncomeTrendQueryRequest.java
│ │ │ │ ├── ReportController.java
│ │ │ │ ├── ReportService.java
│ │ │ │ ├── ReportsExceptionHandler.java
│ │ │ │ └── TrendTimeQueryRequest.java
│ │ │ ├── response/
│ │ │ │ ├── BaseResponse.java
│ │ │ │ ├── DataResponse.java
│ │ │ │ ├── ErrorResponse.java
│ │ │ │ └── HasNameVO.java
│ │ │ ├── scheduled/
│ │ │ │ ├── Scheduled.java
│ │ │ │ ├── ScheduledAddRequest.java
│ │ │ │ ├── ScheduledController.java
│ │ │ │ ├── ScheduledExpenseAddRequest.java
│ │ │ │ ├── ScheduledRepository.java
│ │ │ │ └── ScheduledService.java
│ │ │ ├── setting/
│ │ │ │ └── Setting.java
│ │ │ ├── tag/
│ │ │ │ ├── Tag.java
│ │ │ │ ├── TagAddRequest.java
│ │ │ │ ├── TagController.java
│ │ │ │ ├── TagExceptionHandler.java
│ │ │ │ ├── TagHasTransactionException.java
│ │ │ │ ├── TagMaxCountException.java
│ │ │ │ ├── TagQueryRequest.java
│ │ │ │ ├── TagRepository.java
│ │ │ │ ├── TagService.java
│ │ │ │ ├── TagSpec.java
│ │ │ │ ├── TagTreeVO.java
│ │ │ │ ├── TagUpdateRequest.java
│ │ │ │ └── TagVOForList.java
│ │ │ ├── tag_relation/
│ │ │ │ ├── TagRelation.java
│ │ │ │ ├── TagRelationController.java
│ │ │ │ ├── TagRelationRepository.java
│ │ │ │ ├── TagRelationService.java
│ │ │ │ ├── TagRelationUpdateRequest.java
│ │ │ │ └── TagRelationVOForList.java
│ │ │ ├── transaction/
│ │ │ │ ├── Transaction.java
│ │ │ │ ├── TransactionAddRequest.java
│ │ │ │ ├── TransactionRepository.java
│ │ │ │ ├── TransactionSpec.java
│ │ │ │ ├── TransactionUpdateRequest.java
│ │ │ │ └── TransactionVOForList.java
│ │ │ ├── transfer/
│ │ │ │ ├── Transfer.java
│ │ │ │ ├── TransferAddRequest.java
│ │ │ │ ├── TransferController.java
│ │ │ │ ├── TransferExceptionHandler.java
│ │ │ │ ├── TransferFromEqualsToException.java
│ │ │ │ ├── TransferQueryResultVO.java
│ │ │ │ ├── TransferRepository.java
│ │ │ │ ├── TransferService.java
│ │ │ │ ├── TransferSpec.java
│ │ │ │ ├── TransferUpdateRequest.java
│ │ │ │ └── TransferVOForList.java
│ │ │ ├── user/
│ │ │ │ ├── InviteCodeErrorException.java
│ │ │ │ ├── IpNotAllowedException.java
│ │ │ │ ├── OldPasswordErrorException.java
│ │ │ │ ├── RegisterNameExistsException.java
│ │ │ │ ├── SessionUserNotFoundException.java
│ │ │ │ ├── SessionVO.java
│ │ │ │ ├── SigninFailedException.java
│ │ │ │ ├── UploadNotImageException.java
│ │ │ │ ├── User.java
│ │ │ │ ├── UserController.java
│ │ │ │ ├── UserDisabledException.java
│ │ │ │ ├── UserExceptionHandler.java
│ │ │ │ ├── UserGroupRelation.java
│ │ │ │ ├── UserGroupRelationRepository.java
│ │ │ │ ├── UserRegisterRequest.java
│ │ │ │ ├── UserRepository.java
│ │ │ │ ├── UserService.java
│ │ │ │ ├── UserSessionVO.java
│ │ │ │ ├── UserSignInRequest.java
│ │ │ │ ├── UserSignInResponse.java
│ │ │ │ └── UserUpdatePasswordRequest.java
│ │ │ ├── user_log/
│ │ │ │ ├── FlowMaxCountException.java
│ │ │ │ ├── UserActionExceptionHandler.java
│ │ │ │ ├── UserActionLog.java
│ │ │ │ ├── UserActionLogRepository.java
│ │ │ │ └── UserActionLogService.java
│ │ │ ├── utils/
│ │ │ │ ├── CalendarUtils.java
│ │ │ │ ├── CommonUtils.java
│ │ │ │ └── EnumUtils.java
│ │ │ └── validation/
│ │ │ ├── AmountAccumulateValidator.java
│ │ │ ├── AmountValidator.java
│ │ │ ├── AprValidator.java
│ │ │ ├── AvatarValidator.java
│ │ │ ├── BalanceValidator.java
│ │ │ ├── BillDayValidator.java
│ │ │ ├── CreditLimitValidator.java
│ │ │ ├── DescriptionValidator.java
│ │ │ ├── NameValidator.java
│ │ │ ├── NoValidator.java
│ │ │ ├── NotesValidator.java
│ │ │ ├── PasswordValidator.java
│ │ │ ├── TimeValidator.java
│ │ │ ├── TransactionStatusValidator.java
│ │ │ └── UserNameValidator.java
│ │ └── resources/
│ │ ├── application.properties
│ │ └── i18n/
│ │ └── messages.properties
│ ├── start.sh
│ ├── startup.sh
│ └── stop.sh
├── bookkeeping-user-fe/
│ ├── .editorconfig
│ ├── .eslintrc
│ ├── .prettierignore
│ ├── .prettierrc
│ ├── .umirc.js
│ ├── Dockerfile
│ ├── docker/
│ │ ├── gzip.conf
│ │ ├── nginx.conf
│ │ └── nginx.conf.template
│ ├── mock/
│ │ └── .gitkeep
│ ├── package.json
│ ├── src/
│ │ ├── app.js
│ │ ├── components/
│ │ │ ├── AccountRecordTable/
│ │ │ │ └── index.jsx
│ │ │ ├── AdjustBalanceModal/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── AlertTotalSearch/
│ │ │ │ └── index.jsx
│ │ │ ├── AmountInput/
│ │ │ │ └── index.jsx
│ │ │ ├── AmountInputPositive/
│ │ │ │ └── index.jsx
│ │ │ ├── AvatarDropdown/
│ │ │ │ ├── UpdatePasswordModal.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── Breadcrumb/
│ │ │ │ └── index.jsx
│ │ │ ├── ExpenseModal/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── FlagTag/
│ │ │ │ └── index.jsx
│ │ │ ├── FlowButtons/
│ │ │ │ └── index.jsx
│ │ │ ├── FlowImageUploadModal/
│ │ │ │ └── index.jsx
│ │ │ ├── FlowRecordTable/
│ │ │ │ └── index.jsx
│ │ │ ├── FlowStatusDisplay/
│ │ │ │ └── index.jsx
│ │ │ ├── FlowTagDisplay/
│ │ │ │ ├── TagModal.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── Footer/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── FormItemAccount/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemAccounts/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemAmountRange/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemCategories/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemCategoryReport/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemCreateTime/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemDateRange/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemDateRangeWithBreak/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemDescription/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemDescriptionSearch/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemPayee/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemPayees/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemStatus/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemTag/
│ │ │ │ ├── AddTagModal.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── FormItemTagReport/
│ │ │ │ └── index.jsx
│ │ │ ├── FormItemTags/
│ │ │ │ └── index.jsx
│ │ │ ├── FormListCategory/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── FormModal/
│ │ │ │ └── index.jsx
│ │ │ ├── HeaderDropdown/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── IncomeModal/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── Loading/
│ │ │ │ └── index.jsx
│ │ │ ├── ModalRoot/
│ │ │ │ └── index.jsx
│ │ │ ├── PrimaryMenu/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── TransferModal/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ └── charts/
│ │ │ ├── Bar/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── Line/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ ├── Pie/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.less
│ │ │ └── Pie2/
│ │ │ ├── index.jsx
│ │ │ └── index.less
│ │ ├── global.less
│ │ ├── layouts/
│ │ │ ├── BasicLayout.jsx
│ │ │ ├── BasicLayout.less
│ │ │ ├── UserLayout.jsx
│ │ │ ├── UserLayout.less
│ │ │ └── index.js
│ │ ├── locales/
│ │ │ ├── en-US/
│ │ │ │ ├── account.js
│ │ │ │ ├── book.js
│ │ │ │ ├── category.js
│ │ │ │ ├── common.js
│ │ │ │ ├── dashboard.js
│ │ │ │ ├── flow.js
│ │ │ │ ├── footer.js
│ │ │ │ ├── group.js
│ │ │ │ ├── header.js
│ │ │ │ ├── menu.js
│ │ │ │ ├── register.js
│ │ │ │ ├── report.js
│ │ │ │ ├── rules.js
│ │ │ │ ├── schedule.js
│ │ │ │ └── signin.js
│ │ │ ├── en-US.js
│ │ │ ├── zh-CN/
│ │ │ │ ├── account.js
│ │ │ │ ├── book.js
│ │ │ │ ├── category.js
│ │ │ │ ├── common.js
│ │ │ │ ├── flow.js
│ │ │ │ ├── footer.js
│ │ │ │ ├── group.js
│ │ │ │ ├── menu.js
│ │ │ │ ├── report.js
│ │ │ │ ├── rules.js
│ │ │ │ ├── schedule.js
│ │ │ │ └── user.js
│ │ │ └── zh-CN.js
│ │ ├── models/
│ │ │ ├── account.js
│ │ │ ├── currency.js
│ │ │ ├── expenseCategory.js
│ │ │ ├── flow.js
│ │ │ ├── incomeCategory.js
│ │ │ ├── modal.js
│ │ │ ├── payee.js
│ │ │ ├── session.js
│ │ │ └── tag.js
│ │ ├── pages/
│ │ │ ├── accounts/
│ │ │ │ ├── AssetAccountModal.jsx
│ │ │ │ ├── AssetAccountTable.jsx
│ │ │ │ ├── CheckingAccountModal.jsx
│ │ │ │ ├── CheckingAccountTable.jsx
│ │ │ │ ├── CreditAccountModal.jsx
│ │ │ │ ├── CreditAccountTable.jsx
│ │ │ │ ├── DebtAccountModal.jsx
│ │ │ │ ├── DebtAccountTable.jsx
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ └── model.js
│ │ │ ├── asset-accounts/
│ │ │ │ ├── GeneralBar.jsx
│ │ │ │ ├── ItemCard.jsx
│ │ │ │ ├── List.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── audit/
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── RecordTable.jsx
│ │ │ │ ├── TotalAlert.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ └── model.js
│ │ │ ├── balance-logs/
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── OperationModal.jsx
│ │ │ │ ├── RecordTable.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── books/
│ │ │ │ ├── ConfigModal.jsx
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── OperationModal.jsx
│ │ │ │ ├── RecordTable.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── categories/
│ │ │ │ ├── ExpenseCategoryFilterBar.jsx
│ │ │ │ ├── ExpenseCategoryModal.jsx
│ │ │ │ ├── ExpenseCategoryTable.jsx
│ │ │ │ ├── IncomeCategoryFilterBar.jsx
│ │ │ │ ├── IncomeCategoryModal.jsx
│ │ │ │ ├── IncomeCategoryTable.jsx
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── PayeeFilterBar.jsx
│ │ │ │ ├── PayeeModal.jsx
│ │ │ │ ├── PayeeTable.jsx
│ │ │ │ ├── TagFilterBar.jsx
│ │ │ │ ├── TagModal.jsx
│ │ │ │ ├── TagTable.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── checking-accounts/
│ │ │ │ ├── GeneralBar.jsx
│ │ │ │ ├── ItemCard.jsx
│ │ │ │ ├── List.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── credit-accounts/
│ │ │ │ ├── GeneralBar.jsx
│ │ │ │ ├── ItemCard.jsx
│ │ │ │ ├── List.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── dashboard/
│ │ │ │ ├── AssetBar.jsx
│ │ │ │ ├── CardExtra.jsx
│ │ │ │ ├── ExpenseCategory.jsx
│ │ │ │ ├── ExpenseTrend.jsx
│ │ │ │ ├── IncomeCategory.jsx
│ │ │ │ ├── IncomeTrend.jsx
│ │ │ │ ├── TransactionTable.jsx
│ │ │ │ ├── TransactionTable.less
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── debt-accounts/
│ │ │ │ ├── GeneralBar.jsx
│ │ │ │ ├── ItemCard.jsx
│ │ │ │ ├── List.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── document.ejs
│ │ │ ├── expenses/
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── RecordTable.jsx
│ │ │ │ ├── TotalAlert.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ └── model.js
│ │ │ ├── flows/
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── RecordTable.jsx
│ │ │ │ ├── TotalAlert.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ └── model.js
│ │ │ ├── groups/
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── OperationModal.jsx
│ │ │ │ ├── RecordTable.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── incomes/
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── RecordTable.jsx
│ │ │ │ ├── TotalAlert.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ └── model.js
│ │ │ ├── index.jsx
│ │ │ ├── items/
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── OperationModal.jsx
│ │ │ │ ├── RecordTable.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── register/
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── reports/
│ │ │ │ ├── asset-debt-trend/
│ │ │ │ │ ├── Chart.jsx
│ │ │ │ │ ├── OperationBar.jsx
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── model.js
│ │ │ │ ├── balance-sheet/
│ │ │ │ │ ├── AssetCategory.jsx
│ │ │ │ │ ├── DebtCategory.jsx
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── model.js
│ │ │ │ ├── expense-category/
│ │ │ │ │ ├── CategoryPie.jsx
│ │ │ │ │ ├── OperationBar.jsx
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── model.js
│ │ │ │ ├── expense-income-trend/
│ │ │ │ │ ├── Chart.jsx
│ │ │ │ │ ├── OperationBar.jsx
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── model.js
│ │ │ │ ├── expense-tag/
│ │ │ │ │ ├── OperationBar.jsx
│ │ │ │ │ ├── TagPie.jsx
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── model.js
│ │ │ │ ├── income-category/
│ │ │ │ │ ├── CategoryPie.jsx
│ │ │ │ │ ├── OperationBar.jsx
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── model.js
│ │ │ │ └── income-tag/
│ │ │ │ ├── OperationBar.jsx
│ │ │ │ ├── TagPie.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ └── model.js
│ │ │ ├── scheduled/
│ │ │ │ └── index.jsx
│ │ │ ├── settings/
│ │ │ │ └── index.jsx
│ │ │ ├── signin/
│ │ │ │ ├── RememberDropDown.js
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.less
│ │ │ │ └── model.js
│ │ │ ├── test/
│ │ │ │ └── index.jsx
│ │ │ └── transfers/
│ │ │ ├── OperationBar.jsx
│ │ │ ├── RecordTable.jsx
│ │ │ ├── TotalAlert.jsx
│ │ │ ├── index.jsx
│ │ │ └── model.js
│ │ ├── services/
│ │ │ ├── account.js
│ │ │ ├── adjust-balance.js
│ │ │ ├── asset-account.js
│ │ │ ├── book.js
│ │ │ ├── category.js
│ │ │ ├── checking-account.js
│ │ │ ├── credit-account.js
│ │ │ ├── currency.js
│ │ │ ├── dashboard.js
│ │ │ ├── deal.js
│ │ │ ├── debt-account.js
│ │ │ ├── expense-category.js
│ │ │ ├── expense.js
│ │ │ ├── flow-image.js
│ │ │ ├── flow.js
│ │ │ ├── group.js
│ │ │ ├── income-category.js
│ │ │ ├── income.js
│ │ │ ├── item.js
│ │ │ ├── log.js
│ │ │ ├── payee.js
│ │ │ ├── report.js
│ │ │ ├── schedule.js
│ │ │ ├── tag-relation.js
│ │ │ ├── tag.js
│ │ │ ├── transfer.js
│ │ │ └── user.js
│ │ └── utils/
│ │ ├── columns.js
│ │ ├── flow.js
│ │ ├── hooks.js
│ │ ├── model.js
│ │ ├── request.js
│ │ ├── rules.js
│ │ ├── translate.js
│ │ ├── util.js
│ │ └── var.js
│ └── webpack.config.js
├── bookkeeping_user_flutter/
│ ├── .gitignore
│ ├── .metadata
│ ├── README.md
│ ├── analysis_options.yaml
│ ├── android/
│ │ ├── .gitignore
│ │ ├── app/
│ │ │ ├── build.gradle
│ │ │ └── src/
│ │ │ ├── debug/
│ │ │ │ └── AndroidManifest.xml
│ │ │ ├── main/
│ │ │ │ ├── AndroidManifest.xml
│ │ │ │ ├── kotlin/
│ │ │ │ │ └── com/
│ │ │ │ │ └── whlcsj/
│ │ │ │ │ └── bookkeeping_user_flutter/
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ └── res/
│ │ │ │ ├── drawable/
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-v21/
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── values/
│ │ │ │ │ └── styles.xml
│ │ │ │ └── values-night/
│ │ │ │ └── styles.xml
│ │ │ └── profile/
│ │ │ └── AndroidManifest.xml
│ │ ├── build.gradle
│ │ ├── gradle/
│ │ │ └── wrapper/
│ │ │ └── gradle-wrapper.properties
│ │ ├── gradle.properties
│ │ └── settings.gradle
│ ├── ios/
│ │ ├── .gitignore
│ │ ├── Flutter/
│ │ │ ├── AppFrameworkInfo.plist
│ │ │ ├── Debug.xcconfig
│ │ │ └── Release.xcconfig
│ │ ├── Podfile
│ │ ├── Runner/
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── LaunchImage.imageset/
│ │ │ │ ├── Contents.json
│ │ │ │ └── README.md
│ │ │ ├── Base.lproj/
│ │ │ │ ├── LaunchScreen.storyboard
│ │ │ │ └── Main.storyboard
│ │ │ ├── Info.plist
│ │ │ └── Runner-Bridging-Header.h
│ │ ├── Runner.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata/
│ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ └── WorkspaceSettings.xcsettings
│ │ │ └── xcshareddata/
│ │ │ └── xcschemes/
│ │ │ └── Runner.xcscheme
│ │ └── Runner.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
│ ├── lib/
│ │ ├── accounts/
│ │ │ ├── accounts.dart
│ │ │ ├── bloc/
│ │ │ │ ├── account_adjust_balance/
│ │ │ │ │ ├── account_adjust_balance_bloc.dart
│ │ │ │ │ ├── account_adjust_balance_event.dart
│ │ │ │ │ └── account_adjust_balance_state.dart
│ │ │ │ ├── account_enable/
│ │ │ │ │ ├── account_enable_bloc.dart
│ │ │ │ │ ├── account_enable_event.dart
│ │ │ │ │ └── account_enable_state.dart
│ │ │ │ ├── account_expenseable/
│ │ │ │ │ ├── account_expenseable_bloc.dart
│ │ │ │ │ ├── account_expenseable_event.dart
│ │ │ │ │ └── account_expenseable_state.dart
│ │ │ │ ├── account_fetch/
│ │ │ │ │ ├── account_fetch_bloc.dart
│ │ │ │ │ ├── account_fetch_event.dart
│ │ │ │ │ └── account_fetch_state.dart
│ │ │ │ ├── account_form/
│ │ │ │ │ ├── account_form_bloc.dart
│ │ │ │ │ ├── account_form_event.dart
│ │ │ │ │ └── account_form_state.dart
│ │ │ │ ├── account_incomeable/
│ │ │ │ │ ├── account_incomeable_bloc.dart
│ │ │ │ │ ├── account_incomeable_event.dart
│ │ │ │ │ └── account_incomeable_state.dart
│ │ │ │ ├── account_transfer_from_able/
│ │ │ │ │ ├── account_transfer_from_able_bloc.dart
│ │ │ │ │ ├── account_transfer_from_able_event.dart
│ │ │ │ │ └── account_transfer_from_able_state.dart
│ │ │ │ ├── account_transfer_to_able/
│ │ │ │ │ ├── account_transfer_to_able_bloc.dart
│ │ │ │ │ ├── account_transfer_to_able_event.dart
│ │ │ │ │ └── account_transfer_to_able_state.dart
│ │ │ │ └── accounts/
│ │ │ │ ├── accounts_bloc.dart
│ │ │ │ ├── accounts_event.dart
│ │ │ │ └── accounts_state.dart
│ │ │ ├── data/
│ │ │ │ ├── account_repository.dart
│ │ │ │ └── models/
│ │ │ │ ├── account.dart
│ │ │ │ ├── account.g.dart
│ │ │ │ ├── account_form_request.dart
│ │ │ │ ├── account_form_request.g.dart
│ │ │ │ ├── account_query_request.dart
│ │ │ │ ├── account_query_request.g.dart
│ │ │ │ ├── adjust_balance_request.dart
│ │ │ │ ├── adjust_balance_request.g.dart
│ │ │ │ └── credit_account.dart
│ │ │ └── ui/
│ │ │ ├── account_adjust_balance.dart
│ │ │ ├── account_detail_page.dart
│ │ │ ├── account_form_page.dart
│ │ │ ├── accounts_page.dart
│ │ │ ├── asset_account_form_page.dart
│ │ │ ├── checking_account_form_page.dart
│ │ │ ├── credit_account_form_page.dart
│ │ │ ├── debt_account_form_page.dart
│ │ │ └── widgets/
│ │ │ ├── account_form/
│ │ │ │ ├── account_apr_input.dart
│ │ │ │ ├── account_balance_input.dart
│ │ │ │ ├── account_billday_input.dart
│ │ │ │ ├── account_limit_input.dart
│ │ │ │ ├── currency_input.dart
│ │ │ │ ├── expenseable_input.dart
│ │ │ │ ├── inclue_input.dart
│ │ │ │ ├── incomeable_input.dart
│ │ │ │ ├── name_input.dart
│ │ │ │ ├── no_input.dart
│ │ │ │ ├── notes_input.dart
│ │ │ │ ├── transfer_from_able_input.dart
│ │ │ │ └── transfer_to_able_input.dart
│ │ │ ├── adjust_balance/
│ │ │ │ ├── adjust_balance_form.dart
│ │ │ │ ├── balance_input.dart
│ │ │ │ ├── date_time_input.dart
│ │ │ │ ├── description_input.dart
│ │ │ │ └── notes_input.dart
│ │ │ └── order_button.dart
│ │ ├── add_flow/
│ │ │ ├── add_flow.dart
│ │ │ ├── bloc/
│ │ │ │ ├── add_expense/
│ │ │ │ │ ├── add_expense_bloc.dart
│ │ │ │ │ ├── add_expense_event.dart
│ │ │ │ │ └── add_expense_state.dart
│ │ │ │ ├── add_income/
│ │ │ │ │ ├── add_income_bloc.dart
│ │ │ │ │ ├── add_income_event.dart
│ │ │ │ │ └── add_income_state.dart
│ │ │ │ ├── add_transfer/
│ │ │ │ │ ├── add_transfer_bloc.dart
│ │ │ │ │ ├── add_transfer_event.dart
│ │ │ │ │ └── add_transfer_state.dart
│ │ │ │ └── models/
│ │ │ │ ├── deal_add_request.dart
│ │ │ │ ├── deal_add_request.g.dart
│ │ │ │ ├── transfer_add_request.dart
│ │ │ │ └── transfer_add_request.g.dart
│ │ │ └── ui/
│ │ │ ├── add_flow_page.dart
│ │ │ ├── no_tab_page.dart
│ │ │ ├── tab_page.dart
│ │ │ └── widgets/
│ │ │ ├── expense/
│ │ │ │ ├── account_input.dart
│ │ │ │ ├── add_expense_form.dart
│ │ │ │ ├── category_input.dart
│ │ │ │ ├── date_time_input.dart
│ │ │ │ ├── description_input.dart
│ │ │ │ ├── is_confirm_input.dart
│ │ │ │ ├── notes_input.dart
│ │ │ │ ├── payee_input.dart
│ │ │ │ ├── tag_input.dart
│ │ │ │ └── widgets.dart
│ │ │ ├── income/
│ │ │ │ ├── account_input.dart
│ │ │ │ ├── add_income_form.dart
│ │ │ │ ├── category_input.dart
│ │ │ │ ├── date_time_input.dart
│ │ │ │ ├── description_input.dart
│ │ │ │ ├── is_confirm_input.dart
│ │ │ │ ├── notes_input.dart
│ │ │ │ ├── payee_input.dart
│ │ │ │ ├── tag_input.dart
│ │ │ │ └── widgets.dart
│ │ │ └── transfer/
│ │ │ ├── add_transfer_form.dart
│ │ │ ├── amount_input.dart
│ │ │ ├── converted_amount_input.dart
│ │ │ ├── date_time_input.dart
│ │ │ ├── description_input.dart
│ │ │ ├── from_input.dart
│ │ │ ├── is_confirm_input.dart
│ │ │ ├── notes_input.dart
│ │ │ ├── tag_input.dart
│ │ │ ├── to_input.dart
│ │ │ └── widgets.dart
│ │ ├── app.dart
│ │ ├── books/
│ │ │ ├── bloc/
│ │ │ │ ├── book_fetch/
│ │ │ │ │ ├── book_fetch_bloc.dart
│ │ │ │ │ ├── book_fetch_event.dart
│ │ │ │ │ └── book_fetch_state.dart
│ │ │ │ └── books/
│ │ │ │ ├── books_bloc.dart
│ │ │ │ ├── books_event.dart
│ │ │ │ └── books_state.dart
│ │ │ ├── books.dart
│ │ │ ├── data/
│ │ │ │ ├── book_repository.dart
│ │ │ │ └── models/
│ │ │ │ ├── book.dart
│ │ │ │ ├── book.g.dart
│ │ │ │ ├── book_query_request.dart
│ │ │ │ └── book_query_request.g.dart
│ │ │ └── ui/
│ │ │ ├── book_detail_page.dart
│ │ │ └── books_page.dart
│ │ ├── categories/
│ │ │ ├── bloc/
│ │ │ │ ├── category_fetch/
│ │ │ │ │ ├── category_fetch_bloc.dart
│ │ │ │ │ ├── category_fetch_event.dart
│ │ │ │ │ └── category_fetch_state.dart
│ │ │ │ ├── category_form/
│ │ │ │ │ ├── category_form_bloc.dart
│ │ │ │ │ ├── category_form_event.dart
│ │ │ │ │ └── category_form_state.dart
│ │ │ │ ├── category_tree/
│ │ │ │ │ ├── category_tree_bloc.dart
│ │ │ │ │ ├── category_tree_event.dart
│ │ │ │ │ └── category_tree_state.dart
│ │ │ │ ├── expense_category_select/
│ │ │ │ │ ├── expense_category_select_bloc.dart
│ │ │ │ │ ├── expense_category_select_event.dart
│ │ │ │ │ └── expense_category_select_state.dart
│ │ │ │ └── income_category_select/
│ │ │ │ ├── income_category_select_bloc.dart
│ │ │ │ ├── income_category_select_event.dart
│ │ │ │ └── income_category_select_state.dart
│ │ │ ├── categories.dart
│ │ │ ├── data/
│ │ │ │ ├── category_repository.dart
│ │ │ │ └── models/
│ │ │ │ ├── category.dart
│ │ │ │ ├── category.g.dart
│ │ │ │ ├── category_form_request.dart
│ │ │ │ ├── category_form_request.g.dart
│ │ │ │ ├── category_tree.dart
│ │ │ │ └── category_tree.g.dart
│ │ │ └── ui/
│ │ │ ├── category_detail_page.dart
│ │ │ ├── category_form_page.dart
│ │ │ ├── expense_categories_page.dart
│ │ │ ├── expense_category_form_page.dart
│ │ │ ├── income_categories_page.dart
│ │ │ ├── income_category_form_page.dart
│ │ │ └── widgets/
│ │ │ └── category_form/
│ │ │ ├── expense_category_input.dart
│ │ │ ├── income_category_input.dart
│ │ │ ├── name_input.dart
│ │ │ └── notes_input.dart
│ │ ├── charts/
│ │ │ ├── bloc/
│ │ │ │ ├── report_asset/
│ │ │ │ │ ├── report_asset_bloc.dart
│ │ │ │ │ ├── report_asset_event.dart
│ │ │ │ │ └── report_asset_state.dart
│ │ │ │ ├── report_debt/
│ │ │ │ │ ├── report_debt_bloc.dart
│ │ │ │ │ ├── report_debt_event.dart
│ │ │ │ │ └── report_debt_state.dart
│ │ │ │ ├── report_expense_category/
│ │ │ │ │ ├── report_expense_category_bloc.dart
│ │ │ │ │ ├── report_expense_category_event.dart
│ │ │ │ │ └── report_expense_category_state.dart
│ │ │ │ └── report_income_category/
│ │ │ │ ├── report_income_category_bloc.dart
│ │ │ │ ├── report_income_category_event.dart
│ │ │ │ └── report_income_category_state.dart
│ │ │ ├── charts.dart
│ │ │ ├── data/
│ │ │ │ ├── models/
│ │ │ │ │ ├── category_query_request.dart
│ │ │ │ │ ├── category_query_request.g.dart
│ │ │ │ │ ├── x_y.dart
│ │ │ │ │ └── x_y.g.dart
│ │ │ │ └── report_repository.dart
│ │ │ └── ui/
│ │ │ ├── charts_expense_category_filter_page.dart
│ │ │ ├── charts_income_category_filter_page.dart
│ │ │ ├── charts_page.dart
│ │ │ └── widgets/
│ │ │ ├── asset_sheet.dart
│ │ │ ├── circular_legend.dart
│ │ │ ├── date_input_expense.dart
│ │ │ ├── date_input_income.dart
│ │ │ ├── debt_sheet.dart
│ │ │ ├── expense_category.dart
│ │ │ ├── expense_category_input.dart
│ │ │ ├── income_category.dart
│ │ │ └── income_category_input.dart
│ │ ├── commons/
│ │ │ ├── common_util.dart
│ │ │ ├── commons.dart
│ │ │ ├── enums.dart
│ │ │ ├── http_client.dart
│ │ │ ├── id_name_model.dart
│ │ │ ├── id_name_model.g.dart
│ │ │ ├── session.dart
│ │ │ ├── web_view_page.dart
│ │ │ └── widget_util.dart
│ │ ├── components/
│ │ │ ├── components.dart
│ │ │ ├── date_range_picker.dart
│ │ │ ├── date_time_input.dart
│ │ │ ├── empty.dart
│ │ │ ├── lazy_indexed_stack.dart
│ │ │ ├── page_error.dart
│ │ │ ├── page_loading.dart
│ │ │ └── popup_menu.dart
│ │ ├── currency/
│ │ │ ├── bloc/
│ │ │ │ └── currency_all/
│ │ │ │ ├── currency_all_bloc.dart
│ │ │ │ ├── currency_all_event.dart
│ │ │ │ └── currency_all_state.dart
│ │ │ ├── currency.dart
│ │ │ └── data/
│ │ │ ├── currency_repository.dart
│ │ │ └── models/
│ │ │ ├── currency.dart
│ │ │ └── currency.g.dart
│ │ ├── flows/
│ │ │ ├── bloc/
│ │ │ │ ├── flow_fetch/
│ │ │ │ │ ├── flow_fetch_bloc.dart
│ │ │ │ │ ├── flow_fetch_event.dart
│ │ │ │ │ └── flow_fetch_state.dart
│ │ │ │ └── flows/
│ │ │ │ ├── flows_bloc.dart
│ │ │ │ ├── flows_event.dart
│ │ │ │ └── flows_state.dart
│ │ │ ├── data/
│ │ │ │ ├── flow_repository.dart
│ │ │ │ └── models/
│ │ │ │ ├── adjust_balance.dart
│ │ │ │ ├── adjust_balance.g.dart
│ │ │ │ ├── category.dart
│ │ │ │ ├── category.g.dart
│ │ │ │ ├── deal.dart
│ │ │ │ ├── deal.g.dart
│ │ │ │ ├── flow.dart
│ │ │ │ ├── flow.g.dart
│ │ │ │ ├── flow_image.dart
│ │ │ │ ├── flow_image.g.dart
│ │ │ │ ├── flow_query_request.dart
│ │ │ │ ├── flow_query_request.g.dart
│ │ │ │ ├── tag.dart
│ │ │ │ ├── tag.g.dart
│ │ │ │ ├── transfer.dart
│ │ │ │ └── transfer.g.dart
│ │ │ ├── flows.dart
│ │ │ └── ui/
│ │ │ ├── flow_detail.dart
│ │ │ ├── flows_filter.dart
│ │ │ ├── flows_page.dart
│ │ │ └── widgets/
│ │ │ ├── account_input.dart
│ │ │ ├── date_input.dart
│ │ │ ├── expense_category_input.dart
│ │ │ ├── income_category_input.dart
│ │ │ ├── order_button.dart
│ │ │ ├── payee_input.dart
│ │ │ ├── status_input.dart
│ │ │ ├── tag_input.dart
│ │ │ ├── type_input.dart
│ │ │ └── widgets.dart
│ │ ├── groups/
│ │ │ ├── data/
│ │ │ │ └── models/
│ │ │ │ ├── group.dart
│ │ │ │ └── group.g.dart
│ │ │ └── groups.dart
│ │ ├── index.dart
│ │ ├── items/
│ │ │ ├── bloc/
│ │ │ │ ├── item_form/
│ │ │ │ │ ├── item_form_bloc.dart
│ │ │ │ │ ├── item_form_event.dart
│ │ │ │ │ ├── item_form_state.dart
│ │ │ │ │ └── models/
│ │ │ │ │ ├── models.dart
│ │ │ │ │ └── title.dart
│ │ │ │ └── items/
│ │ │ │ ├── items_bloc.dart
│ │ │ │ ├── items_event.dart
│ │ │ │ └── items_state.dart
│ │ │ ├── data/
│ │ │ │ ├── item_repository.dart
│ │ │ │ └── models/
│ │ │ │ ├── item.dart
│ │ │ │ ├── item.g.dart
│ │ │ │ ├── item_add_request.dart
│ │ │ │ ├── item_add_request.g.dart
│ │ │ │ ├── item_query_request.dart
│ │ │ │ └── item_query_request.g.dart
│ │ │ ├── items.dart
│ │ │ └── ui/
│ │ │ ├── item_form/
│ │ │ │ ├── date_input.dart
│ │ │ │ └── title_input.dart
│ │ │ ├── item_form_page.dart
│ │ │ ├── items_index.dart
│ │ │ └── items_page.dart
│ │ ├── login/
│ │ │ ├── bloc/
│ │ │ │ ├── auth/
│ │ │ │ │ ├── auth_bloc.dart
│ │ │ │ │ ├── auth_event.dart
│ │ │ │ │ └── auth_state.dart
│ │ │ │ └── login/
│ │ │ │ ├── login_bloc.dart
│ │ │ │ ├── login_event.dart
│ │ │ │ ├── login_state.dart
│ │ │ │ └── models/
│ │ │ │ ├── ApiUrl.dart
│ │ │ │ ├── password.dart
│ │ │ │ └── username.dart
│ │ │ ├── data/
│ │ │ │ ├── login_repository.dart
│ │ │ │ └── models/
│ │ │ │ ├── session.dart
│ │ │ │ ├── session.g.dart
│ │ │ │ ├── user.dart
│ │ │ │ └── user.g.dart
│ │ │ ├── login.dart
│ │ │ └── ui/
│ │ │ ├── login_page.dart
│ │ │ └── widgets/
│ │ │ ├── api_url_input.dart
│ │ │ ├── login_form.dart
│ │ │ ├── password_input.dart
│ │ │ ├── submit_btn.dart
│ │ │ └── user_name_input.dart
│ │ ├── main.dart
│ │ ├── my/
│ │ │ ├── my.dart
│ │ │ └── my_page.dart
│ │ ├── observer.dart
│ │ ├── payees/
│ │ │ ├── bloc/
│ │ │ │ ├── payee_enable/
│ │ │ │ │ ├── payee_enable_bloc.dart
│ │ │ │ │ ├── payee_enable_event.dart
│ │ │ │ │ └── payee_enable_state.dart
│ │ │ │ ├── payee_expenseable/
│ │ │ │ │ ├── payee_expenseable_bloc.dart
│ │ │ │ │ ├── payee_expenseable_event.dart
│ │ │ │ │ └── payee_expenseable_state.dart
│ │ │ │ ├── payee_fetch/
│ │ │ │ │ ├── payee_fetch_bloc.dart
│ │ │ │ │ ├── payee_fetch_event.dart
│ │ │ │ │ └── payee_fetch_state.dart
│ │ │ │ ├── payee_form/
│ │ │ │ │ ├── payee_form_bloc.dart
│ │ │ │ │ ├── payee_form_event.dart
│ │ │ │ │ └── payee_form_state.dart
│ │ │ │ ├── payee_incomeable/
│ │ │ │ │ ├── payee_incomeable_bloc.dart
│ │ │ │ │ ├── payee_incomeable_event.dart
│ │ │ │ │ └── payee_incomeable_state.dart
│ │ │ │ └── payees/
│ │ │ │ ├── payees_bloc.dart
│ │ │ │ ├── payees_event.dart
│ │ │ │ └── payees_state.dart
│ │ │ ├── data/
│ │ │ │ ├── models/
│ │ │ │ │ ├── payee.dart
│ │ │ │ │ ├── payee.g.dart
│ │ │ │ │ ├── payee_form_request.dart
│ │ │ │ │ ├── payee_form_request.g.dart
│ │ │ │ │ ├── payee_query_request.dart
│ │ │ │ │ └── payee_query_request.g.dart
│ │ │ │ └── payee_repository.dart
│ │ │ ├── payees.dart
│ │ │ └── ui/
│ │ │ ├── payee_detail_page.dart
│ │ │ ├── payee_form/
│ │ │ │ ├── expenseable_input.dart
│ │ │ │ ├── incomeable_input.dart
│ │ │ │ ├── name_input.dart
│ │ │ │ └── notes_input.dart
│ │ │ ├── payee_form_page.dart
│ │ │ └── payees_page.dart
│ │ ├── routes.dart
│ │ ├── start_page.dart
│ │ ├── tags/
│ │ │ ├── bloc/
│ │ │ │ ├── tag_enable/
│ │ │ │ │ ├── tag_enable_bloc.dart
│ │ │ │ │ ├── tag_enable_event.dart
│ │ │ │ │ └── tag_enable_state.dart
│ │ │ │ ├── tag_expenseable/
│ │ │ │ │ ├── tag_expenseable_bloc.dart
│ │ │ │ │ ├── tag_expenseable_event.dart
│ │ │ │ │ └── tag_expenseable_state.dart
│ │ │ │ ├── tag_fetch/
│ │ │ │ │ ├── tag_fetch_bloc.dart
│ │ │ │ │ ├── tag_fetch_event.dart
│ │ │ │ │ └── tag_fetch_state.dart
│ │ │ │ ├── tag_form/
│ │ │ │ │ ├── tag_form_bloc.dart
│ │ │ │ │ ├── tag_form_event.dart
│ │ │ │ │ └── tag_form_state.dart
│ │ │ │ ├── tag_incomeable/
│ │ │ │ │ ├── tag_incomeable_bloc.dart
│ │ │ │ │ ├── tag_incomeable_event.dart
│ │ │ │ │ └── tag_incomeable_state.dart
│ │ │ │ ├── tag_transferable/
│ │ │ │ │ ├── tag_transferable_bloc.dart
│ │ │ │ │ ├── tag_transferable_event.dart
│ │ │ │ │ └── tag_transferable_state.dart
│ │ │ │ └── tag_tree/
│ │ │ │ ├── tag_tree_bloc.dart
│ │ │ │ ├── tag_tree_event.dart
│ │ │ │ └── tag_tree_state.dart
│ │ │ ├── data/
│ │ │ │ ├── models/
│ │ │ │ │ ├── tag.dart
│ │ │ │ │ ├── tag.g.dart
│ │ │ │ │ ├── tag_form_request.dart
│ │ │ │ │ ├── tag_form_request.g.dart
│ │ │ │ │ ├── tag_tree.dart
│ │ │ │ │ └── tag_tree.g.dart
│ │ │ │ └── tag_repository.dart
│ │ │ ├── tags.dart
│ │ │ └── ui/
│ │ │ ├── tag_detail_page.dart
│ │ │ ├── tag_form_page.dart
│ │ │ ├── tags_page.dart
│ │ │ └── widgets/
│ │ │ └── tag_form/
│ │ │ ├── expenseable_input.dart
│ │ │ ├── incomeable_input.dart
│ │ │ ├── name_input.dart
│ │ │ ├── notes_input.dart
│ │ │ ├── parent_input.dart
│ │ │ └── transferable_input.dart
│ │ └── themes.dart
│ ├── linux/
│ │ ├── .gitignore
│ │ ├── CMakeLists.txt
│ │ ├── flutter/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── generated_plugin_registrant.cc
│ │ │ ├── generated_plugin_registrant.h
│ │ │ └── generated_plugins.cmake
│ │ ├── main.cc
│ │ ├── my_application.cc
│ │ └── my_application.h
│ ├── macos/
│ │ ├── .gitignore
│ │ ├── Flutter/
│ │ │ ├── Flutter-Debug.xcconfig
│ │ │ ├── Flutter-Release.xcconfig
│ │ │ └── GeneratedPluginRegistrant.swift
│ │ ├── Podfile
│ │ ├── Runner/
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Assets.xcassets/
│ │ │ │ └── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Base.lproj/
│ │ │ │ └── MainMenu.xib
│ │ │ ├── Configs/
│ │ │ │ ├── AppInfo.xcconfig
│ │ │ │ ├── Debug.xcconfig
│ │ │ │ ├── Release.xcconfig
│ │ │ │ └── Warnings.xcconfig
│ │ │ ├── DebugProfile.entitlements
│ │ │ ├── Info.plist
│ │ │ ├── MainFlutterWindow.swift
│ │ │ └── Release.entitlements
│ │ ├── Runner.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ └── xcshareddata/
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ └── xcshareddata/
│ │ │ └── xcschemes/
│ │ │ └── Runner.xcscheme
│ │ └── Runner.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ ├── pubspec.yaml
│ ├── test/
│ │ └── widget_test.dart
│ └── windows/
│ ├── .gitignore
│ ├── CMakeLists.txt
│ ├── flutter/
│ │ ├── CMakeLists.txt
│ │ ├── generated_plugin_registrant.cc
│ │ ├── generated_plugin_registrant.h
│ │ └── generated_plugins.cmake
│ └── runner/
│ ├── CMakeLists.txt
│ ├── Runner.rc
│ ├── flutter_window.cpp
│ ├── flutter_window.h
│ ├── main.cpp
│ ├── resource.h
│ ├── runner.exe.manifest
│ ├── utils.cpp
│ ├── utils.h
│ ├── win32_window.cpp
│ └── win32_window.h
├── bookkeeping_user_uniapp/
│ ├── App.vue
│ ├── components/
│ │ ├── accounts/
│ │ │ └── accounts.vue
│ │ ├── expense-form/
│ │ │ └── expense-form.vue
│ │ ├── flows/
│ │ │ └── flows.vue
│ │ ├── income-form/
│ │ │ └── income-form.vue
│ │ ├── tab-bar/
│ │ │ └── tab-bar.vue
│ │ └── transfer-form/
│ │ └── transfer-form.vue
│ ├── config/
│ │ ├── api.js
│ │ └── request.js
│ ├── index.html
│ ├── main.js
│ ├── manifest.json
│ ├── package.json
│ ├── pages/
│ │ ├── account-detail/
│ │ │ └── account-detail.vue
│ │ ├── accounts/
│ │ │ └── accounts.vue
│ │ ├── books/
│ │ │ └── books.vue
│ │ ├── charts/
│ │ │ └── charts.vue
│ │ ├── flow-detail/
│ │ │ └── flow-detail.vue
│ │ ├── flow-form/
│ │ │ └── flow-form.vue
│ │ ├── flows/
│ │ │ └── flows.vue
│ │ ├── index/
│ │ │ └── index.vue
│ │ ├── login/
│ │ │ └── login.vue
│ │ ├── my/
│ │ │ └── my.vue
│ │ ├── select/
│ │ │ └── select.vue
│ │ └── test/
│ │ └── test.vue
│ ├── pages.json
│ ├── store/
│ │ ├── index.js
│ │ └── modules/
│ │ ├── account.js
│ │ ├── expenseCategory.js
│ │ ├── incomeCategory.js
│ │ ├── modelForm.js
│ │ ├── payee.js
│ │ ├── select.js
│ │ ├── session.js
│ │ └── tag.js
│ ├── uni.scss
│ └── uni_modules/
│ ├── qiun-data-charts/
│ │ ├── changelog.md
│ │ ├── components/
│ │ │ ├── qiun-data-charts/
│ │ │ │ └── qiun-data-charts.vue
│ │ │ ├── qiun-error/
│ │ │ │ └── qiun-error.vue
│ │ │ └── qiun-loading/
│ │ │ ├── loading1.vue
│ │ │ ├── loading2.vue
│ │ │ ├── loading3.vue
│ │ │ ├── loading4.vue
│ │ │ ├── loading5.vue
│ │ │ └── qiun-loading.vue
│ │ ├── js_sdk/
│ │ │ └── u-charts/
│ │ │ ├── config-echarts.js
│ │ │ ├── config-ucharts.js
│ │ │ ├── readme.md
│ │ │ └── u-charts.js
│ │ ├── license.md
│ │ ├── package.json
│ │ └── readme.md
│ ├── uni-datetime-picker/
│ │ ├── changelog.md
│ │ ├── components/
│ │ │ └── uni-datetime-picker/
│ │ │ ├── calendar-item.vue
│ │ │ ├── calendar.vue
│ │ │ ├── i18n/
│ │ │ │ ├── en.json
│ │ │ │ ├── index.js
│ │ │ │ ├── zh-Hans.json
│ │ │ │ └── zh-Hant.json
│ │ │ ├── keypress.js
│ │ │ ├── time-picker.vue
│ │ │ ├── uni-datetime-picker.vue
│ │ │ └── util.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── uni-icons/
│ │ ├── changelog.md
│ │ ├── components/
│ │ │ └── uni-icons/
│ │ │ ├── icons.js
│ │ │ ├── uni-icons.vue
│ │ │ └── uniicons.css
│ │ ├── package.json
│ │ └── readme.md
│ ├── uni-list/
│ │ ├── changelog.md
│ │ ├── components/
│ │ │ ├── uni-list/
│ │ │ │ ├── uni-list.vue
│ │ │ │ ├── uni-refresh.vue
│ │ │ │ └── uni-refresh.wxs
│ │ │ ├── uni-list-ad/
│ │ │ │ └── uni-list-ad.vue
│ │ │ ├── uni-list-chat/
│ │ │ │ ├── uni-list-chat.scss
│ │ │ │ └── uni-list-chat.vue
│ │ │ └── uni-list-item/
│ │ │ └── uni-list-item.vue
│ │ ├── package.json
│ │ └── readme.md
│ └── uview-ui/
│ ├── LICENSE
│ ├── README.md
│ ├── changelog.md
│ ├── components/
│ │ ├── u--form/
│ │ │ └── u--form.vue
│ │ ├── u--image/
│ │ │ └── u--image.vue
│ │ ├── u--input/
│ │ │ └── u--input.vue
│ │ ├── u--text/
│ │ │ └── u--text.vue
│ │ ├── u--textarea/
│ │ │ └── u--textarea.vue
│ │ ├── u-action-sheet/
│ │ │ ├── props.js
│ │ │ └── u-action-sheet.vue
│ │ ├── u-album/
│ │ │ ├── props.js
│ │ │ └── u-album.vue
│ │ ├── u-alert/
│ │ │ ├── props.js
│ │ │ └── u-alert.vue
│ │ ├── u-avatar/
│ │ │ ├── props.js
│ │ │ └── u-avatar.vue
│ │ ├── u-avatar-group/
│ │ │ ├── props.js
│ │ │ └── u-avatar-group.vue
│ │ ├── u-back-top/
│ │ │ ├── props.js
│ │ │ └── u-back-top.vue
│ │ ├── u-badge/
│ │ │ ├── props.js
│ │ │ └── u-badge.vue
│ │ ├── u-button/
│ │ │ ├── nvue.scss
│ │ │ ├── props.js
│ │ │ ├── u-button.vue
│ │ │ └── vue.scss
│ │ ├── u-calendar/
│ │ │ ├── header.vue
│ │ │ ├── month.vue
│ │ │ ├── props.js
│ │ │ ├── u-calendar.vue
│ │ │ └── util.js
│ │ ├── u-car-keyboard/
│ │ │ ├── props.js
│ │ │ └── u-car-keyboard.vue
│ │ ├── u-cell/
│ │ │ ├── props.js
│ │ │ └── u-cell.vue
│ │ ├── u-cell-group/
│ │ │ ├── props.js
│ │ │ └── u-cell-group.vue
│ │ ├── u-checkbox/
│ │ │ ├── props.js
│ │ │ └── u-checkbox.vue
│ │ ├── u-checkbox-group/
│ │ │ ├── props.js
│ │ │ └── u-checkbox-group.vue
│ │ ├── u-circle-progress/
│ │ │ ├── props.js
│ │ │ └── u-circle-progress.vue
│ │ ├── u-code/
│ │ │ ├── props.js
│ │ │ └── u-code.vue
│ │ ├── u-code-input/
│ │ │ ├── props.js
│ │ │ └── u-code-input.vue
│ │ ├── u-col/
│ │ │ ├── props.js
│ │ │ └── u-col.vue
│ │ ├── u-collapse/
│ │ │ ├── props.js
│ │ │ └── u-collapse.vue
│ │ ├── u-collapse-item/
│ │ │ ├── props.js
│ │ │ └── u-collapse-item.vue
│ │ ├── u-column-notice/
│ │ │ ├── props.js
│ │ │ └── u-column-notice.vue
│ │ ├── u-count-down/
│ │ │ ├── props.js
│ │ │ ├── u-count-down.vue
│ │ │ └── utils.js
│ │ ├── u-count-to/
│ │ │ ├── props.js
│ │ │ └── u-count-to.vue
│ │ ├── u-datetime-picker/
│ │ │ ├── props.js
│ │ │ └── u-datetime-picker.vue
│ │ ├── u-divider/
│ │ │ ├── props.js
│ │ │ └── u-divider.vue
│ │ ├── u-dropdown/
│ │ │ ├── props.js
│ │ │ └── u-dropdown.vue
│ │ ├── u-dropdown-item/
│ │ │ ├── props.js
│ │ │ └── u-dropdown-item.vue
│ │ ├── u-empty/
│ │ │ ├── props.js
│ │ │ └── u-empty.vue
│ │ ├── u-form/
│ │ │ ├── props.js
│ │ │ └── u-form.vue
│ │ ├── u-form-item/
│ │ │ ├── props.js
│ │ │ └── u-form-item.vue
│ │ ├── u-gap/
│ │ │ ├── props.js
│ │ │ └── u-gap.vue
│ │ ├── u-grid/
│ │ │ ├── props.js
│ │ │ └── u-grid.vue
│ │ ├── u-grid-item/
│ │ │ ├── props.js
│ │ │ └── u-grid-item.vue
│ │ ├── u-icon/
│ │ │ ├── icons.js
│ │ │ ├── props.js
│ │ │ └── u-icon.vue
│ │ ├── u-image/
│ │ │ ├── props.js
│ │ │ └── u-image.vue
│ │ ├── u-index-anchor/
│ │ │ ├── props.js
│ │ │ └── u-index-anchor.vue
│ │ ├── u-index-item/
│ │ │ ├── props.js
│ │ │ └── u-index-item.vue
│ │ ├── u-index-list/
│ │ │ ├── props.js
│ │ │ └── u-index-list.vue
│ │ ├── u-input/
│ │ │ ├── props.js
│ │ │ └── u-input.vue
│ │ ├── u-keyboard/
│ │ │ ├── props.js
│ │ │ └── u-keyboard.vue
│ │ ├── u-line/
│ │ │ ├── props.js
│ │ │ └── u-line.vue
│ │ ├── u-line-progress/
│ │ │ ├── props.js
│ │ │ └── u-line-progress.vue
│ │ ├── u-link/
│ │ │ ├── props.js
│ │ │ └── u-link.vue
│ │ ├── u-list/
│ │ │ ├── props.js
│ │ │ └── u-list.vue
│ │ ├── u-list-item/
│ │ │ ├── props.js
│ │ │ └── u-list-item.vue
│ │ ├── u-loading-icon/
│ │ │ ├── props.js
│ │ │ └── u-loading-icon.vue
│ │ ├── u-loading-page/
│ │ │ ├── props.js
│ │ │ └── u-loading-page.vue
│ │ ├── u-loadmore/
│ │ │ ├── props.js
│ │ │ └── u-loadmore.vue
│ │ ├── u-modal/
│ │ │ ├── props.js
│ │ │ └── u-modal.vue
│ │ ├── u-navbar/
│ │ │ ├── props.js
│ │ │ └── u-navbar.vue
│ │ ├── u-no-network/
│ │ │ ├── props.js
│ │ │ └── u-no-network.vue
│ │ ├── u-notice-bar/
│ │ │ ├── props.js
│ │ │ └── u-notice-bar.vue
│ │ ├── u-notify/
│ │ │ ├── props.js
│ │ │ └── u-notify.vue
│ │ ├── u-number-box/
│ │ │ ├── props.js
│ │ │ └── u-number-box.vue
│ │ ├── u-number-keyboard/
│ │ │ ├── props.js
│ │ │ └── u-number-keyboard.vue
│ │ ├── u-overlay/
│ │ │ ├── props.js
│ │ │ └── u-overlay.vue
│ │ ├── u-parse/
│ │ │ ├── node/
│ │ │ │ └── node.vue
│ │ │ ├── parser.js
│ │ │ ├── props.js
│ │ │ └── u-parse.vue
│ │ ├── u-picker/
│ │ │ ├── props.js
│ │ │ └── u-picker.vue
│ │ ├── u-picker-column/
│ │ │ ├── props.js
│ │ │ └── u-picker-column.vue
│ │ ├── u-popup/
│ │ │ ├── props.js
│ │ │ └── u-popup.vue
│ │ ├── u-radio/
│ │ │ ├── props.js
│ │ │ └── u-radio.vue
│ │ ├── u-radio-group/
│ │ │ ├── props.js
│ │ │ └── u-radio-group.vue
│ │ ├── u-rate/
│ │ │ ├── props.js
│ │ │ └── u-rate.vue
│ │ ├── u-read-more/
│ │ │ ├── props.js
│ │ │ └── u-read-more.vue
│ │ ├── u-row/
│ │ │ ├── props.js
│ │ │ └── u-row.vue
│ │ ├── u-row-notice/
│ │ │ ├── props.js
│ │ │ └── u-row-notice.vue
│ │ ├── u-safe-bottom/
│ │ │ ├── props.js
│ │ │ └── u-safe-bottom.vue
│ │ ├── u-scroll-list/
│ │ │ ├── nvue.js
│ │ │ ├── other.js
│ │ │ ├── props.js
│ │ │ ├── scrollWxs.wxs
│ │ │ └── u-scroll-list.vue
│ │ ├── u-search/
│ │ │ ├── props.js
│ │ │ └── u-search.vue
│ │ ├── u-skeleton/
│ │ │ ├── props.js
│ │ │ └── u-skeleton.vue
│ │ ├── u-slider/
│ │ │ ├── mpother.js
│ │ │ ├── mpwxs.js
│ │ │ ├── mpwxs.wxs
│ │ │ ├── nvue - 副本.js
│ │ │ ├── nvue.js
│ │ │ ├── props.js
│ │ │ └── u-slider.vue
│ │ ├── u-status-bar/
│ │ │ ├── props.js
│ │ │ └── u-status-bar.vue
│ │ ├── u-steps/
│ │ │ ├── props.js
│ │ │ └── u-steps.vue
│ │ ├── u-steps-item/
│ │ │ ├── props.js
│ │ │ └── u-steps-item.vue
│ │ ├── u-sticky/
│ │ │ ├── props.js
│ │ │ └── u-sticky.vue
│ │ ├── u-subsection/
│ │ │ ├── props.js
│ │ │ └── u-subsection.vue
│ │ ├── u-swipe-action/
│ │ │ ├── props.js
│ │ │ └── u-swipe-action.vue
│ │ ├── u-swipe-action-item/
│ │ │ ├── index - backup.wxs
│ │ │ ├── index.wxs
│ │ │ ├── nvue - backup.js
│ │ │ ├── nvue.js
│ │ │ ├── props.js
│ │ │ ├── u-swipe-action-item.vue
│ │ │ └── wxs.js
│ │ ├── u-swiper/
│ │ │ ├── props.js
│ │ │ └── u-swiper.vue
│ │ ├── u-swiper-indicator/
│ │ │ ├── props.js
│ │ │ └── u-swiper-indicator.vue
│ │ ├── u-switch/
│ │ │ ├── props.js
│ │ │ └── u-switch.vue
│ │ ├── u-tabbar/
│ │ │ ├── props.js
│ │ │ └── u-tabbar.vue
│ │ ├── u-tabbar-item/
│ │ │ ├── props.js
│ │ │ └── u-tabbar-item.vue
│ │ ├── u-table/
│ │ │ ├── props.js
│ │ │ └── u-table.vue
│ │ ├── u-tabs/
│ │ │ ├── props.js
│ │ │ └── u-tabs.vue
│ │ ├── u-tabs-item/
│ │ │ ├── props.js
│ │ │ └── u-tabs-item.vue
│ │ ├── u-tag/
│ │ │ ├── props.js
│ │ │ └── u-tag.vue
│ │ ├── u-td/
│ │ │ ├── props.js
│ │ │ └── u-td.vue
│ │ ├── u-text/
│ │ │ ├── props.js
│ │ │ ├── u-text.vue
│ │ │ └── value.js
│ │ ├── u-textarea/
│ │ │ ├── props.js
│ │ │ └── u-textarea.vue
│ │ ├── u-toast/
│ │ │ └── u-toast.vue
│ │ ├── u-toolbar/
│ │ │ ├── props.js
│ │ │ └── u-toolbar.vue
│ │ ├── u-tooltip/
│ │ │ ├── props.js
│ │ │ └── u-tooltip.vue
│ │ ├── u-tr/
│ │ │ ├── props.js
│ │ │ └── u-tr.vue
│ │ ├── u-transition/
│ │ │ ├── nvue.ani-map.js
│ │ │ ├── props.js
│ │ │ ├── transition.js
│ │ │ ├── u-transition.vue
│ │ │ └── vue.ani-style.scss
│ │ ├── u-upload/
│ │ │ ├── mixin.js
│ │ │ ├── props.js
│ │ │ ├── u-upload.vue
│ │ │ └── utils.js
│ │ └── uview-ui/
│ │ └── uview-ui.vue
│ ├── index.js
│ ├── index.scss
│ ├── libs/
│ │ ├── config/
│ │ │ ├── color.js
│ │ │ ├── config.js
│ │ │ ├── props/
│ │ │ │ ├── actionSheet.js
│ │ │ │ ├── album.js
│ │ │ │ ├── alert.js
│ │ │ │ ├── avatar.js
│ │ │ │ ├── avatarGroup.js
│ │ │ │ ├── backtop.js
│ │ │ │ ├── badge.js
│ │ │ │ ├── button.js
│ │ │ │ ├── calendar.js
│ │ │ │ ├── carKeyboard.js
│ │ │ │ ├── cell.js
│ │ │ │ ├── cellGroup.js
│ │ │ │ ├── checkbox.js
│ │ │ │ ├── checkboxGroup.js
│ │ │ │ ├── circleProgress.js
│ │ │ │ ├── code.js
│ │ │ │ ├── codeInput.js
│ │ │ │ ├── col.js
│ │ │ │ ├── collapse.js
│ │ │ │ ├── collapseItem.js
│ │ │ │ ├── columnNotice.js
│ │ │ │ ├── countDown.js
│ │ │ │ ├── countTo.js
│ │ │ │ ├── datetimePicker.js
│ │ │ │ ├── divider.js
│ │ │ │ ├── empty.js
│ │ │ │ ├── form.js
│ │ │ │ ├── formItem.js
│ │ │ │ ├── gap.js
│ │ │ │ ├── grid.js
│ │ │ │ ├── gridItem.js
│ │ │ │ ├── icon.js
│ │ │ │ ├── image.js
│ │ │ │ ├── indexAnchor.js
│ │ │ │ ├── indexList.js
│ │ │ │ ├── input.js
│ │ │ │ ├── keyboard.js
│ │ │ │ ├── line.js
│ │ │ │ ├── lineProgress.js
│ │ │ │ ├── link.js
│ │ │ │ ├── list.js
│ │ │ │ ├── listItem.js
│ │ │ │ ├── loadingIcon.js
│ │ │ │ ├── loadingPage.js
│ │ │ │ ├── loadmore.js
│ │ │ │ ├── modal.js
│ │ │ │ ├── navbar.js
│ │ │ │ ├── noNetwork.js
│ │ │ │ ├── noticeBar.js
│ │ │ │ ├── notify.js
│ │ │ │ ├── numberBox.js
│ │ │ │ ├── numberKeyboard.js
│ │ │ │ ├── overlay.js
│ │ │ │ ├── parse.js
│ │ │ │ ├── picker.js
│ │ │ │ ├── popup.js
│ │ │ │ ├── radio.js
│ │ │ │ ├── radioGroup.js
│ │ │ │ ├── rate.js
│ │ │ │ ├── readMore.js
│ │ │ │ ├── row.js
│ │ │ │ ├── rowNotice.js
│ │ │ │ ├── scrollList.js
│ │ │ │ ├── search.js
│ │ │ │ ├── section.js
│ │ │ │ ├── skeleton.js
│ │ │ │ ├── slider.js
│ │ │ │ ├── statusBar.js
│ │ │ │ ├── steps.js
│ │ │ │ ├── stepsItem.js
│ │ │ │ ├── sticky.js
│ │ │ │ ├── subsection.js
│ │ │ │ ├── swipeAction.js
│ │ │ │ ├── swipeActionItem.js
│ │ │ │ ├── swiper.js
│ │ │ │ ├── swipterIndicator.js
│ │ │ │ ├── switch.js
│ │ │ │ ├── tabbar.js
│ │ │ │ ├── tabbarItem.js
│ │ │ │ ├── tabs.js
│ │ │ │ ├── tag.js
│ │ │ │ ├── text.js
│ │ │ │ ├── textarea.js
│ │ │ │ ├── toast.js
│ │ │ │ ├── toolbar.js
│ │ │ │ ├── tooltip.js
│ │ │ │ ├── transition.js
│ │ │ │ └── upload.js
│ │ │ ├── props.js
│ │ │ └── zIndex.js
│ │ ├── css/
│ │ │ ├── color.scss
│ │ │ ├── common.scss
│ │ │ ├── components.scss
│ │ │ ├── flex.scss
│ │ │ ├── h5.scss
│ │ │ ├── mixin.scss
│ │ │ ├── mp.scss
│ │ │ ├── nvue.scss
│ │ │ └── vue.scss
│ │ ├── function/
│ │ │ ├── colorGradient.js
│ │ │ ├── debounce.js
│ │ │ ├── digit.js
│ │ │ ├── index.js
│ │ │ ├── platform.js
│ │ │ ├── test.js
│ │ │ └── throttle.js
│ │ ├── luch-request/
│ │ │ ├── adapters/
│ │ │ │ └── index.js
│ │ │ ├── core/
│ │ │ │ ├── InterceptorManager.js
│ │ │ │ ├── Request.js
│ │ │ │ ├── buildFullPath.js
│ │ │ │ ├── defaults.js
│ │ │ │ ├── dispatchRequest.js
│ │ │ │ ├── mergeConfig.js
│ │ │ │ └── settle.js
│ │ │ ├── helpers/
│ │ │ │ ├── buildURL.js
│ │ │ │ ├── combineURLs.js
│ │ │ │ └── isAbsoluteURL.js
│ │ │ ├── index.d.ts
│ │ │ ├── index.js
│ │ │ ├── utils/
│ │ │ │ └── clone.js
│ │ │ └── utils.js
│ │ ├── mixin/
│ │ │ ├── button.js
│ │ │ ├── mixin.js
│ │ │ ├── mpMixin.js
│ │ │ ├── mpShare.js
│ │ │ ├── openType.js
│ │ │ ├── style.js
│ │ │ └── touch.js
│ │ └── util/
│ │ ├── async-validator.js
│ │ ├── calendar.js
│ │ ├── dayjs.js
│ │ ├── emitter.js
│ │ └── route.js
│ ├── package.json
│ └── theme.scss
├── docker/
│ └── mysql/
│ ├── bookkeeping.sql
│ ├── config/
│ │ └── my.cnf
│ └── currency.sql
├── docker-compose.yaml
├── docker-compose2.yaml
└── notes.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
# https://github.com/alexkaratarakis/gitattributes
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
* text=auto
#
# The above will handle all files NOT found below
#
# These files are text and should be normalized (Convert crlf => lf)
*.css text
*.df text
*.htm text
*.html text
*.java text
*.js text
*.json text
*.jsp text
*.jspf text
*.jspx text
*.properties text
*.sh text
*.tld text
*.txt text
*.tag text
*.tagx text
*.xml text
*.yml text
# These files are binary and should be left untouched
# (binary is a macro for -text -diff)
*.class binary
*.dll binary
*.ear binary
*.gif binary
*.ico binary
*.jar binary
*.jpg binary
*.jpeg binary
*.png binary
*.so binary
*.war binary
================================================
FILE: .gitignore
================================================
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
nbproject/private/
nbbuild/
dist/
nbdist/
.nb-gradle/
build/
### VS Code ###
.vscode/
.gradle/
bin/
.DS_Store
build/
.apt_generated_tests/
# dependencies
node_modules
npm-debug.log*
yarn-error.log
# umi
.umi
.umi-production
#dist
dist
.hbuilderx
unpackage
.env
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2022] [feiyu-rs]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# DEPRECATED
This is no longer supported, please consider using [https://github.com/getmoneynote/moneynote-api](https://github.com/getmoneynote/moneynote-api) instead.
================================================
FILE: bookkeeping-user-api/.mvn/wrapper/MavenWrapperDownloader.java
================================================
/*
* Copyright 2007-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.net.*;
import java.io.*;
import java.nio.channels.*;
import java.util.Properties;
public class MavenWrapperDownloader {
private static final String WRAPPER_VERSION = "0.5.6";
/**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
/**
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
* use instead of the default one.
*/
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
".mvn/wrapper/maven-wrapper.properties";
/**
* Path where the maven-wrapper.jar will be saved to.
*/
private static final String MAVEN_WRAPPER_JAR_PATH =
".mvn/wrapper/maven-wrapper.jar";
/**
* Name of the property which should be used to override the default download url for the wrapper.
*/
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
public static void main(String args[]) {
System.out.println("- Downloader started");
File baseDirectory = new File(args[0]);
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
// If the maven-wrapper.properties exists, read it and check if it contains a custom
// wrapperUrl parameter.
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
String url = DEFAULT_DOWNLOAD_URL;
if(mavenWrapperPropertyFile.exists()) {
FileInputStream mavenWrapperPropertyFileInputStream = null;
try {
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
Properties mavenWrapperProperties = new Properties();
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
} catch (IOException e) {
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
} finally {
try {
if(mavenWrapperPropertyFileInputStream != null) {
mavenWrapperPropertyFileInputStream.close();
}
} catch (IOException e) {
// Ignore ...
}
}
}
System.out.println("- Downloading from: " + url);
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
if(!outputFile.getParentFile().exists()) {
if(!outputFile.getParentFile().mkdirs()) {
System.out.println(
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
}
}
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
try {
downloadFileFromURL(url, outputFile);
System.out.println("Done");
System.exit(0);
} catch (Throwable e) {
System.out.println("- Error downloading");
e.printStackTrace();
System.exit(1);
}
}
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
String username = System.getenv("MVNW_USERNAME");
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
}
URL website = new URL(urlString);
ReadableByteChannel rbc;
rbc = Channels.newChannel(website.openStream());
FileOutputStream fos = new FileOutputStream(destination);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
fos.close();
rbc.close();
}
}
================================================
FILE: bookkeeping-user-api/.mvn/wrapper/maven-wrapper.properties
================================================
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
================================================
FILE: bookkeeping-user-api/Dockerfile
================================================
#FROM openjdk:11-slim
#COPY target/*.jar app.jar
#ENTRYPOINT ["java","-jar","/app.jar"]
FROM openjdk:11-slim as build
WORKDIR /workspace/app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN ./mvnw -B -DskipTests clean package
FROM openjdk:11-slim
VOLUME /tmp
COPY --from=build /workspace/app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
================================================
FILE: bookkeeping-user-api/mvnw
================================================
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
else
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
fi
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if $cygwin; then
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
fi
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget "$jarUrl" -O "$wrapperJarPath"
else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl -o "$wrapperJarPath" "$jarUrl" -f
else
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaClass=`cygpath --path --windows "$javaClass"`
fi
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
================================================
FILE: bookkeeping-user-api/mvnw.cmd
================================================
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%
================================================
FILE: bookkeeping-user-api/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.6.0
com.jiukuaitech
bookkeeping-user
0.1
bookkeeping-user
Bookkeeping API for User
jar
UTF-8
11
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-validation
mysql
mysql-connector-java
com.qiniu
qiniu-java-sdk
[7.7.0, 7.7.99]
org.projectlombok
lombok
1.18.20
provided
org.hibernate
hibernate-jpamodelgen
5.5.6.Final
provided
org.springframework.boot
spring-boot-devtools
true
runtime
org.springframework.boot
spring-boot-maven-plugin
maven-compiler-plugin
org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor
================================================
FILE: bookkeeping-user-api/restart.sh
================================================
#!/bin/bash
. stop.sh
rm -rf 1.log
. startup.sh
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/Application.java
================================================
package com.jiukuaitech.bookkeeping.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/RegexTest.java
================================================
package com.jiukuaitech.bookkeeping.user;
public class RegexTest implements Runnable {
private static int i;
public synchronized static void change() {
for (int j = 0; j < 5000000; j++) {
i++;
}
}
@Override
public void run() {
synchronized ("123".intern()) {
for (int j = 0; j < 5000000; j++) {
i++;
}
}
}
public static void main(String[] args) throws Exception {
new Thread(new RegexTest()).start();
new Thread(new RegexTest()).start();
Thread.sleep(3000);
System.out.println(i);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/Account.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import com.jiukuaitech.bookkeeping.user.base.NameNotesEnableEntity;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.validation.BalanceValidator;
import com.jiukuaitech.bookkeeping.user.validation.NoValidator;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Entity
@Table(name = "t_account", uniqueConstraints = {@UniqueConstraint(columnNames = {"group_id", "name"})})
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.INTEGER, columnDefinition = "TINYINT(1)")
@Getter
@Setter
public class Account extends NameNotesEnableEntity {
@Column(insertable = false, updatable = false)
private Integer type; //1活期,2信用,3贷款,4资产
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "group_id")
@NotNull
private Group group; // 账簿必须属于某个组
@Column(length = 32)
@NoValidator
private String no; //卡号
@Column(nullable = false) //最多9亿
@NotNull
@BalanceValidator
private BigDecimal balance; // 当前余额
@Column(nullable = false)
@NotNull
private Boolean include = true; //净资产是否包含
@Column(nullable = false)
@NotNull
private Boolean expenseable = true; //是否可支出
@Column(nullable = false)
@NotNull
private Boolean incomeable = true; //是否可收入
@Column(nullable = false)
@NotNull
private Boolean transferFromAble = true; //是否转账可转出
@Column(nullable = false)
@NotNull
private Boolean transferToAble = true; //是否转账可转入
@Column(nullable = false, length = 8)
@NotNull
private String currencyCode;
@Column
@BalanceValidator
private BigDecimal initialBalance; // 期初余额
public Account() { }
public Account(Integer id) {
super.setId(id);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountAddRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import java.math.BigDecimal;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import com.jiukuaitech.bookkeeping.user.validation.BalanceValidator;
import com.jiukuaitech.bookkeeping.user.validation.NameValidator;
import com.jiukuaitech.bookkeeping.user.validation.NoValidator;
import com.jiukuaitech.bookkeeping.user.validation.NotesValidator;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AccountAddRequest {
@NotBlank(message="name must not be blank")
@NameValidator
private String name;
@NoValidator
private String no;
@NotNull
@BalanceValidator
private BigDecimal balance;
private Boolean include = true;
private Boolean transferFromAble = true;
private Boolean transferToAble = true;
private Boolean expenseable = true;
private Boolean incomeable = true;
@NotesValidator
private String notes;
@NotBlank
private String currencyCode;
public void copyPrimitive(Account po) {
po.setName(name);
po.setNo(no);
po.setBalance(balance);
po.setInitialBalance(balance);
po.setInclude(include);
po.setTransferFromAble(transferFromAble);
po.setTransferToAble(transferToAble);
po.setExpenseable(expenseable);
po.setIncomeable(incomeable);
po.setNotes(notes);
po.setCurrencyCode(currencyCode);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountAdjustBalanceNotValidException.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
public class AccountAdjustBalanceNotValidException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountController.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceAddRequest;
import com.jiukuaitech.bookkeeping.user.base.BaseController;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/accounts")
public class AccountController extends BaseController {
@Resource
private AccountService accountService;
// 搜索流水的下拉框需要显示所有可用的account
@RequestMapping(method = RequestMethod.GET, value = "/enable")
public BaseResponse handleEnable(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.getEnable(userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/expenseable")
public BaseResponse handleExpenseble(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.getAllExpenseable(userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/incomeable")
public BaseResponse handleIncomeble(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.getAllIncomeable(userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/transfer-from-able")
public BaseResponse handleTransferFromAble(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.getAllTransferFromAble(userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/transfer-to-able")
public BaseResponse handleTransferToAble(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.getAllTransferToAble(userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.get(id, userSignInId));
}
@RequestMapping(method = RequestMethod.POST, value = "/{id}/adjust-balance")
public BaseResponse handleAdjustBalance(
@PathVariable("id") Integer id,
@Valid @RequestBody AdjustBalanceAddRequest request,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.adjustBalance(id, request, userSignInId));
}
@RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.remove(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggle")
public BaseResponse handleToggle(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.toggle(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleInclude")
public BaseResponse handleToggleInclude(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.toggleInclude(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleExpenseable")
public BaseResponse handleToggleExpenseable(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.toggleExpenseable(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleIncomeable")
public BaseResponse handleToggleIncomeable(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.toggleIncomeable(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleTransferFromAble")
public BaseResponse handleToggleTransferFromAble(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.toggleTransferFromAble(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggleTransferToAble")
public BaseResponse handleToggleTransferToAble(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.toggleTransferToAble(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}")
public BaseResponse handleUpdate(
@PathVariable("id") Integer id,
@Valid @RequestBody AccountUpdateRequest request,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.update(id, request, userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountExceptionHandler.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.ErrorResponse;
import org.springframework.context.MessageSource;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.annotation.Resource;
import java.util.Locale;
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class AccountExceptionHandler {
private static final Locale LANG = Locale.CHINA;
@Resource
private MessageSource messageSource;
@ExceptionHandler(value = AccountNameExistsException.class)
// @ResponseStatus(HttpStatus.CONFLICT)
@ResponseBody
public BaseResponse handleException(AccountNameExistsException e) {
return new ErrorResponse(409, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = AccountMaxCountException.class)
@ResponseBody
public BaseResponse handleException(AccountMaxCountException e) {
return new ErrorResponse(501, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = AccountHasTransactionException.class)
@ResponseBody
public BaseResponse handleException(AccountHasTransactionException e) {
return new ErrorResponse(601, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = AccountAdjustBalanceNotValidException.class)
@ResponseBody
public BaseResponse handleException(AccountAdjustBalanceNotValidException e) {
return new ErrorResponse(602, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = DefaultExpenseAccountException.class)
@ResponseBody
public BaseResponse handleException(DefaultExpenseAccountException e) {
return new ErrorResponse(603, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = DefaultIncomeAccountException.class)
@ResponseBody
public BaseResponse handleException(DefaultIncomeAccountException e) {
return new ErrorResponse(604, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = DefaultTransferFromAccountException.class)
@ResponseBody
public BaseResponse handleException(DefaultTransferFromAccountException e) {
return new ErrorResponse(605, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = DefaultTransferToAccountException.class)
@ResponseBody
public BaseResponse handleException(DefaultTransferToAccountException e) {
return new ErrorResponse(606, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountHasTransactionException.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
public class AccountHasTransactionException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountMaxCountException.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
public class AccountMaxCountException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountNameExistsException.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
public class AccountNameExistsException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountQueryRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AccountQueryRequest {
private Boolean enable;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import com.jiukuaitech.bookkeeping.user.base.BaseRepository;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.user.User;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface AccountRepository extends BaseRepository {
Optional findOneByGroupAndName(Group group, String name);
Optional findOneByGroupAndId(Group group, Integer id);
long countByGroup(Group group);
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountService.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalance;
import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceAddRequest;
import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceRepository;
import com.jiukuaitech.bookkeeping.user.asset_account.AssetAccount;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.checking_account.CheckingAccount;
import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccount;
import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccount;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.user.User;
import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccountAddRequest;
import com.jiukuaitech.bookkeeping.user.currency.CurrencyService;
import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccountAddRequest;
import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException;
import com.jiukuaitech.bookkeeping.user.transaction.TransactionRepository;
import com.jiukuaitech.bookkeeping.user.transfer.TransferRepository;
import com.jiukuaitech.bookkeeping.user.user.UserService;
import com.jiukuaitech.bookkeeping.user.user_log.UserActionLog;
import com.jiukuaitech.bookkeeping.user.user_log.UserActionLogRepository;
import com.jiukuaitech.bookkeeping.user.user_log.UserActionLogService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class AccountService {
@Resource
private UserService userService;
@Resource
private AccountRepository accountRepository;
@Resource
private AdjustBalanceRepository adjustBalanceRepository;
@Resource
private TransactionRepository transactionRepository;
@Resource
private TransferRepository transferRepository;
@Resource
private UserActionLogRepository userActionLogRepository;
@Resource
private CurrencyService currencyService;
@Resource
private UserActionLogService userActionLogService;
@Value("${account.max.count}")
private Integer accountMaxCount;
public List getEnable(Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
List entityList = accountRepository.findAll(AccountSpec.inGroupAndEnable(group));
return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList());
}
public List getAllExpenseable(Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
List entityList = accountRepository.findAll(AccountSpec.inGroupAndExpenseable(group));
return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList());
}
public List getAllIncomeable(Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
List entityList = accountRepository.findAll(AccountSpec.inGroupAndIncomeable(group));
return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList());
}
public List getAllTransferFromAble(Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
List entityList = accountRepository.findAll(AccountSpec.inGroupAndTransferFromAble(group));
return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList());
}
public List getAllTransferToAble(Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
List entityList = accountRepository.findAll(AccountSpec.inGroupAndTransferToAble(group));
return entityList.stream().map(AccountVOForExtend::fromEntity).collect(Collectors.toList());
}
public AccountVOForList get(Integer id, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
AccountVOForList vo = new AccountVOForList();
vo.setValue(po);
vo.setConvertedBalance(currencyService.convert(vo.getBalance(), vo.getCurrencyCode(), group.getDefaultCurrencyCode()));
if (po instanceof CreditAccount) {
CreditAccount po2 = (CreditAccount) po;
vo.setLimit(po2.getLimit());
vo.setBillDay(po2.getBillDay());
}
if (po instanceof DebtAccount) {
DebtAccount po2 = (DebtAccount) po;
vo.setApr(po2.getApr());
vo.setLimit(po2.getLimit());
}
if (po instanceof AssetAccount) {
AssetAccount po2 = (AssetAccount) po;
vo.setAsOfDate(po2.getAsOfDate());
}
return vo;
}
@Transactional
public AccountVOForExtend adjustBalance(Integer id, AdjustBalanceAddRequest request, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Account account = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
if (account.getBalance().compareTo(request.getBalance()) == 0) {
throw new AccountAdjustBalanceNotValidException();
}
BigDecimal adjustAmount = request.getBalance().subtract(account.getBalance());
account.setBalance(request.getBalance());
// 如果是资产账户改一下截止日期
if (account instanceof AssetAccount) {
AssetAccount assetAccount = (AssetAccount) account;
assetAccount.setAsOfDate(request.getCreateTime());
}
accountRepository.save(account);
AdjustBalance po = new AdjustBalance();
request.copyPrimitive(po);
po.setCreator(user);
po.setBook(user.getDefaultBook());
po.setGroup(user.getDefaultGroup());
po.setAmount(adjustAmount);
po.setAccount(account);
po.setStatus(1);
adjustBalanceRepository.save(po);
userActionLogRepository.save(new UserActionLog(user, 1, Instant.now().toEpochMilli()));
return AccountVOForExtend.fromEntity(po.getAccount());
}
// 1. 余额调整记录删除,2. 日志删除
@Transactional
public AccountVOForExtend remove(Integer id, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
// 检查关联性,有账单关联的不能删除
if (transactionRepository.countByAccount_id(id) > 0 || transferRepository.countByTo_id(id) > 0) {
throw new AccountHasTransactionException();
}
adjustBalanceRepository.deleteByAccount_Id(id);
accountRepository.delete(po);
return AccountVOForExtend.fromEntity(po);
}
public AccountVOForExtend toggle(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
Book book = user.getDefaultBook();
if (po.equals(book.getDefaultExpenseAccount())) throw new DefaultExpenseAccountException();
if (po.equals(book.getDefaultIncomeAccount())) throw new DefaultIncomeAccountException();
po.setEnable(!po.getEnable());
accountRepository.save(po);
return AccountVOForExtend.fromEntity(accountRepository.save(po));
}
public boolean toggleInclude(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
po.setInclude(!po.getInclude());
accountRepository.save(po);
return true;
}
public boolean toggleExpenseable(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
Book book = user.getDefaultBook();
if (po.equals(book.getDefaultExpenseAccount())) throw new DefaultExpenseAccountException();
po.setExpenseable(!po.getExpenseable());
accountRepository.save(po);
return true;
}
public boolean toggleIncomeable(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
Book book = user.getDefaultBook();
if (po.equals(book.getDefaultIncomeAccount())) throw new DefaultIncomeAccountException();
po.setIncomeable(!po.getIncomeable());
accountRepository.save(po);
return true;
}
public boolean toggleTransferFromAble(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
Book book = user.getDefaultBook();
if (po.equals(book.getDefaultTransferFromAccount())) throw new DefaultTransferFromAccountException();
po.setTransferFromAble(!po.getTransferFromAble());
accountRepository.save(po);
return true;
}
public boolean toggleTransferToAble(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
Book book = user.getDefaultBook();
if (po.equals(book.getDefaultTransferToAccount())) throw new DefaultTransferToAccountException();
po.setTransferToAble(!po.getTransferToAble());
accountRepository.save(po);
return true;
}
public boolean add(Integer type, AccountAddRequest request, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
if (accountRepository.countByGroup(group) >= accountMaxCount) {
throw new AccountMaxCountException();
}
if (accountRepository.findOneByGroupAndName(group, request.getName()).isPresent()) {
throw new AccountNameExistsException();
}
currencyService.checkCode(request.getCurrencyCode());
Account po = null;
switch (type) {
case 1:
po = new CheckingAccount();
request.copyPrimitive(po);
break;
case 2:
po = new CreditAccount();
((CreditAccountAddRequest) request).copyPrimitive((CreditAccount) po);
break;
case 3:
po = new DebtAccount();
((DebtAccountAddRequest) request).copyPrimitive((DebtAccount) po);
break;
case 4:
po = new AssetAccount();
request.copyPrimitive(po);
((AssetAccount)po).setAsOfDate(Instant.now().toEpochMilli());
break;
}
po.setGroup(group);
accountRepository.save(po);
return true;
}
public AccountVOForExtend update(Integer id, AccountUpdateRequest request, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
Account po = accountRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
if (StringUtils.hasText(request.getName())) {
if (!po.getName().equals(request.getName())) {
if (accountRepository.findOneByGroupAndName(group, request.getName()).isPresent()) {
throw new AccountNameExistsException();
}
}
}
request.updatePrimitive(po);
accountRepository.save(po);
return AccountVOForExtend.fromEntity(po);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountSpec.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec;
import org.springframework.data.jpa.domain.Specification;
public final class AccountSpec {
public static Specification includeTrue() {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.include), true);
}
public static Specification expenseable() {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.expenseable), true);
}
public static Specification incomeable() {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.incomeable), true);
}
public static Specification transferFromAble() {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.transferFromAble), true);
}
public static Specification transferToAble() {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.transferToAble), true);
}
public static Specification inGroup(Group group) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Account_.group), group);
}
public static Specification inGroupAndEnable(Group group) {
Specification specification = inGroup(group);
return specification.and(BookNameNotesEnableSpec.enable());
}
public static Specification inGroupAndInclude(Group group) {
Specification specification = inGroupAndEnable(group);
return specification.and(includeTrue());
}
public static Specification inGroupAndExpenseable(Group group) {
Specification specification = inGroupAndEnable(group);
return specification.and(expenseable());
}
public static Specification inGroupAndIncomeable(Group group) {
Specification specification = inGroupAndEnable(group);
return specification.and(incomeable());
}
public static Specification inGroupAndTransferFromAble(Group group) {
Specification specification = inGroupAndEnable(group);
return specification.and(transferFromAble());
}
public static Specification inGroupAndTransferToAble(Group group) {
Specification specification = inGroupAndEnable(group);
return specification.and(transferToAble());
}
public static Specification buildSpecification(AccountQueryRequest request, Group group) {
Specification specification = inGroup(group);
if (request.getEnable() != null) {
specification = specification.and(BookNameNotesEnableSpec.isEnable(request.getEnable()));
}
return specification;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountSumVO.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class AccountSumVO {
private BigDecimal balance;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountUpdateRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import java.math.BigDecimal;
import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccount;
import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccount;
import com.jiukuaitech.bookkeeping.user.validation.*;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AccountUpdateRequest {
@NameValidator
private String name;
@NoValidator
private String no;
private Boolean include;
private Boolean transferFromAble;
private Boolean transferToAble;
private Boolean expenseable;
private Boolean incomeable;
@NotesValidator
private String notes;
// For Credit
@CreditLimitValidator
private BigDecimal limit; // 信用额度
@BillDayValidator
private Integer billDay; // 每月多少号是账单日
@AprValidator
private BigDecimal apr;
public void updatePrimitive(Account po) {
if (name != null) po.setName(name);
if (no != null) po.setNo(no);
if (include != null) po.setInclude(include);
if (transferFromAble != null) po.setTransferFromAble(transferFromAble);
if (transferToAble != null) po.setTransferToAble(transferToAble);
if (expenseable != null) po.setExpenseable(expenseable);
if (incomeable != null) po.setIncomeable(incomeable);
if (notes != null) po.setNotes(notes);
if (po instanceof CreditAccount) {
if (limit != null) ((CreditAccount)po).setLimit(limit);
if (billDay != null) ((CreditAccount)po).setBillDay(billDay);
} else if (po instanceof DebtAccount) {
if (limit != null) ((DebtAccount)po).setLimit(limit);
if (apr != null) ((DebtAccount)po).setApr(apr);
}
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountVOForExtend.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import com.jiukuaitech.bookkeeping.user.utils.EnumUtils;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class AccountVOForExtend {
private Integer type; //1活期,2信用,3贷款,4资产
private Integer id;
private String name;
private String no;
private BigDecimal balance;
private BigDecimal convertedBalance;
private String currencyCode;
private Boolean enable;
private Boolean include;
private Boolean expenseable;
private Boolean incomeable;
private Boolean transferFromAble;
private Boolean transferToAble;
private BigDecimal initialBalance;
private String notes;
public void setValue(Account po) {
setType(po.getType());
setId(po.getId());
setName(po.getName());
setNo(po.getNo());
setBalance(po.getBalance());
setEnable(po.getEnable());
setInclude(po.getInclude());
setExpenseable(po.getExpenseable());
setIncomeable(po.getIncomeable());
setTransferFromAble(po.getTransferFromAble());
setTransferToAble(po.getTransferToAble());
setInitialBalance(po.getInitialBalance());
setNotes(po.getNotes());
setCurrencyCode(po.getCurrencyCode());
}
public static AccountVOForExtend fromEntity(Account po) {
if (po == null) return null;
AccountVOForExtend vo = new AccountVOForExtend();
vo.setValue(po);
return vo;
}
public String getTypeName() {
return EnumUtils.translateAccountType(type);
}
public String getBalanceFormatted() {
return String.format("%.2f", balance);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/AccountVOForList.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class AccountVOForList extends AccountVOForExtend {
private BigDecimal limit;
private Integer billDay;
private BigDecimal apr;
private Long asOfDate;
public BigDecimal getRemainLimit() {
if (limit == null) return null;
return limit.add(getBalance());
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/DefaultExpenseAccountException.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
public class DefaultExpenseAccountException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/DefaultIncomeAccountException.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
public class DefaultIncomeAccountException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/DefaultTransferFromAccountException.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
public class DefaultTransferFromAccountException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/account/DefaultTransferToAccountException.java
================================================
package com.jiukuaitech.bookkeeping.user.account;
public class DefaultTransferToAccountException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalance.java
================================================
package com.jiukuaitech.bookkeeping.user.adjust_balance;
import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlow;
import javax.persistence.*;
@Entity
@DiscriminatorValue(value = "4")
public class AdjustBalance extends BalanceFlow {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceAddRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.adjust_balance;
import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowAddRequest;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Getter
@Setter
public class AdjustBalanceAddRequest extends BalanceFlowAddRequest {
@NotNull
private BigDecimal balance;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceController.java
================================================
package com.jiukuaitech.bookkeeping.user.adjust_balance;
import com.jiukuaitech.bookkeeping.user.base.BaseController;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowUpdateRequest;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/adjust-balances")
public class AdjustBalanceController extends BaseController {
@Resource
private AdjustBalanceService adjustBalanceService;
@RequestMapping(method = RequestMethod.PUT, value = "/{id}")
public BaseResponse handleUpdate(
@PathVariable("id") Integer id,
@Valid @RequestBody BalanceFlowUpdateRequest request,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(adjustBalanceService.update(id, request, userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.adjust_balance;
import com.jiukuaitech.bookkeeping.user.base.HasBookRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AdjustBalanceRepository extends HasBookRepository {
void deleteByAccount_Id(Integer id);
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceService.java
================================================
package com.jiukuaitech.bookkeeping.user.adjust_balance;
import com.jiukuaitech.bookkeeping.user.account.Account;
import com.jiukuaitech.bookkeeping.user.account.AccountRepository;
import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.user.User;
import com.jiukuaitech.bookkeeping.user.user.UserService;
import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowUpdateRequest;
import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class AdjustBalanceService {
@Resource
private UserService userService;
@Resource
private AccountRepository accountRepository;
@Resource
private AdjustBalanceRepository adjustBalanceRepository;
@Transactional
public AccountVOForExtend remove(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
AdjustBalance po = adjustBalanceRepository.findOneByBookAndId(user.getDefaultBook(), id).orElseThrow(ItemNotFoundException::new);
// 处理退款
Account account = po.getAccount();
account.setBalance(account.getBalance().subtract(po.getAmount()));
accountRepository.save(account);
adjustBalanceRepository.delete(po);
return AccountVOForExtend.fromEntity(po.getAccount());
}
public boolean update(Integer id, BalanceFlowUpdateRequest request, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
AdjustBalance po = adjustBalanceRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
request.updatePrimitive(po);
adjustBalanceRepository.save(po);
return true;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/adjust_balance/AdjustBalanceVOForList.java
================================================
package com.jiukuaitech.bookkeeping.user.adjust_balance;
import com.jiukuaitech.bookkeeping.user.balance_flow.BalanceFlowVOForExtend;
public class AdjustBalanceVOForList extends BalanceFlowVOForExtend {
public static AdjustBalanceVOForList fromEntity(AdjustBalance po) {
AdjustBalanceVOForList vo = new AdjustBalanceVOForList();
vo.setValue(po);
return vo;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/aop/TestAspect.java
================================================
package com.jiukuaitech.bookkeeping.user.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
@Aspect
@Configuration
public class TestAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Before("execution(* com.jiukuaitech.bookkeeping.user.service.*.*(..))")
public void before(JoinPoint joinPoint) {
logger.info(" Check before-------------------------------------------------------- ");
logger.info(" Allowed execution for {}", "");
logger.info(Arrays.toString(joinPoint.getArgs()));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccount.java
================================================
package com.jiukuaitech.bookkeeping.user.asset_account;
import com.jiukuaitech.bookkeeping.user.account.Account;
import com.jiukuaitech.bookkeeping.user.validation.TimeValidator;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@DiscriminatorValue(value = "4")
@Getter
@Setter
public class AssetAccount extends Account {
@TimeValidator
private Long asOfDate; //截止时间,资产余额对应的时间
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccountController.java
================================================
package com.jiukuaitech.bookkeeping.user.asset_account;
import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest;
import com.jiukuaitech.bookkeeping.user.account.AccountService;
import com.jiukuaitech.bookkeeping.user.base.BaseController;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import com.jiukuaitech.bookkeeping.user.account.AccountAddRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/asset-accounts")
public class AssetAccountController extends BaseController {
@Resource
private AssetAccountService assetAccountService;
@Resource
private AccountService accountService;
@RequestMapping(method = RequestMethod.GET, value = "")
public BaseResponse handleQuery(
@Valid AccountQueryRequest request,
@PageableDefault(sort = "balance", direction = Sort.Direction.DESC) Pageable page,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(assetAccountService.query(request, page, userSignInId));
}
@RequestMapping(method = RequestMethod.POST, value = "")
public BaseResponse handleAdd(
@Valid @RequestBody AccountAddRequest request,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.add(4, request, userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/sum")
public BaseResponse handleSum(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(assetAccountService.sum(userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccountRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.asset_account;
import com.jiukuaitech.bookkeeping.user.base.BaseRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AssetAccountRepository extends BaseRepository {
/*
@Query("SELECT COALESCE(SUM(p.balance), 0), " +
"COALESCE(SUM(p.expense), 0), " +
"COALESCE(SUM(p.income), 0), " +
"COALESCE(SUM(p.transferFrom), 0), " +
"COALESCE(SUM(p.transferTo), 0) " +
"FROM AssetAccount p WHERE p.book = :book AND p.enable = true")
List findSum(Book book);
@Query("SELECT COALESCE(SUM(p.balance), 0) FROM AssetAccount p WHERE p.book = :book AND p.enable = true AND p.include = true")
BigDecimal findSumBalance(Book book);
*/
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccountService.java
================================================
package com.jiukuaitech.bookkeeping.user.asset_account;
import com.jiukuaitech.bookkeeping.user.account.Account;
import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest;
import com.jiukuaitech.bookkeeping.user.account.AccountSpec;
import com.jiukuaitech.bookkeeping.user.currency.CurrencyService;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.user.UserService;
import com.jiukuaitech.bookkeeping.user.account.AccountSumVO;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
@Service
public class AssetAccountService {
@Resource
private UserService userService;
@Resource
private AssetAccountRepository assetAccountRepository;
@Resource
private CurrencyService currencyService;
public Page query(AccountQueryRequest request, Pageable page, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
Specification specification = AccountSpec.buildSpecification(request, group);
Page poPage = assetAccountRepository.findAll(specification, page);
Page voPage = poPage.map(AssetAccountVOForList::fromEntity);
voPage.map(vo->{
vo.setConvertedBalance(currencyService.convert(vo.getBalance(), vo.getCurrencyCode(), group.getDefaultCurrencyCode()));
return vo;
});
return voPage;
}
public AccountSumVO sum(Integer userSignInId) {
AccountSumVO vo = new AccountSumVO();
Group group = userService.getUser(userSignInId).getDefaultGroup();
List accounts = assetAccountRepository.findAll(AccountSpec.inGroupAndEnable(group));
BigDecimal balance = BigDecimal.ZERO;
for (int i = 0; i < accounts.size(); i++) {
Account account = accounts.get(i);
balance = balance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode()));
}
vo.setBalance(balance);
return vo;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/asset_account/AssetAccountVOForList.java
================================================
package com.jiukuaitech.bookkeeping.user.asset_account;
import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AssetAccountVOForList extends AccountVOForExtend {
private Long asOfDate;
public static AssetAccountVOForList fromEntity(AssetAccount po) {
AssetAccountVOForList vo = new AssetAccountVOForList();
vo.setValue(po);
vo.setAsOfDate(po.getAsOfDate());
return vo;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/AccountInvalidateException.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
public class AccountInvalidateException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/AmountInvalidateException.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
public class AmountInvalidateException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlow.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.account.Account;
import com.jiukuaitech.bookkeeping.user.base.HasBookEntity;
import com.jiukuaitech.bookkeeping.user.flow_images.FlowImage;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.user.User;
import com.jiukuaitech.bookkeeping.user.validation.*;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name="t_balance_flow")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.INTEGER, columnDefinition = "TINYINT(1)")
@Getter
@Setter
public class BalanceFlow extends HasBookEntity {
@Column(nullable = false)
@NotNull
@AmountValidator
private BigDecimal amount; // 金额
@AmountValidator
private BigDecimal convertedAmount; //汇率换算之后的金额
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@NotNull
private Account account;
@Column(nullable = false)
@NotNull
@TimeValidator
private Long createTime;
@Column(nullable = false, columnDefinition = "TINYINT(1)")
@NotNull
@TransactionStatusValidator
private Integer status = 1; // 1正常 2是待确认 3是已退款
@Column(insertable = false, updatable = false)
private Integer type; //1支出,2收入,3转账,4余额调整
@Column(length = 16)
@DescriptionValidator
private String description; //描述
@Column(length = 1024)
@NotesValidator
private String notes; //备注
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@NotNull
private User creator;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@NotNull
private Group group;
@OneToMany(mappedBy = "flow", fetch = FetchType.LAZY)
private Set images = new HashSet<>();
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowAddRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.validation.DescriptionValidator;
import com.jiukuaitech.bookkeeping.user.validation.NotesValidator;
import com.jiukuaitech.bookkeeping.user.validation.TimeValidator;
import lombok.Getter;
import lombok.Setter;
import java.time.Instant;
@Getter
@Setter
public class BalanceFlowAddRequest {
@TimeValidator
private Long createTime;
@DescriptionValidator
private String description;
@NotesValidator
private String notes;
public void copyPrimitive(BalanceFlow po) {
if (createTime == null) {
po.setCreateTime(Instant.now().toEpochMilli());
} else {
po.setCreateTime(createTime);
}
po.setDescription(description);
po.setNotes(notes);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowController.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Set;
@RestController
@RequestMapping("/flows")
public class BalanceFlowController {
@Resource
private BalanceFlowService balanceFlowService;
@RequestMapping(method = RequestMethod.GET, value = "")
public BaseResponse handleQuery(
@Valid BalanceFlowQueryRequest request,
@PageableDefault(sort = {"createTime", "id"}, direction = Sort.Direction.DESC) Pageable page,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceFlowService.queryWithDefaultBook(request, page, userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "audit")
public BaseResponse handleAudit(
@Valid BalanceFlowQueryRequest request,
@PageableDefault(sort = {"createTime", "id"}, direction = Sort.Direction.DESC) Pageable page,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceFlowService.query(request, page, userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceFlowService.get(id, userSignInId));
}
@RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
public BaseResponse handleDelete(
@PathVariable("id") Integer id,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceFlowService.remove(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}/confirm")
public BaseResponse handleConfirm(
@PathVariable("id") Integer id,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceFlowService.confirm(id, userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/{id}/images")
public BaseResponse handleImages(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceFlowService.getImages(id, userSignInId));
}
@RequestMapping(method = RequestMethod.POST, value = "/{id}/images")
public BaseResponse handleUpdateImages(
@PathVariable("id") Integer id,
@Valid @RequestBody Set images,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceFlowService.updateImages(id, images, userSignInId));
}
@RequestMapping(method = RequestMethod.POST, value = "/{id}/image")
public BaseResponse handleAddImage(
@PathVariable("id") Integer id,
@Valid @RequestBody Integer imageId,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceFlowService.addImage(id, imageId, userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowExceptionHandler.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.ErrorResponse;
import org.springframework.context.MessageSource;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.annotation.Resource;
import java.util.Locale;
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class BalanceFlowExceptionHandler {
private static final Locale LANG = Locale.CHINA;
@Resource
private MessageSource messageSource;
@ExceptionHandler(value = AccountInvalidateException.class)
@ResponseBody
public BaseResponse handleException(AccountInvalidateException e) {
return new ErrorResponse(702, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = StatusNotValidateException.class)
@ResponseBody
public BaseResponse handleException(StatusNotValidateException e) {
return new ErrorResponse(703, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = AmountInvalidateException.class)
@ResponseBody
public BaseResponse handleException(AmountInvalidateException e) {
return new ErrorResponse(704, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowQueryRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class BalanceFlowQueryRequest {
private Double minAmount;
private Double maxAmount;
private Long minTime;
private Long maxTime;
private Integer bookId;
private Integer accountId;
private Set accounts;
private Integer status;
private Integer type;
private String description;
private Integer creatorId;
private Set tags;
private Set categories;
private Set payees;
private Set fromAccounts;
private Set toAccounts;
// 统计支出分类用到
private Integer categoryId;
// 按id查询,比如找出退款对应的两条记录
private Set id;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowQueryResultVO.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.domain.Page;
import java.math.BigDecimal;
@Getter
@Setter
public class BalanceFlowQueryResultVO {
private Page result;
private BigDecimal expense;
private BigDecimal income;
public BigDecimal getSurplus() {
return income.subtract(expense);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.base.HasBookRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BalanceFlowRepository extends HasBookRepository {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowService.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.expense.Expense;
import com.jiukuaitech.bookkeeping.user.flow_images.FlowImageRepository;
import com.jiukuaitech.bookkeeping.user.flow_images.FlowImageVOForList;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.income.Income;
import com.jiukuaitech.bookkeeping.user.transfer.Transfer;
import com.jiukuaitech.bookkeeping.user.user.User;
import com.jiukuaitech.bookkeeping.user.user.UserService;
import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceService;
import com.jiukuaitech.bookkeeping.user.deal.DealService;
import com.jiukuaitech.bookkeeping.user.deal.DealSpec;
import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException;
import com.jiukuaitech.bookkeeping.user.expense.Expense_;
import com.jiukuaitech.bookkeeping.user.flow_images.FlowImage;
import com.jiukuaitech.bookkeeping.user.income.Income_;
import com.jiukuaitech.bookkeeping.user.transfer.TransferService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class BalanceFlowService {
@Resource
private UserService userService;
@Resource
private BalanceFlowRepository balanceFlowRepository;
@Resource
private DealService dealService;
@Resource
private TransferService transferService;
@Resource
private AdjustBalanceService adjustBalanceService;
@Resource
private FlowImageRepository flowImageRepository;
public BalanceFlowQueryResultVO queryWithDefaultBook(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) {
User user = userService.getUser(userSignInId);
request.setBookId(user.getDefaultBook().getId());
return query(request, page, userSignInId);
}
public BalanceFlowQueryResultVO query(BalanceFlowQueryRequest request, Pageable page, Integer userSignInId) {
BalanceFlowQueryResultVO result = new BalanceFlowQueryResultVO();
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Specification specification = BalanceFlowSpec.buildFlowSpecification(request, group);
Page poPage = balanceFlowRepository.findAll(specification, page);
Page voPage = poPage.map(flow-> {
BalanceFlowVOForList vo = BalanceFlowVOForList.fromEntity(flow);
vo.setCurrencyCode(flow.getAccount().getCurrencyCode());
if (flow.getType() == 1 || flow.getType() == 2) {
if (flow.getBook().getDefaultCurrencyCode().equals(flow.getAccount().getCurrencyCode())) {
vo.setNeedConvert(false);
} else {
vo.setNeedConvert(true);
vo.setToCurrencyCode(flow.getBook().getDefaultCurrencyCode());
}
} else if (flow.getType() == 3) {
String toCurrencyCode = ((Transfer) flow).getTo().getCurrencyCode();
if (flow.getAccount().getCurrencyCode().equals(toCurrencyCode)) {
vo.setNeedConvert(false);
} else {
vo.setNeedConvert(true);
vo.setToCurrencyCode(toCurrencyCode);
}
} else {
vo.setNeedConvert(false);
}
return vo;
});
result.setResult(voPage);
Specification specification1 = DealSpec.buildSpecification(request, group);
if (request.getType() == null || request.getType() == 1) { //只有支出类型才需要计算总额
result.setExpense(balanceFlowRepository.calcAggregate(specification1, Expense_.convertedAmount, Expense.class));
} else { //不是查询的支出类型,则支出总额肯定为空
result.setExpense(BigDecimal.ZERO);
}
Specification specification2 = DealSpec.buildSpecification(request, group);
if (request.getType() == null || request.getType() == 2) {
result.setIncome(balanceFlowRepository.calcAggregate(specification2, Income_.convertedAmount, Income.class));
} else {
result.setIncome(BigDecimal.ZERO);
}
return result;
}
public BalanceFlowVOForList get(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Book book = user.getDefaultBook();
BalanceFlow flow = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
BalanceFlowVOForList vo = BalanceFlowVOForList.fromEntity(flow);
vo.setCurrencyCode(flow.getAccount().getCurrencyCode());
if (flow.getType() == 1 || flow.getType() == 2) {
if (flow.getBook().getDefaultCurrencyCode().equals(flow.getAccount().getCurrencyCode())) {
vo.setNeedConvert(false);
} else {
vo.setNeedConvert(true);
vo.setToCurrencyCode(flow.getBook().getDefaultCurrencyCode());
}
} else if (flow.getType() == 3) {
String toCurrencyCode = ((Transfer) flow).getTo().getCurrencyCode();
if (flow.getAccount().getCurrencyCode().equals(toCurrencyCode)) {
vo.setNeedConvert(false);
} else {
vo.setNeedConvert(true);
vo.setToCurrencyCode(toCurrencyCode);
}
} else {
vo.setNeedConvert(false);
}
return vo;
}
public boolean remove(Integer id, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
BalanceFlow flow = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
switch (flow.getType()) {
case 1:
return dealService.remove(1, id, userSignInId);
case 2:
return dealService.remove(2, id, userSignInId);
case 3:
return transferService.remove(id, userSignInId);
case 4:
adjustBalanceService.remove(id, userSignInId);
return true;
default:
throw new ItemNotFoundException();
}
}
public boolean confirm(Integer id, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
BalanceFlow flow = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
switch (flow.getType()) {
case 1:
return dealService.confirm(1, id, userSignInId);
case 2:
return dealService.confirm(2, id, userSignInId);
case 3:
return transferService.confirm(id, userSignInId);
default:
throw new ItemNotFoundException();
}
}
public boolean updateImages(Integer id, Set images, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Book book = user.getDefaultBook();
BalanceFlow po = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
po.getImages().forEach(i -> {
i.setFlow(null);
flowImageRepository.save(i);
});
images.forEach(i -> {
FlowImage image = flowImageRepository.getById(i);
image.setFlow(po); //必须加上这个才能关联上
po.getImages().add(image);
});
balanceFlowRepository.save(po);
return true;
}
public List getImages(Integer id, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
BalanceFlow po = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
List images = flowImageRepository.findByFlow(po);
return images.stream().map(FlowImageVOForList::fromEntity).collect(Collectors.toList());
}
public boolean addImage(Integer id, Integer imageId, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Book book = user.getDefaultBook();
BalanceFlow po = balanceFlowRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
FlowImage image = flowImageRepository.getById(imageId);
image.setFlow(po);
flowImageRepository.save(image);
return true;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowSpec.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.account.Account;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation;
import com.jiukuaitech.bookkeeping.user.deal.Deal;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.payee.Payee;
import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelation;
import com.jiukuaitech.bookkeeping.user.transfer.Transfer;
import com.jiukuaitech.bookkeeping.user.user.User;
import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec;
import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation_;
import com.jiukuaitech.bookkeeping.user.deal.Deal_;
import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelation_;
import com.jiukuaitech.bookkeeping.user.transaction.Transaction;
import com.jiukuaitech.bookkeeping.user.transaction.Transaction_;
import com.jiukuaitech.bookkeeping.user.transfer.Transfer_;
import com.jiukuaitech.bookkeeping.user.utils.CalendarUtils;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.persistence.criteria.*;
import java.util.Set;
public final class BalanceFlowSpec {
public static Specification amountGreaterThanOrEqualTo(Double amount) {
return (root, query, criteriaBuilder) -> criteriaBuilder.ge(root.get(BalanceFlow_.AMOUNT), amount);
}
public static Specification amountLessThanOrEqualTo(Double amount) {
return (root, query, criteriaBuilder) -> criteriaBuilder.le(root.get(BalanceFlow_.AMOUNT), amount);
}
public static Specification createTimeGreaterThanOrEqualTo(Long time) {
return (root, query, criteriaBuilder) -> criteriaBuilder.ge(root.get(BalanceFlow_.CREATE_TIME), time);
}
public static Specification createTimeLessThanOrEqualTo(Long time) {
return (root, query, criteriaBuilder) -> criteriaBuilder.le(root.get(BalanceFlow_.CREATE_TIME), time);
}
public static Specification creatorEqual(User user) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.CREATOR), user);
}
public static Specification accountIn(Set accounts) {
return (root, query, criteriaBuilder) -> {
CriteriaBuilder.In in = criteriaBuilder.in(root.get(BalanceFlow_.ACCOUNT));
accounts.forEach(i -> in.value(new Account(i)));
return in;
};
}
public static Specification idIn(Set id) {
return (root, query, criteriaBuilder) -> {
CriteriaBuilder.In in = criteriaBuilder.in(root.get(BalanceFlow_.ID));
id.forEach(i -> in.value(i));
return in;
};
}
public static Specification typeEqual(Integer type) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.TYPE), type);
}
public static Specification isGroup(Group group) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.GROUP), group);
}
public static Specification statusEqual(Integer status) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.STATUS), status);
}
// 不是待确认的状态
public static Specification statusConfirmed() {
return (root, query, criteriaBuilder) -> criteriaBuilder.notEqual(root.get(BalanceFlow_.STATUS), 2);
}
public static Specification descriptionEqual(String description) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BalanceFlow_.DESCRIPTION), description);
}
public static Specification distinct() {
return (root, query, cb) -> {
query.distinct(true);
// query.groupBy(root.get(BalanceFlow_.id));
return null;
};
}
private static Specification buildBaseSpecification(BalanceFlowQueryRequest request, Group group) {
Specification specification = isGroup(group);
if (request.getBookId() != null) {
specification = specification.and(BookNameNotesEnableSpec.isBook(new Book(request.getBookId())));
}
if (request.getMinAmount() != null) {
specification = specification.and(amountGreaterThanOrEqualTo(request.getMinAmount()));
}
if (request.getMaxAmount() != null) {
specification = specification.and(amountLessThanOrEqualTo(request.getMaxAmount()));
}
if (request.getMinTime() != null) {
specification = specification.and(createTimeGreaterThanOrEqualTo(CalendarUtils.getStartOfDay(request.getMinTime())));
}
if (request.getMaxTime() != null) {
specification = specification.and(createTimeLessThanOrEqualTo(CalendarUtils.getEndOfDay(request.getMaxTime())));
}
if (request.getCreatorId() != null) {
specification = specification.and(creatorEqual(new User(request.getCreatorId())));
}
if (request.getStatus() != null) {
specification = specification.and(statusEqual(request.getStatus()));
}
if (StringUtils.hasText(request.getDescription())) {
specification = specification.and(descriptionEqual(request.getDescription()));
}
if (!CollectionUtils.isEmpty(request.getId())) {
specification = specification.and(idIn(request.getId()));
}
specification = specification.and(distinct());
return specification;
}
// 给子类调用
public static Specification buildSpecification(BalanceFlowQueryRequest request, Group group) {
Specification specification = buildBaseSpecification(request, group);
if (!CollectionUtils.isEmpty(request.getAccounts())) {
specification = specification.and(accountIn(request.getAccounts()));
}
return specification;
}
public static Specification buildFlowSpecification(BalanceFlowQueryRequest request, Group group) {
Specification specification = buildBaseSpecification(request, group);
if (request.getAccountId() != null) {
specification = specification.and((Specification) (root, query, criteriaBuilder) -> {
Account account = new Account(request.getAccountId());
Predicate predicate1 = criteriaBuilder.equal(root.get(BalanceFlow_.account), account);
// https://stackoverflow.com/questions/34251930/jpa-criteria-api-query-subclass-property
// Root transferRoot = criteriaBuilder.treat(root, Transfer.class);
@SuppressWarnings("unchecked")
Root transferRoot = (Root) (Root>) root;
Predicate predicate2 = criteriaBuilder.equal(transferRoot.get(Transfer_.to), account);
return criteriaBuilder.or(predicate1, predicate2);
});
} else if (!CollectionUtils.isEmpty(request.getAccounts())) {
specification = specification.and((Specification) (root, query, criteriaBuilder) -> {
CriteriaBuilder.In in1 = criteriaBuilder.in(root.get(BalanceFlow_.account));
request.getAccounts().forEach(i -> in1.value(new Account(i)));
@SuppressWarnings("unchecked")
Root transferRoot = (Root) (Root>) root;
CriteriaBuilder.In in2 = criteriaBuilder.in(transferRoot.get(Transfer_.to));
request.getAccounts().forEach(i -> in2.value(new Account(i)));
return criteriaBuilder.or(in1, in2);
});
}
if (!CollectionUtils.isEmpty(request.getPayees())) {
specification = specification.and((Specification) (root, query, criteriaBuilder) -> {
// Root dealRoot = criteriaBuilder.treat(root, Deal.class);
Root dealRoot = (Root) (Root>) root;
CriteriaBuilder.In in = criteriaBuilder.in(dealRoot.get(Deal_.payee));
request.getPayees().forEach(i -> in.value(new Payee(i)));
return in;
});
}
if (!CollectionUtils.isEmpty(request.getCategories())) {
specification = specification.and((Specification) (root, query, criteriaBuilder) -> {
Root dealRoot = (Root) (Root>) root;
Join join = dealRoot.join(Deal_.categories);
return join.get(CategoryRelation_.category).in(request.getCategories());
});
}
if (!CollectionUtils.isEmpty(request.getTags())) {
specification = specification.and((Specification) (root, query, criteriaBuilder) -> {
Root transactionRoot = criteriaBuilder.treat(root, Transaction.class);
Join join = transactionRoot.join(Transaction_.tags);
return join.get(TagRelation_.tag).in(request.getTags());
});
}
if (request.getType() != null) {
specification = specification.and(typeEqual(request.getType()));
}
return specification;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowUpdateRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.validation.DescriptionValidator;
import com.jiukuaitech.bookkeeping.user.validation.NotesValidator;
import com.jiukuaitech.bookkeeping.user.validation.TimeValidator;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class BalanceFlowUpdateRequest {
@TimeValidator
private Long createTime;
@DescriptionValidator
private String description;
@NotesValidator
private String notes;
public void updatePrimitive(BalanceFlow po) {
if (createTime != null) po.setCreateTime(createTime);
if (description != null) po.setDescription(description);
if (notes != null) po.setNotes(notes);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowVOForExtend.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend;
import com.jiukuaitech.bookkeeping.user.utils.EnumUtils;
import com.jiukuaitech.bookkeeping.user.response.HasNameVO;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class BalanceFlowVOForExtend {
private Integer id;
private HasNameVO book;
private BigDecimal amount;
private BigDecimal convertedAmount;
private String currencyCode;
private Boolean needConvert;
private String toCurrencyCode;
private AccountVOForExtend account;
private String accountName;
private Long createTime;
private String description;
private String notes;
private Integer status;
public void setValue(BalanceFlow po) {
setId(po.getId());
setBook(new HasNameVO(po.getBook().getId(), po.getBook().getName()));
setAmount(po.getAmount());
setConvertedAmount(po.getConvertedAmount());
if (po.getAccount() != null) setAccount(AccountVOForExtend.fromEntity(po.getAccount()));
setAccountName(getAccount() == null ? null : getAccount().getName());
setCreateTime(po.getCreateTime());
setDescription(po.getDescription());
setNotes(po.getNotes());
setStatus(po.getStatus());
}
public static BalanceFlowVOForExtend fromEntity(BalanceFlow balanceFlow) {
BalanceFlowVOForExtend result = new BalanceFlowVOForExtend();
result.setValue(balanceFlow);
return result;
}
public String getStatusName() {
return EnumUtils.translateFlowStatus(getStatus());
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/BalanceFlowVOForList.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalance;
import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationVOForList;
import com.jiukuaitech.bookkeeping.user.deal.DealVOForList;
import com.jiukuaitech.bookkeeping.user.expense.Expense;
import com.jiukuaitech.bookkeeping.user.income.Income;
import com.jiukuaitech.bookkeeping.user.tag_relation.TagRelationVOForList;
import com.jiukuaitech.bookkeeping.user.transfer.Transfer;
import com.jiukuaitech.bookkeeping.user.utils.EnumUtils;
import com.jiukuaitech.bookkeeping.user.adjust_balance.AdjustBalanceVOForList;
import com.jiukuaitech.bookkeeping.user.response.HasNameVO;
import com.jiukuaitech.bookkeeping.user.transfer.TransferVOForList;
import lombok.Getter;
import lombok.Setter;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@Getter
@Setter
public class BalanceFlowVOForList {
private Integer id;
private Integer type; // 1是expense,2是income,3是transfer,4是调整余额
private DealVOForList expense;
private DealVOForList income;
private TransferVOForList transfer;
private AdjustBalanceVOForList adjustBalance;
private BigDecimal amount;
private BigDecimal convertedAmount;
private String currencyCode;
private Boolean needConvert;
private String toCurrencyCode;
private Long createTime;
private Integer status;
private String description;
private Integer accountId;
private Integer toId;
private String accountName;
private String fromAccountName;
private String toAccountName;
private String categoryName;
private Set categories = new HashSet<>();
private String bookName;
private Set tags = new HashSet<>();
private HasNameVO payee;
private String notes;
public static BalanceFlowVOForList fromEntity(BalanceFlow po) {
if (po == null) return null;
BalanceFlowVOForList vo = new BalanceFlowVOForList();
vo.setId(po.getId());
vo.setBookName(po.getBook().getName());
vo.setType(po.getType());
vo.setStatus(po.getStatus());
vo.setDescription(po.getDescription());
vo.setAmount(po.getAmount());
vo.setConvertedAmount(po.getConvertedAmount());
vo.setCreateTime(po.getCreateTime());
vo.setNotes(po.getNotes());
if (po instanceof Expense) {
vo.setExpense(DealVOForList.fromEntity((Expense) po));
vo.setAccountId(vo.getExpense().getAccount().getId());
vo.setAccountName(vo.getExpense().getAccountName());
vo.setCategoryName(vo.getExpense().getCategoryName());
vo.setCategories(vo.getExpense().getCategories());
vo.setTags(vo.getExpense().getTags());
vo.setPayee(vo.getExpense().getPayee());
} else if (po instanceof Income) {
vo.setIncome(DealVOForList.fromEntity((Income) po));
vo.setAccountId(vo.getIncome().getAccount().getId());
vo.setAccountName(vo.getIncome().getAccountName());
vo.setCategoryName(vo.getIncome().getCategoryName());
vo.setCategories(vo.getIncome().getCategories());
vo.setTags(vo.getIncome().getTags());
vo.setPayee(vo.getIncome().getPayee());
} else if (po instanceof Transfer) {
vo.setTransfer(TransferVOForList.fromEntity((Transfer) po));
vo.setAccountId(vo.getTransfer().getFromId());
vo.setToId(vo.getTransfer().getToId());
vo.setAccountName(vo.getTransfer().getAccountName());
vo.setFromAccountName(vo.getTransfer().getFrom().getName());
vo.setToAccountName(vo.getTransfer().getTo().getName());
vo.setTags(vo.getTransfer().getTags());
} else if (po instanceof AdjustBalance) {
vo.setAdjustBalance(AdjustBalanceVOForList.fromEntity((AdjustBalance) po));
vo.setAccountName(vo.getAdjustBalance().getAccountName());
}
return vo;
}
public String getCreateTimeFormatted() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return simpleDateFormat.format(new Date(createTime));
}
private String getCreateDateFormatted() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
return simpleDateFormat.format(new Date(createTime));
}
public String getAmountFormatted() {
return String.format("%.2f", amount);
}
public String getTitle() {
StringBuilder result = new StringBuilder();
if (StringUtils.hasText(description)) {
result.append(description);
} else {
if (type == 1) {
result.append(expense.getCategories().stream().map(CategoryRelationVOForList::getCategoryName).collect(Collectors.joining(", ")));
} else if (type == 2) {
result.append(income.getCategories().stream().map(CategoryRelationVOForList::getCategoryName).collect(Collectors.joining(", ")));
} else if (type == 3) {
result.append(accountName);
} else {
result.append("调整余额");
}
}
if (getPayee() != null) {
result.append(" - ").append(getPayee().getName());
}
return result.toString();
}
public String getSubTitle() {
return getTypeName() + " " + getCreateDateFormatted() + " " + getTagsName();
}
public String getTagsName() {
if (type == 1) {
return expense.getTags().stream().map(TagRelationVOForList::getTagName).collect(Collectors.joining(", "));
}
if (type == 2) {
return income.getTags().stream().map(TagRelationVOForList::getTagName).collect(Collectors.joining(", "));
}
if (type == 3) {
return transfer.getTags().stream().map(TagRelationVOForList::getTagName).collect(Collectors.joining(", "));
}
return "";
}
public String getTypeName() {
return EnumUtils.translateFlowType(type);
}
public String getStatusName() {
return EnumUtils.translateFlowStatus(status);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_flow/StatusNotValidateException.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_flow;
public class StatusNotValidateException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLog.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_log;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.user.User;
import com.jiukuaitech.bookkeeping.user.validation.BalanceValidator;
import com.jiukuaitech.bookkeeping.user.validation.TimeValidator;
import com.jiukuaitech.bookkeeping.user.base.BaseEntity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Entity
@Table(name="t_balance_log")
@Getter
@Setter
public class BalanceLog extends BaseEntity {
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "group_id")
@NotNull
private Group group; // 账簿必须属于某个组
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@NotNull
private User creator;
@Column(nullable = false) //最多9亿
@NotNull
@BalanceValidator
private BigDecimal asset;
@Column(nullable = false) //最多9亿
@NotNull
@BalanceValidator
private BigDecimal debt;
@Column(nullable = false)
@NotNull
@TimeValidator
private Long createTime;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogAddRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_log;
import com.jiukuaitech.bookkeeping.user.validation.BalanceValidator;
import com.jiukuaitech.bookkeeping.user.validation.TimeValidator;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Getter
@Setter
public class BalanceLogAddRequest {
@NotNull
@BalanceValidator
private BigDecimal asset;
@NotNull
@BalanceValidator
private BigDecimal debt;
@Column(nullable = false)
@NotNull
@TimeValidator
private Long createTime;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogController.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_log;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/logs")
public class BalanceLogController {
@Resource
private BalanceLogService balanceLogService;
@RequestMapping(method = RequestMethod.GET, value = "")
public BaseResponse handleQuery(
@PageableDefault(sort = "createTime", direction = Sort.Direction.DESC) Pageable page,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceLogService.query(page, userSignInId));
}
@RequestMapping(method = RequestMethod.POST, value = "")
public BaseResponse handleAdd(
@Valid @RequestBody BalanceLogAddRequest request,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceLogService.add(request, userSignInId));
}
@RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(balanceLogService.remove(id, userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_log;
import com.jiukuaitech.bookkeeping.user.base.BaseRepository;
import com.jiukuaitech.bookkeeping.user.group.Group;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface BalanceLogRepository extends BaseRepository {
Optional findOneByGroupAndId(Group group, Integer integer);
List findByGroupAndCreateTimeBetweenOrderByCreateTime(Group group, Long min, Long max);
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogService.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_log;
import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.user.User;
import com.jiukuaitech.bookkeeping.user.user.UserService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
@Service
public class BalanceLogService {
@Resource
private BalanceLogRepository balanceLogRepository;
@Resource
private UserService userService;
public Page query(Pageable page, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
Specification specification = (root, query, criteriaBuilder) -> {
List predicates = new ArrayList<>();
predicates.add(criteriaBuilder.equal(root.get(BalanceLog_.group), group));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
Page poPage = balanceLogRepository.findAll(specification, page);
return poPage.map(BalanceLogVOForList::fromEntity);
}
public BalanceLogVOForList add(BalanceLogAddRequest request, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
BalanceLog po = new BalanceLog();
po.setGroup(group);
po.setCreator(user);
po.setAsset(request.getAsset());
po.setDebt(request.getDebt());
po.setCreateTime(request.getCreateTime());
return BalanceLogVOForList.fromEntity(balanceLogRepository.save(po));
}
public BalanceLogVOForList remove(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
BalanceLog po = balanceLogRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
balanceLogRepository.delete(po);
return BalanceLogVOForList.fromEntity(po);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/balance_log/BalanceLogVOForList.java
================================================
package com.jiukuaitech.bookkeeping.user.balance_log;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class BalanceLogVOForList {
private Integer id;
private BigDecimal asset;
private BigDecimal debt;
private Long createTime;
public static BalanceLogVOForList fromEntity(BalanceLog po) {
BalanceLogVOForList vo = new BalanceLogVOForList();
vo.setId(po.getId());
vo.setAsset(po.getAsset());
vo.setDebt(po.getDebt());
vo.setCreateTime(po.getCreateTime());
return vo;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseController.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import lombok.Getter;
import lombok.Setter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Getter
@Setter
public class BaseController {
@Resource
private HttpServletRequest request;
@Resource
private HttpServletResponse response;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseEntity.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
@Getter
@Setter
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// 不要用id重写hashcode和equals,之前的tag更新出了Bug
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import javax.persistence.metamodel.SingularAttribute;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/*
https://www.baeldung.com/spring-data-jpa-method-in-all-repositories
https://github.com/pkainulainen/spring-data-jpa-examples/blob/master/custom-method-all-repos/src/main/java/net/petrikainulainen/springdata/jpa/common/BaseRepository.java
*/
@NoRepositoryBean
public interface BaseRepository extends JpaRepository, JpaSpecificationExecutor {
// P extends Number change to BigDecimal
BigDecimal calcAggregate(Specification spec, SingularAttribute, BigDecimal> column, Class clazz);
List calcAggregate(Specification spec, List> columns, Class clazz);
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseRepositoryFactoryBean.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import javax.persistence.EntityManager;
import java.io.Serializable;
public class BaseRepositoryFactoryBean, T,
I extends Serializable> extends JpaRepositoryFactoryBean {
public BaseRepositoryFactoryBean(Class extends R> repositoryInterface) {
super(repositoryInterface);
}
@SuppressWarnings("rawtypes")
protected RepositoryFactorySupport createRepositoryFactory(EntityManager em) {
return new CustomRepositoryFactory(em);
}
private static class CustomRepositoryFactory
extends JpaRepositoryFactory {
private final EntityManager em;
public CustomRepositoryFactory(EntityManager em) {
super(em);
this.em = em;
}
@SuppressWarnings("unchecked")
protected Object getTargetRepository(RepositoryMetadata metadata) {
return new BaseRepositoryImpl(
(Class) metadata.getDomainType(), em);
}
protected Class> getRepositoryBaseClass(RepositoryMetadata metadata) {
return BaseRepositoryImpl.class;
}
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BaseRepositoryImpl.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import com.jiukuaitech.bookkeeping.user.utils.CommonUtils;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import javax.persistence.metamodel.SingularAttribute;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/*
https://stackoverflow.com/questions/38934549/spring-specification-with-sum-function
*/
public class BaseRepositoryImpl extends SimpleJpaRepository implements BaseRepository {
private final EntityManager entityManager;
public BaseRepositoryImpl(Class domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
this.entityManager = entityManager;
}
public BaseRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
@Override
public BigDecimal calcAggregate(Specification spec, SingularAttribute, BigDecimal> column, Class clazz) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery query = criteriaBuilder.createQuery(column.getJavaType());
Root root = query.from(clazz);
if (spec != null) {
query.where(spec.toPredicate(root, query, criteriaBuilder));
}
query.select(criteriaBuilder.sum(root.get(column.getName())));
TypedQuery typedQuery = entityManager.createQuery(query);
BigDecimal result = typedQuery.getSingleResult();
if (result == null) return BigDecimal.valueOf(0);
return result;
}
@Override
public List calcAggregate(Specification spec, List> columns, Class clazz) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery query = criteriaBuilder.createQuery(BigDecimal[].class);
Root root = query.from(clazz);
if (spec != null) {
query.where(spec.toPredicate(root, query, criteriaBuilder));
}
List> selectionList = new ArrayList<>();
columns.forEach(i -> selectionList.add(criteriaBuilder.sum(root.get(i.getName()))));
query.multiselect(selectionList);
Object[] result = entityManager.createQuery(query).getSingleResult();
return CommonUtils.coalesce(Arrays.copyOf(result, result.length, BigDecimal[].class));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BookNameNotesEnableEntity.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import com.jiukuaitech.bookkeeping.user.validation.NameValidator;
import com.jiukuaitech.bookkeeping.user.validation.NotesValidator;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@MappedSuperclass
@Getter
@Setter
public abstract class BookNameNotesEnableEntity extends HasBookEntity {
@Column(length = 16, nullable = false)
@NotNull
@NameValidator
private String name;
@Column(length = 128)
@NotesValidator
private String notes;
@Column(nullable = false)
@NotNull
private Boolean enable = true;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/BookNameNotesEnableSpec.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import com.jiukuaitech.bookkeeping.user.book.Book;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import java.util.Set;
public final class BookNameNotesEnableSpec {
public static Specification isBook(Book book) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BookNameNotesEnableEntity_.BOOK), book);
}
public static Specification inBooks(Set books) {
return (root, query, criteriaBuilder) -> {
CriteriaBuilder.In in = criteriaBuilder.in(root.get(BookNameNotesEnableEntity_.BOOK));
books.forEach(i -> in.value(i));
return in;
};
}
public static Specification enable() {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BookNameNotesEnableEntity_.ENABLE), true);
}
public static Specification isEnable(Boolean enable) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(BookNameNotesEnableEntity_.ENABLE), enable);
}
public static Specification nameLike(String name) {
return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get(BookNameNotesEnableEntity_.NAME), "%"+name+"%");
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/HasBookEntity.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import com.jiukuaitech.bookkeeping.user.book.Book;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.MappedSuperclass;
import javax.validation.constraints.NotNull;
@MappedSuperclass
@Getter
@Setter
public abstract class HasBookEntity extends BaseEntity {
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "book_id")
@NotNull
private Book book;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/HasBookRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import com.jiukuaitech.bookkeeping.user.book.Book;
import org.springframework.data.repository.NoRepositoryBean;
import java.util.Optional;
@NoRepositoryBean
public interface HasBookRepository extends BaseRepository {
Optional findOneByBookAndId(Book book, Integer id);
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/JpaDataConfig.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
@Configuration
@EnableJpaRepositories(basePackages = "com.jiukuaitech.bookkeeping.user",
repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class)
@EnableSpringDataWebSupport
public class JpaDataConfig {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/NameNotesEnableEntity.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import com.jiukuaitech.bookkeeping.user.validation.NameValidator;
import com.jiukuaitech.bookkeeping.user.validation.NotesValidator;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import javax.validation.constraints.NotNull;
@MappedSuperclass
@Getter
@Setter
public abstract class NameNotesEnableEntity extends BaseEntity {
@Column(length = 16, nullable = false)
@NotNull
@NameValidator
private String name;
@Column(length = 1024)
@NotesValidator
private String notes;
@Column(nullable = false)
@NotNull
private Boolean enable = true;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/base/TestController.java
================================================
package com.jiukuaitech.bookkeeping.user.base;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
@RestController
public class TestController {
@RequestMapping(method = RequestMethod.GET, value = "/test1")
public BaseResponse handleTest1() {
return new DataResponse<>(19);
}
@GetMapping("/test2")
public BaseResponse getBaseUrl(HttpServletRequest request) {
String baseUrl = ServletUriComponentsBuilder
.fromRequestUri(request)
.replacePath(null)
.build()
.toUriString();
return new DataResponse<>(baseUrl);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/Book.java
================================================
package com.jiukuaitech.bookkeeping.user.book;
import com.jiukuaitech.bookkeeping.user.expense_category.ExpenseCategory;
import com.jiukuaitech.bookkeeping.user.income_category.IncomeCategory;
import com.jiukuaitech.bookkeeping.user.account.Account;
import com.jiukuaitech.bookkeeping.user.base.NameNotesEnableEntity;
import com.jiukuaitech.bookkeeping.user.group.Group;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name="t_book", uniqueConstraints = {@UniqueConstraint(columnNames = {"group_id", "name"})})
@Getter
@Setter
/**
* 账簿类,它属于某个组管理。下面会有很多账户。
* group+name决定一个账簿。
*/
public class Book extends NameNotesEnableEntity {
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "group_id")
@NotNull
private Group group; // 账簿必须属于某个组
@OneToOne
private Account defaultExpenseAccount;
@OneToOne
private Account defaultIncomeAccount;
@OneToOne
private Account defaultTransferFromAccount;
@OneToOne
private Account defaultTransferToAccount;
@OneToOne
private ExpenseCategory defaultExpenseCategory;
@OneToOne
private IncomeCategory defaultIncomeCategory;
@Column(nullable = false)
@NotNull
private Boolean descriptionEnable = true;
@Column(nullable = false)
@NotNull
private Boolean timeEnable = false;
@Column(nullable = false)
@NotNull
private Boolean imageEnable = false;
@Column(nullable = false, length = 8)
@NotNull
private String defaultCurrencyCode;//默认的币种
public Book() { }
public Book(Integer id) {
super.setId(id);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookAddRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.book;
import com.jiukuaitech.bookkeeping.user.validation.NameValidator;
import com.jiukuaitech.bookkeeping.user.validation.NotesValidator;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
@Getter
@Setter
public class BookAddRequest {
@NotBlank
@NameValidator
private String name;
@NotesValidator
private String notes;
private Boolean descriptionEnable = true;
private Boolean timeEnable = false;
private Boolean imageEnable = false;
@NotBlank
private String defaultCurrencyCode;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookController.java
================================================
package com.jiukuaitech.bookkeeping.user.book;
import com.jiukuaitech.bookkeeping.user.base.BaseController;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/books")
public class BookController extends BaseController {
@Resource
private BookService bookService;
/*
@RequestMapping(method = RequestMethod.GET, value = "")
public BaseResponse handleGetAll(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(bookService.getAll(userSignInId));
}
*/
@RequestMapping(method = RequestMethod.GET, value = "")
public BaseResponse handleQuery(
@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable page,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(bookService.query(page, userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(bookService.get(id, userSignInId));
}
@RequestMapping(method = RequestMethod.POST, value = "")
public BaseResponse handleAdd(@Valid @RequestBody BookAddRequest request, @RequestAttribute("userSignInId") Integer userSignInId) {
return new BaseResponse(bookService.add(request, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}")
public BaseResponse handleUpdate(
@PathVariable("id") Integer id,
@Valid @RequestBody BookUpdateRequest request,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(bookService.update(id, request, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/config")
public BaseResponse handleConfig(
@Valid @RequestBody BookUpdateRequest request,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(bookService.config(request, userSignInId));
}
@RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new BaseResponse(bookService.remove(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggle")
public BaseResponse handleToggle(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(bookService.toggle(id, userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookExceptionHandler.java
================================================
package com.jiukuaitech.bookkeeping.user.book;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.ErrorResponse;
import org.springframework.context.MessageSource;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.annotation.Resource;
import java.util.Locale;
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class BookExceptionHandler {
private static final Locale LANG = Locale.CHINA;
@Resource
private MessageSource messageSource;
@ExceptionHandler(value = BookMaxCountException.class)
@ResponseBody
public BaseResponse handleException(BookMaxCountException e) {
return new ErrorResponse(602, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookMaxCountException.java
================================================
package com.jiukuaitech.bookkeeping.user.book;
public class BookMaxCountException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.book;
import com.jiukuaitech.bookkeeping.user.base.BaseRepository;
import com.jiukuaitech.bookkeeping.user.group.Group;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface BookRepository extends BaseRepository {
Integer countByGroup_id(Integer groupId);
Optional findOneByGroupAndId(Group group, Integer id);
Optional findOneByGroupAndName(Group group, String name);
long deleteByGroup_id(Integer groupId);
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookService.java
================================================
package com.jiukuaitech.bookkeeping.user.book;
import java.util.ArrayList;
import java.util.List;
import com.jiukuaitech.bookkeeping.user.currency.CurrencyService;
import com.jiukuaitech.bookkeeping.user.expense_category.ExpenseCategory;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.group.GroupRepository;
import com.jiukuaitech.bookkeeping.user.income_category.IncomeCategory;
import com.jiukuaitech.bookkeeping.user.user.*;
import com.jiukuaitech.bookkeeping.user.account.AccountRepository;
import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException;
import com.jiukuaitech.bookkeeping.user.exception.NameExistsException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.persistence.criteria.Predicate;
@Service
public class BookService {
@Resource
private UserRepository userRepository;
@Resource
private UserService userService;
@Resource
private GroupRepository groupRepository;
@Resource
private BookRepository bookRepository;
@Resource
private AccountRepository accountRepository;
@Resource
private CurrencyService currencyService;
@Value("${book.max.count}")
private Integer maxCount;
public Page query(Pageable page, Integer userSignInId) {
Specification specification = (root, query, criteriaBuilder) -> {
List predicates = new ArrayList<>();
User user = userService.getUser(userSignInId);
predicates.add(criteriaBuilder.equal(root.get(Book_.group), user.getDefaultGroup()));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
Page poPage = bookRepository.findAll(specification, page);
return poPage.map(BookVOForList::fromEntity);
}
public BookVOForList get(Integer id, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
Book po = bookRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
return BookVOForList.fromEntity(po);
}
public boolean add(BookAddRequest request, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
if (bookRepository.findOneByGroupAndName(group, request.getName()).isPresent()) {
throw new NameExistsException();
}
if (bookRepository.countByGroup_id(group.getId()) >= maxCount) {
throw new BookMaxCountException();
}
currencyService.checkCode(request.getDefaultCurrencyCode());
Book po = new Book();
po.setName(request.getName());
po.setDefaultCurrencyCode(request.getDefaultCurrencyCode());
po.setNotes(request.getNotes());
po.setGroup(group);
po.setDescriptionEnable(request.getDescriptionEnable());
po.setTimeEnable(request.getTimeEnable());
po.setImageEnable(request.getImageEnable());
bookRepository.save(po);
return true;
}
public BookVOForList update(Integer id, BookUpdateRequest request, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
Book po = bookRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
if (StringUtils.hasText(request.getName())) {
if (!po.getName().equals(request.getName())) {
if (bookRepository.findOneByGroupAndName(po.getGroup(), request.getName()).isPresent()) {
throw new NameExistsException();
}
}
}
if (StringUtils.hasText(request.getName())) po.setName(request.getName());
if (request.getNotes() != null) po.setNotes(request.getNotes());
bookRepository.save(po);
return BookVOForList.fromEntity(po);
}
public BookVOForList config(BookUpdateRequest request, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Book po = user.getDefaultBook();
// 不检查默认的账户和分类是不是该账本下的了,后果就是无法记账默认值而已。
if (request.getDefaultExpenseAccountId() != null) {
po.setDefaultExpenseAccount(accountRepository.getById(request.getDefaultExpenseAccountId()));
} else {
po.setDefaultExpenseAccount(null);
}
if (request.getDefaultIncomeAccountId() != null) {
po.setDefaultIncomeAccount(accountRepository.getById(request.getDefaultIncomeAccountId()));
} else {
po.setDefaultIncomeAccount(null);
}
if (request.getDefaultTransferFromAccountId() != null) {
po.setDefaultTransferFromAccount(accountRepository.getById(request.getDefaultTransferFromAccountId()));
} else {
po.setDefaultTransferFromAccount(null);
}
if (request.getDefaultTransferToAccountId() != null) {
po.setDefaultTransferToAccount(accountRepository.getById(request.getDefaultTransferToAccountId()));
} else {
po.setDefaultTransferToAccount(null);
}
if (request.getDefaultExpenseCategoryId() != null) {
po.setDefaultExpenseCategory(new ExpenseCategory(request.getDefaultExpenseCategoryId()));
} else {
po.setDefaultExpenseCategory(null);
}
if (request.getDefaultIncomeCategoryId() != null) {
po.setDefaultIncomeCategory(new IncomeCategory(request.getDefaultIncomeCategoryId()));
} else {
po.setDefaultIncomeCategory(null);
}
if (request.getDescriptionEnable() != null) po.setDescriptionEnable(request.getDescriptionEnable());
if (request.getTimeEnable() != null) po.setTimeEnable(request.getTimeEnable());
if (request.getImageEnable() != null) po.setImageEnable(request.getImageEnable());
bookRepository.save(po);
return BookVOForList.fromEntity(po);
}
public boolean remove(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Book po = bookRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
// UserGroupRelation userGroupRelation = userGroupRelationRepository.findOneByUserAndGroup(user, group);
// if (userGroupRelation == null || userGroupRelation.getRole() != 1) {
// throw new PermissionException("No Permission");
// }
if (user.getDefaultBook().getId().equals(po.getId())) {
user.setDefaultBook(null);
userRepository.save(user);
}
if (group.getDefaultBook().getId().equals(po.getId())) {
group.setDefaultBook(null);
groupRepository.save(group);
}
bookRepository.delete(po);
return true;
}
public boolean toggle(Integer id, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Group group = user.getDefaultGroup();
Book po = bookRepository.findOneByGroupAndId(group, id).orElseThrow(ItemNotFoundException::new);
if (user.getDefaultBook().equals(po)) {
throw new ItemNotFoundException();
}
po.setEnable(!po.getEnable());
bookRepository.save(po);
return true;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookUpdateRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.book;
import com.jiukuaitech.bookkeeping.user.validation.NameValidator;
import com.jiukuaitech.bookkeeping.user.validation.NotesValidator;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class BookUpdateRequest {
@NameValidator
private String name;
@NotesValidator
private String notes;
private Integer defaultExpenseAccountId;
private Integer defaultIncomeAccountId;
private Integer defaultTransferFromAccountId;
private Integer defaultTransferToAccountId;
private Integer defaultExpenseCategoryId;
private Integer defaultIncomeCategoryId;
private Boolean descriptionEnable;
private Boolean timeEnable;
private Boolean imageEnable;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/book/BookVOForList.java
================================================
package com.jiukuaitech.bookkeeping.user.book;
import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend;
import com.jiukuaitech.bookkeeping.user.response.HasNameVO;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class BookVOForList {
private Integer id;
private String name;
private String notes;
private HasNameVO group;
private AccountVOForExtend defaultExpenseAccount;
private AccountVOForExtend defaultIncomeAccount;
private AccountVOForExtend defaultTransferFromAccount;
private AccountVOForExtend defaultTransferToAccount;
private HasNameVO defaultExpenseCategory;
private HasNameVO defaultIncomeCategory;
private Boolean descriptionEnable;
private Boolean timeEnable;
private Boolean imageEnable;
private Boolean enable;
private String defaultCurrencyCode;
public static BookVOForList fromEntity(Book po) {
BookVOForList vo = new BookVOForList();
vo.setId(po.getId());
vo.setName(po.getName());
vo.setDefaultCurrencyCode(po.getDefaultCurrencyCode());
vo.setNotes(po.getNotes());
vo.setGroup(new HasNameVO(po.getGroup().getId(), po.getGroup().getName()));
vo.setDescriptionEnable(po.getDescriptionEnable());
vo.setTimeEnable(po.getTimeEnable());
vo.setImageEnable(po.getImageEnable());
vo.setEnable(po.getEnable());
if (po.getDefaultExpenseAccount() != null) {
vo.setDefaultExpenseAccount(AccountVOForExtend.fromEntity(po.getDefaultExpenseAccount()));
}
if (po.getDefaultIncomeAccount() != null) {
vo.setDefaultIncomeAccount(AccountVOForExtend.fromEntity(po.getDefaultIncomeAccount()));
}
if (po.getDefaultTransferFromAccount() != null) {
vo.setDefaultTransferFromAccount(AccountVOForExtend.fromEntity(po.getDefaultTransferFromAccount()));
}
if (po.getDefaultTransferToAccount() != null) {
vo.setDefaultTransferToAccount(AccountVOForExtend.fromEntity(po.getDefaultTransferToAccount()));
}
if (po.getDefaultExpenseCategory() != null) {
vo.setDefaultExpenseCategory(new HasNameVO(po.getDefaultExpenseCategory().getId(), po.getDefaultExpenseCategory().getName()));
}
if (po.getDefaultIncomeCategory() != null) {
vo.setDefaultIncomeCategory(new HasNameVO(po.getDefaultIncomeCategory().getId(), po.getDefaultIncomeCategory().getName()));
}
return vo;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/Category.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableEntity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.util.CollectionUtils;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name="t_category", uniqueConstraints = {@UniqueConstraint(columnNames = {"parent_id", "name"})})
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.INTEGER, columnDefinition = "TINYINT(1)")
@Getter
@Setter
@NoArgsConstructor
public class Category extends BookNameNotesEnableEntity {
@Column(insertable = false, updatable = false)
private Integer type; //1支出分类,2收入分类
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@Column(nullable = false)
@NotNull
private Integer level;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List children = new ArrayList<>();
public Category(Integer id) {
super.setId(id);
}
public List getChildren(List categories) {
List result = new ArrayList<>();
for (Category item : categories) {
if (item.getParent() != null && this.getId().equals(item.getParent().getId())) {
result.add(item);
}
}
return result;
}
public List getOffspring(List categories) {
List result = new ArrayList<>();
List children = this.getChildren(categories);
if (!CollectionUtils.isEmpty(children)) {
result.addAll(children);
for (Category item : children) {
result.addAll(item.getOffspring(categories));
}
}
return result;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryAddRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import com.jiukuaitech.bookkeeping.user.validation.NameValidator;
import com.jiukuaitech.bookkeeping.user.validation.NotesValidator;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
@Getter
@Setter
public class CategoryAddRequest {
@NotBlank
@NameValidator
private String name;
@NotesValidator
private String notes;
private Integer parentId;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryController.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import com.jiukuaitech.bookkeeping.user.base.BaseController;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/categories")
public class CategoryController extends BaseController {
@Resource
private CategoryService categoryService;
@RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
public BaseResponse handleDelete(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new BaseResponse(categoryService.remove(id, userSignInId));
}
@RequestMapping(method = RequestMethod.PUT, value = "/{id}/toggle")
public BaseResponse handleToggle(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new BaseResponse(categoryService.toggle(id, userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
public BaseResponse handleGet(@PathVariable("id") Integer id, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(categoryService.get(id, userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryExceptionHandler.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.ErrorResponse;
import org.springframework.context.MessageSource;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.annotation.Resource;
import java.util.Locale;
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CategoryExceptionHandler {
private static final Locale LANG = Locale.CHINA;
@Resource
private MessageSource messageSource;
@ExceptionHandler(value = ParentCategoryNotEnableException.class)
@ResponseBody
public BaseResponse handleException(ParentCategoryNotEnableException e) {
return new ErrorResponse(607, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = CategoryNameExistsException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public BaseResponse handleException(CategoryNameExistsException e) {
return new ErrorResponse(409, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = CategoryHasDealException.class)
@ResponseBody
public BaseResponse handleException(CategoryHasDealException e) {
return new ErrorResponse(410, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = CategoryLevelException.class)
@ResponseBody
public BaseResponse handleException(CategoryLevelException e) {
return new ErrorResponse(703, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = CategoryIsDefaultExpenseException.class)
@ResponseBody
public BaseResponse handleException(CategoryIsDefaultExpenseException e) {
return new ErrorResponse(704, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = CategoryIsDefaultIncomeException.class)
@ResponseBody
public BaseResponse handleException(CategoryIsDefaultIncomeException e) {
return new ErrorResponse(705, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
@ExceptionHandler(value = CategoryMaxCountException.class)
@ResponseBody
public BaseResponse handleException(CategoryMaxCountException e) {
return new ErrorResponse(706, messageSource.getMessage(e.getClass().getSimpleName(), null, LANG));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryHasDealException.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
/*
删除有账单的分类
*/
public class CategoryHasDealException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryIsDefaultExpenseException.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
/*
CategoryIsDefaultExpenseException
*/
public class CategoryIsDefaultExpenseException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryIsDefaultIncomeException.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
/*
账本的默认收入类别不能删除
*/
public class CategoryIsDefaultIncomeException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryLevelException.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
/*
添加支出时,层级超过
*/
public class CategoryLevelException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryMaxCountException.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
public class CategoryMaxCountException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryNameExistsException.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
/*
添加分类时,名称重复
*/
public class CategoryNameExistsException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryQueryRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CategoryQueryRequest {
private String name;
private Boolean enable;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.base.HasBookRepository;
import com.jiukuaitech.bookkeeping.user.group.Group;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CategoryRepository extends HasBookRepository {
Optional findOneByBookAndNameAndParentAndType(Book book, String name, Category parent, Integer type);
List findAllByBookAndType(Book book, Integer type);
List findAllByBook(Book book);
List findAllByBookAndTypeAndEnable(Book book, Integer type, Boolean enable);
long countByBookAndType(Book book, Integer type);
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryService.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.expense_category.ExpenseCategory;
import com.jiukuaitech.bookkeeping.user.income_category.IncomeCategory;
import com.jiukuaitech.bookkeeping.user.user.User;
import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec;
import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationRepository;
import com.jiukuaitech.bookkeeping.user.exception.ItemNotFoundException;
import com.jiukuaitech.bookkeeping.user.user.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class CategoryService {
@Resource
private CategoryRepository categoryRepository;
@Resource
private CategoryRelationRepository categoryRelationRepository;
@Resource
private UserService userService;
@Value("${category.max.level}")
private Integer maxLevel;
@Value("${category.max.count}")
private Integer maxCount;
/*
删除分类需要检查:1. 有无支出;2. 有无账本的默认; 3. 有无子类
*/
public boolean remove(Integer id, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
Category po = categoryRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
// 检查关联性,有账单关联的不能删除
if (categoryRelationRepository.countByCategory_id(id) > 0) {
throw new CategoryHasDealException();
}
if (po.equals(book.getDefaultExpenseCategory())) throw new CategoryIsDefaultExpenseException();
if (po.equals(book.getDefaultIncomeCategory())) throw new CategoryIsDefaultIncomeException();
categoryRepository.delete(po);
return true;
}
@Transactional
public boolean toggle(Integer id, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
Category po = categoryRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
List entityList = categoryRepository.findAll(BookNameNotesEnableSpec.isBook(po.getBook()));
List offSpring = po.getOffspring(entityList);
offSpring.add(po);
for (Category item : offSpring) {
if (item.equals(book.getDefaultExpenseCategory())) throw new CategoryIsDefaultExpenseException();
if (item.equals(book.getDefaultIncomeCategory())) throw new CategoryIsDefaultIncomeException();
item.setEnable(!po.getEnable());
}
categoryRepository.saveAll(offSpring);
return true;
}
public boolean add(Integer type, CategoryAddRequest request, Integer userSignInId) {
User user = userService.getUser(userSignInId);
Book book = user.getDefaultBook();
Category parent = null;
if (request.getParentId() != null) {
parent = categoryRepository.findOneByBookAndId(book, request.getParentId()).orElseThrow(ItemNotFoundException::new);
if (!parent.getEnable()) {
throw new ParentCategoryNotEnableException();
}
if (parent.getLevel().equals(maxLevel-1)) {
throw new CategoryLevelException();
}
}
if (categoryRepository.countByBookAndType(book, type) >= maxCount) {
throw new CategoryMaxCountException();
}
if (categoryRepository.findOneByBookAndNameAndParentAndType(book, request.getName(), parent, type).isPresent()) {
throw new CategoryNameExistsException();
}
Category po = null;
if (type == 1) {
po = new ExpenseCategory();
} else {
po = new IncomeCategory();
}
po.setName(request.getName());
po.setNotes(request.getNotes());
po.setBook(book);
po.setParent(parent);
if (parent == null) po.setLevel(0);
else po.setLevel(parent.getLevel()+1);
categoryRepository.save(po);
return true;
}
public boolean update(Integer type, Integer id, CategoryUpdateRequest request, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
Category po = categoryRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
Category parent = null;
if (request.getParentId() != null) {
parent = categoryRepository.findOneByBookAndId(book, request.getParentId()).orElseThrow(ItemNotFoundException::new);
if (!parent.getEnable()) {
throw new ParentCategoryNotEnableException();
}
if (parent.getLevel().equals(maxLevel-1)) {
throw new CategoryLevelException();
}
}
po.setParent(parent);
if (StringUtils.hasText(request.getName())) {
if(!request.getName().equals(po.getName())) {
if (categoryRepository.findOneByBookAndNameAndParentAndType(book, request.getName(), parent, type).isPresent()) {
throw new CategoryNameExistsException();
}
po.setName(request.getName());
}
}
if (request.getNotes() != null) po.setNotes(request.getNotes());
if (parent == null) po.setLevel(0);
else po.setLevel(parent.getLevel()+1);
categoryRepository.save(po);
return true;
}
public List getAllEnable(Integer type, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
List entityList = categoryRepository.findAllByBookAndTypeAndEnable(book, type, true);
return entityList.stream().map(CategorySimpleVO::fromEntity).collect(Collectors.toList());
}
public List getAllTree(CategoryQueryRequest request, Integer type, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
Specification specification = CategorySpec.buildSpecification(request, type, book);
List entityList = categoryRepository.findAll(specification);
return CategoryTreeVO.valueOfList(entityList);
}
public Page query(Integer type, Pageable page, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
Specification specification = CategorySpec.buildSpecification(null, type, book);
Page poPage = categoryRepository.findAll(specification, page);
return poPage.map(CategorySimpleVO::fromEntity);
}
public CategoryTreeVO get(Integer id, Integer userSignInId) {
Book book = userService.getUser(userSignInId).getDefaultBook();
Category category = categoryRepository.findOneByBookAndId(book, id).orElseThrow(ItemNotFoundException::new);
return CategoryTreeVO.valueOf(category);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategorySimpleVO.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CategorySimpleVO {
private Integer id;
private String name;
private String notes;
private Boolean enable;
private Integer parentId;
public static CategorySimpleVO fromEntity(Category po) {
if (po == null) return null;
CategorySimpleVO vo = new CategorySimpleVO();
vo.setId(po.getId());
vo.setName(po.getName());
vo.setNotes(po.getNotes());
vo.setEnable(po.getEnable());
vo.setParentId(po.getParent() != null ? po.getParent().getId() : 0);
return vo;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategorySpec.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.base.BookNameNotesEnableSpec;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
public final class CategorySpec {
public static Specification typeEqual(Integer type) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Category_.type), type);
}
public static Specification buildSpecification(CategoryQueryRequest request, Integer type, Book book) {
Specification specification = BookNameNotesEnableSpec.isBook(book);
if (request != null) {
if (StringUtils.hasText(request.getName())) {
specification = specification.and(BookNameNotesEnableSpec.nameLike(request.getName()));
}
if (request.getEnable() != null) {
specification = specification.and(BookNameNotesEnableSpec.isEnable(request.getEnable()));
}
}
specification = specification.and(typeEqual(type));
return specification;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryTreeVO.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import lombok.Getter;
import lombok.Setter;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class CategoryTreeVO {
private Integer id;
private String name;
private String notes;
private Boolean enable;
private List children;
private Integer parentId;
private String parentName;
public static List valueOfList(List categories) {
List categoryTreeVOList = new ArrayList<>();
for (Category item : categories) {
if (item.getParent() == null) {
categoryTreeVOList.add(CategoryTreeVO.valueOf(item, categories));
}
}
return categoryTreeVOList;
}
public static CategoryTreeVO valueOf(Category category, List categories) {
CategoryTreeVO categoryTreeVO = new CategoryTreeVO();
categoryTreeVO.setId(category.getId());
categoryTreeVO.setName(category.getName());
categoryTreeVO.setNotes(category.getNotes());
categoryTreeVO.setEnable(category.getEnable());
categoryTreeVO.setParentId(category.getParent() == null ? null : category.getParent().getId());
categoryTreeVO.setParentName(category.getParent() == null ? null : category.getParent().getName());
if (!CollectionUtils.isEmpty(category.getChildren(categories))) {
for (Category item : category.getChildren(categories)) {
if (categoryTreeVO.getChildren() == null) {
categoryTreeVO.setChildren(new ArrayList<>());
}
categoryTreeVO.getChildren().add(valueOf(item, categories));
}
}
return categoryTreeVO;
}
public static CategoryTreeVO valueOf(Category category) {
CategoryTreeVO categoryTreeVO = new CategoryTreeVO();
categoryTreeVO.setId(category.getId());
categoryTreeVO.setName(category.getName());
categoryTreeVO.setNotes(category.getNotes());
categoryTreeVO.setEnable(category.getEnable());
if (category.getParent() != null) categoryTreeVO.setParentId(category.getParent().getId());
if (category.getParent() != null) categoryTreeVO.setParentName(category.getParent().getName());
return categoryTreeVO;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/CategoryUpdateRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
import com.jiukuaitech.bookkeeping.user.validation.NameValidator;
import com.jiukuaitech.bookkeeping.user.validation.NotesValidator;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CategoryUpdateRequest {
@NameValidator
private String name;
@NotesValidator
private String notes;
private Integer parentId;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/DefaultExpenseCategoryException.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
public class DefaultExpenseCategoryException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/DefaultIncomeCategoryException.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
public class DefaultIncomeCategoryException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category/ParentCategoryNotEnableException.java
================================================
package com.jiukuaitech.bookkeeping.user.category;
/*
添加子分类时,父分类不可用
*/
public class ParentCategoryNotEnableException extends RuntimeException {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category_relation/CategoryRelation.java
================================================
package com.jiukuaitech.bookkeeping.user.category_relation;
import com.jiukuaitech.bookkeeping.user.category.Category;
import com.jiukuaitech.bookkeeping.user.deal.Deal;
import com.jiukuaitech.bookkeeping.user.validation.AmountValidator;
import com.jiukuaitech.bookkeeping.user.base.BaseEntity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Entity
//@Table(name = "t_category_relation", uniqueConstraints = {@UniqueConstraint(columnNames = {"deal_id", "category_id"})})
@Table(name = "t_category_relation")
@Getter
@Setter
public class CategoryRelation extends BaseEntity {
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
@NotNull
private Category category;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "deal_id")
@NotNull
private Deal deal;
@Column(nullable = false)
@NotNull
@AmountValidator
private BigDecimal amount; // 金额
@Column(nullable = false)
@NotNull
@AmountValidator
private BigDecimal convertedAmount; // 金额
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category_relation/CategoryRelationAddRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.category_relation;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.category.Category;
import com.jiukuaitech.bookkeeping.user.deal.Deal;
import com.jiukuaitech.bookkeeping.user.validation.AmountValidator;
import com.jiukuaitech.bookkeeping.user.balance_flow.AmountInvalidateException;
import com.jiukuaitech.bookkeeping.user.deal.CategoryConflictException;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Getter
@Setter
public class CategoryRelationAddRequest {
@NotNull
private Integer categoryId;
@NotNull
@AmountValidator
// @Positive ///可以负数,代表退款
private BigDecimal amount;
private BigDecimal convertedAmount;
public static void checkCategory(List categories) {
Set set = new HashSet<>();
for (CategoryRelationAddRequest item : categories) {
if (BigDecimal.ZERO.compareTo(item.getAmount()) == 0) {
throw new AmountInvalidateException();
}
if (item.getConvertedAmount() != null && BigDecimal.ZERO.compareTo(item.getConvertedAmount()) == 0) {
throw new AmountInvalidateException();
}
if (set.contains(item.getCategoryId())) {
throw new CategoryConflictException();
} else {
set.add(item.getCategoryId());
}
}
}
public CategoryRelation getRelation(Deal deal, Book book) {
CategoryRelation po = new CategoryRelation();
po.setAmount(amount);
if (deal.getAccount().getCurrencyCode().equals(book.getDefaultCurrencyCode())) {
po.setConvertedAmount(amount);
} else {
po.setConvertedAmount(convertedAmount);
}
po.setCategory(new Category(categoryId));
po.setDeal(deal);
return po;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category_relation/CategoryRelationRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.category_relation;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.base.BaseRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CategoryRelationRepository extends BaseRepository {
@Query("SELECT p1 FROM CategoryRelation p1 INNER JOIN p1.deal p2 WHERE p2.book = :book AND p2.status <> 2 AND p2.createTime BETWEEN :start AND :end")
List findByBookAndCreateTimeBetween(Book book, Long start, Long end);
Integer countByCategory_id(Integer id);
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/category_relation/CategoryRelationVOForList.java
================================================
package com.jiukuaitech.bookkeeping.user.category_relation;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class CategoryRelationVOForList {
private Integer id;
private BigDecimal amount;
private BigDecimal convertedAmount;
private Integer categoryId;
private String categoryName;
public static CategoryRelationVOForList fromEntity(CategoryRelation po) {
if (po == null) return null;
CategoryRelationVOForList vo = new CategoryRelationVOForList();
vo.setId(po.getId());
vo.setAmount(po.getAmount());
vo.setConvertedAmount(po.getConvertedAmount());
vo.setCategoryId(po.getCategory().getId());
vo.setCategoryName(po.getCategory().getName());
return vo;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/checking_account/CheckingAccount.java
================================================
package com.jiukuaitech.bookkeeping.user.checking_account;
import com.jiukuaitech.bookkeeping.user.account.Account;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
/**
* 活期账户,例如银行卡的活期账户,支付宝余额,余额宝,微信零钱等,一般是可以直接用于支出,也可作为收入的入账账户。
*/
@Entity
@DiscriminatorValue(value = "1")
public class CheckingAccount extends Account {
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/checking_account/CheckingAccountController.java
================================================
package com.jiukuaitech.bookkeeping.user.checking_account;
import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest;
import com.jiukuaitech.bookkeeping.user.account.AccountService;
import com.jiukuaitech.bookkeeping.user.base.BaseController;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import com.jiukuaitech.bookkeeping.user.account.AccountAddRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/checking-accounts")
public class CheckingAccountController extends BaseController {
@Resource
private CheckingAccountService checkingAccountService;
@Resource
private AccountService accountService;
@RequestMapping(method = RequestMethod.GET, value = "")
public BaseResponse handleQuery(
@Valid AccountQueryRequest request,
@PageableDefault(sort = "balance", direction = Sort.Direction.DESC) Pageable page,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(checkingAccountService.query(request, page, userSignInId));
}
@RequestMapping(method = RequestMethod.POST, value = "")
public BaseResponse handleAdd(
@Valid @RequestBody AccountAddRequest request,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.add(1, request, userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/sum")
public BaseResponse handleSum(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(checkingAccountService.sum(userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/checking_account/CheckingAccountRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.checking_account;
import com.jiukuaitech.bookkeeping.user.base.BaseRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CheckingAccountRepository extends BaseRepository {
/*
@Query("SELECT COALESCE(SUM(p.balance), 0), " +
"COALESCE(SUM(p.expense), 0), " +
"COALESCE(SUM(p.income), 0), " +
"COALESCE(SUM(p.transferFrom), 0), " +
"COALESCE(SUM(p.transferTo), 0) " +
"FROM CheckingAccount p WHERE p.book = :book AND p.enable = true")
List findSum(Book book);
@Query("SELECT COALESCE(SUM(p.balance), 0) FROM CheckingAccount p WHERE p.book = :book AND p.enable = true AND p.include = true")
BigDecimal findSumBalance(Book book);
*/
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/checking_account/CheckingAccountService.java
================================================
package com.jiukuaitech.bookkeeping.user.checking_account;
import com.jiukuaitech.bookkeeping.user.account.*;
import com.jiukuaitech.bookkeeping.user.currency.CurrencyService;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.user.UserService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
@Service
public class CheckingAccountService {
@Resource
private UserService userService;
@Resource
private CheckingAccountRepository checkingAccountRepository;
@Resource
private CurrencyService currencyService;
public Page query(AccountQueryRequest request, Pageable page, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
Specification specification = AccountSpec.buildSpecification(request, group);
Page poPage = checkingAccountRepository.findAll(specification, page);
Page voPage = poPage.map(AccountVOForExtend::fromEntity);
voPage.map(vo->{
vo.setConvertedBalance(currencyService.convert(vo.getBalance(), vo.getCurrencyCode(), group.getDefaultCurrencyCode()));
return vo;
});
return voPage;
}
public AccountSumVO sum(Integer userSignInId) {
AccountSumVO vo = new AccountSumVO();
Group group = userService.getUser(userSignInId).getDefaultGroup();
List accounts = checkingAccountRepository.findAll(AccountSpec.inGroupAndEnable(group));
BigDecimal balance = BigDecimal.ZERO;
for (int i = 0; i < accounts.size(); i++) {
Account account = accounts.get(i);
balance = balance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode()));
}
vo.setBalance(balance);
return vo;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccount.java
================================================
package com.jiukuaitech.bookkeeping.user.credit_account;
import com.jiukuaitech.bookkeeping.user.account.Account;
import com.jiukuaitech.bookkeeping.user.validation.BillDayValidator;
import com.jiukuaitech.bookkeeping.user.validation.CreditLimitValidator;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.validation.constraints.NotNull;
/**
* 信用账户,例如信用卡,花呗,白条等。一般可以直接用于支出,但不能作为收入的入账账户。
*/
@Entity
@DiscriminatorValue(value = "2")
@Getter
@Setter
public class CreditAccount extends Account {
// 非空
@Column(name = "credit_limit")
@NotNull
@CreditLimitValidator
private BigDecimal limit; // 信用额度
@BillDayValidator
private Integer billDay; // 每月多少号是账单日
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountAddRequest.java
================================================
package com.jiukuaitech.bookkeeping.user.credit_account;
import com.jiukuaitech.bookkeeping.user.account.AccountAddRequest;
import com.jiukuaitech.bookkeeping.user.validation.BillDayValidator;
import com.jiukuaitech.bookkeeping.user.validation.CreditLimitValidator;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Getter
@Setter
public class CreditAccountAddRequest extends AccountAddRequest {
@NotNull
@CreditLimitValidator
private BigDecimal limit; // 信用额度
@BillDayValidator
private Integer billDay; // 每月多少号是账单日
public void copyPrimitive(CreditAccount po) {
super.copyPrimitive(po);
po.setLimit(limit);
po.setBillDay(billDay);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountController.java
================================================
package com.jiukuaitech.bookkeeping.user.credit_account;
import com.jiukuaitech.bookkeeping.user.account.AccountService;
import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest;
import com.jiukuaitech.bookkeeping.user.base.BaseController;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/credit-accounts")
public class CreditAccountController extends BaseController {
@Resource
private CreditAccountService creditAccountService;
@Resource
private AccountService accountService;
@RequestMapping(method = RequestMethod.GET, value = "")
public BaseResponse handleQuery(
@Valid AccountQueryRequest request,
@PageableDefault(sort = "balance", direction = Sort.Direction.ASC) Pageable page,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(creditAccountService.query(request, page, userSignInId));
}
@RequestMapping(method = RequestMethod.POST, value = "")
public BaseResponse handleAdd(
@Valid @RequestBody CreditAccountAddRequest request,
@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(accountService.add(2, request, userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "/sum")
public BaseResponse handleSum(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(creditAccountService.sum(userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.credit_account;
import com.jiukuaitech.bookkeeping.user.base.BaseRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CreditAccountRepository extends BaseRepository {
/*
@Query("SELECT COALESCE(SUM(p.balance), 0), " +
"COALESCE(SUM(p.expense), 0), " +
"COALESCE(SUM(p.income), 0), " +
"COALESCE(SUM(p.transferFrom), 0), " +
"COALESCE(SUM(p.transferTo), 0), " +
"COALESCE(SUM(p.limit), 0) " +
"FROM CreditAccount p WHERE p.book = :book AND p.enable = true")
List findSum(Book book);
@Query("SELECT COALESCE(SUM(p.balance), 0) FROM CreditAccount p WHERE p.book = :book AND p.enable = true AND p.include = true")
BigDecimal findSumBalance(Book book);
*/
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountService.java
================================================
package com.jiukuaitech.bookkeeping.user.credit_account;
import com.jiukuaitech.bookkeeping.user.account.AccountQueryRequest;
import com.jiukuaitech.bookkeeping.user.account.AccountSpec;
import com.jiukuaitech.bookkeeping.user.currency.CurrencyService;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.user.UserService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
@Service
public class CreditAccountService {
@Resource
private UserService userService;
@Resource
private CreditAccountRepository creditAccountRepository;
@Resource
private CurrencyService currencyService;
public Page query(AccountQueryRequest request, Pageable page, Integer userSignInId) {
Group group = userService.getUser(userSignInId).getDefaultGroup();
Specification specification = AccountSpec.buildSpecification(request, group);
Page poPage = creditAccountRepository.findAll(specification, page);
Page voPage = poPage.map(CreditAccountVOForList::fromEntity);
voPage.map(vo->{
vo.setConvertedBalance(currencyService.convert(vo.getBalance(), vo.getCurrencyCode(), group.getDefaultCurrencyCode()));
return vo;
});
return voPage;
}
public CreditAccountSumVO sum(Integer userSignInId) {
CreditAccountSumVO vo = new CreditAccountSumVO();
Group group = userService.getUser(userSignInId).getDefaultGroup();
List accounts = creditAccountRepository.findAll(AccountSpec.inGroupAndEnable(group));
BigDecimal balance = BigDecimal.ZERO;
BigDecimal limit = BigDecimal.ZERO;
for (int i = 0; i < accounts.size(); i++) {
CreditAccount account = accounts.get(i);
balance = balance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode()));
limit = limit.add(currencyService.convert(account.getLimit(), account.getCurrencyCode(), group.getDefaultCurrencyCode()));
}
vo.setBalance(balance);
vo.setLimit(limit);
return vo;
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountSumVO.java
================================================
package com.jiukuaitech.bookkeeping.user.credit_account;
import com.jiukuaitech.bookkeeping.user.account.AccountSumVO;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class CreditAccountSumVO extends AccountSumVO {
private BigDecimal limit;
public BigDecimal getRemainLimit() {
return limit.add(getBalance());
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/credit_account/CreditAccountVOForList.java
================================================
package com.jiukuaitech.bookkeeping.user.credit_account;
import com.jiukuaitech.bookkeeping.user.account.AccountVOForExtend;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class CreditAccountVOForList extends AccountVOForExtend {
private BigDecimal limit;
private Integer billDay;
public static CreditAccountVOForList fromEntity(CreditAccount po) {
CreditAccountVOForList vo = new CreditAccountVOForList();
vo.setValue(po);
vo.setLimit(po.getLimit());
vo.setBillDay(po.getBillDay());
return vo;
}
public BigDecimal getRemainLimit() {
return limit.add(getBalance());
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/currency/Currency.java
================================================
package com.jiukuaitech.bookkeeping.user.currency;
import com.jiukuaitech.bookkeeping.user.base.BaseEntity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Entity
@Table(name = "t_currency")
@Getter
@Setter
public class Currency extends BaseEntity {
@Column(length = 8, nullable = false, unique = true)
@NotNull
private String code;
@Column(length = 128, nullable = false)
@NotNull
private String description;
@Column(nullable = false) //最多9亿
@NotNull
private BigDecimal rate;
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/currency/CurrencyController.java
================================================
package com.jiukuaitech.bookkeeping.user.currency;
import com.jiukuaitech.bookkeeping.user.base.BaseController;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/currency")
public class CurrencyController extends BaseController {
@Resource
private CurrencyService currencyService;
@RequestMapping(method = RequestMethod.GET, value = "/all")
public BaseResponse handleAll() {
return new DataResponse<>(currencyService.getAll());
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/currency/CurrencyRepository.java
================================================
package com.jiukuaitech.bookkeeping.user.currency;
import com.jiukuaitech.bookkeeping.user.base.BaseRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface CurrencyRepository extends BaseRepository {
Optional findOneByCode(String code);
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/currency/CurrencyService.java
================================================
package com.jiukuaitech.bookkeeping.user.currency;
import com.jiukuaitech.bookkeeping.user.exception.InputNotValidException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class CurrencyService {
@Resource
private CurrencyRepository currencyRepository;
public List getAll() {
return currencyRepository.findAll();
}
public void checkCode(String code) {
if (code == null) return;;
List currencyList = getAll();
List currencyCodeList = currencyList.stream().map(Currency::getCode).collect(Collectors.toList());
if (!currencyCodeList.contains(code)) {
throw new InputNotValidException();
}
}
// TODO 定时任务,数据存入缓存
public BigDecimal convert(String fromCode, String toCode) {
Currency fromCurrency = currencyRepository.findOneByCode(fromCode).orElseThrow();
Currency toCurrency = currencyRepository.findOneByCode(toCode).orElseThrow();
BigDecimal fromRate = fromCurrency.getRate();
BigDecimal toRate = toCurrency.getRate();
return toRate.divide(fromRate, 2, RoundingMode.CEILING);
}
public BigDecimal convert(BigDecimal amount, String fromCode, String toCode) {
if (fromCode.equals(toCode)) {
return amount;
}
return amount.multiply(convert(fromCode, toCode)).setScale(2, RoundingMode.CEILING);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/dashboard/AssetOverviewVO.java
================================================
package com.jiukuaitech.bookkeeping.user.dashboard;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
public class AssetOverviewVO {
private BigDecimal asset;//资产
private BigDecimal debt;//负债
public BigDecimal getNetWorth() {
return asset.subtract(debt);
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/dashboard/DashboardController.java
================================================
package com.jiukuaitech.bookkeeping.user.dashboard;
import com.jiukuaitech.bookkeeping.user.base.BaseController;
import com.jiukuaitech.bookkeeping.user.response.BaseResponse;
import com.jiukuaitech.bookkeeping.user.response.DataResponse;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/dashboard")
public class DashboardController extends BaseController {
@Resource
private DashboardService dashboardService;
@RequestMapping(method = RequestMethod.GET, value = "asset-overview")
public BaseResponse handleAssetOverview(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(dashboardService.assetOverview(userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "expense-income-table")
public BaseResponse handleTable(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(dashboardService.expenseIncomeTable(userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "expense-trend")
public BaseResponse handleExpenseTrend(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(dashboardService.expenseTrend(userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "income-trend")
public BaseResponse handleIncomeTrend(@RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(dashboardService.incomeTrend(userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "expense-category")
public BaseResponse handleExpenseCategory(@RequestParam Long start, @RequestParam Long end, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(dashboardService.expenseCategory(start, end, userSignInId));
}
@RequestMapping(method = RequestMethod.GET, value = "income-category")
public BaseResponse handleIncomeCategory(@RequestParam Long start, @RequestParam Long end, @RequestAttribute("userSignInId") Integer userSignInId) {
return new DataResponse<>(dashboardService.incomeCategory(start, end, userSignInId));
}
}
================================================
FILE: bookkeeping-user-api/src/main/java/com/jiukuaitech/bookkeeping/user/dashboard/DashboardService.java
================================================
package com.jiukuaitech.bookkeeping.user.dashboard;
import com.jiukuaitech.bookkeeping.user.account.Account;
import com.jiukuaitech.bookkeeping.user.account.AccountRepository;
import com.jiukuaitech.bookkeeping.user.account.AccountSpec;
import com.jiukuaitech.bookkeeping.user.asset_account.AssetAccount;
import com.jiukuaitech.bookkeeping.user.asset_account.AssetAccountRepository;
import com.jiukuaitech.bookkeeping.user.base.BaseEntity;
import com.jiukuaitech.bookkeeping.user.book.Book;
import com.jiukuaitech.bookkeeping.user.category.Category;
import com.jiukuaitech.bookkeeping.user.category.CategoryRepository;
import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelation;
import com.jiukuaitech.bookkeeping.user.category_relation.CategoryRelationRepository;
import com.jiukuaitech.bookkeeping.user.checking_account.CheckingAccount;
import com.jiukuaitech.bookkeeping.user.checking_account.CheckingAccountRepository;
import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccount;
import com.jiukuaitech.bookkeeping.user.credit_account.CreditAccountRepository;
import com.jiukuaitech.bookkeeping.user.currency.CurrencyService;
import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccount;
import com.jiukuaitech.bookkeeping.user.debt_account.DebtAccountRepository;
import com.jiukuaitech.bookkeeping.user.expense.ExpenseRepository;
import com.jiukuaitech.bookkeeping.user.group.Group;
import com.jiukuaitech.bookkeeping.user.income.IncomeRepository;
import com.jiukuaitech.bookkeeping.user.user.UserService;
import com.jiukuaitech.bookkeeping.user.reports.ChartVO;
import com.jiukuaitech.bookkeeping.user.utils.CalendarUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class DashboardService {
@Resource
private UserService userService;
@Resource
private AccountRepository accountRepository;
@Resource
private CheckingAccountRepository checkingAccountRepository;
@Resource
private CreditAccountRepository creditAccountRepository;
@Resource
private DebtAccountRepository debtAccountRepository;
@Resource
private AssetAccountRepository assetAccountRepository;
@Resource
private ExpenseRepository expenseRepository;
@Resource
private IncomeRepository incomeRepository;
@Resource
private CategoryRepository categoryRepository;
@Resource
private CategoryRelationRepository categoryRelationRepository;
@Resource
private CurrencyService currencyService;
public AssetOverviewVO assetOverview(Integer userSignInId) {
AssetOverviewVO vo = new AssetOverviewVO();
Group group = userService.getUser(userSignInId).getDefaultGroup();
List checkingAccounts = checkingAccountRepository.findAll(AccountSpec.inGroupAndInclude(group));
List assetAccounts = assetAccountRepository.findAll(AccountSpec.inGroupAndInclude(group));
BigDecimal assetBalance = BigDecimal.ZERO;
for (int i = 0; i < checkingAccounts.size(); i++) {
Account account = checkingAccounts.get(i);
assetBalance = assetBalance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode()));
}
for (int i = 0; i < assetAccounts.size(); i++) {
Account account = assetAccounts.get(i);
assetBalance = assetBalance.add(currencyService.convert(account.getBalance(), account.getCurrencyCode(), group.getDefaultCurrencyCode()));
}
vo.setAsset(assetBalance);
List