├── .dockerignore ├── .env ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ ├── other.yml │ └── question.yml └── workflows │ ├── release-docker.yaml │ ├── release-mysql-docker.yaml │ ├── release-pgsql-docker.yaml │ ├── release-sqlite-docker.yaml │ ├── release-sqlserver-docker.yaml │ └── test-docker.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app.config.ts ├── app.vue ├── assets └── css │ ├── calendar.css │ └── main.css ├── components ├── GlobalAlert.vue ├── GlobalConfirm.vue ├── charts │ ├── AttributionPie.vue │ ├── DailyLineChart.vue │ ├── IndustryTypePie.vue │ ├── MonthBar.vue │ └── PayTypePie.vue ├── datas │ ├── CsvFlowTable.vue │ ├── FlowTable.vue │ └── MonthAnalysis.vue └── dialog │ ├── BookDialog.vue │ ├── ChangePasswordDialog.vue │ ├── FlowAutoDeduplicationDialog.vue │ ├── FlowAutoMergeDialog.vue │ ├── FlowCustomImport.vue │ ├── FlowEditDialog.vue │ ├── FlowEditInvoiceDialog.vue │ ├── FlowJsonImportDialog.vue │ └── SetConvertDialog.vue ├── doc ├── DISPLAY.md ├── NOTICE.md └── readme.txt ├── docker ├── docker-compose.yaml └── entrypoint.sh ├── i18n.config.ts ├── layouts ├── admin.vue ├── default.vue └── public.vue ├── lib └── prisma.ts ├── locales ├── en │ ├── en.json │ └── index.ts └── zh │ ├── index.ts │ └── zh.json ├── middleware ├── admin.ts └── auth.ts ├── modules └── initdb.ts ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages ├── 500.vue ├── admin │ ├── books │ │ ├── EditInfoDialog.vue │ │ ├── api.ts │ │ ├── flag.ts │ │ └── index.client.vue │ ├── getpassword.vue │ ├── index.client.vue │ ├── login.client.vue │ ├── settings │ │ └── index.client.vue │ ├── typeRelations │ │ ├── EditInfoDialog.vue │ │ ├── api.ts │ │ ├── flag.ts │ │ └── index.client.vue │ └── users │ │ ├── EditInfoDialog.vue │ │ ├── api.ts │ │ ├── flag.ts │ │ └── index.client.vue ├── analysis │ └── index.client.vue ├── books │ ├── EditInfoDialog.vue │ ├── GetShareDialog.vue │ ├── api.ts │ ├── flag.ts │ └── index.client.vue ├── budget │ └── index.client.vue ├── calendar │ └── index.client.vue ├── flows │ └── index.client.vue ├── index.vue ├── login.vue └── types │ └── index.client.vue ├── plugins └── system.ts ├── prisma ├── migrations │ ├── 20250116134313_init │ │ └── migration.sql │ ├── 20250206090137_update_user_email │ │ └── migration.sql │ ├── 20250209061645_add_flow_and_book │ │ └── migration.sql │ ├── 20250315083738_add_budget │ │ └── migration.sql │ ├── 20250316033948_fix_budget_month │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── cashbook-mini.jpg ├── csvtemplate.csv ├── logo.png ├── manifest.json ├── pwa │ ├── manifest-icon-192.maskable.png │ └── manifest-icon-512.maskable.png └── robots.txt ├── server ├── api │ ├── admin │ │ ├── entry │ │ │ ├── books │ │ │ │ ├── all.ts │ │ │ │ ├── del.ts │ │ │ │ ├── list.ts │ │ │ │ ├── page.ts │ │ │ │ └── update.ts │ │ │ ├── settings │ │ │ │ ├── export.ts │ │ │ │ ├── exportImg.ts │ │ │ │ ├── get.ts │ │ │ │ ├── import.ts │ │ │ │ ├── importImg.ts │ │ │ │ └── update.ts │ │ │ ├── typeRelations │ │ │ │ ├── add.ts │ │ │ │ ├── all.ts │ │ │ │ ├── del.ts │ │ │ │ ├── list.ts │ │ │ │ ├── page.ts │ │ │ │ └── update.ts │ │ │ └── users │ │ │ │ ├── add.ts │ │ │ │ ├── all.ts │ │ │ │ ├── del.ts │ │ │ │ ├── list.ts │ │ │ │ ├── page.ts │ │ │ │ └── update.ts │ │ ├── getPassword.ts │ │ ├── login.ts │ │ └── logout.ts │ ├── check.ts │ ├── checkuser.ts │ ├── config.ts │ ├── entry │ │ ├── analytics │ │ │ ├── attribution.ts │ │ │ ├── daily.ts │ │ │ ├── industryType.ts │ │ │ ├── month.ts │ │ │ ├── monthAnalysis.ts │ │ │ └── payType.ts │ │ ├── book │ │ │ ├── add.ts │ │ │ ├── all.ts │ │ │ ├── del.ts │ │ │ ├── inshare.ts │ │ │ ├── list.ts │ │ │ ├── page.ts │ │ │ ├── share.ts │ │ │ └── update.ts │ │ ├── budget │ │ │ ├── add.ts │ │ │ ├── all.ts │ │ │ ├── del.ts │ │ │ ├── list.ts │ │ │ ├── reloadUsedAmount.ts │ │ │ └── update.ts │ │ ├── fixedFlow │ │ │ ├── add.ts │ │ │ ├── all.ts │ │ │ ├── del.ts │ │ │ ├── list.ts │ │ │ └── update.ts │ │ ├── flow │ │ │ ├── add.ts │ │ │ ├── all.ts │ │ │ ├── condidate │ │ │ │ ├── autos.ts │ │ │ │ ├── confirm.ts │ │ │ │ ├── ignore.ts │ │ │ │ └── ignoreAll.ts │ │ │ ├── deduplication │ │ │ │ └── autos.ts │ │ │ ├── del.ts │ │ │ ├── dels.ts │ │ │ ├── getAttributions.ts │ │ │ ├── getNames.ts │ │ │ ├── imports.ts │ │ │ ├── invoice │ │ │ │ ├── clean.ts │ │ │ │ ├── del.ts │ │ │ │ ├── show.get.ts │ │ │ │ └── upload.ts │ │ │ ├── list.ts │ │ │ ├── page.ts │ │ │ ├── type │ │ │ │ ├── getAll.ts │ │ │ │ ├── getIndustryType.ts │ │ │ │ ├── getPayType.ts │ │ │ │ └── update.ts │ │ │ ├── update.ts │ │ │ └── updates.ts │ │ ├── test.ts │ │ ├── typeRelation │ │ │ ├── list.ts │ │ │ └── update.ts │ │ └── user │ │ │ ├── changePassword.ts │ │ │ └── info.ts │ ├── login.ts │ ├── logout.ts │ └── register.ts ├── middleware │ └── auth.ts ├── plugins │ └── initdata.ts ├── routes │ └── test.ts └── utils │ ├── common.ts │ ├── data.ts │ └── test.js ├── tsconfig.json └── utils ├── alert.ts ├── api.ts ├── apis.ts ├── common.ts ├── confirm.ts ├── constant.ts ├── fileUtils.ts ├── flag.ts ├── flowConvert.ts ├── model.ts ├── store.ts └── table.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | .nuxt/ 2 | .output/ 3 | .github/ 4 | .git/ 5 | 6 | node_modules/ 7 | data/ 8 | doc/ -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" # development, production 2 | DATEBASE_PROVIDER="postgresql" 3 | DATABASE_URL="postgresql://postgres:123456@127.0.0.1:5432/cashbook?schema=sample" 4 | 5 | NUXT_APP_VERSION="4.1.2" 6 | NUXT_DATA_PATH="E:/Code/cashbook/data" 7 | 8 | NUXT_AUTH_SECRET="auth123" 9 | 10 | NUXT_ADMIN_USERNAME="admin" 11 | NUXT_ADMIN_PASSWORD="fb35e9343a1c095ce1c1d1eb6973dc570953159441c3ee315ecfefb6ed05f4cc" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "Bug👻反馈" 2 | description: 提交 Bug 反馈 3 | title: "[BUG]: " 4 | labels: [bug] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: 问题概述 9 | description: 请简要描述您遇到的问题。 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | attributes: 15 | label: 环境信息 16 | description: 请提供以下环境信息: 17 | value: | 18 | - 服务器操作系统: 19 | - Cashbook部署方式: 20 | - Cashbook版本: 21 | - 浏览器版本: 22 | - 其他相关信息: 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | attributes: 28 | label: 截图/录屏/日志 29 | description: 请附上截图、录屏或错误日志信息,以便我们更直观地了解问题。 30 | value: | 31 | **截图/录屏**: 32 | 33 | **日志**: 34 | 35 | - type: textarea 36 | attributes: 37 | label: 复现步骤/预期结果/实际结果 38 | description: 请详细描述如何复现问题,包括操作步骤、截图或录屏等。 39 | value: | 40 | **复现步骤**: 41 | 42 | **预期结果**: 43 | 44 | **实际结果**: 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 官方 QQ 群 4 | link: https://qm.qq.com/q/nmaDsy62jI 5 | about: '点击链接加入群聊【Cashbook交流群】: 564081656' 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 功能❤️建议 2 | description: 提交我的有趣想法 3 | title: "[功能建议]: " 4 | labels: [enhancement] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: 详细描述 9 | description: 请详细描述您希望添加的功能,包括具体用法、界面设计等。 10 | 11 | - type: textarea 12 | attributes: 13 | label: 理由 14 | description: 请说明您提出此建议的理由,以及该功能可能带来的好处。 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.yml: -------------------------------------------------------------------------------- 1 | name: 其他👽 2 | description: 提交其他类型Issue 3 | title: "[其他]: " 4 | labels: [other] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: 问题描述 9 | description: 请在此处填写您的 Issue 内容。 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: 萌新🤗提问 2 | description: 有不懂的问题,向大家提问 3 | title: "[提问]: " 4 | labels: [question] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: 问题描述 9 | description: 请详细描述您遇到的问题,包括具体错误信息、操作步骤等。 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | attributes: 15 | label: 其他说明 16 | description: 如果您有其他需要补充说明的内容,请在此处填写。 17 | -------------------------------------------------------------------------------- /.github/workflows/release-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Release-Docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | docker_build: 10 | environment: docker_hub 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Get version 18 | id: get_version 19 | run: | 20 | VERSION=$(git describe --tags --always --match 'v*' | sed -n 's|^v\([0-9]*\.[0-9]*\.[0-9]*\)\(-.*\)\{0,1\}|\1|p') 21 | echo "VERSION=$VERSION" >> $GITHUB_ENV # Store version as an environment variable 22 | 23 | - name: Log in to Docker Hub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKER_USERNAME }} 27 | password: ${{ secrets.DOCKER_PASSWORD }} 28 | 29 | - name: Build Docker image 30 | run: docker build -t dingdangdog/cashbook:${VERSION} -t dingdangdog/cashbook:latest . 31 | 32 | - name: Push Docker image 33 | run: | 34 | docker push dingdangdog/cashbook:${VERSION} 35 | docker push dingdangdog/cashbook:latest 36 | 37 | # 设置 Docker Buildx 38 | - name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v3 40 | 41 | # 构建 arm64 镜像并推送 42 | - name: Build and push arm64 image 43 | run: | 44 | docker buildx build \ 45 | --platform linux/arm64 \ 46 | --tag dingdangdog/cashbook:${VERSION}-arm64 \ 47 | --tag dingdangdog/cashbook:latest-arm64 \ 48 | --push . 49 | 50 | - name: Clean up Docker images 51 | run: docker system prune -af -------------------------------------------------------------------------------- /.github/workflows/release-mysql-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Release-MySQL-Docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | docker_build: 10 | environment: docker_hub 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | ref: main-mysql 18 | 19 | - name: Get version 20 | id: get_version 21 | run: | 22 | # Get the tag name from the GitHub context (without the 'v' prefix) 23 | VERSION="${GITHUB_REF#refs/tags/v}" 24 | echo "VERSION=$VERSION" >> $GITHUB_ENV # Store version as an environment variable 25 | 26 | - name: Log in to Docker Hub 27 | uses: docker/login-action@v3 28 | with: 29 | username: ${{ secrets.DOCKER_USERNAME }} 30 | password: ${{ secrets.DOCKER_PASSWORD }} 31 | 32 | - name: Build Docker image 33 | run: docker build -t dingdangdog/cashbook:${VERSION}-mysql -t dingdangdog/cashbook:latest-mysql . 34 | 35 | - name: Push Docker image 36 | run: | 37 | docker push dingdangdog/cashbook:${VERSION}-mysql 38 | docker push dingdangdog/cashbook:latest-mysql 39 | 40 | # 设置 Docker Buildx 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v3 43 | 44 | # 构建 arm64 镜像并推送 45 | - name: Build and push arm64 image 46 | run: | 47 | docker buildx build \ 48 | --platform linux/arm64 \ 49 | --tag dingdangdog/cashbook:${VERSION}-arm64-mysql \ 50 | --tag dingdangdog/cashbook:latest-arm64-mysql \ 51 | --push . 52 | 53 | - name: Clean up Docker images 54 | run: docker system prune -af 55 | -------------------------------------------------------------------------------- /.github/workflows/release-pgsql-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Release-PgSQL-Docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | docker_build: 10 | environment: docker_hub 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | ref: main-pgsql 18 | 19 | - name: Get version 20 | id: get_version 21 | run: | 22 | # Get the tag name from the GitHub context (without the 'v' prefix) 23 | VERSION="${GITHUB_REF#refs/tags/v}" 24 | echo "VERSION=$VERSION" >> $GITHUB_ENV # Store version as an environment variable 25 | 26 | - name: Log in to Docker Hub 27 | uses: docker/login-action@v3 28 | with: 29 | username: ${{ secrets.DOCKER_USERNAME }} 30 | password: ${{ secrets.DOCKER_PASSWORD }} 31 | 32 | - name: Build Docker image 33 | run: docker build -t dingdangdog/cashbook:${VERSION}-pgsql -t dingdangdog/cashbook:latest-pgsql . 34 | 35 | - name: Push Docker image 36 | run: | 37 | docker push dingdangdog/cashbook:${VERSION}-pgsql 38 | docker push dingdangdog/cashbook:latest-pgsql 39 | 40 | # 设置 Docker Buildx 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v3 43 | 44 | # 构建 arm64 镜像并推送 45 | - name: Build and push arm64 image 46 | run: | 47 | docker buildx build \ 48 | --platform linux/arm64 \ 49 | --tag dingdangdog/cashbook:${VERSION}-arm64-pgsql \ 50 | --tag dingdangdog/cashbook:latest-arm64-pgsql \ 51 | --push . 52 | 53 | - name: Clean up Docker images 54 | run: docker system prune -af 55 | -------------------------------------------------------------------------------- /.github/workflows/release-sqlite-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Release-SQLite-Docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | docker_build: 10 | environment: docker_hub 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | ref: main-sqlite 18 | 19 | - name: Get version 20 | id: get_version 21 | run: | 22 | # Get the tag name from the GitHub context (without the 'v' prefix) 23 | VERSION="${GITHUB_REF#refs/tags/v}" 24 | echo "VERSION=$VERSION" >> $GITHUB_ENV # Store version as an environment variable 25 | 26 | - name: Log in to Docker Hub 27 | uses: docker/login-action@v3 28 | with: 29 | username: ${{ secrets.DOCKER_USERNAME }} 30 | password: ${{ secrets.DOCKER_PASSWORD }} 31 | 32 | - name: Build Docker image 33 | run: docker build -t dingdangdog/cashbook:${VERSION}-sqlite -t dingdangdog/cashbook:latest-sqlite . 34 | 35 | - name: Push Docker image 36 | run: | 37 | docker push dingdangdog/cashbook:${VERSION}-sqlite 38 | docker push dingdangdog/cashbook:latest-sqlite 39 | 40 | # 设置 Docker Buildx 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v3 43 | 44 | # 构建 arm64 镜像并推送 45 | - name: Build and push arm64 image 46 | run: | 47 | docker buildx build \ 48 | --platform linux/arm64 \ 49 | --tag dingdangdog/cashbook:${VERSION}-arm64-sqlite \ 50 | --tag dingdangdog/cashbook:latest-arm64-sqlite \ 51 | --push . 52 | 53 | - name: Clean up Docker images 54 | run: docker system prune -af 55 | -------------------------------------------------------------------------------- /.github/workflows/release-sqlserver-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Release-SqlServer-Docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | docker_build: 10 | environment: docker_hub 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | ref: main-sqlserver 18 | 19 | - name: Get version 20 | id: get_version 21 | run: | 22 | # Get the tag name from the GitHub context (without the 'v' prefix) 23 | VERSION="${GITHUB_REF#refs/tags/v}" 24 | echo "VERSION=$VERSION" >> $GITHUB_ENV # Store version as an environment variable 25 | 26 | - name: Log in to Docker Hub 27 | uses: docker/login-action@v3 28 | with: 29 | username: ${{ secrets.DOCKER_USERNAME }} 30 | password: ${{ secrets.DOCKER_PASSWORD }} 31 | 32 | - name: Build Docker image 33 | run: docker build -t dingdangdog/cashbook:${VERSION}-sqlserver -t dingdangdog/cashbook:latest-sqlserver . 34 | 35 | - name: Push Docker image 36 | run: | 37 | docker push dingdangdog/cashbook:${VERSION}-sqlserver 38 | docker push dingdangdog/cashbook:latest-sqlserver 39 | 40 | # 设置 Docker Buildx 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v3 43 | 44 | # 构建 arm64 镜像并推送 45 | - name: Build and push arm64 image 46 | run: | 47 | docker buildx build \ 48 | --platform linux/arm64 \ 49 | --tag dingdangdog/cashbook:${VERSION}-arm64-sqlserver \ 50 | --tag dingdangdog/cashbook:latest-arm64-sqlserver \ 51 | --push . 52 | 53 | - name: Clean up Docker images 54 | run: docker system prune -af 55 | -------------------------------------------------------------------------------- /.github/workflows/test-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Test Docker 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | target: 7 | description: 输入打包版本号 8 | required: true 9 | 10 | jobs: 11 | docker_build: 12 | environment: docker_hub 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Build Docker image 20 | run: docker build -t dingdangdog/cashbook:${{ github.event.inputs.target }} . 21 | 22 | - name: Log in to Docker Hub 23 | uses: docker/login-action@v3 24 | with: 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} 27 | 28 | - name: Push Docker image 29 | run: | 30 | docker push dingdangdog/cashbook:${{ github.event.inputs.target }} 31 | 32 | - name: Clean up Docker images 33 | run: docker system prune -af 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | data 5 | .nuxt 6 | .nitro 7 | .cache 8 | dist 9 | 10 | # Node dependencies 11 | node_modules 12 | 13 | # Logs 14 | logs 15 | *.log 16 | 17 | # Misc 18 | .DS_Store 19 | .fleet 20 | .idea 21 | 22 | # Local env files 23 | .env 24 | .env.* 25 | !.env.example 26 | 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine3.21 AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | # 安装依赖并生成 Prisma Client 8 | RUN npm install 9 | 10 | COPY . . 11 | 12 | RUN npx prisma generate 13 | RUN npm run build 14 | # RUN npm run prisma:build 15 | 16 | FROM node:20-alpine3.21 AS runner 17 | 18 | LABEL author.name="DingDangDog" 19 | LABEL author.email="dingdangdogx@outlook.com" 20 | LABEL project.name="cashbook" 21 | LABEL project.version="3" 22 | 23 | WORKDIR /app 24 | 25 | # 复制生产环境需要的文件 26 | COPY --from=builder /app/.output/ ./ 27 | COPY --from=builder /app/.output/server/node_modules/ ./node_modules/ 28 | COPY --from=builder /app/.output/server/node_modules/.prisma/ ./.prisma/ 29 | COPY ./prisma/ ./prisma/ 30 | COPY ./docker/entrypoint.sh ./entrypoint.sh 31 | RUN chmod +x entrypoint.sh 32 | 33 | # RUN ls 34 | 35 | # 预装prisma,可以提升容器启动速度,但镜像体积会大很多 36 | RUN npm install -g prisma@6.2.1 37 | 38 | ENV DATABASE_URL="postgresql://postgres:123456@localhost:5432/cashbook?schema=public" 39 | 40 | ENV NUXT_APP_VERSION="4.1.6" 41 | ENV NUXT_DATA_PATH="/app/data" 42 | 43 | ENV NUXT_AUTH_SECRET="auth123" 44 | 45 | ENV NUXT_ADMIN_USERNAME="admin" 46 | # 密码是加密后的,加密方法见 server/utils 中的 test.js 或 common.ts 47 | ENV NUXT_ADMIN_PASSWORD="fb35e9343a1c095ce1c1d1eb6973dc570953159441c3ee315ecfefb6ed05f4cc" 48 | 49 | ENV PORT="9090" 50 | 51 | VOLUME /app/data/ 52 | 53 | EXPOSE 9090 54 | # ENTRYPOINT [ "sh","entrypoint.sh" ] 55 | ENTRYPOINT ["/app/entrypoint.sh"] 56 | # CMD ["/app/entrypoint.sh"] 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 dingdangdog 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | cashbook 3 |

Cashbook

4 |
5 | 6 |

7 | release 8 | stars 9 | dorks 10 |

11 |

12 | issues-open 13 | issues-close 14 | license 15 | Docker Pulls 16 | 17 |

18 | 19 | - 官方文档站:[https://doc.cashbook.oldmoon.top](https://doc.cashbook.oldmoon.top) 20 | - 在线体验:[cashbook.oldmoon.top](https://cashbook.oldmoon.top/) (体验账号: `cashbook`/`cashbook`) 21 | - 在线体验后台:[cashbook.oldmoon.top/admin](https://cashbook.oldmoon.top/admin) (体验账号: `admin`/`admin123456`) 22 | - QQ交流群:`564081656` 23 | 24 | ## 简述(Description) 25 | 26 | Cashbook记账本。 27 | 28 | - 在数据记录上追求简单、易用、自主可控; 29 | - 在统计分析上力求清晰、美观、简洁有效。 30 | 31 | **重要提示:如果需要部署到公网,请自行修改各类环境变量!!!** 32 | (如:后台账号密码、数据库密码等) 33 | 34 | ## 开始使用(Get Started) 35 | 36 | - [使用说明](https://doc.cashbook.oldmoon.top/guide/) 37 | - [部署手册](https://doc.cashbook.oldmoon.top/deploy/) 38 | - [开发指南](https://doc.cashbook.oldmoon.top/development/) 39 | - [常见问题](https://doc.cashbook.oldmoon.top/question/) 40 | 41 | ## 版本对照表 42 | 43 | 请阅读下面的版本对照表,选择适合你的版本进行部署! 44 | 45 | |数据库差异|latest(x86_64)|latest(arm64)|指定版本(x86_64)|指定版本(arm64)| 46 | |---|---|---|---|---| 47 | |pgsql(latest)|latest|latest-arm64|4.1.6|4.1.6-arm64| 48 | |pgsql|latest-pgsql|latest-arm64-pgsql|4.1.6-pgsql|4.1.6-arm64-pgsql| 49 | |mysql|latest-mysql|latest-arm64-mysql|4.1.6-mysql|4.1.6-arm64-mysql| 50 | |sqlite|latest-sqlite|latest-arm64-sqlite|4.1.6-sqlite|4.1.6-arm64-sqlite| 51 | |sqlserver|latest-sqlserver|latest-arm64-sqlserver|4.1.6-sqlserver|4.1.6-arm64-sqlserver| 52 | 53 | 详情请查看 [DockerHub](https://hub.docker.com/repository/docker/dingdangdog/cashbook/tags) 54 | 55 | ## 主要功能 56 | 57 | - [x] 前台后台分离,独立后台方便对系统进行管理; 58 | - [x] 前台注册功能; 59 | - [x] 明暗主题; 60 | - [x] `Docker` 部署; 61 | - [x] 支持 *支付宝CSV* 账单文件导入; 62 | - [x] 支持 *微信CSV* 账单文件导入; 63 | - [x] 支持 *京东金融CSV* 账单文件导入; 64 | - [x] 三方数据导入时,消费类型自动转换(可以自行配置转换结果); 65 | - [x] 支持 *模板导入* ; 66 | - [x] 直观的消费日历看板; 67 | - [x] 月度账单分析(后期集成个AI?); 68 | - [x] 美观的数据分析图表,包括图标如下: 69 | - [x] 支出类型统计饼图; 70 | - [x] 支付方式统计饼图; 71 | - [x] 每日流水统计曲线图; 72 | - [x] 每月流水统计柱状图; 73 | - [x] 流水归属统计饼图; 74 | - [x] 多用户模式,用户之间数据隔离; 75 | - [x] 多账本模式,账本之间数据独立; 76 | - [x] 需要数据库:Postgre数据库; 77 | - [x] 可以上传小票图片; 78 | - [x] 账本数据快速迁移(账本数据导入/导出); 79 | - [x] 系统数据快速迁移(系统数据导入/导出); 80 | - [x] 自助平账(收入/支出抵消); 81 | - [x] 共享账本(多用户共用一个账本); 82 | - [ ] 移动端适配(难搞); 83 | - [ ] 主题系统(没做过,不会做,但想做); 84 | - [ ] …… 85 | 86 | ## Star 87 | 88 | [![Star History Chart](https://api.star-history.com/svg?repos=dingdangdog/cashbook&type=Date)](https://star-history.com/#dingdangdog/cashbook&Date) 89 | 90 | ## 贡献者(Contributor) 91 | 92 | 93 | 94 | 102 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | title: 'Hello Nuxt', 3 | theme: { 4 | dark: true, 5 | colors: { 6 | primary: '#ff0000' 7 | } 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | overflow-y: auto; 3 | } 4 | 5 | ::-webkit-scrollbar { 6 | width: 6px; 7 | height: 6px; 8 | } 9 | ::-webkit-scrollbar-track { 10 | background-color: rgb(179, 179, 179, 0.5); 11 | } 12 | ::-webkit-scrollbar-thumb { 13 | background-color: rgba(0, 144, 254, 0.5); 14 | } 15 | 16 | .top-index { 17 | z-index: 999; 18 | } 19 | 20 | .table-default-column { 21 | width: 6rem; 22 | text-overflow: ellipsis; 23 | word-wrap: break-word; 24 | } 25 | 26 | .chart-common-container { 27 | margin: 0.5rem; 28 | padding: 0.5rem; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | border-radius: 1rem; 33 | border: 1px solid rgb(160, 160, 160); 34 | } 35 | 36 | .queryParam { 37 | margin-bottom: 0.5rem; 38 | min-width: 12rem; 39 | } 40 | -------------------------------------------------------------------------------- /components/GlobalAlert.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | 37 | 44 | -------------------------------------------------------------------------------- /components/GlobalConfirm.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /components/datas/MonthAnalysis.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 113 | 114 | 120 | -------------------------------------------------------------------------------- /components/dialog/ChangePasswordDialog.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /components/dialog/FlowJsonImportDialog.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /doc/DISPLAY.md: -------------------------------------------------------------------------------- 1 | # 截图展示(Screenshot Display) 2 | 3 | ## 主界面 4 | 5 | ![main](./images/old/black-1.jpg) 6 | 7 | ![main](./images/old/light-1.jpg) 8 | 9 | ## 支出类型统计 10 | 11 | 12 | ![main](./images/old/black-2.jpg) 13 | 14 | ![main](./images/old/light-2.jpg) 15 | 16 | ## 消费日历 17 | 18 | ![main](./images/old/black-3.jpg) 19 | 20 | ![main](./images/old/light-3.jpg) 21 | 22 | 23 | ## 字典管理 24 | 25 | 26 | ![main](./images/old/black-4.jpg) 27 | 28 | ![main](./images/old/light-4.jpg) -------------------------------------------------------------------------------- /doc/NOTICE.md: -------------------------------------------------------------------------------- 1 | # 重要说明(Important Notice) 2 | 3 | ## 在线同步 4 | 5 | **功能说明**:2023年6月28日,在线同步功能已上线。要使用在线同步功能,需要`服务器地址`和`服务器授权码`两个信息。 6 | 7 | 1. 建议将`在线同步功能`作为`导入导出`的备选功能!当由于客观原因无法使用`导入导出`同步数据时,再选择`在线同步`! 8 | 2. 在线同步功能以服务器授权码存储数据,不区分账本(即多个账本可以使用同一个授权码) 9 | 3. 目前每次上传的数据都会保存,但下载只会拉取对应授权码最后一次上传的数据。 10 | 4. 授权码可能有上传次数限制,但下载不会限制次数。 11 | 5. **当前用于支持在线同步的云服务为个人搭建并测试,可能会存在不稳定、不安全情况,一旦出现任何问题概不负责。** 12 | 13 | > - 在线同步功能需要云服务的支持,目前已经搭建了一个可供大家使用的服务,地址为`https://cc.oldmoon.top`。 14 | > - 在线同步功能需要服务器授权码,因为功能刚上线,可能会有一些问题,所以给大家提供四个免费的测试授权码: 15 | > - `a1180cee3e414e8386e2bdd08ae940aa` 16 | > - `18a2068db29148848f37a2bec9a21ae7` 17 | > - `25cdafad4b6045869f034561fe5abfd8` 18 | > - `1c58f9d047124952ad52fdf6c481d045` 19 | 20 | **再次提示::** 21 | 22 | - 1、每个授权码每天只有三次上传数据的机会,如果测试发现上传失败,可能是因为次数耗尽; 23 | - 2、授权码下载时只会下载最后一次上传的数据,多人使用同一个授权码时可能在造成数据混乱; 24 | - 3、现在还在试用阶段,出现任何问题概不负责; 25 | - 4、想要一个自己使用的授权码,可以联系我; 26 | - 5、该功能后续还会有多个迭代,可能会产生较大变动; 27 | - 6、该功能后期可能会根据使用情况变更为收费功能; 28 | - 7、该功能支持私人搭建云服务,但需要一些技术成本,有兴趣或有能力者可以自行尝试。 29 | - 8、从2023年7月4日开始,测试授权码有效期为180天。 30 | 31 | 32 | ## 字典 33 | 34 | - 字典类型(distType)是最基础的字典,可以认为是字典的字典,`1 distType distType 字典类型 1`必须存在,否则不能新增字典类型。 35 | - 消费类型、支付方式字典的`key`和`value`建议保持一致。为了减少代码量和连表操作,统计时直接用的`key`。 36 | 37 | ## 导出导入 38 | 39 | - 导出功能会将当前账本的全部流水统一导出; 40 | - 导出数据为`json`格式文件,若有需要,可将数据随意使用; 41 | - 导出数据可以无缝导入其他设备或账本; 42 | - 导入功能暂时只支持`json`格式,且结构要与本软件导出的结构一致。 43 | 44 | ## 软件升级 45 | 46 | - 经测试,软件升级直接下载新版安装包安装即可,数据会保留(**不要卸载**) 47 | - 升级前建议使用导出功能先将流水数据导出 -------------------------------------------------------------------------------- /doc/readme.txt: -------------------------------------------------------------------------------- 1 | 一些文档放在这里,大多是说明文档。 -------------------------------------------------------------------------------- /docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | main: 4 | container_name: cashbook4 5 | depends_on: 6 | - "db" 7 | image: dingdangdog/cashbook:4.1.0 8 | restart: always 9 | # network_mode: "host" 10 | volumes: 11 | - ./data:/app/data # 数据挂载到本地,不建议修改 12 | environment: 13 | DATABASE_URL: "postgresql://postgres:postgres@cashbook_db:5432/cashbook?schema=public" # 数据库链接,【请自行修改!与你的数据库一致】 14 | NUXT_DATA_PATH: "/app/data" # 数据存储未知,现在只有小票图片了,不建议修改 15 | NUXT_AUTH_SECRET: "auth_secret" # 前台登录加密使用的密钥 【自行修改!】 16 | NUXT_ADMIN_USERNAME: "admin" # 后台登录用户名 17 | # 【自行修改】后台登录密码,密码是加密后的,生成密码可前往 https://cashbook.oldmoon.top/admin/GetPassword 或独立部署后访问 `你的url/admin/GetPassword` 18 | NUXT_ADMIN_PASSWORD: "fb35e9343a1c095ce1c1d1eb6973dc570953159441c3ee315ecfefb6ed05f4cc" 19 | ports: 20 | - 9090:9090 # 账本开放端口 【自行修改!】 21 | db: 22 | container_name: cashbook_db 23 | image: postgres:17.4-alpine3.21 24 | restart: always 25 | #network_mode: "host" 26 | # set shared memory limit when using docker-compose 27 | shm_size: 128mb 28 | # or set shared memory limit when deploy via swarm stack 29 | volumes: 30 | - ./db:/var/lib/postgresql/data # 数据库容器数据挂载到本地,不建议修改 31 | environment: 32 | #POSTGRES_USER: postgres # 数据库用户名,不填默认为postgres 33 | POSTGRES_PASSWORD: postgres # 数据库密码 【自行修改!】 34 | POSTGRES_DB: cashbook 35 | #ports: 36 | # - 5432:5432 # 数据库端口,想要远程连接请放开注释,并建议自行修改端口 -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "=============================================" 3 | echo "欢迎使用 Cashbook" 4 | echo "=============================================" 5 | # 打印环境信息 6 | # echo "Checking and creating database if it does not exist..." 7 | echo "Starting application with Prisma database initialization..." 8 | npx prisma migrate deploy 9 | echo "Success Run npx prisma migrate deploy." 10 | 11 | # 启动应用程序 12 | echo "Starting application..." 13 | exec node server/index.mjs -------------------------------------------------------------------------------- /i18n.config.ts: -------------------------------------------------------------------------------- 1 | import en from "./locales/en"; 2 | import zh from "./locales/zh"; 3 | 4 | // You can use `defineI18nConfig` to get type inferences for options to pass to vue-i18n. 5 | export default defineI18nConfig(() => { 6 | return { 7 | legacy: false, 8 | locale: "en", 9 | messages: { 10 | en, 11 | zh, 12 | }, 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient(); 5 | }; 6 | 7 | declare const globalThis: { 8 | prismaGlobal: ReturnType; 9 | } & typeof global; 10 | 11 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); 12 | 13 | export default prisma; 14 | 15 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma; 16 | -------------------------------------------------------------------------------- /locales/en/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{ait}LOG", 3 | "hello": "Welcome", 4 | "menu": { 5 | "menu1": "menu1", 6 | "menu2": "menu2", 7 | "menu3": "menu3", 8 | "menu4": "menu4" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /locales/en/index.ts: -------------------------------------------------------------------------------- 1 | // locales/en/index.ts 2 | import { en as ENG } from "vuetify/locale"; 3 | import en from "./en.json"; 4 | 5 | const messages = { 6 | ...en, 7 | someKey: "Some message", 8 | $vuetify: ENG, 9 | }; 10 | export default messages; 11 | -------------------------------------------------------------------------------- /locales/zh/index.ts: -------------------------------------------------------------------------------- 1 | // locales/en/index.ts 2 | import { zhHans } from "vuetify/locale"; 3 | import zh from "./zh.json"; 4 | 5 | const messages = { 6 | ...zh, 7 | someKey: "Some message", 8 | $vuetify: zhHans, 9 | }; 10 | export default messages; 11 | -------------------------------------------------------------------------------- /locales/zh/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "{ait}老哥", 3 | "hello": "欢迎", 4 | "menu": { 5 | "menu1": "菜单1", 6 | "menu2": "菜单2", 7 | "menu3": "菜单3", 8 | "menu4": "菜单4" 9 | }, 10 | "noDataText": "暂无数据" 11 | } 12 | -------------------------------------------------------------------------------- /middleware/admin.ts: -------------------------------------------------------------------------------- 1 | // 登录过滤 2 | export default defineNuxtRouteMiddleware((to, from) => { 3 | // 需要登陆的地址,校验登陆状态 4 | const token = useCookie("Admin").value; 5 | // console.log(token); 6 | if (!token) { 7 | return navigateTo({ path: `/admin/login` }); 8 | } 9 | return; 10 | }); 11 | -------------------------------------------------------------------------------- /middleware/auth.ts: -------------------------------------------------------------------------------- 1 | // 登录过滤 2 | export default defineNuxtRouteMiddleware(async (to, from) => { 3 | // 需要登陆的地址,校验登陆状态 4 | // const token = useCookie("Authorization").value; 5 | const { data: res, error } = await useFetch>( 6 | "/api/checkuser", 7 | { 8 | method: "get", 9 | headers: { 10 | Authorization: useCookie("Authorization").value || "", 11 | }, 12 | } 13 | ); 14 | // console.log("res", res, "error", error); 15 | if (error.value) { 16 | return navigateTo({ path: "/500", query: { e: String(error.value) } }); 17 | } 18 | if (!res.value?.d) { 19 | localStorage.removeItem("Authorization"); 20 | localStorage.removeItem("bookName"); 21 | localStorage.removeItem("bookId"); 22 | Alert.error("用户异常,请重新登录!"); 23 | return navigateTo({ path: "/login", query: { callbackUrl: to.fullPath } }); 24 | } 25 | // console.log(res); 26 | // 用户信息获取成功,正常跳转 27 | if (res.value && res.value.c == 200 && res.value.d) { 28 | GlobalUserInfo.value = res.value.d; 29 | return; 30 | } else if (res.value && res.value.c == 400) { 31 | // console.log(400); 32 | // 跳转登录 33 | return navigateTo({ 34 | path: "/login", 35 | query: { callbackUrl: to.fullPath }, 36 | }); 37 | } else { 38 | return navigateTo({ path: "/500", query: { e: JSON.stringify(res) } }); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /modules/initdb.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtModule } from "nuxt/kit"; 2 | import prisma from "~/lib/prisma"; 3 | // import {usePrismaClient} from "" 4 | 5 | export default defineNuxtModule({ 6 | meta: { 7 | name: "initdb", 8 | }, 9 | setup(options, nuxt) { 10 | nuxt.hook("nitro:init", async (nitro) => { 11 | // This code will run when Nitro is initialized 12 | console.log("nitro:init"); 13 | }); 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { transformAssetUrls } from "vite-plugin-vuetify"; 2 | 3 | export default defineNuxtConfig({ 4 | ssr: false, 5 | compatibilityDate: "2025-01-10", 6 | devtools: { enabled: true }, 7 | css: ["~/assets/css/main.css"], 8 | app: { 9 | head: { 10 | title: "Cashbook", 11 | charset: "utf-8", 12 | viewport: "width=device-width, initial-scale=1", 13 | link: [ 14 | { 15 | rel: "manifest", 16 | href: "/manifest.json", 17 | }, 18 | { 19 | rel: "shortcut icon", 20 | href: "/logo.png", 21 | }, 22 | { 23 | rel: "icon", 24 | href: "/logo.png", 25 | }, 26 | ], 27 | }, 28 | }, 29 | devServer: { 30 | port: 9090, 31 | }, 32 | nitro: { 33 | // routeRules: { 34 | // "/api/**": { swr: true }, 35 | // }, 36 | }, 37 | runtimeConfig: { 38 | public: {}, 39 | appVersion: "", 40 | dataPath: "", 41 | authSecret: "", 42 | adminUsername: "", 43 | adminPassword: "", 44 | }, 45 | 46 | modules: [ 47 | "@nuxtjs/i18n", 48 | "vuetify-nuxt-module", 49 | "@nuxtjs/tailwindcss", 50 | "nuxt-echarts", 51 | "@prisma/nuxt", 52 | ], 53 | 54 | i18n: { 55 | strategy: "prefix_except_default", 56 | defaultLocale: "zh", 57 | locales: [ 58 | { 59 | code: "en", 60 | name: "English", 61 | }, 62 | { 63 | code: "zh", 64 | name: "简体中文", 65 | }, 66 | ], 67 | vueI18n: "./i18n.config.ts", 68 | detectBrowserLanguage: { 69 | useCookie: true, 70 | cookieKey: "i18n_redirected", 71 | redirectOn: "root", 72 | }, 73 | }, 74 | // 动态引入echars图表 75 | echarts: { 76 | features: ["LabelLayout", "UniversalTransition"], 77 | charts: ["BarChart", "LineChart", "PieChart"], 78 | components: [ 79 | "DatasetComponent", 80 | "GridComponent", 81 | "TooltipComponent", 82 | "LegendComponent", 83 | "ToolboxComponent", 84 | "DataZoomComponent", 85 | ], 86 | }, 87 | tailwindcss: { 88 | config: { 89 | prefix: "tw-", 90 | }, 91 | }, 92 | vuetify: { 93 | moduleOptions: { 94 | ssrClientHints: {}, 95 | }, 96 | vuetifyOptions: { 97 | theme: { 98 | defaultTheme: "dark", 99 | themes: { 100 | light: { 101 | dark: false, 102 | colors: { 103 | primary: '#1976D2', 104 | secondary: '#26A69A', 105 | accent: '#FF4081', 106 | error: '#F44336', 107 | info: '#2196F3', 108 | success: '#4CAF50', 109 | warning: '#FB8C00', 110 | background: '#F5F5F5', 111 | surface: '#FFFFFF', 112 | } 113 | }, 114 | dark: { 115 | dark: true, 116 | colors: { 117 | primary: '#2196F3', 118 | secondary: '#26A69A', 119 | accent: '#FF4081', 120 | error: '#FF5252', 121 | info: '#42A5F5', 122 | success: '#66BB6A', 123 | warning: '#FFA726', 124 | background: '#121212', 125 | surface: '#1E1E1E', 126 | } 127 | } 128 | }, 129 | }, 130 | }, 131 | }, 132 | vite: { 133 | vue: { 134 | template: { 135 | transformAssetUrls, 136 | }, 137 | }, 138 | resolve: { 139 | alias: { 140 | ".prisma/client/index-browser": 141 | "./node_modules/.prisma/client/index-browser.js", 142 | }, 143 | }, 144 | }, 145 | }); 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cashbook4", 3 | "version": "4.1.6", 4 | "private": true, 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "nuxt build", 9 | "dev": "nuxt dev --host 0.0.0.0", 10 | "generate": "nuxt generate", 11 | "preview": "nuxt preview", 12 | "postinstall": "nuxt prepare" 13 | }, 14 | "dependencies": { 15 | "@mdi/font": "^7.4.47", 16 | "@nuxtjs/i18n": "^8.3.1", 17 | "@nuxtjs/tailwindcss": "^6.12.2", 18 | "@prisma/client": "^6.8.2", 19 | "@prisma/nuxt": "^0.2.0", 20 | "echarts": "^5.6.0", 21 | "jsonwebtoken": "^9.0.2", 22 | "jszip": "^3.10.1", 23 | "nuxt": "^3.15.4", 24 | "nuxt-echarts": "^0.2.4", 25 | "vue": "latest", 26 | "vuetify-nuxt-module": "^0.18.3", 27 | "xlsx": "^0.18.5" 28 | }, 29 | "devDependencies": { 30 | "@types/jsonwebtoken": "^9.0.9" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/500.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /pages/admin/books/EditInfoDialog.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 85 | 86 | 91 | -------------------------------------------------------------------------------- /pages/admin/books/api.ts: -------------------------------------------------------------------------------- 1 | const prefiex = "api/admin/entry/books"; 2 | 3 | export const page = ( 4 | page: PageParam, 5 | param: Book 6 | ): Promise<{ total: number; data: Book[] }> => { 7 | return doApi.post(`${prefiex}/page`, { ...page, ...param }); 8 | }; 9 | 10 | export const add = (data: Book): Promise => { 11 | return doApi.post(`${prefiex}/add`, data); 12 | }; 13 | 14 | export const del = (id: number): Promise => { 15 | return doApi.post(`${prefiex}/del`, { id }); 16 | }; 17 | 18 | export const update = (data: Book): Promise => { 19 | return doApi.post(`${prefiex}/update`, data); 20 | }; 21 | 22 | export const list = (data: Book): Promise => { 23 | return doApi.post(`${prefiex}/list`, data); 24 | }; 25 | 26 | export const all = (): Promise => { 27 | return doApi.post(`${prefiex}/all`, {}); 28 | }; 29 | -------------------------------------------------------------------------------- /pages/admin/books/flag.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export const editInfoFlag = ref(false); 4 | -------------------------------------------------------------------------------- /pages/admin/books/index.client.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /pages/admin/getpassword.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /pages/admin/index.client.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /pages/admin/login.client.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 96 | 97 | 126 | -------------------------------------------------------------------------------- /pages/admin/typeRelations/EditInfoDialog.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 95 | 96 | 101 | -------------------------------------------------------------------------------- /pages/admin/typeRelations/api.ts: -------------------------------------------------------------------------------- 1 | const prefiex = "api/admin/entry/typeRelations"; 2 | 3 | export const page = ( 4 | page: PageParam, 5 | param: TypeRelation 6 | ): Promise<{ total: number; data: TypeRelation[] }> => { 7 | return doApi.post(`${prefiex}/page`, { ...page, ...param }); 8 | }; 9 | 10 | export const add = (data: TypeRelation): Promise => { 11 | return doApi.post(`${prefiex}/add`, data); 12 | }; 13 | 14 | export const del = (id: number): Promise => { 15 | return doApi.post(`${prefiex}/del`, { id }); 16 | }; 17 | 18 | export const update = (data: TypeRelation): Promise => { 19 | return doApi.post(`${prefiex}/update`, data); 20 | }; 21 | 22 | export const list = (data: TypeRelation): Promise => { 23 | return doApi.post(`${prefiex}/list`, data); 24 | }; 25 | 26 | export const all = (): Promise => { 27 | return doApi.post(`${prefiex}/all`, {}); 28 | }; 29 | -------------------------------------------------------------------------------- /pages/admin/typeRelations/flag.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export const editInfoFlag = ref(false); 4 | -------------------------------------------------------------------------------- /pages/admin/typeRelations/index.client.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /pages/admin/users/EditInfoDialog.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 113 | 114 | 119 | -------------------------------------------------------------------------------- /pages/admin/users/api.ts: -------------------------------------------------------------------------------- 1 | const prefiex = "api/admin/entry/users"; 2 | 3 | export const page = ( 4 | page: PageParam, 5 | param: User 6 | ): Promise<{ total: number; data: User[] }> => { 7 | return doApi.post(`${prefiex}/page`, { ...page, ...param }); 8 | }; 9 | 10 | export const add = (data: User): Promise => { 11 | return doApi.post(`${prefiex}/add`, data); 12 | }; 13 | 14 | export const del = (id: number): Promise => { 15 | return doApi.post(`${prefiex}/del`, { id }); 16 | }; 17 | 18 | export const update = (data: User): Promise => { 19 | return doApi.post(`${prefiex}/update`, data); 20 | }; 21 | 22 | export const list = (data: User): Promise => { 23 | return doApi.post(`${prefiex}/list`, data); 24 | }; 25 | 26 | export const all = (): Promise => { 27 | return doApi.post(`${prefiex}/all`, {}); 28 | }; 29 | -------------------------------------------------------------------------------- /pages/admin/users/flag.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export const editInfoFlag = ref(false); 4 | -------------------------------------------------------------------------------- /pages/analysis/index.client.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 125 | 126 | 135 | -------------------------------------------------------------------------------- /pages/books/EditInfoDialog.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 115 | 116 | 121 | -------------------------------------------------------------------------------- /pages/books/GetShareDialog.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 74 | 75 | 80 | -------------------------------------------------------------------------------- /pages/books/api.ts: -------------------------------------------------------------------------------- 1 | const prefiex = "api/entry/book"; 2 | 3 | export const page = ( 4 | page: PageParam, 5 | param: Book 6 | ): Promise<{ total: number; data: Book[] }> => { 7 | return doApi.post(`${prefiex}/page`, { ...page, ...param }); 8 | }; 9 | 10 | export const add = (data: Book): Promise => { 11 | return doApi.post(`${prefiex}/add`, data); 12 | }; 13 | 14 | export const del = (id: number): Promise => { 15 | return doApi.post(`${prefiex}/del`, { id }); 16 | }; 17 | 18 | export const update = (data: Book): Promise => { 19 | return doApi.post(`${prefiex}/update`, data); 20 | }; 21 | 22 | export const list = (data: Book): Promise => { 23 | return doApi.post(`${prefiex}/list`, data); 24 | }; 25 | 26 | export const all = (): Promise => { 27 | return doApi.post(`${prefiex}/all`, {}); 28 | }; 29 | -------------------------------------------------------------------------------- /pages/books/flag.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export const editInfoFlag = ref(false); 4 | export const showGetShareDialog = ref(false); 5 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /plugins/system.ts: -------------------------------------------------------------------------------- 1 | import type { Result } from "~/utils/model"; 2 | import { SystemConfig } from "~/utils/store"; 3 | 4 | export default defineNuxtPlugin({ 5 | hooks: { 6 | "app:created": async () => { 7 | try { 8 | const { data: res, error } = await useFetch>( 9 | "/api/config" 10 | ); 11 | // console.log(res.value); 12 | if (error.value != null) { 13 | // console.log(error.value); 14 | console.error(error.value); 15 | return; 16 | } 17 | if (res.value && res.value.c == 200) { 18 | // 查询成功 19 | SystemConfig.value = res.value.d; 20 | // const nuxtApp = useNuxtApp(); 21 | // nuxtApp.provide("system", res.value.d); 22 | } 23 | } catch (error) { 24 | // console.error("getUnreadNum Error", error); 25 | } 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /prisma/migrations/20250116134313_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CREATE DATABASE cashbook; 2 | -- CreateTable 3 | CREATE TABLE "SystemSetting" ( 4 | "id" INTEGER NOT NULL, 5 | "title" TEXT, 6 | "description" TEXT, 7 | "keywords" TEXT, 8 | "version" TEXT, 9 | "openRegister" BOOLEAN NOT NULL DEFAULT false, 10 | "createDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | "updateBy" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | 13 | CONSTRAINT "SystemSetting_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateTable 17 | CREATE TABLE "User" ( 18 | "id" SERIAL NOT NULL, 19 | "username" TEXT NOT NULL, 20 | "password" TEXT NOT NULL, 21 | "name" TEXT, 22 | "email" TEXT NOT NULL, 23 | "createDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 24 | 25 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 26 | ); 27 | 28 | -- CreateTable 29 | CREATE TABLE "Book" ( 30 | "id" SERIAL NOT NULL, 31 | "bookId" TEXT NOT NULL, 32 | "bookName" TEXT NOT NULL, 33 | "userId" INTEGER NOT NULL, 34 | "createDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 35 | 36 | CONSTRAINT "Book_pkey" PRIMARY KEY ("id") 37 | ); 38 | 39 | -- CreateTable 40 | CREATE TABLE "Flow" ( 41 | "id" SERIAL NOT NULL, 42 | "userId" INTEGER NOT NULL, 43 | "bookId" TEXT NOT NULL, 44 | "day" TEXT NOT NULL, 45 | "flowType" TEXT, 46 | "industryType" TEXT, 47 | "payType" TEXT, 48 | "money" DOUBLE PRECISION, 49 | "name" TEXT, 50 | "description" TEXT, 51 | "invoice" TEXT, 52 | 53 | CONSTRAINT "Flow_pkey" PRIMARY KEY ("id") 54 | ); 55 | 56 | -- CreateTable 57 | CREATE TABLE "Plan" ( 58 | "id" SERIAL NOT NULL, 59 | "bookId" TEXT NOT NULL, 60 | "month" TEXT NOT NULL, 61 | "limitMoney" DOUBLE PRECISION, 62 | "usedMoney" DOUBLE PRECISION, 63 | 64 | CONSTRAINT "Plan_pkey" PRIMARY KEY ("id") 65 | ); 66 | 67 | -- CreateTable 68 | CREATE TABLE "TypeRelation" ( 69 | "id" SERIAL NOT NULL, 70 | "userId" INTEGER NOT NULL, 71 | "bookId" TEXT NOT NULL, 72 | "source" TEXT NOT NULL, 73 | "target" TEXT NOT NULL, 74 | 75 | CONSTRAINT "TypeRelation_pkey" PRIMARY KEY ("id") 76 | ); 77 | 78 | -- CreateIndex 79 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 80 | -------------------------------------------------------------------------------- /prisma/migrations/20250206090137_update_user_email/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "User_email_key"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "User" ALTER COLUMN "email" DROP NOT NULL; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20250209061645_add_flow_and_book/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Book" ADD COLUMN "shareKey" TEXT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Flow" ADD COLUMN "attribution" TEXT, 6 | ADD COLUMN "eliminate" INTEGER DEFAULT 0, 7 | ADD COLUMN "origin" TEXT; 8 | -------------------------------------------------------------------------------- /prisma/migrations/20250315083738_add_budget/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Plan` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Book" ADD COLUMN "budget" DOUBLE PRECISION; 9 | 10 | -- DropTable 11 | DROP TABLE "Plan"; 12 | 13 | -- CreateTable 14 | CREATE TABLE "Budget" ( 15 | "id" SERIAL NOT NULL, 16 | "bookId" TEXT NOT NULL, 17 | "userId" INTEGER NOT NULL, 18 | "month" TEXT NOT NULL UNIQUE, 19 | "budget" DOUBLE PRECISION, 20 | "used" DOUBLE PRECISION, 21 | 22 | CONSTRAINT "Budget_pkey" PRIMARY KEY ("id") 23 | ); 24 | 25 | -- CreateTable 26 | CREATE TABLE "FixedFlow" ( 27 | "id" SERIAL NOT NULL, 28 | "bookId" TEXT NOT NULL, 29 | "userId" INTEGER NOT NULL, 30 | "month" TEXT, 31 | "money" DOUBLE PRECISION, 32 | "name" TEXT, 33 | "description" TEXT, 34 | "flowType" TEXT, 35 | "industryType" TEXT, 36 | "payType" TEXT, 37 | "attribution" TEXT, 38 | 39 | CONSTRAINT "FixedFlow_pkey" PRIMARY KEY ("id") 40 | ); 41 | -------------------------------------------------------------------------------- /prisma/migrations/20250316033948_fix_budget_month/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | ALTER TABLE "Budget" DROP CONSTRAINT "Budget_month_key"; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | // provider = env("DATEBASE_PROVIDER") 10 | provider = "postgresql" 11 | url = env("DATABASE_URL") 12 | } 13 | 14 | model SystemSetting { 15 | id Int @id 16 | title String? 17 | description String? 18 | keywords String? 19 | version String? 20 | openRegister Boolean @default(false) 21 | createDate DateTime @default(now()) 22 | updateBy DateTime @default(now()) 23 | } 24 | 25 | // 用户表 26 | model User { 27 | id Int @id @default(autoincrement()) 28 | username String 29 | password String 30 | name String? 31 | email String? 32 | createDate DateTime @default(now()) 33 | } 34 | 35 | model Book { 36 | id Int @id @default(autoincrement()) 37 | bookId String 38 | bookName String 39 | shareKey String? // 分享key 40 | userId Int 41 | budget Float? // 账本预算 42 | createDate DateTime @default(now()) 43 | } 44 | 45 | model Flow { 46 | id Int @id @default(autoincrement()) 47 | userId Int 48 | bookId String 49 | day String 50 | flowType String? // 流水类型:收入、支出、不计收支 51 | industryType String? // 行业分类(支出类型/收入类型) 52 | payType String? // 支付方式/收款方式 53 | money Float? 54 | name String? 55 | description String? 56 | invoice String? 57 | origin String? // 流水来源:谁谁-支付宝导入;谁谁手动输出 58 | attribution String? // 流水归属(谁的收入/支出) 59 | eliminate Int? @default(0) // 平账标志,0未平账;1已平账,-1忽略平账 60 | } 61 | 62 | // Budget 支出计划 63 | model Budget { 64 | id Int @id @default(autoincrement()) 65 | bookId String 66 | userId Int 67 | month String 68 | budget Float? 69 | used Float? 70 | } 71 | 72 | model FixedFlow { 73 | id Int @id @default(autoincrement()) 74 | bookId String 75 | userId Int 76 | month String? 77 | money Float? 78 | name String? 79 | description String? 80 | flowType String? // 流水类型:收入、支出、不计收支 81 | industryType String? // 行业分类(支出类型/收入类型) 82 | payType String? // 支付方式/收款方式 83 | attribution String? // 流水归属(谁的收入/支出) 84 | } 85 | 86 | model TypeRelation { 87 | id Int @id @default(autoincrement()) 88 | userId Int 89 | bookId String 90 | source String 91 | target String 92 | } 93 | -------------------------------------------------------------------------------- /public/cashbook-mini.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingdangdog/cashbook/1e37a51ee4cac32ac1bd0045e68bdcad38d22333/public/cashbook-mini.jpg -------------------------------------------------------------------------------- /public/csvtemplate.csv: -------------------------------------------------------------------------------- 1 | 交易时间,收/支,交易分类,收/付款方式,金额,流水归属,交易对方,备注 2 | 2025-01-02,收入,上天恩赐,现金,100,张三,意外收获,路边捡到的! 3 | 2025-01-02,支出,餐饮,微信,28.8,张三,黄焖鸡米饭,晚餐吃点好的! -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingdangdog/cashbook/1e37a51ee4cac32ac1bd0045e68bdcad38d22333/public/logo.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cashbook", 3 | "short_name": "Cashbook", 4 | "description": "Cashbook, 快速记账!", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "icons": [ 9 | { 10 | "src": "/pwa/manifest-icon-192.maskable.png", 11 | "sizes": "192x192", 12 | "type": "image/png", 13 | "purpose": "any" 14 | }, 15 | { 16 | "src": "/pwa/manifest-icon-192.maskable.png", 17 | "sizes": "192x192", 18 | "type": "image/png", 19 | "purpose": "maskable" 20 | }, 21 | { 22 | "src": "/pwa/manifest-icon-512.maskable.png", 23 | "sizes": "512x512", 24 | "type": "image/png", 25 | "purpose": "any" 26 | }, 27 | { 28 | "src": "/pwa/manifest-icon-512.maskable.png", 29 | "sizes": "512x512", 30 | "type": "image/png", 31 | "purpose": "maskable" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /public/pwa/manifest-icon-192.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingdangdog/cashbook/1e37a51ee4cac32ac1bd0045e68bdcad38d22333/public/pwa/manifest-icon-192.maskable.png -------------------------------------------------------------------------------- /public/pwa/manifest-icon-512.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingdangdog/cashbook/1e37a51ee4cac32ac1bd0045e68bdcad38d22333/public/pwa/manifest-icon-512.maskable.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | disallow: / -------------------------------------------------------------------------------- /server/api/admin/entry/books/all.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const books = await prisma.book.findMany(); 5 | return success(books); 6 | }); 7 | -------------------------------------------------------------------------------- /server/api/admin/entry/books/del.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { id } = await readBody(event); // 从请求体获取 ID 5 | if (!id) { 6 | return error("Not Find ID"); 7 | } 8 | // 删除数据 9 | const deleted = await prisma.book.delete({ 10 | where: { id }, 11 | }); 12 | 13 | return success(deleted); 14 | }); 15 | -------------------------------------------------------------------------------- /server/api/admin/entry/books/list.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | 6 | const where: any = {}; // 条件查询 7 | 8 | // 普通查询条件 9 | if (body.id) { 10 | // equals 等于查询 11 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 12 | where.id = { 13 | equals: body.id, 14 | }; 15 | } 16 | if (body.bookName) { 17 | where.bookName = { 18 | equals: body.bookName, 19 | }; 20 | } 21 | if (body.userId) { 22 | where.userId = { 23 | equals: body.userId, 24 | }; 25 | } 26 | 27 | const users = await prisma.book.findMany({ 28 | where, // 使用条件查询 29 | }); 30 | 31 | return success(users); 32 | }); 33 | -------------------------------------------------------------------------------- /server/api/admin/entry/books/page.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | 6 | const where: any = {}; // 条件查询 7 | 8 | // 普通查询条件 9 | if (body.bookId) { 10 | // equals 等于查询 11 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 12 | where.bookId = { 13 | equals: body.bookId, 14 | }; 15 | } 16 | if (body.bookName) { 17 | where.bookName = { 18 | equals: body.bookName, 19 | }; 20 | } 21 | if (body.userId) { 22 | where.userId = { 23 | equals: body.userId, 24 | }; 25 | } 26 | 27 | // 分页条件 28 | const pageNum = Number(body.pageNum ? body.pageNum : 1); 29 | const pageSize = Number(body.pageSize ? body.pageSize : 15); 30 | const skip = (pageNum - 1) * pageSize; // 计算跳过的条目数 31 | 32 | // 排序条件 33 | const orderBy: any = [ 34 | { 35 | createDate: "desc", 36 | }, 37 | ]; 38 | if (pageSize == -1) { 39 | // 查询全部 40 | const datas = await prisma.book.findMany({ where, orderBy }); 41 | return success({ total: datas.length, data: datas, pages: 1 }); 42 | } 43 | // 【条件、排序、分页】 组合查询 44 | const users = await prisma.book.findMany({ 45 | where, 46 | orderBy, 47 | skip, 48 | take: pageSize, 49 | }); 50 | // 计算总页数 51 | const totals = await prisma.book.count({ where }); 52 | const totalPages = Math.ceil(totals / pageSize); 53 | 54 | return success({ total: totals, data: users, pages: totalPages }); 55 | }); 56 | -------------------------------------------------------------------------------- /server/api/admin/entry/books/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | const { id, bookName } = body; 6 | if (!id) { 7 | return error("Not Find ID"); 8 | } 9 | const updated = await prisma.book.update({ 10 | where: { id }, 11 | data: { 12 | bookName, 13 | }, 14 | }); 15 | return success(updated); 16 | }); 17 | -------------------------------------------------------------------------------- /server/api/admin/entry/settings/export.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | // 获取所有表名 5 | const tableNames: string[] = [ 6 | "_prisma_migrations", 7 | "SystemSetting", 8 | "User", 9 | "Book", 10 | "Flow", 11 | "Budget", 12 | "FixedFlow", 13 | "TypeRelation", 14 | ]; 15 | // const tableNames: any[] = await prisma.$queryRaw` 16 | // SELECT table_name 17 | // FROM information_schema.tables 18 | // WHERE table_schema = 'public' -- 替换为你的schema名称 19 | // AND table_type = 'BASE TABLE' 20 | // `; 21 | // console.log(tableNames); 22 | const allData: any = {}; 23 | 24 | // 遍历所有表,查询数据并添加到allData对象中 25 | for (const tableName of tableNames) { 26 | console.log(tableName); 27 | // const sql = 'SELECT * FROM "' + tableName + '"'; 28 | const data = await prisma.$queryRawUnsafe( 29 | 'SELECT * FROM "' + tableName + '";' 30 | ); 31 | allData[tableName] = data; 32 | } 33 | 34 | // 返回文件流 35 | return success(allData); 36 | }); 37 | -------------------------------------------------------------------------------- /server/api/admin/entry/settings/exportImg.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import JSZip from "jszip"; 5 | 6 | export default defineEventHandler(async (event) => { 7 | try { 8 | const runtimeConfig = useRuntimeConfig(); 9 | let dataPath = String(runtimeConfig.dataPath); 10 | if (!dataPath) { 11 | dataPath = process.cwd(); 12 | } 13 | const invoicePath = path.join(dataPath, "images"); 14 | 15 | // 确保图片目录存在 16 | if (!fs.existsSync(invoicePath)) { 17 | return error("图片目录不存在"); 18 | } 19 | 20 | // 读取所有图片文件 21 | const files = await fs.promises.readdir(invoicePath); 22 | 23 | // 创建一个新的ZIP文件 24 | const zip = new JSZip(); 25 | 26 | // 图片信息数组,用于记录图片元数据 27 | const imageInfos = []; 28 | 29 | // 添加所有图片到ZIP文件 30 | for (const file of files) { 31 | const filePath = path.join(invoicePath, file); 32 | const stats = await fs.promises.stat(filePath); 33 | 34 | // 只处理文件,跳过目录 35 | if (stats.isFile()) { 36 | // 读取文件内容 37 | const fileData = await fs.promises.readFile(filePath); 38 | 39 | // 添加到ZIP文件 40 | zip.file(file, fileData); 41 | 42 | // 记录图片信息 43 | imageInfos.push({ 44 | name: file, 45 | size: stats.size, 46 | lastModified: stats.mtime, 47 | }); 48 | } 49 | } 50 | 51 | // 生成图片信息的JSON文件 52 | zip.file("image_info.json", JSON.stringify(imageInfos, null, 2)); 53 | 54 | // 生成ZIP文件 55 | const zipBuffer = await zip.generateAsync({ 56 | type: "nodebuffer", 57 | compression: "DEFLATE", 58 | compressionOptions: { 59 | level: 9, // 最高压缩级别 60 | }, 61 | }); 62 | 63 | // 设置响应头,指示这是一个ZIP文件 64 | event.node.res.setHeader( 65 | "Content-Disposition", 66 | 'attachment; filename="images.zip"' 67 | ); 68 | event.node.res.setHeader("Content-Type", "application/zip"); 69 | 70 | // 返回ZIP文件 71 | return zipBuffer; 72 | } catch (err) { 73 | console.error("导出图片失败:", err); 74 | return error( 75 | "导出图片失败: " + (err instanceof Error ? err.message : String(err)) 76 | ); 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /server/api/admin/entry/settings/get.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const settings = await prisma.systemSetting.findUnique({ 5 | where: { 6 | id: 1, 7 | }, 8 | }); 9 | 10 | return success(settings); 11 | }); 12 | -------------------------------------------------------------------------------- /server/api/admin/entry/settings/importImg.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import JSZip from "jszip"; 5 | 6 | export default defineEventHandler(async (event) => { 7 | try { 8 | // 1. 读取上传的文件 9 | const data = await readMultipartFormData(event); 10 | if (!data) { 11 | return error("请上传文件"); 12 | } 13 | const file = data.find((item) => item.name === "file"); 14 | 15 | if (!file) { 16 | return error("请上传ZIP文件"); 17 | } 18 | 19 | // 检查文件类型 20 | if (!file.filename?.toLowerCase().endsWith(".zip")) { 21 | return error("请上传ZIP格式的文件"); 22 | } 23 | 24 | // 获取图片保存路径 25 | const runtimeConfig = useRuntimeConfig(); 26 | let dataPath = String(runtimeConfig.dataPath); 27 | if (!dataPath) { 28 | dataPath = process.cwd(); 29 | } 30 | const invoicePath = path.join(dataPath, "images"); 31 | 32 | // 确保图片目录存在 33 | await fs.promises.mkdir(invoicePath, { recursive: true }); 34 | 35 | // 读取ZIP文件内容 36 | const zipBuffer = file.data; 37 | const zip = new JSZip(); 38 | 39 | // 解析ZIP文件 40 | const zipContent = await zip.loadAsync(zipBuffer); 41 | 42 | // 导入统计信息 43 | const importStats = { 44 | total: 0, 45 | imported: 0, 46 | skipped: 0, 47 | errors: 0, 48 | }; 49 | 50 | // 遍历ZIP文件中的所有文件 51 | for (const [filename, zipEntry] of Object.entries(zipContent.files)) { 52 | // 跳过目录和image_info.json文件 53 | if (zipEntry.dir || filename === "image_info.json") { 54 | continue; 55 | } 56 | 57 | importStats.total++; 58 | 59 | try { 60 | // 获取文件内容 61 | const content = await zipEntry.async("nodebuffer"); 62 | 63 | // 构建保存路径 64 | const savePath = path.join(invoicePath, path.basename(filename)); 65 | 66 | // 检查文件是否已存在 67 | const fileExists = fs.existsSync(savePath); 68 | 69 | // 写入文件 70 | await fs.promises.writeFile(savePath, content); 71 | 72 | if (fileExists) { 73 | importStats.skipped++; 74 | } else { 75 | importStats.imported++; 76 | } 77 | } catch (err) { 78 | console.error(`导入图片 ${filename} 失败:`, err); 79 | importStats.errors++; 80 | } 81 | } 82 | 83 | return success({ 84 | message: `图片导入完成,共导入 ${importStats.imported} 个图片,跳过 ${importStats.skipped} 个已存在图片,失败 ${importStats.errors} 个`, 85 | stats: importStats, 86 | }); 87 | } catch (err) { 88 | console.error("导入图片失败:", err); 89 | return error( 90 | "导入图片失败: " + (err instanceof Error ? err.message : String(err)) 91 | ); 92 | } 93 | }); 94 | -------------------------------------------------------------------------------- /server/api/admin/entry/settings/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { title, description, keywords, version, openRegister } = 5 | await readBody(event); 6 | 7 | const settings = await prisma.systemSetting.update({ 8 | where: { id: 1 }, 9 | data: { 10 | title, 11 | description, 12 | keywords, 13 | version, 14 | openRegister, 15 | updateBy: new Date(), 16 | }, 17 | }); 18 | 19 | return success(settings); 20 | }); 21 | -------------------------------------------------------------------------------- /server/api/admin/entry/typeRelations/add.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | 6 | const { source, target } = body; 7 | 8 | // const userId = await getUserId(event); 9 | // 在数据库中添加新数据 10 | const created = await prisma.typeRelation.create({ 11 | data: { 12 | userId: 0, 13 | bookId: "0", 14 | source, 15 | target, 16 | }, 17 | }); 18 | return success(created); 19 | }); 20 | -------------------------------------------------------------------------------- /server/api/admin/entry/typeRelations/all.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const typeRelations = await prisma.typeRelation.findMany(); 5 | return success(typeRelations); 6 | }); 7 | -------------------------------------------------------------------------------- /server/api/admin/entry/typeRelations/del.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { id } = await readBody(event); // 从请求体获取 ID 5 | if (!id) { 6 | return error("Not Find ID"); 7 | } 8 | // 删除数据 9 | const deleted = await prisma.typeRelation.delete({ 10 | where: { id }, 11 | }); 12 | 13 | return success(deleted); 14 | }); 15 | -------------------------------------------------------------------------------- /server/api/admin/entry/typeRelations/list.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | 6 | const where: any = {}; // 条件查询 7 | 8 | // 普通查询条件 9 | if (body.id) { 10 | // equals 等于查询 11 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 12 | where.id = { 13 | equals: body.id, 14 | }; 15 | } 16 | if (body.bookId) { 17 | where.bookId = { 18 | equals: body.bookId, 19 | }; 20 | } 21 | if (body.userId) { 22 | where.userId = { 23 | equals: Number(body.userId), 24 | }; 25 | } 26 | 27 | const typeRelations = await prisma.typeRelation.findMany({ 28 | where, // 使用条件查询 29 | }); 30 | 31 | return success(typeRelations); 32 | }); 33 | -------------------------------------------------------------------------------- /server/api/admin/entry/typeRelations/page.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | 6 | const where: any = {}; // 条件查询 7 | 8 | // 普通查询条件 9 | if (body.id) { 10 | // equals 等于查询 11 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 12 | where.id = { 13 | equals: body.id, 14 | }; 15 | } 16 | if (body.bookId) { 17 | where.bookId = { 18 | equals: "0", 19 | }; 20 | } 21 | if (body.userId) { 22 | where.userId = { 23 | // equals: Number(body.userId), 24 | equals: 0, 25 | }; 26 | } 27 | if (body.source) { 28 | where.source = { 29 | contains: body.source, 30 | }; 31 | } 32 | if (body.target) { 33 | where.target = { 34 | contains: body.target, 35 | }; 36 | } 37 | 38 | // 分页条件 39 | const pageNum = Number(body.pageNum ? body.pageNum : 1); 40 | const pageSize = Number(body.pageSize ? body.pageSize : 15); 41 | const skip = (pageNum - 1) * pageSize; // 计算跳过的条目数 42 | 43 | // 排序条件 44 | const orderBy: any = [ 45 | { 46 | bookId: "asc", 47 | }, 48 | { 49 | target: "desc", 50 | }, 51 | ]; 52 | if (pageSize == -1) { 53 | // 查询全部 54 | const datas = await prisma.typeRelation.findMany({ where, orderBy }); 55 | return success({ total: datas.length, data: datas, pages: 1 }); 56 | } 57 | // 【条件、排序、分页】 组合查询 58 | const users = await prisma.typeRelation.findMany({ 59 | where, 60 | orderBy, 61 | skip, 62 | take: pageSize, 63 | }); 64 | // 计算总页数 65 | const totalUsers = await prisma.typeRelation.count({ where }); 66 | const totalPages = Math.ceil(totalUsers / pageSize); 67 | 68 | return success({ total: totalUsers, data: users, pages: totalPages }); 69 | }); 70 | -------------------------------------------------------------------------------- /server/api/admin/entry/typeRelations/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | const { id, target, source } = body; 6 | if (!id) { 7 | return error("Not Find ID"); 8 | } 9 | const updated = await prisma.typeRelation.update({ 10 | where: { id }, 11 | data: { 12 | target, 13 | source, 14 | }, 15 | }); 16 | return success(updated); 17 | }); 18 | -------------------------------------------------------------------------------- /server/api/admin/entry/users/add.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | 6 | const username = String(body.username); 7 | const name = String(body.name); 8 | const password = String(body.password); 9 | const email = String(body.email); 10 | const entryPassword = encryptBySHA256(username, password); 11 | // 在数据库中添加新数据 12 | const created = await prisma.user.create({ 13 | data: { 14 | username, 15 | email, 16 | name, 17 | password: entryPassword, 18 | }, 19 | }); 20 | return success(created); 21 | }); 22 | -------------------------------------------------------------------------------- /server/api/admin/entry/users/all.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const users = await prisma.user.findMany(); 5 | return success(users); 6 | }); 7 | -------------------------------------------------------------------------------- /server/api/admin/entry/users/del.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { id } = await readBody(event); // 从请求体获取 ID 5 | if (!id) { 6 | return error("Not Find ID"); 7 | } 8 | // 删除数据 9 | const deleted = await prisma.user.delete({ 10 | where: { id: Number(id) }, 11 | }); 12 | 13 | return success(deleted); 14 | }); 15 | -------------------------------------------------------------------------------- /server/api/admin/entry/users/list.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { name, username, email, id } = await readBody(event); // 获取查询参数 5 | 6 | const where: any = {}; // 条件查询 7 | 8 | // 添加条件:如果 `name` 存在,则根据 `name` 查询 9 | if (id) { 10 | // equals 等于查询 11 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 12 | where.id = { 13 | equals: Number(id), 14 | }; 15 | } 16 | 17 | // 如果 `email` 存在,则根据 `email` 查询 18 | if (name) { 19 | where.name = { 20 | contains: name, 21 | }; 22 | } 23 | if (username) { 24 | where.username = { 25 | contains: username, 26 | }; 27 | } 28 | if (email) { 29 | where.email = { 30 | contains: email, 31 | }; 32 | } 33 | 34 | const users = await prisma.user.findMany({ 35 | where, // 使用条件查询 36 | }); 37 | 38 | return success(users); 39 | }); 40 | -------------------------------------------------------------------------------- /server/api/admin/entry/users/page.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | const { name, username, email, id } = await readBody(event); // 获取查询参数 6 | 7 | const where: any = {}; // 条件查询 8 | 9 | // 添加条件:如果 `name` 存在,则根据 `name` 查询 10 | if (id) { 11 | // equals 等于查询 12 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 13 | where.id = { 14 | equals: Number(id), 15 | }; 16 | } 17 | 18 | // 如果 `email` 存在,则根据 `email` 查询 19 | if (name) { 20 | where.name = { 21 | contains: name, 22 | }; 23 | } 24 | if (username) { 25 | where.username = { 26 | contains: username, 27 | }; 28 | } 29 | if (email) { 30 | where.email = { 31 | contains: email, 32 | }; 33 | } 34 | 35 | // 分页条件 36 | const pageNum = Number(body.pageNum ? body.pageNum : 1); 37 | const pageSize = Number(body.pageSize ? body.pageSize : 15); 38 | const skip = (pageNum - 1) * pageSize; // 计算跳过的条目数 39 | 40 | // 排序条件 41 | const orderBy: any = { 42 | createDate: "desc", 43 | }; 44 | if (pageSize == -1) { 45 | // 查询全部 46 | const datas = await prisma.user.findMany({ where, orderBy }); 47 | return success({ total: datas.length, data: datas, pages: 1 }); 48 | } 49 | // 【条件、排序、分页】 组合查询 50 | const users = await prisma.user.findMany({ 51 | where, 52 | orderBy, 53 | skip, 54 | take: pageSize, 55 | }); 56 | // 计算总页数 57 | const totalUsers = await prisma.user.count({ where }); 58 | const totalPages = Math.ceil(totalUsers / pageSize); 59 | 60 | return success({ total: totalUsers, data: users, pages: totalPages }); 61 | }); 62 | -------------------------------------------------------------------------------- /server/api/admin/entry/users/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | const { id, name, username, password, email } = body; 6 | if (!id) { 7 | return error("Not Find ID"); 8 | } 9 | const data: any = {}; 10 | if (name) { 11 | data.name = name; 12 | } 13 | if (email) { 14 | data.email = email; 15 | } 16 | if (username && password) { 17 | data.username = username; 18 | data.password = encryptBySHA256(username, password); 19 | } 20 | // data.update 21 | const updated = await prisma.user.update({ 22 | where: { id: Number(id) }, 23 | data: data, 24 | }); 25 | return success(updated); 26 | }); 27 | -------------------------------------------------------------------------------- /server/api/admin/getPassword.ts: -------------------------------------------------------------------------------- 1 | // 退出登录 2 | export default defineEventHandler(async (event) => { 3 | // const Authorization = getHeader(event, "Admin"); 4 | const body = await readBody(event); 5 | const newPassword = encryptBySHA256( 6 | String(body.username).trim(), 7 | String(body.password).trim() 8 | ); 9 | return success(newPassword); 10 | }); 11 | -------------------------------------------------------------------------------- /server/api/admin/login.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | const body = await readBody(event); 3 | const { account, password } = body; 4 | 5 | // const entrypassword = encryptBySHA256(username || "", password || ""); 6 | const adminpass = encryptBySHA256(account, password); 7 | const runtimeConfig = useRuntimeConfig(); 8 | if ( 9 | runtimeConfig.adminUsername == account && 10 | runtimeConfig.adminPassword == adminpass 11 | ) { 12 | // 二次加密用作token,TODO 可以优化为使用JWT生成 13 | const adminToken = encryptBySHA256(account, adminpass); 14 | 15 | setCookie(event, "Admin", adminToken, { 16 | maxAge: 7 * 24 * 60 * 60, 17 | }); 18 | 19 | return success({ token: adminToken }); 20 | } else { 21 | return error("账号密码错误"); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /server/api/admin/logout.ts: -------------------------------------------------------------------------------- 1 | // 退出登录 2 | export default defineEventHandler(async (event) => { 3 | // const Authorization = getHeader(event, "Admin"); 4 | 5 | deleteCookie(event, "Admin"); 6 | 7 | return success("退出成功"); 8 | }); 9 | -------------------------------------------------------------------------------- /server/api/check.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const userCount = await prisma.user.count(); 5 | if (userCount === 0) { 6 | return success(false); 7 | } 8 | return success(true); 9 | }); 10 | -------------------------------------------------------------------------------- /server/api/checkuser.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const userId = await getUserId(event); 5 | 6 | const user = await prisma.user.findUnique({ 7 | where: { 8 | id: userId, 9 | }, 10 | }); 11 | if (!user) { 12 | deleteCookie(event, "Authorization"); 13 | return success(false); 14 | } 15 | return success(true); 16 | }); 17 | -------------------------------------------------------------------------------- /server/api/config.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const settings = await prisma.systemSetting.findUnique({ 5 | select: { 6 | title: true, 7 | description: true, 8 | keywords: true, 9 | version: true, 10 | openRegister: true, 11 | }, 12 | where: { 13 | id: 1, 14 | }, 15 | }); 16 | return success(settings); 17 | }); 18 | -------------------------------------------------------------------------------- /server/api/entry/analytics/attribution.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | if (!body.bookId) { 6 | return error("请先选择账本"); 7 | } 8 | 9 | const where: any = { 10 | bookId: { 11 | equals: body.bookId, 12 | }, 13 | }; 14 | if (body.flowType) { 15 | where.flowType = { 16 | equals: body.flowType, 17 | }; 18 | } 19 | if (body.startDay && body.endDay) { 20 | where.day = { 21 | gte: body.startDay, 22 | lte: body.endDay, 23 | }; 24 | } else if (body.startDay) { 25 | where.day = { 26 | gte: body.startDay, 27 | }; 28 | } else if (body.endDay) { 29 | where.day = { 30 | lte: body.endDay, 31 | }; 32 | } 33 | // flowType = 收入 / 支出 / 不计收支 34 | const dayGroups = await prisma.flow.groupBy({ 35 | by: ["attribution", "flowType"], 36 | _sum: { 37 | money: true, 38 | }, 39 | orderBy: [ 40 | { 41 | attribution: "asc", 42 | }, 43 | { 44 | flowType: "asc", 45 | }, 46 | ], 47 | where, // 使用条件查询 48 | }); 49 | 50 | // 初始化结果格式 51 | const datas = []; 52 | const groupedByPayType: Record< 53 | string, 54 | { 55 | type: string; 56 | inSum: number; 57 | outSum: number; 58 | zeroSum: number; 59 | } 60 | > = {}; 61 | 62 | // 按 day 分组,合并数据到目标格式 63 | dayGroups.reduce((acc, item) => { 64 | let attribution = item.attribution; 65 | if (!attribution) { 66 | attribution = "未知"; 67 | } 68 | const flowType = item.flowType; 69 | const moneySum = item._sum.money || 0; // 如果 money 为 null,默认值为 0 70 | 71 | // 如果当前 day 不存在,则初始化 72 | if (!acc[attribution]) { 73 | acc[attribution] = { 74 | type: attribution, 75 | inSum: 0, 76 | outSum: 0, 77 | zeroSum: 0, 78 | }; 79 | } 80 | 81 | // 根据 flowType 填充对应的 sum 82 | if (flowType === "收入") { 83 | acc[attribution].inSum += moneySum; 84 | } else if (flowType === "支出") { 85 | acc[attribution].outSum += moneySum; 86 | } else if (flowType === "不计收支") { 87 | acc[attribution].zeroSum += moneySum; 88 | } 89 | 90 | return acc; 91 | }, groupedByPayType); 92 | 93 | // 转换为数组格式 94 | for (const attribution in groupedByPayType) { 95 | datas.push(groupedByPayType[attribution]); 96 | } 97 | return success(datas); 98 | }); 99 | -------------------------------------------------------------------------------- /server/api/entry/analytics/daily.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { bookId, flowType } = await readBody(event); // 获取查询参数 5 | if (!bookId) { 6 | return error("请先选择账本"); 7 | } 8 | 9 | const where: any = { 10 | bookId, 11 | }; // 条件查询 12 | 13 | if (flowType) { 14 | where.flowType = { 15 | equals: flowType, 16 | }; 17 | } 18 | 19 | // flowType = 收入 / 支出 / 不计收支 20 | const dayGroups = await prisma.flow.groupBy({ 21 | by: ["day", "flowType"], 22 | _sum: { 23 | money: true, 24 | }, 25 | where, // 使用条件查询 26 | orderBy: [ 27 | { 28 | day: "asc", 29 | }, 30 | { 31 | flowType: "asc", 32 | }, 33 | ], 34 | }); 35 | 36 | // 初始化结果格式 37 | const datas = []; 38 | const groupedByDay: Record< 39 | string, 40 | { 41 | type: string; 42 | inSum: number; 43 | outSum: number; 44 | zeroSum: number; 45 | } 46 | > = {}; 47 | // 按 day 分组,合并数据到目标格式 48 | dayGroups.reduce((acc, item) => { 49 | const day = item.day; 50 | const flowType = item.flowType; 51 | const moneySum = item._sum.money || 0; // 如果 money 为 null,默认值为 0 52 | 53 | // 如果当前 day 不存在,则初始化 54 | if (!acc[day]) { 55 | acc[day] = { 56 | type: day, 57 | inSum: 0, 58 | outSum: 0, 59 | zeroSum: 0, 60 | }; 61 | } 62 | 63 | // 根据 flowType 填充对应的 sum 64 | if (flowType === "收入") { 65 | acc[day].inSum += moneySum; 66 | } else if (flowType === "支出") { 67 | acc[day].outSum += moneySum; 68 | } else if (flowType === "不计收支") { 69 | acc[day].zeroSum += moneySum; 70 | } 71 | 72 | return acc; 73 | }, groupedByDay); 74 | 75 | // 转换为数组格式 76 | for (const day in groupedByDay) { 77 | groupedByDay[day].inSum = parseFloat(groupedByDay[day].inSum.toFixed(2)); 78 | groupedByDay[day].outSum = parseFloat(groupedByDay[day].outSum.toFixed(2)); 79 | groupedByDay[day].zeroSum = parseFloat( 80 | groupedByDay[day].zeroSum.toFixed(2) 81 | ); 82 | datas.push(groupedByDay[day]); 83 | } 84 | return success(datas); 85 | }); 86 | -------------------------------------------------------------------------------- /server/api/entry/analytics/industryType.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | if (!body.bookId) { 6 | return error("请先选择账本"); 7 | } 8 | 9 | const where: any = { 10 | bookId: { 11 | equals: body.bookId, 12 | }, 13 | }; 14 | if (body.flowType) { 15 | where.flowType = { 16 | equals: body.flowType, 17 | }; 18 | } 19 | if (body.startDay && body.endDay) { 20 | where.day = { 21 | gte: body.startDay, 22 | lte: body.endDay, 23 | }; 24 | } else if (body.startDay) { 25 | where.day = { 26 | gte: body.startDay, 27 | }; 28 | } else if (body.endDay) { 29 | where.day = { 30 | lte: body.endDay, 31 | }; 32 | } 33 | 34 | // flowType = 收入 / 支出 / 不计收支 35 | const dayGroups = await prisma.flow.groupBy({ 36 | by: ["industryType", "flowType"], 37 | _sum: { 38 | money: true, 39 | }, 40 | orderBy: [ 41 | { 42 | industryType: "asc", 43 | }, 44 | { 45 | flowType: "asc", 46 | }, 47 | ], 48 | where, // 使用条件查询 49 | }); 50 | 51 | // 初始化结果格式 52 | const datas = []; 53 | const groupedByIndustry: Record< 54 | string, 55 | { 56 | type: string; 57 | inSum: number; 58 | outSum: number; 59 | zeroSum: number; 60 | } 61 | > = {}; 62 | 63 | // 按 day 分组,合并数据到目标格式 64 | dayGroups.reduce((acc, item) => { 65 | let industryType = item.industryType; 66 | if (!industryType) { 67 | industryType = "未知"; 68 | } 69 | const flowType = item.flowType; 70 | const moneySum = item._sum.money || 0; // 如果 money 为 null,默认值为 0 71 | 72 | // 如果当前 day 不存在,则初始化 73 | if (!acc[industryType]) { 74 | acc[industryType] = { 75 | type: industryType, 76 | inSum: 0, 77 | outSum: 0, 78 | zeroSum: 0, 79 | }; 80 | } 81 | 82 | // 根据 flowType 填充对应的 sum 83 | if (flowType === "收入") { 84 | acc[industryType].inSum += moneySum; 85 | } else if (flowType === "支出") { 86 | acc[industryType].outSum += moneySum; 87 | } else if (flowType === "不计收支") { 88 | acc[industryType].zeroSum += moneySum; 89 | } 90 | 91 | return acc; 92 | }, groupedByIndustry); 93 | 94 | // 转换为数组格式 95 | for (const industryType in groupedByIndustry) { 96 | datas.push(groupedByIndustry[industryType]); 97 | } 98 | return success(datas); 99 | }); 100 | -------------------------------------------------------------------------------- /server/api/entry/analytics/month.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | if (!body.bookId) { 6 | return error("请先选择账本"); 7 | } 8 | 9 | const where: any = { 10 | bookId: { 11 | equals: body.bookId, 12 | }, 13 | }; 14 | if (body.flowType) { 15 | where.flowType = { 16 | equals: body.flowType, 17 | }; 18 | } 19 | if (body.startDay && body.endDay) { 20 | where.day = { 21 | gte: body.startDay, 22 | lte: body.endDay, 23 | }; 24 | } else if (body.startDay) { 25 | where.day = { 26 | gte: body.startDay, 27 | }; 28 | } else if (body.endDay) { 29 | where.day = { 30 | lte: body.endDay, 31 | }; 32 | } 33 | 34 | // flowType = 收入 / 支出 / 不计收支 35 | // 使用 `to_char` 将日期按月份分组 (PostgreSQL 支持) 36 | const monthGroups: any[] = await prisma.$queryRaw` 37 | SELECT 38 | SUBSTRING(day, 1, 7) AS month, 39 | "flowType", 40 | SUM("money") AS money_sum 41 | FROM "Flow" 42 | WHERE "bookId" = ${String(body.bookId)} 43 | GROUP BY SUBSTRING(day, 1, 7), "flowType" 44 | ORDER BY month ASC, "flowType" ASC; 45 | `; 46 | 47 | // 初始化结果格式 48 | const datas = []; 49 | const groupedByMonth: Record< 50 | string, 51 | { 52 | type: string; 53 | inSum: number; 54 | outSum: number; 55 | zeroSum: number; 56 | } 57 | > = {}; 58 | // 按 month 分组,合并数据到目标格式 59 | monthGroups.forEach((item: any) => { 60 | const month = item.month; 61 | const flowType = item.flowType; 62 | const moneySum = parseFloat(item.money_sum) || 0; // 防止 NULL 转为数字 63 | 64 | // 如果当前 month 不存在,则初始化 65 | if (!groupedByMonth[month]) { 66 | groupedByMonth[month] = { 67 | type: month, 68 | inSum: 0, 69 | outSum: 0, 70 | zeroSum: 0, 71 | }; 72 | } 73 | 74 | // 根据 flowType 填充对应的 sum 75 | if (flowType === "收入") { 76 | groupedByMonth[month].inSum += moneySum; 77 | } else if (flowType === "支出") { 78 | groupedByMonth[month].outSum += moneySum; 79 | } else if (flowType === "不计收支") { 80 | groupedByMonth[month].zeroSum += moneySum; 81 | } 82 | }); 83 | 84 | // 转换为数组格式 85 | for (const day in groupedByMonth) { 86 | datas.push(groupedByMonth[day]); 87 | } 88 | return success(datas); 89 | }); 90 | -------------------------------------------------------------------------------- /server/api/entry/analytics/monthAnalysis.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { bookId, flowType, month } = await readBody(event); // 获取查询参数 5 | if (!bookId) { 6 | return error("请先选择账本"); 7 | } 8 | if (!month) { 9 | return error("Not Find Month"); 10 | } 11 | 12 | const where: any = { 13 | bookId, 14 | day: { 15 | startsWith: month, 16 | }, 17 | }; // 条件查询 18 | 19 | if (flowType) { 20 | where.flowType = { 21 | equals: flowType, 22 | }; 23 | } 24 | 25 | const count = await prisma.flow.count({ where }); 26 | if (count <= 0) { 27 | return error("暂无数据"); 28 | } 29 | 30 | // 31 | // Month string `json:"month"` 32 | // OutSum string `json:"outSum"` // 总支出 33 | // InSum string `json:"inSum"` // 总收入 34 | // ZeroSum string `json:"zeroSum"` // 总不计收支 35 | // MaxType string `json:"maxType"` // 最大支出类型 36 | // MaxTypeSum string `json:"maxTypeSum"` // 最大支出金额 37 | // MaxOut Flow `json:"maxOut"` // 最大单笔支出 38 | // MaxIn Flow `json:"maxIn"` // 最大单笔收入 39 | const res: any = { 40 | month, 41 | inSum: 0, 42 | outSum: 0, 43 | zeroSum: 0, 44 | maxInType: "", 45 | maxInTypeSum: 0, 46 | maxOutType: "", 47 | maxOutTypeSum: 0, 48 | maxIn: {}, 49 | maxOut: {}, 50 | maxZero: {}, 51 | }; 52 | 53 | // 1. 按月查询当月总收入、总支出、总不计收支 54 | const monthSum = await prisma.flow.groupBy({ 55 | by: ["flowType"], 56 | _sum: { 57 | money: true, 58 | }, 59 | where, 60 | }); 61 | monthSum.forEach((item) => { 62 | if (item.flowType == "收入") { 63 | res.inSum = (item._sum.money || 0).toFixed(2); 64 | } else if (item.flowType == "支出") { 65 | res.outSum = (item._sum.money || 0).toFixed(2); 66 | } else if (item.flowType == "不计收支") { 67 | res.zeroSum = (item._sum.money || 0).toFixed(2); 68 | } 69 | }); 70 | 71 | // 2. 查询当月最高收入类型 72 | const maxInType = await prisma.flow.groupBy({ 73 | by: ["industryType"], 74 | _sum: { 75 | money: true, 76 | }, 77 | where: { 78 | ...where, 79 | flowType: "收入", 80 | }, 81 | orderBy: { 82 | _sum: { 83 | money: "desc", // 按消费金额降序排列 84 | }, 85 | }, 86 | take: 1, // 只取第一个结果 87 | }); 88 | if (maxInType[0]) { 89 | res.maxInType = maxInType[0].industryType || ""; 90 | res.maxInTypeSum = (maxInType[0]._sum.money || 0).toFixed(2); 91 | } 92 | 93 | // 3. 查询当月最高支出类型 94 | const maxOutType = await prisma.flow.groupBy({ 95 | by: ["industryType"], 96 | _sum: { 97 | money: true, 98 | }, 99 | where: { 100 | ...where, 101 | flowType: "支出", 102 | }, 103 | orderBy: { 104 | _sum: { 105 | money: "desc", // 按消费金额降序排列 106 | }, 107 | }, 108 | take: 1, // 只取第一个结果 109 | }); 110 | if (maxOutType[0]) { 111 | res.maxOutType = maxOutType[0].industryType || ""; 112 | res.maxOutTypeSum = (maxOutType[0]._sum.money || 0).toFixed(2); 113 | } 114 | 115 | // 4. 查询当月最高单笔收入 116 | const maxIn = await prisma.flow.findFirst({ 117 | where: { 118 | ...where, 119 | flowType: "收入", 120 | }, 121 | orderBy: { 122 | money: "desc", 123 | }, 124 | }); 125 | res.maxIn = maxIn || {}; 126 | // 5. 查询当月最高单笔支出 127 | const maxOut = await prisma.flow.findFirst({ 128 | where: { 129 | ...where, 130 | flowType: "支出", 131 | }, 132 | orderBy: { 133 | money: "desc", 134 | }, 135 | }); 136 | res.maxOut = maxOut || {}; 137 | // 6. 查询当月最高单笔支出 138 | const maxZero = await prisma.flow.findFirst({ 139 | where: { 140 | ...where, 141 | flowType: "不计收支", 142 | }, 143 | orderBy: { 144 | money: "desc", 145 | }, 146 | }); 147 | res.maxZero = maxZero || {}; 148 | return success(res); 149 | }); 150 | -------------------------------------------------------------------------------- /server/api/entry/analytics/payType.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | if (!body.bookId) { 6 | return error("请先选择账本"); 7 | } 8 | 9 | const where: any = { 10 | bookId: { 11 | equals: body.bookId, 12 | }, 13 | }; 14 | if (body.flowType) { 15 | where.flowType = { 16 | equals: body.flowType, 17 | }; 18 | } 19 | if (body.startDay && body.endDay) { 20 | where.day = { 21 | gte: body.startDay, 22 | lte: body.endDay, 23 | }; 24 | } else if (body.startDay) { 25 | where.day = { 26 | gte: body.startDay, 27 | }; 28 | } else if (body.endDay) { 29 | where.day = { 30 | lte: body.endDay, 31 | }; 32 | } 33 | // flowType = 收入 / 支出 / 不计收支 34 | const dayGroups = await prisma.flow.groupBy({ 35 | by: ["payType", "flowType"], 36 | _sum: { 37 | money: true, 38 | }, 39 | orderBy: [ 40 | { 41 | payType: "asc", 42 | }, 43 | { 44 | flowType: "asc", 45 | }, 46 | ], 47 | where, // 使用条件查询 48 | }); 49 | 50 | // 初始化结果格式 51 | const datas = []; 52 | const groupedByPayType: Record< 53 | string, 54 | { 55 | type: string; 56 | inSum: number; 57 | outSum: number; 58 | zeroSum: number; 59 | } 60 | > = {}; 61 | 62 | // 按 day 分组,合并数据到目标格式 63 | dayGroups.reduce((acc, item) => { 64 | let payType = item.payType; 65 | if (!payType) { 66 | payType = "未知"; 67 | } 68 | const flowType = item.flowType; 69 | const moneySum = item._sum.money || 0; // 如果 money 为 null,默认值为 0 70 | 71 | // 如果当前 day 不存在,则初始化 72 | if (!acc[payType]) { 73 | acc[payType] = { 74 | type: payType, 75 | inSum: 0, 76 | outSum: 0, 77 | zeroSum: 0, 78 | }; 79 | } 80 | 81 | // 根据 flowType 填充对应的 sum 82 | if (flowType === "收入") { 83 | acc[payType].inSum += moneySum; 84 | } else if (flowType === "支出") { 85 | acc[payType].outSum += moneySum; 86 | } else if (flowType === "不计收支") { 87 | acc[payType].zeroSum += moneySum; 88 | } 89 | 90 | return acc; 91 | }, groupedByPayType); 92 | 93 | // 转换为数组格式 94 | for (const payType in groupedByPayType) { 95 | datas.push(groupedByPayType[payType]); 96 | } 97 | return success(datas); 98 | }); 99 | -------------------------------------------------------------------------------- /server/api/entry/book/add.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | import { getUUID } from "~/utils/common"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | const body = await readBody(event); // 获取请求体 6 | const { bookName, budget } = body; 7 | 8 | const userId = await getUserId(event); 9 | 10 | const bookId = userId + "-" + getUUID(8); 11 | // 在数据库中添加新数据 12 | const created = await prisma.book.create({ 13 | data: { 14 | bookId, 15 | userId, 16 | bookName, 17 | budget: Number(budget || 0), 18 | }, 19 | }); 20 | 21 | // 初始化 book 的 TypeRelation 数据 22 | const dTypes = await prisma.typeRelation.findMany({ 23 | where: { 24 | bookId: "0", 25 | userId: 0, 26 | }, 27 | }); 28 | 29 | const newTypes: any = []; 30 | dTypes.forEach((t) => { 31 | newTypes.push({ 32 | bookId, 33 | userId, 34 | source: t.source, 35 | target: t.target, 36 | }); 37 | }); 38 | await prisma.typeRelation.createMany({ 39 | data: newTypes, 40 | }); 41 | 42 | return success(created); 43 | }); 44 | -------------------------------------------------------------------------------- /server/api/entry/book/all.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const userId = await getUserId(event); 5 | const books = await prisma.book.findMany({ where: { userId } }); 6 | return success(books); 7 | }); 8 | -------------------------------------------------------------------------------- /server/api/entry/book/del.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { id } = await readBody(event); // 从请求体获取 ID 5 | const userId = await getUserId(event); 6 | 7 | if (!id) { 8 | return error("Not Find ID"); 9 | } 10 | // 删除数据 11 | const deleted = await prisma.book.delete({ 12 | where: { id, userId }, 13 | }); 14 | 15 | return success(deleted); 16 | }); 17 | -------------------------------------------------------------------------------- /server/api/entry/book/inshare.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | const userId = await getUserId(event); 6 | const { key } = body; 7 | if (!key) { 8 | return error("Not Find key"); 9 | } 10 | const books = await prisma.book.findMany({ 11 | where: { 12 | shareKey: { 13 | equals: String(key), 14 | }, 15 | }, 16 | }); 17 | if (books.length > 0) { 18 | if (books.filter((b) => (b.userId == userId)).length > 0) { 19 | return error("账本已存在"); 20 | } 21 | const book = books[0]; 22 | await prisma.book.create({ 23 | data: { 24 | userId, 25 | bookId: book.bookId, 26 | bookName: book.bookName, 27 | createDate: book.createDate, 28 | shareKey: book.shareKey, 29 | }, 30 | }); 31 | } else { 32 | return error("无效Key!"); 33 | } 34 | 35 | return success(); 36 | }); 37 | -------------------------------------------------------------------------------- /server/api/entry/book/list.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | const userId = await getUserId(event); 6 | 7 | const where: any = { 8 | userId, 9 | }; // 条件查询 10 | 11 | // 添加条件:如果 `name` 存在,则根据 `name` 查询 12 | if (body.id) { 13 | // equals 等于查询 14 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 15 | where.id = { 16 | equals: Number(body.id), 17 | }; 18 | } 19 | 20 | // 如果 `email` 存在,则根据 `email` 查询 21 | if (body.bookName) { 22 | where.bookName = { 23 | contains: String(body.bookName), 24 | }; 25 | } 26 | 27 | const books = await prisma.book.findMany({ 28 | where, // 使用条件查询 29 | }); 30 | 31 | return success(books); 32 | }); 33 | -------------------------------------------------------------------------------- /server/api/entry/book/page.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | const userId = await getUserId(event); 6 | 7 | const where: any = { 8 | userId, 9 | }; // 条件查询 10 | 11 | // 普通查询条件 12 | if (body.id) { 13 | // equals 等于查询 14 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 15 | where.id = { 16 | equals: body.id, 17 | }; 18 | } 19 | if (body.bookName) { 20 | where.bookName = { 21 | contains: body.bookName, 22 | }; 23 | } 24 | if (body.shareKey) { 25 | where.shareKey = { 26 | equals: body.shareKey, 27 | }; 28 | } 29 | 30 | // 分页条件 31 | const pageNum = Number(body.pageNum ? body.pageNum : 1); 32 | const pageSize = Number(body.pageSize ? body.pageSize : 15); 33 | const skip = (pageNum - 1) * pageSize; // 计算跳过的条目数 34 | 35 | // 排序条件 36 | const orderBy: any = [ 37 | { 38 | createDate: "desc", 39 | }, 40 | ]; 41 | if (pageSize == -1) { 42 | // 查询全部 43 | const datas = await prisma.book.findMany({ where, orderBy }); 44 | return success({ total: datas.length, data: datas, pages: 1 }); 45 | } 46 | 47 | // 【条件、排序、分页】 组合查询 48 | const users = await prisma.book.findMany({ 49 | where, 50 | orderBy, 51 | skip, 52 | take: pageSize, 53 | }); 54 | // 计算总页数 55 | const totalUsers = await prisma.book.count({ where }); 56 | const totalPages = Math.ceil(totalUsers / pageSize); 57 | 58 | return success({ total: totalUsers, data: users, pages: totalPages }); 59 | }); 60 | -------------------------------------------------------------------------------- /server/api/entry/book/share.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | import { getUUID } from "~/utils/common"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | const body = await readBody(event); 6 | const userId = await getUserId(event); 7 | const { id } = body; 8 | if (!id) { 9 | return error("Not Find ID"); 10 | } 11 | 12 | // 生成共享密钥 13 | const key = getUUID(8); 14 | const shareKey = `${userId}${id}${key}`; 15 | 16 | const updated = await prisma.book.update({ 17 | where: { id, userId }, 18 | data: { 19 | shareKey, 20 | }, 21 | }); 22 | return success(updated); 23 | }); 24 | -------------------------------------------------------------------------------- /server/api/entry/book/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | const { bookName, budget, bookId } = body; 6 | if (!bookId) { 7 | return error("Not Find bookID"); 8 | } 9 | const updated = await prisma.book.updateMany({ 10 | where: { bookId }, 11 | data: { 12 | bookName, 13 | budget: Number(budget || 0), 14 | }, 15 | }); 16 | return success(updated); 17 | }); 18 | -------------------------------------------------------------------------------- /server/api/entry/budget/add.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { bookId, month, budget } = body; 6 | 7 | if (!bookId) { 8 | return error("请先选择账本"); 9 | } 10 | const userId = await getUserId(event); 11 | 12 | // 在数据库中添加新数据 13 | const created = await prisma.budget.create({ 14 | data: { 15 | bookId, 16 | userId, 17 | month, 18 | budget: Number(budget || 0), 19 | }, 20 | }); 21 | 22 | return success(created); 23 | }); 24 | -------------------------------------------------------------------------------- /server/api/entry/budget/all.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { bookId } = body; 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | const datas = await prisma.budget.findMany({ where: { bookId } }); 10 | return success(datas); 11 | }); 12 | -------------------------------------------------------------------------------- /server/api/entry/budget/del.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { bookId, id } = body; 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | if (!id) { 11 | return error("Not Find ID"); 12 | } 13 | // 删除数据 14 | const deleted = await prisma.budget.delete({ 15 | where: { id, bookId }, 16 | }); 17 | 18 | return success(deleted); 19 | }); 20 | -------------------------------------------------------------------------------- /server/api/entry/budget/list.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { bookId, month } = body; 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | const where: any = { 11 | bookId, 12 | }; // 条件查询 13 | 14 | // 添加条件:如果 `name` 存在,则根据 `name` 查询 15 | if (body.id) { 16 | // equals 等于查询 17 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 18 | where.id = { 19 | equals: Number(body.id), 20 | }; 21 | } 22 | if (month) { 23 | where.month = { 24 | equals: month, 25 | }; 26 | } 27 | 28 | const datas = await prisma.budget.findMany({ 29 | where, // 使用条件查询 30 | }); 31 | 32 | // 如果 datas 为空,则查询 book表中设置的预算,并添加一条记录到预算表 33 | if (datas.length === 0) { 34 | const book = await prisma.book.findFirst({ 35 | where: { 36 | bookId: bookId, 37 | }, 38 | }); 39 | console.log(book); 40 | if (book) { 41 | const budget = await prisma.budget.create({ 42 | data: { 43 | bookId, 44 | month: body.month, 45 | budget: book.budget, 46 | userId: book.userId, 47 | }, 48 | }); 49 | datas.push(budget); 50 | } 51 | } 52 | 53 | return success(datas); 54 | }); 55 | -------------------------------------------------------------------------------- /server/api/entry/budget/reloadUsedAmount.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { bookId, month } = body; 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | const where: any = { 11 | bookId, 12 | }; // 条件查询 13 | 14 | if (month) { 15 | where.month = { 16 | equals: month, 17 | }; 18 | } 19 | 20 | const usedAmount = await prisma.flow.groupBy({ 21 | where: { bookId: bookId, flowType: "支出", day: { startsWith: month } }, 22 | by: ["flowType"], 23 | _sum: { 24 | money: true, 25 | }, 26 | }); 27 | 28 | // 更新预算表的used字段 29 | const updated = await prisma.budget.updateMany({ 30 | where: { 31 | bookId, 32 | month, 33 | }, 34 | data: { 35 | used: usedAmount[0]._sum.money || 0, 36 | }, 37 | }); 38 | 39 | return success(updated); 40 | }); 41 | -------------------------------------------------------------------------------- /server/api/entry/budget/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { bookId, id, budget, month } = body; 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | if (!id) { 11 | return error("Not Find ID"); 12 | } 13 | if (!month) { 14 | return error("Not Find Month"); 15 | } 16 | const updated = await prisma.budget.update({ 17 | where: { id, bookId, month }, 18 | data: { 19 | budget: Number(budget || 0), 20 | }, 21 | }); 22 | return success(updated); 23 | }); 24 | -------------------------------------------------------------------------------- /server/api/entry/fixedFlow/add.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { 6 | bookId, 7 | startMonth, 8 | endMonth, 9 | money, 10 | name, 11 | description, 12 | flowType, 13 | industryType, 14 | payType, 15 | attribution, 16 | } = body; 17 | 18 | if (!bookId) { 19 | return error("请先选择账本"); 20 | } 21 | const userId = await getUserId(event); 22 | 23 | const start = new Date(startMonth); 24 | const end = new Date(endMonth); 25 | const createdRecords = []; 26 | 27 | // 遍历每个月份并新增固定支出记录 28 | for (let month = start; month <= end; month.setMonth(month.getMonth() + 1)) { 29 | const monthString = month.toISOString().slice(0, 7); // 格式化为 YYYY-MM 30 | 31 | const created = await prisma.fixedFlow.create({ 32 | data: { 33 | bookId, 34 | userId, 35 | month: monthString, 36 | money: Number(money || 0), 37 | name, 38 | description, 39 | flowType, 40 | industryType, 41 | payType, 42 | attribution, 43 | }, 44 | }); 45 | 46 | createdRecords.push(created); 47 | } 48 | 49 | return success(createdRecords); 50 | }); 51 | -------------------------------------------------------------------------------- /server/api/entry/fixedFlow/all.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { bookId } = body; 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | const datas = await prisma.fixedFlow.findMany({ where: { bookId } }); 10 | return success(datas); 11 | }); 12 | -------------------------------------------------------------------------------- /server/api/entry/fixedFlow/del.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { bookId, id } = body; 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | if (!id) { 11 | return error("Not Find ID"); 12 | } 13 | // 删除数据 14 | const deleted = await prisma.fixedFlow.delete({ 15 | where: { id, bookId }, 16 | }); 17 | 18 | return success(deleted); 19 | }); 20 | -------------------------------------------------------------------------------- /server/api/entry/fixedFlow/list.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { bookId } = body; 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | const where: any = { 11 | bookId, 12 | }; // 条件查询 13 | 14 | // 添加条件:如果 `name` 存在,则根据 `name` 查询 15 | if (body.id) { 16 | // equals 等于查询 17 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 18 | where.id = { 19 | equals: Number(body.id), 20 | }; 21 | } 22 | 23 | if (body.month) { 24 | where.month = { 25 | equals: body.month, 26 | }; 27 | } 28 | 29 | const datas = await prisma.fixedFlow.findMany({ 30 | where, // 使用条件查询 31 | }); 32 | 33 | return success(datas); 34 | }); 35 | -------------------------------------------------------------------------------- /server/api/entry/fixedFlow/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | const { 6 | bookId, 7 | id, 8 | month, 9 | money, 10 | name, 11 | description, 12 | flowType, 13 | industryType, 14 | payType, 15 | attribution, 16 | } = body; 17 | if (!bookId) { 18 | return error("请先选择账本"); 19 | } 20 | 21 | if (!id) { 22 | return error("Not Find ID"); 23 | } 24 | const updated = await prisma.fixedFlow.update({ 25 | where: { id, bookId }, 26 | data: { 27 | month, 28 | money: Number(money || 0), 29 | name, 30 | description, 31 | flowType, 32 | industryType, 33 | payType, 34 | attribution, 35 | }, 36 | }); 37 | return success(updated); 38 | }); 39 | -------------------------------------------------------------------------------- /server/api/entry/flow/add.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | 6 | if (!body.bookId) { 7 | return; 8 | } 9 | const userId = await getUserId(event); 10 | const flow = { 11 | userId: userId, 12 | bookId: String(body.bookId), 13 | day: String(body.day || ""), 14 | flowType: String(body.flowType || ""), // 流水类型:收入、支出 15 | industryType: String(body.industryType || ""), // 行业分类 原 type(收入类型、支出类型) 16 | payType: String(body.payType || ""), // 支付方式 17 | name: String(body.name || ""), 18 | money: Number(body.money || ""), 19 | description: String(body.description || ""), 20 | // invoice: String(body.invoice || ""), 21 | attribution: String(body.attribution || ""), 22 | }; 23 | 24 | // 在数据库中添加新数据 25 | const created = await prisma.flow.create({ 26 | data: flow, 27 | }); 28 | return success(created); 29 | }); 30 | -------------------------------------------------------------------------------- /server/api/entry/flow/all.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const query = getQuery(event); 5 | if (!query.bookId) { 6 | return error("请先选择账本"); 7 | } 8 | const flows = await prisma.flow.findMany({ 9 | where: { bookId: String(query.bookId) }, 10 | }); 11 | return success(flows); 12 | }); 13 | -------------------------------------------------------------------------------- /server/api/entry/flow/condidate/autos.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | // 此处的相似性判断示例:金额完全相等(你可根据业务需要添加金额误差、日期范围等条件) 4 | export default defineEventHandler(async (event) => { 5 | const body = await readBody(event); // 获取请求体 6 | if (!body.bookId) { 7 | return error("No Find bookid"); 8 | } 9 | // const userId = await getUserId(event); 10 | // 获取所有未平账的支出数据 11 | const expenditures = await prisma.flow.findMany({ 12 | where: { flowType: "支出", eliminate: 0, bookId: String(body.bookId) }, 13 | orderBy: [ 14 | { 15 | day: "desc", 16 | }, 17 | ], 18 | }); 19 | 20 | const candidatePairs = []; 21 | 22 | // 对每笔支出查找候选记录 23 | for (const expense of expenditures) { 24 | const candidate = await prisma.flow.findFirst({ 25 | where: { 26 | flowType: { in: ["收入", "不计收支"] }, 27 | // 例如这里以金额完全相等为条件,实际可修改为近似匹配: 28 | money: expense.money, 29 | bookId: String(body.bookId), 30 | // 如有需要,可增加日期、描述等其它条件 31 | }, 32 | }); 33 | if (candidate) { 34 | candidatePairs.push({ 35 | out: expense, 36 | in: candidate, 37 | }); 38 | } 39 | } 40 | 41 | return success(candidatePairs); 42 | }); 43 | -------------------------------------------------------------------------------- /server/api/entry/flow/condidate/confirm.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | // const userId = await getUserId(event); 6 | const { outId, bookId, inIds } = body; 7 | if (!outId || !bookId) { 8 | return error("Not Find ID"); 9 | } 10 | if (!inIds) { 11 | return error("Not Find IDS"); 12 | } 13 | await prisma.flow.update({ 14 | where: { id: outId, bookId }, 15 | data: { 16 | eliminate: 1, 17 | flowType: "不计收支", 18 | }, 19 | }); 20 | inIds.forEach(async (id: any) => { 21 | await prisma.flow.update({ 22 | where: { id: Number(id), bookId }, 23 | data: { 24 | eliminate: 1, 25 | flowType: "不计收支", 26 | }, 27 | }); 28 | }); 29 | return success(); 30 | }); 31 | -------------------------------------------------------------------------------- /server/api/entry/flow/condidate/ignore.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | // const userId = await getUserId(event); 6 | const { id, bookId } = body; 7 | if (!id || !bookId) { 8 | return error("Not Find ID"); 9 | } 10 | const updated = await prisma.flow.update({ 11 | where: { id, bookId }, 12 | data: { 13 | eliminate: -1, 14 | }, 15 | }); 16 | return success(updated); 17 | }); 18 | -------------------------------------------------------------------------------- /server/api/entry/flow/condidate/ignoreAll.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | // const userId = await getUserId(event); 6 | const { ids, bookId } = body; 7 | if (!ids || !bookId) { 8 | return error("Not Find ID"); 9 | } 10 | if (ids.length <= 0) { 11 | return error("无数据"); 12 | } 13 | const idsJoin = ids.join(","); 14 | // console.log( 15 | // `UPDATE "Flow" SET "eliminate" = -1 WHERE "bookId" = \"${String( 16 | // bookId 17 | // )}\" AND "id" in (${idsJoin});` 18 | // ); 19 | const updated = await prisma.$executeRawUnsafe( 20 | `UPDATE "Flow" SET "eliminate" = -1 WHERE "bookId" = '${String( 21 | bookId 22 | )}' AND "id" in (${idsJoin});` 23 | ); 24 | 25 | return success(updated); 26 | }); 27 | -------------------------------------------------------------------------------- /server/api/entry/flow/deduplication/autos.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | // 查找疑似重复的数据:同一天+金额相同+支出类型相同的数据 4 | export default defineEventHandler(async (event) => { 5 | const body = await readBody(event); // 获取请求体 6 | if (!body.bookId) { 7 | return error("No Find bookid"); 8 | } 9 | 10 | // 获取检测条件,如果未提供则使用默认值(全部条件) 11 | const criteria = body.criteria || { 12 | name: true, 13 | description: true, 14 | industryType: true, 15 | flowType: true, 16 | payType: true, 17 | }; 18 | 19 | // 获取所有流水数据 20 | const allFlows = await prisma.flow.findMany({ 21 | where: { 22 | bookId: String(body.bookId), 23 | // 可以根据需要添加其他过滤条件,如时间范围等 24 | }, 25 | orderBy: [ 26 | { 27 | day: "desc", 28 | }, 29 | ], 30 | }); 31 | 32 | // 用于存储可能的重复组 33 | const duplicateGroups = []; 34 | 35 | // 创建一个Map来跟踪已处理的记录ID 36 | const processedIds = new Set(); 37 | 38 | // 遍历所有流水记录 39 | for (let i = 0; i < allFlows.length; i++) { 40 | const current = allFlows[i]; 41 | 42 | // 如果当前记录已经被处理过,则跳过 43 | if (processedIds.has(current.id)) continue; 44 | 45 | // 查找与当前记录相似的记录 46 | const similarRecords = allFlows.filter((flow, index) => { 47 | // 基础条件:不是同一条记录、未被处理过、同一天、金额相同(这些是必选条件) 48 | let isSimilar = 49 | index !== i && 50 | !processedIds.has(flow.id) && 51 | flow.day === current.day && 52 | flow.money === current.money; 53 | 54 | // 如果基础条件不满足,直接返回false 55 | if (!isSimilar) return false; 56 | 57 | // 根据用户选择的条件进行动态判断 58 | if (criteria.name && flow.name !== current.name) { 59 | return false; 60 | } 61 | 62 | if (criteria.description && flow.description !== current.description) { 63 | return false; 64 | } 65 | 66 | if (criteria.industryType && flow.industryType !== current.industryType) { 67 | return false; 68 | } 69 | 70 | if (criteria.flowType && flow.flowType !== current.flowType) { 71 | return false; 72 | } 73 | 74 | if (criteria.payType && flow.payType !== current.payType) { 75 | return false; 76 | } 77 | 78 | // 所有选中的条件都匹配,则认为是相似记录 79 | return true; 80 | }); 81 | 82 | // 如果找到相似记录,则创建一个组 83 | if (similarRecords.length > 0) { 84 | const group = [current, ...similarRecords]; 85 | duplicateGroups.push(group); 86 | 87 | // 将组内所有记录标记为已处理 88 | group.forEach((record) => processedIds.add(record.id)); 89 | } 90 | } 91 | 92 | return success({ 93 | duplicateGroups, 94 | totalGroups: duplicateGroups.length, 95 | totalDuplicates: processedIds.size, 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /server/api/entry/flow/del.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { id, bookId } = await readBody(event); // 从请求体获取 ID 5 | 6 | if (!id || !bookId) { 7 | return error("Not Find ID"); 8 | } 9 | // 删除数据 10 | const deleted = await prisma.flow.delete({ 11 | where: { id, bookId }, 12 | }); 13 | 14 | return success(deleted); 15 | }); 16 | -------------------------------------------------------------------------------- /server/api/entry/flow/dels.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { ids, bookId } = await readBody(event); // 从请求体获取 ID 5 | // const userId = await getUserId(event); 6 | 7 | if (!ids || !bookId) { 8 | return error("Not Find ID"); 9 | } 10 | const deleted = await prisma.flow.deleteMany({ 11 | where: { 12 | id: { 13 | in: ids, 14 | }, 15 | bookId: String(bookId), 16 | }, 17 | }); 18 | return success(deleted); 19 | }); 20 | -------------------------------------------------------------------------------- /server/api/entry/flow/getAttributions.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { bookId } = await readBody(event); 5 | 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | const where: any = { 11 | bookId, 12 | }; 13 | 14 | const flows = await prisma.flow.findMany({ 15 | distinct: ["attribution"], 16 | select: { 17 | attribution: true, 18 | }, 19 | orderBy: [ 20 | { 21 | attribution: "asc", 22 | }, 23 | ], 24 | where, 25 | }); 26 | 27 | // 提取 attribution 属性并返回集合 28 | const names = flows 29 | .map((flow) => flow.attribution) 30 | .filter((attribution) => attribution && attribution.trim() !== ""); 31 | 32 | return success(names); 33 | }); 34 | -------------------------------------------------------------------------------- /server/api/entry/flow/getNames.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { bookId } = await readBody(event); 5 | 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | const where: any = { 11 | bookId, 12 | }; 13 | 14 | const flows = await prisma.flow.findMany({ 15 | distinct: ["name"], 16 | select: { 17 | name: true, 18 | }, 19 | orderBy: [ 20 | { 21 | name: "asc", 22 | }, 23 | ], 24 | where, 25 | }); 26 | 27 | // 提取 name 属性并返回集合 28 | // const names = flows.map((flow) => flow.name); 29 | 30 | const names = flows 31 | .map((flow) => flow.name) 32 | .filter((attribution) => attribution && attribution.trim() !== ""); 33 | 34 | return success(names); 35 | }); 36 | -------------------------------------------------------------------------------- /server/api/entry/flow/imports.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取请求体 5 | 6 | if (!body.bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | // add/overwrite 11 | const mode = String(body.mode); 12 | const flows: any[] = body.flows; 13 | const userId = await getUserId(event); 14 | 15 | if (mode == "overwrite") { 16 | const del = await prisma.flow.deleteMany({ 17 | where: { 18 | bookId: body.bookId, 19 | }, 20 | }); 21 | } 22 | const datas: any[] = []; 23 | flows.forEach((flow) => { 24 | datas.push({ 25 | userId, 26 | bookId: body.bookId, 27 | name: flow.name, 28 | day: flow.day, 29 | description: flow.description, 30 | flowType: flow.flowType, 31 | invoice: flow.invoice ? String(flow.invoice) : null, 32 | money: Number(flow.money), 33 | payType: flow.payType, 34 | industryType: flow.type ? flow.type : flow.industryType || "", 35 | attribution: flow.attribution, 36 | }); 37 | }); 38 | // 在数据库中添加新数据 39 | const created = await prisma.flow.createMany({ 40 | data: datas, 41 | }); 42 | return success(created); 43 | }); 44 | -------------------------------------------------------------------------------- /server/api/entry/flow/invoice/clean.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import prisma from "~/lib/prisma"; 4 | 5 | export default defineEventHandler(async (event) => { 6 | const { id, bookId } = await readBody(event); 7 | 8 | if (!id) { 9 | return error("Not Find ID"); 10 | } 11 | 12 | if (!bookId) { 13 | return error("Not Find BookID"); 14 | } 15 | 16 | try { 17 | const flow = await prisma.flow.findUnique({ 18 | where: { 19 | id: Number(id), 20 | bookId: String(bookId), 21 | }, 22 | }); 23 | 24 | if (!flow) { 25 | return; 26 | } 27 | 28 | const invoices = flow.invoice ? flow.invoice.split(",") : []; 29 | 30 | // 删除文件 31 | for (const invoice of invoices) { 32 | const runtimeConfig = useRuntimeConfig(); 33 | let dataPath = String(runtimeConfig.dataPath); 34 | 35 | if (!dataPath) { 36 | dataPath = process.cwd(); 37 | } 38 | 39 | const imagePath = path.join(dataPath, "images", invoice); 40 | 41 | // 校验图片是否存在 42 | if (fs.existsSync(imagePath)) { 43 | await fs.promises.unlink(imagePath); 44 | } 45 | } 46 | 47 | // 更新流水信息 48 | await prisma.flow.update({ 49 | where: { 50 | id: Number(id), 51 | bookId: String(bookId), 52 | }, 53 | data: { 54 | invoice: null, 55 | }, 56 | }); 57 | 58 | return success(); 59 | } catch (err) { 60 | console.error(err); 61 | return error("小票清空失败"); 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /server/api/entry/flow/invoice/del.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import prisma from "~/lib/prisma"; 4 | export default defineEventHandler(async (event) => { 5 | const { id, bookId, invoice } = await readBody(event); 6 | 7 | if (!bookId) { 8 | return error("请先选择账本"); 9 | } 10 | 11 | if (!id) { 12 | return error("Not Find ID"); 13 | } 14 | 15 | if (!invoice) { 16 | return error("Not Find ImageName"); 17 | } 18 | 19 | try { 20 | const runtimeConfig = useRuntimeConfig(); 21 | let dataPath = String(runtimeConfig.dataPath); 22 | 23 | if (!dataPath) { 24 | dataPath = process.cwd(); 25 | } 26 | 27 | const imagePath = path.join(dataPath, "images", String(invoice)); 28 | 29 | const flow = await prisma.flow.findUnique({ 30 | where: { 31 | id: Number(id), 32 | bookId: String(bookId), 33 | }, 34 | }); 35 | // 校验图片是否存在 36 | if (!fs.existsSync(imagePath)) { 37 | // 图片不存在 无需删除 38 | } else { 39 | // 删除文件 40 | await fs.promises.unlink(imagePath); 41 | } 42 | 43 | if (!flow) { 44 | return error("流水信息不存在"); 45 | } 46 | const invoices = flow.invoice ? flow.invoice.split(",") : []; 47 | const newInvoices = invoices.filter((item) => item !== invoice); 48 | 49 | // 更新流水信息 50 | await prisma.flow.update({ 51 | where: { 52 | id: Number(id), 53 | bookId: String(bookId), 54 | }, 55 | data: { 56 | invoice: newInvoices.join(","), 57 | }, 58 | }); 59 | 60 | return success(); 61 | } catch (err) { 62 | console.error(err); 63 | return error("小票删除失败"); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /server/api/entry/flow/invoice/show.get.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | // 获取文件路径参数 6 | // const file = decodeURIComponent(getRouterParam(event, "file") || ""); 7 | const { invoice } = getQuery(event); 8 | 9 | // 检查文件是否存在 10 | if (invoice) { 11 | const runtimeConfig = useRuntimeConfig(); 12 | let dataPath = String(runtimeConfig.dataPath); 13 | if (!dataPath) { 14 | dataPath = process.cwd(); 15 | } 16 | const invoicePath = path.join(dataPath, "images"); 17 | const basePath = path.join(invoicePath, String(invoice)); // 替换为你的实际路径 18 | // 如果格式不支持,直接返回原图 19 | return sendStream(event, fs.createReadStream(basePath)); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /server/api/entry/flow/invoice/upload.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import prisma from "~/lib/prisma"; 4 | 5 | export default defineEventHandler(async (event) => { 6 | const formdata = await readFormData(event); 7 | try { 8 | const bookId = String(formdata.get("bookId")); 9 | if (!bookId) { 10 | return error("请先选择账本"); 11 | } 12 | const files: File[] = formdata.getAll("invoice") as File[]; 13 | if (!files || files.length < 1) { 14 | return error("Not Find File"); 15 | } 16 | // console.log(formdata.getAll("images")); 17 | const flowId = Number(formdata.get("id")); 18 | if (!flowId) { 19 | return error("Not Find ID"); 20 | } 21 | const runtimeConfig = useRuntimeConfig(); 22 | let dataPath = String(runtimeConfig.dataPath); 23 | if (!dataPath) { 24 | dataPath = process.cwd(); 25 | } 26 | const invoicePath = path.join(dataPath, "images"); 27 | // 确保保存路径存在 28 | await fs.promises.mkdir(invoicePath, { recursive: true }); 29 | 30 | const imageNames: string[] = []; 31 | for (let i = 0; i < files.length; i++) { 32 | const file = files[i]; 33 | // 获取文件扩展名 34 | const extension = path.extname(file.name); 35 | const fileName = `${bookId}-${flowId}-${Date.now()}-${i + 1}${extension}`; 36 | 37 | // 读取文件数据 38 | const fileBuffer = Buffer.from(await file.arrayBuffer()); 39 | 40 | // 对于不支持处理的格式,如svg、ico,直接保存 41 | await fs.promises.writeFile(path.join(invoicePath, fileName), fileBuffer); 42 | imageNames.push(fileName); 43 | } 44 | const flow = await prisma.flow.findUnique({ 45 | where: { 46 | id: flowId, 47 | bookId, 48 | }, 49 | }); 50 | if (!flow) { 51 | return; 52 | } 53 | const newInvoices = []; 54 | if (flow.invoice) { 55 | newInvoices.push(...flow.invoice.split(",")); 56 | } 57 | newInvoices.push(...imageNames); 58 | // 更新流水信息 59 | await prisma.flow.update({ 60 | where: { 61 | id: flowId, 62 | bookId, 63 | }, 64 | data: { 65 | invoice: newInvoices.join(","), 66 | }, 67 | }); 68 | 69 | return success(); 70 | } catch (err) { 71 | console.error(err); 72 | return error("小票上传失败"); 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /server/api/entry/flow/list.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | 6 | if (!body.bookId) { 7 | return error("请先选择账本"); 8 | } 9 | // 条件查询 10 | const where: any = { 11 | bookId: body.bookId, 12 | }; 13 | 14 | // 添加条件:如果 `name` 存在,则根据 `name` 查询 15 | if (body.id) { 16 | // equals 等于查询 17 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 18 | where.id = { 19 | equals: Number(body.id), 20 | }; 21 | } 22 | // 类型条件 23 | if (body.flowType) { 24 | where.flowType = { 25 | equals: body.flowType, 26 | }; 27 | } 28 | if (body.industryType) { 29 | where.industryType = { 30 | equals: body.industryType, 31 | }; 32 | } 33 | if (body.payType) { 34 | where.payType = { 35 | equals: body.payType, 36 | }; 37 | } 38 | 39 | // 时间条件 40 | if (body.startDay && body.endDay) { 41 | where.day = { 42 | gte: body.startDay, 43 | lte: body.endDay, 44 | }; 45 | } else if (body.startDay) { 46 | where.day = { 47 | gte: body.startDay, 48 | }; 49 | } else if (body.endDay) { 50 | where.day = { 51 | lte: body.endDay, 52 | }; 53 | } 54 | 55 | // 模糊条件 56 | if (body.name) { 57 | where.name = { 58 | contains: body.name, 59 | }; 60 | } 61 | if (body.attribution) { 62 | where.attribution = { 63 | contains: body.attribution, 64 | }; 65 | } 66 | if (body.description) { 67 | where.description = { 68 | contains: body.description, 69 | }; 70 | } 71 | 72 | const flows = await prisma.flow.findMany({ 73 | where, // 使用条件查询 74 | }); 75 | 76 | return success(flows); 77 | }); 78 | -------------------------------------------------------------------------------- /server/api/entry/flow/page.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); // 获取查询参数 5 | 6 | if (!body.bookId) { 7 | return error("请先选择账本"); 8 | } 9 | 10 | const where: any = { 11 | bookId: { 12 | equals: body.bookId, 13 | }, 14 | }; // 条件查询 15 | 16 | // 普通查询条件 17 | if (body.id) { 18 | // equals 等于查询 19 | // contains 模糊查询(pgsql和mongo中,可以增加额外参数限制忽略大小写 mode: 'insensitive') 20 | where.id = { 21 | equals: Number(body.id), 22 | }; 23 | } 24 | if (body.flowType) { 25 | where.flowType = { 26 | equals: body.flowType, 27 | }; 28 | } 29 | if (body.industryType) { 30 | where.industryType = { 31 | equals: body.industryType, 32 | }; 33 | } 34 | if (body.payType) { 35 | where.payType = { 36 | equals: body.payType, 37 | }; 38 | } 39 | if (body.startDay && body.endDay) { 40 | where.day = { 41 | gte: body.startDay, 42 | lte: body.endDay, 43 | }; 44 | } else if (body.startDay) { 45 | where.day = { 46 | gte: body.startDay, 47 | }; 48 | } else if (body.endDay) { 49 | where.day = { 50 | lte: body.endDay, 51 | }; 52 | } 53 | if (body.name) { 54 | where.name = { 55 | contains: body.name, 56 | }; 57 | } 58 | if (body.attribution) { 59 | where.attribution = { 60 | contains: body.attribution, 61 | }; 62 | } 63 | if (body.description) { 64 | where.description = { 65 | contains: body.description, 66 | }; 67 | } 68 | 69 | // 分页条件 70 | const pageNum = Number(body.pageNum ? body.pageNum : 1); 71 | const pageSize = Number(body.pageSize ? body.pageSize : 15); 72 | const skip = (pageNum - 1) * pageSize; // 计算跳过的条目数 73 | 74 | // 排序条件 75 | const orderBy: any = [ 76 | { 77 | day: "desc", 78 | }, 79 | ]; 80 | if (body.moneySort) { 81 | // console.log(body.moneySort) 82 | // 将金额排序设置到第一个 83 | orderBy.unshift({ money: String(body.moneySort) }); 84 | } 85 | let flows; 86 | if (pageSize == -1) { 87 | // 查询全部 88 | flows = await prisma.flow.findMany({ where, orderBy }); 89 | // return success(books); 90 | } else { 91 | // 【条件、排序、分页】 组合查询 92 | flows = await prisma.flow.findMany({ 93 | where, 94 | orderBy, 95 | skip, 96 | take: pageSize, 97 | }); 98 | } 99 | // 计算总页数 100 | const totalFlows = await prisma.flow.count({ where }); 101 | const sumMoney = await prisma.flow.groupBy({ 102 | by: ["flowType"], 103 | where, 104 | _sum: { 105 | money: true, 106 | }, 107 | }); 108 | const totalPages = Math.ceil(totalFlows / pageSize); 109 | // const sum = sumMoney[0]; 110 | // console.log(sumMoney); 111 | let totalIn = 0; 112 | let totalOut = 0; 113 | let notInOut = 0; 114 | sumMoney.forEach((t) => { 115 | if (t.flowType == "收入") { 116 | totalIn = Number(t._sum.money); 117 | } else if (t.flowType == "支出") { 118 | totalOut = Number(t._sum.money); 119 | } else if (t.flowType == "不计收支") { 120 | notInOut = Number(t._sum.money); 121 | } 122 | }); 123 | 124 | return success({ 125 | total: totalFlows, 126 | data: flows, 127 | pages: totalPages, 128 | totalIn, 129 | totalOut, 130 | notInOut, 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /server/api/entry/flow/type/getAll.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { bookId, flowType } = await readBody(event); // 获取查询参数 5 | 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | const where: any = { 10 | bookId, 11 | }; // 条件查询 12 | 13 | if (flowType) { 14 | where.flowType = { 15 | equals: flowType, 16 | }; 17 | } 18 | 19 | const industryTypes = await prisma.flow.findMany({ 20 | distinct: ["industryType"], 21 | select: { 22 | industryType: true, 23 | flowType: true, 24 | }, 25 | orderBy: [ 26 | { 27 | flowType: "asc", 28 | }, 29 | { 30 | industryType: "asc", 31 | }, 32 | ], 33 | where, // 使用条件查询 34 | }); 35 | const payTypes = await prisma.flow.findMany({ 36 | distinct: ["payType"], 37 | select: { 38 | payType: true, 39 | flowType: true, 40 | }, 41 | orderBy: [ 42 | { 43 | flowType: "asc", 44 | }, 45 | { 46 | payType: "asc", 47 | }, 48 | ], 49 | where, // 使用条件查询 50 | }); 51 | 52 | // console.log(industryTypes); 53 | // console.log(payTypes); 54 | const types: any = []; 55 | industryTypes.forEach((t) => { 56 | types.push({ 57 | type: "支出类型/收入类型", 58 | flowType: t.flowType, 59 | value: t.industryType, 60 | }); 61 | }); 62 | payTypes.forEach((t) => { 63 | types.push({ 64 | type: "支付方式/收款方式", 65 | flowType: t.flowType, 66 | value: t.payType, 67 | }); 68 | }); 69 | 70 | return success(types); 71 | }); 72 | -------------------------------------------------------------------------------- /server/api/entry/flow/type/getIndustryType.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { bookId, flowType } = await readBody(event); // 获取查询参数 5 | 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | const where: any = { 10 | bookId, 11 | }; // 条件查询 12 | 13 | if (flowType) { 14 | where.flowType = { 15 | equals: flowType, 16 | }; 17 | } 18 | 19 | const flows = await prisma.flow.findMany({ 20 | distinct: ["industryType"], 21 | select: { 22 | industryType: true, 23 | }, 24 | orderBy: [ 25 | { 26 | flowType: "asc", 27 | }, 28 | { 29 | industryType: "asc", 30 | }, 31 | ], 32 | where, // 使用条件查询 33 | }); 34 | 35 | return success(flows); 36 | }); 37 | -------------------------------------------------------------------------------- /server/api/entry/flow/type/getPayType.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { bookId, flowType } = await readBody(event); // 获取查询参数 5 | 6 | if (!bookId) { 7 | return error("请先选择账本"); 8 | } 9 | const where: any = { 10 | bookId, 11 | }; // 条件查询 12 | 13 | if (flowType) { 14 | where.flowType = { 15 | equals: flowType, 16 | }; 17 | } 18 | 19 | const flows = await prisma.flow.findMany({ 20 | distinct: ["payType"], 21 | select: { 22 | payType: true, 23 | }, 24 | orderBy: [ 25 | { 26 | flowType: "asc", 27 | }, 28 | { 29 | payType: "asc", 30 | }, 31 | ], 32 | where, // 使用条件查询 33 | }); 34 | 35 | return success(flows); 36 | }); 37 | -------------------------------------------------------------------------------- /server/api/entry/flow/type/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | 6 | if (!body.bookId) { 7 | return error("Not Find ID"); 8 | } 9 | if (!body.value || !body.type || !body.oldValue) { 10 | return error("Not Find value"); 11 | } 12 | 13 | const where: any = { 14 | bookId: body.bookId, 15 | }; 16 | const data: any = {}; 17 | if (body.type == "支出类型/收入类型") { 18 | where.industryType = String(body.oldValue); 19 | data.industryType = String(body.value); 20 | } else if (body.type == "支付方式/收款方式") { 21 | where.payType = String(body.oldValue); 22 | data.payType = String(body.value); 23 | } else { 24 | return error("Unknown Type"); 25 | } 26 | 27 | const updated = await prisma.flow.updateMany({ 28 | where, 29 | data, 30 | }); 31 | return success(updated); 32 | }); 33 | -------------------------------------------------------------------------------- /server/api/entry/flow/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | if (!body.id || !body.bookId) { 6 | return error("Not Find ID"); 7 | } 8 | const flow = { 9 | day: String(body.day || ""), 10 | flowType: String(body.flowType || ""), // 流水类型:收入、支出 11 | industryType: String(body.industryType || ""), // 行业分类 原 type(收入类型、支出类型) 12 | payType: String(body.payType || ""), // 支付方式 13 | money: Number(body.money || ""), 14 | name: String(body.name || ""), 15 | description: String(body.description || ""), 16 | attribution: String(body.attribution || ""), 17 | }; 18 | const updated = await prisma.flow.update({ 19 | where: { id: Number(body.id), bookId: body.bookId }, 20 | data: flow, 21 | }); 22 | return success(updated); 23 | }); 24 | -------------------------------------------------------------------------------- /server/api/entry/flow/updates.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { ids, bookId, flowType, industryType, payType, attribution } = 5 | await readBody(event); // 从请求体获取 ID 6 | // const userId = await getUserId(event); 7 | 8 | if (!ids || !bookId) { 9 | return error("Not Find ID"); 10 | } 11 | 12 | const updateInfo: any = {}; 13 | if (flowType) { 14 | updateInfo.flowType = String(flowType); 15 | } 16 | if (industryType) { 17 | updateInfo.industryType = String(industryType); 18 | } 19 | if (payType) { 20 | updateInfo.payType = String(payType); 21 | } 22 | if (attribution) { 23 | updateInfo.attribution = String(attribution); 24 | } 25 | 26 | const updated = await prisma.flow.updateMany({ 27 | data: updateInfo, 28 | where: { 29 | id: { 30 | in: ids, 31 | }, 32 | }, 33 | }); 34 | return success(updated); 35 | }); 36 | -------------------------------------------------------------------------------- /server/api/entry/test.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const users = await prisma.user.findMany(); 5 | console.log("users", users); 6 | return success(users); 7 | }); 8 | -------------------------------------------------------------------------------- /server/api/entry/typeRelation/list.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | import { initTypeRelation } from "~/server/utils/data"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | const { bookId } = await readBody(event); // 获取查询参数 6 | // const userId = await getUserId(event); 7 | 8 | if (!bookId) { 9 | return; 10 | } 11 | const where: any = { 12 | // userId, 13 | bookId: String(bookId), 14 | }; // 条件查询 15 | 16 | const relations = await prisma.typeRelation.findMany({ 17 | where, // 使用条件查询 18 | orderBy: [ 19 | { 20 | target: "asc", 21 | }, 22 | ], 23 | }); 24 | 25 | // const datas: Record = {}; 26 | // relations.forEach((l) => { 27 | // datas[l.source] = l.target; 28 | // }); 29 | 30 | if (relations.length <= 0) { 31 | await initTypeRelation(String(bookId)); 32 | 33 | const newRelations = await prisma.typeRelation.findMany({ 34 | where, // 使用条件查询 35 | orderBy: [ 36 | { 37 | target: "asc", 38 | }, 39 | ], 40 | }); 41 | return success(newRelations); 42 | } 43 | 44 | return success(relations); 45 | }); 46 | -------------------------------------------------------------------------------- /server/api/entry/typeRelation/update.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | if (!body.bookId) { 6 | return error("请先选择账本"); 7 | } 8 | const userId = await getUserId(event); 9 | const types: any[] = body.types; 10 | const updates: any[] = []; 11 | const creates: any[] = []; 12 | const ids: number[] = []; 13 | // console.log(types); 14 | types.forEach((t) => { 15 | if (t.id) { 16 | ids.push(t.id); 17 | updates.push(t); 18 | } else { 19 | t.userId = userId; 20 | creates.push(t); 21 | } 22 | }); 23 | // console.log(updates); 24 | // console.log(creates); 25 | if (ids.length > 0) { 26 | // 删除原有但是现在没有的类型配置(删除了) 27 | await prisma.typeRelation.deleteMany({ 28 | where: { 29 | id: { 30 | notIn: ids, 31 | }, 32 | bookId: body.bookId, 33 | }, 34 | }); 35 | // 更新有ID的原有配置 36 | for (let t of updates) { 37 | const updated = await prisma.typeRelation.updateMany({ 38 | data: t, 39 | where: { 40 | id: t.id, 41 | }, 42 | }); 43 | } 44 | } 45 | // 创建没有ID的新配置 46 | const created = await prisma.typeRelation.createMany({ 47 | data: creates, 48 | }); 49 | 50 | return success("更新成功"); 51 | }); 52 | -------------------------------------------------------------------------------- /server/api/entry/user/changePassword.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody(event); 5 | const userId = await getUserId(event); 6 | 7 | if (!body.old || !body.new) { 8 | return; 9 | } 10 | if (body.new != body.againNew) { 11 | return; 12 | } 13 | 14 | const user = await prisma.user.findUnique({ 15 | where: { id: userId }, 16 | }); 17 | if (!user) { 18 | return; 19 | } 20 | const oldPassword = encryptBySHA256(user.username, String(body.old)); 21 | if (oldPassword != user.password) { 22 | return error("原密码不正确!"); 23 | } 24 | const newPassword = encryptBySHA256(user.username, String(body.new)); 25 | 26 | const newUser = await prisma.user.update({ 27 | where: { id: userId }, 28 | data: { 29 | password: newPassword, 30 | }, 31 | }); 32 | 33 | return success("更新成功"); 34 | }); 35 | -------------------------------------------------------------------------------- /server/api/entry/user/info.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const userId = await getUserId(event); 5 | 6 | const where: any = { 7 | id: userId, 8 | }; 9 | 10 | const user = await prisma.user.findUnique({ 11 | select: { 12 | id: true, 13 | name: true, 14 | username: true, 15 | createDate: true, 16 | }, 17 | where, // 使用条件查询 18 | }); 19 | 20 | return success(user); 21 | }); 22 | -------------------------------------------------------------------------------- /server/api/login.ts: -------------------------------------------------------------------------------- 1 | import { encryptBySHA256 } from "../utils/common"; 2 | import prisma from "~/lib/prisma"; 3 | import jwt from "jsonwebtoken"; 4 | 5 | export default defineEventHandler(async (event) => { 6 | const body = await readBody(event); 7 | // console.log("body", body); 8 | if (!body.username || !body.password) { 9 | return error("用户名或密码不能为空"); 10 | } 11 | // console.log(credentials); 12 | const username = String(body.username); 13 | const password = String(body.password); 14 | 15 | const entrypassword = encryptBySHA256(username, password); 16 | const users = await prisma.user.findMany({ 17 | where: { 18 | username: username, 19 | password: entrypassword, 20 | }, 21 | }); 22 | if (!users || users.length != 1) { 23 | return error("用户名或密码错误"); 24 | } 25 | const user = users[0]; 26 | const secretKey = useRuntimeConfig().authSecret; 27 | // 设置过期时间为 100 年后 (时间戳单位为秒) 28 | // const expiresInYears = 30; 29 | const expiresInDays = 30; 30 | const expiresInSeconds = expiresInDays * 24 * 60 * 60; 31 | const token = jwt.sign( 32 | { id: user.id, name: user.username, email: user.email }, 33 | secretKey, 34 | { 35 | expiresIn: expiresInSeconds, 36 | } 37 | ); 38 | const returnUser = { 39 | id: user.id, 40 | name: user.username, 41 | email: user.email, 42 | token, 43 | // image: user.avatar, 44 | }; 45 | // useCookie() 46 | setCookie(event, "Authorization", token, { 47 | maxAge: expiresInSeconds, 48 | }); 49 | return success(returnUser); 50 | }); 51 | -------------------------------------------------------------------------------- /server/api/logout.ts: -------------------------------------------------------------------------------- 1 | // 退出登录 2 | export default defineEventHandler(async (event) => { 3 | // const Authorization = getHeader(event, "Admin"); 4 | 5 | deleteCookie(event, "Authorization"); 6 | 7 | return success("退出成功"); 8 | }); 9 | -------------------------------------------------------------------------------- /server/api/register.ts: -------------------------------------------------------------------------------- 1 | import { encryptBySHA256 } from "../utils/common"; 2 | import prisma from "~/lib/prisma"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | const body = await readBody(event); 6 | // console.log("body", body); 7 | if (!body.username || !body.password) { 8 | return; 9 | } 10 | 11 | const username = String(body.username); 12 | const password = encryptBySHA256(username, body.password); 13 | const name = body.name ? body.name : `MYNUXT_${getUUID(6)}`; 14 | 15 | const num = await prisma.user.count({ 16 | where: { 17 | username: { 18 | equals: username, 19 | }, 20 | }, 21 | }); 22 | if (num > 0) { 23 | return error("账号已存在"); 24 | } 25 | 26 | const user = await prisma.user.create({ 27 | data: { 28 | name: String(name), 29 | username, 30 | password, 31 | }, 32 | }); 33 | 34 | if (user) { 35 | return success("注册成功"); 36 | } 37 | return error("注册失败"); 38 | }); 39 | -------------------------------------------------------------------------------- /server/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { noPermissions } from "../utils/common"; 2 | import jwt from "jsonwebtoken"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | const url = getRequestURL(event); 6 | 7 | // 校验 API 请求 8 | if (url.pathname.startsWith("/api/entry")) { 9 | const token = getHeader(event, "Authorization"); 10 | 11 | if (!token) { 12 | // Token is missing, return 401 Unauthorized 13 | return noPermissions("No Authorization"); 14 | } 15 | const secretKey = useRuntimeConfig().authSecret; 16 | 17 | // 验证 JWT 18 | try { 19 | jwt.verify(token, secretKey); 20 | } catch (err) { 21 | return noPermissions("Forbidden: Invalid or expired token"); 22 | } 23 | } else if (url.pathname.startsWith("/api/admin/entry")) { 24 | // 后台管理接口 25 | const Admin = getHeader(event, "Admin"); 26 | if (!Admin) { 27 | console.error(new Date().toLocaleDateString() + " No Admin"); 28 | return noPermissions(); 29 | } 30 | const runtimeConfig = useRuntimeConfig(); 31 | if ( 32 | Admin != 33 | encryptBySHA256(runtimeConfig.adminUsername, runtimeConfig.adminPassword) 34 | ) { 35 | console.error(new Date().toLocaleDateString() + " Admin is Wrong!"); 36 | deleteCookie(event, "Admin"); 37 | return noPermissions(); 38 | } 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /server/plugins/initdata.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | import { initTypeRelation } from "../utils/data"; 3 | 4 | export default defineNitroPlugin((nitroApp) => { 5 | // 只执行一次的Hook,初始化数据库 6 | nitroApp.hooks.hookOnce("request", async () => { 7 | // 初始化系统设置 8 | const nums = await prisma.systemSetting.count(); 9 | if (nums < 1) { 10 | await prisma.systemSetting.create({ 11 | data: { 12 | id: 1, 13 | title: "Cashbook", 14 | description: "Cashbook", 15 | keywords: "Cashbook", 16 | }, 17 | }); 18 | console.log("Init System Settings"); 19 | } 20 | await prisma.systemSetting.update({ 21 | data: { version: String(useRuntimeConfig().appVersion) }, 22 | where: { 23 | id: 1, 24 | }, 25 | }); 26 | // 保证eliminate字段有值,防止业务出错 27 | await prisma.$executeRaw`UPDATE "Flow" SET "eliminate" = 0 WHERE "eliminate" is null;`; 28 | initTypeRelation(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /server/routes/test.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | return success("Api Test"); 3 | }); 4 | -------------------------------------------------------------------------------- /server/utils/common.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | // EncryptBySHA256 使用 SHA-256 算法加密字符串 4 | export const encryptBySHA256 = (userName: string, password: string) => { 5 | const hash = crypto.createHash("sha256"); 6 | hash.update(userName + password); 7 | return hash.digest("hex"); 8 | }; 9 | 10 | export const success = (data?: any): Result => { 11 | return { 12 | c: 200, 13 | m: "", 14 | d: data, 15 | }; 16 | }; 17 | 18 | export const error = (m: any, d?: any): Result => { 19 | return { 20 | c: 500, 21 | m: m, 22 | d: d, 23 | }; 24 | }; 25 | 26 | export const noPermissions = (message?: string): Result => { 27 | return { 28 | c: 400, 29 | m: message || "NO Permissions", 30 | d: "", 31 | }; 32 | }; 33 | 34 | import jwt from "jsonwebtoken"; 35 | 36 | export const getUserId = async ( 37 | // @ts-ignore 38 | event: H3Event 39 | ): Promise => { 40 | const token = getHeader(event, "Authorization"); 41 | const secretKey = useRuntimeConfig().authSecret; 42 | 43 | if (!token) { 44 | return 0; // Token 不存在,返回 null 表示未认证 45 | } 46 | 47 | return new Promise((resolve, reject) => { 48 | jwt.verify(token, secretKey, (err, decoded: any) => { 49 | if (err) { 50 | // Token 验证失败 51 | console.error("JWT verification failed:", err.message); // 记录错误信息 (可选) 52 | return resolve(0); // 返回 null 表示验证失败,获取不到 userId 53 | // 或者,你也可以 reject(err) 并让调用者处理错误,取决于你的错误处理策略 54 | } 55 | 56 | // Token 验证成功 57 | const userId = Number(decoded?.id || 0); // 提取 userId,并确保是数字类型 58 | resolve(userId); 59 | }); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /server/utils/data.ts: -------------------------------------------------------------------------------- 1 | import prisma from "~/lib/prisma"; 2 | 3 | export const initTypeRelation = async (bookId?: string) => { 4 | if (!bookId) { 5 | bookId = "0"; 6 | } 7 | // 初始化 TypeRelation 配置 8 | const types = await prisma.typeRelation.count({ where: { bookId } }); 9 | if (types < 1) { 10 | console.log("~初始化类型转换数据~" + bookId); 11 | const typeRelations = { 12 | 食品酒饮: "餐饮美食", 13 | 餐饮美食: "餐饮美食", 14 | 家居家装: "日用百货", 15 | 日用百货: "日用百货", 16 | 鞋服箱包: "日用百货", 17 | 清洁纸品: "日用百货", 18 | 医疗保健: "医疗健康", 19 | 医疗健康: "医疗健康", 20 | 充值缴费: "充值缴费", 21 | 教育培训: "教育培训", 22 | 图书文娱: "文化休闲", 23 | 运动户外: "文化休闲", 24 | 文体玩具: "文化休闲", 25 | 文化休闲: "文化休闲", 26 | 微信红包: "转账红包", 27 | "微信红包(单发)": "转账红包", 28 | "微信红包(群红包)": "转账红包", 29 | 转账红包: "转账红包", 30 | 转账: "转账红包", 31 | 二维码收款: "微信交易", 32 | 微信交易: "微信交易", 33 | 商户消费: "微信交易", 34 | 扫二维码付款: "微信交易", 35 | 小金库: "投资理财", 36 | 投资理财: "投资理财", 37 | 收入: "投资理财", 38 | "转入零钱通-来自零钱": "投资理财", 39 | 手机通讯: "数码电器", 40 | 数码电器: "数码电器", 41 | 电脑办公: "数码电器", 42 | 服饰内衣: "服饰装扮", 43 | 服饰装扮: "服饰装扮", 44 | 钟表眼镜: "服饰装扮", 45 | 美妆个护: "美容美发", 46 | 美容美发: "美容美发", 47 | 汽车用品: "爱车养车", 48 | 爱车养车: "爱车养车", 49 | 亲友代付: "亲友代付", 50 | 亲属卡交易: "亲友代付", 51 | "亲属卡交易-退款": "退款", 52 | "美团平台商户-退款": "退款", 53 | 退款: "退款", 54 | }; 55 | 56 | const dataList: any[] = []; 57 | 58 | for (const [s, t] of Object.entries(typeRelations)) { 59 | dataList.push({ 60 | bookId: bookId, 61 | userId: 0, 62 | source: s, 63 | target: t, 64 | }); 65 | } 66 | await prisma.typeRelation.createMany({ 67 | data: dataList, 68 | }); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /server/utils/test.js: -------------------------------------------------------------------------------- 1 | // 生成加密测试密码 2 | var crypto = await import("node:crypto"); 3 | 4 | var encryptBySHA256 = function (userName, password) { 5 | var hash = crypto.createHash("sha256"); 6 | hash.update(userName + password); 7 | return hash.digest("hex"); 8 | }; 9 | // 默认测试账号密码 admin admin123456 10 | console.log(encryptBySHA256("admin", "admin123456")); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /utils/alert.ts: -------------------------------------------------------------------------------- 1 | export interface AlertInfo { 2 | id: string; 3 | type: "success" | "info" | "warning" | "error" | undefined; 4 | message: string; 5 | } 6 | 7 | export const newAlert = ref({ 8 | id: "alert" + 0, 9 | type: undefined, 10 | message: "", 11 | }); 12 | 13 | export const alert = ( 14 | type: "success" | "info" | "warning" | "error" | undefined, 15 | message: string 16 | ) => { 17 | newAlert.value.id = Math.random().toString(); 18 | newAlert.value.type = type; 19 | newAlert.value.message = message; 20 | }; 21 | 22 | export class Alert { 23 | static error = (message?: string) => { 24 | alert("error", message || "错误信息"); 25 | }; 26 | 27 | static success = (message?: string) => { 28 | alert("success", message || "成功信息"); 29 | }; 30 | 31 | static info = (message?: string) => { 32 | alert("info", message || "提示信息"); 33 | }; 34 | 35 | static warning = (message?: string) => { 36 | alert("warning", message || "警告信息"); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /utils/api.ts: -------------------------------------------------------------------------------- 1 | import { Alert } from "./alert"; 2 | import type { Result, UserInfo } from "./model"; 3 | import { GlobalUserInfo } from "./store"; 4 | 5 | const API_PREFIEX = "/"; 6 | 7 | // 调用后端接口统一封装,节省Header处理 8 | export const doApi = { 9 | // get 常用于获取公开数据 10 | get: async (path: string, query?: any): Promise => { 11 | const res = await $fetch>(`${API_PREFIEX}${path}`, { 12 | method: "GET", 13 | query: query, 14 | headers: getHeaders(), 15 | credentials: "include", 16 | }); 17 | 18 | return intercepterResponse(res); 19 | }, 20 | // post 用于大部分业务接口调用 21 | post: async (path: string, data?: any): Promise => { 22 | const res = await $fetch>(`${API_PREFIEX}${path}`, { 23 | method: "POST", 24 | headers: { 25 | ...getHeaders(), 26 | "Content-Type": "application/json;chartset=utf-8", 27 | }, 28 | body: data, 29 | credentials: "include", 30 | }); 31 | return intercepterResponse(res); 32 | }, 33 | 34 | // postform 常用于上传文件 35 | postform: async (path: string, data: FormData): Promise => { 36 | // const formData = new FormData(); 37 | // for (let key in data) { 38 | // formData.append(key, data[key]); 39 | // } 40 | const res = await $fetch>(`${API_PREFIEX}${path}`, { 41 | method: "POST", 42 | // postform 使用默认header,如果有问题,建议自行尝试添加 'Content-Type': 'multipart/form-data' 43 | headers: getHeaders(), 44 | body: data, 45 | }); 46 | return intercepterResponse(res); 47 | }, 48 | 49 | // download 常用于获取文件,返回结果一般是文件流,常用于实现下载文件、展示图片等功能 50 | download: async (path: string, query?: any): Promise => { 51 | return await $fetch(`${API_PREFIEX}${path}`, { 52 | method: "GET", 53 | query: query, 54 | responseType: "blob", 55 | headers: getHeaders(), 56 | }); 57 | }, 58 | }; 59 | 60 | const intercepterResponse = (res: Result): T => { 61 | if (res.c == 200) { 62 | return res.d; 63 | } else { 64 | if (res.c == 400) { 65 | // 清除登陆状态(@sidebase/nuxt-auth框架) 66 | // useAuth().signOut(); 67 | const route = useRoute(); 68 | navigateTo({ 69 | path: "/auth/login", 70 | query: { callbackUrl: route.fullPath }, 71 | }); 72 | } 73 | Alert.error(res.m); 74 | throw Error(res.m); 75 | } 76 | }; 77 | 78 | const getHeaders = () => { 79 | return { 80 | Authorization: useCookie("Authorization").value || "", 81 | Admin: useCookie("Admin").value || "", 82 | }; 83 | }; 84 | 85 | export const getUserInfo = async () => { 86 | try { 87 | const user = await doApi.get("api/entry/user/info"); 88 | 89 | if (user) { 90 | GlobalUserInfo.value = user; 91 | return user; 92 | } else { 93 | return null; 94 | } 95 | } catch (e) { 96 | return null; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /utils/apis.ts: -------------------------------------------------------------------------------- 1 | import type { CommonChartData } from "./model"; 2 | 3 | export const getSystemInfo = async () => { 4 | return await doApi.get("api/config"); 5 | }; 6 | 7 | export const daily = async (data: any): Promise => { 8 | return doApi.post("api/entry/analytics/daily", { 9 | ...data, 10 | bookId: localStorage.getItem("bookId"), 11 | }); 12 | }; 13 | 14 | export const getIndustryType = (flowType: string): Promise => { 15 | return doApi.post("api/entry/flow/type/getIndustryType", { 16 | flowType, 17 | bookId: localStorage.getItem("bookId"), 18 | }); 19 | }; 20 | 21 | export const getPayType = (flowType: string): Promise => { 22 | return doApi.post("api/entry/flow/type/getPayType", { 23 | flowType, 24 | bookId: localStorage.getItem("bookId"), 25 | }); 26 | }; 27 | 28 | export const getTypeRelation = (): Promise => { 29 | return doApi.post("api/entry/typeRelation/list", { 30 | bookId: localStorage.getItem("bookId"), 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /utils/common.ts: -------------------------------------------------------------------------------- 1 | // 判断屏幕尺寸,窄屏为 true 2 | export const miniFullscreen = () => { 3 | return window.innerWidth < 1080; 4 | }; 5 | 6 | export const checkSignIn = () => { 7 | // console.log(useAuth().status.value); 8 | return useCookie("Authorization").value ? true : false; 9 | }; 10 | 11 | export const getUUID = (num: number) => { 12 | const codes = 13 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; 14 | 15 | let uuid = ""; 16 | for (let i = 0; i < num; i++) { 17 | const randomNumber = Math.floor(Math.random() * 62) + 1; 18 | uuid += codes[randomNumber]; 19 | } 20 | return uuid; 21 | }; 22 | 23 | export const formatDate = (time?: number | Date | string): string => { 24 | let date; 25 | if (!time) { 26 | date = new Date(); 27 | } else if (time instanceof Date) { 28 | date = time; 29 | } else { 30 | date = new Date(time); 31 | } 32 | 33 | const pad = (num: number) => String(num).padStart(2, "0"); 34 | 35 | const year = date.getFullYear(); 36 | const month = pad(date.getMonth() + 1); // 月份从 0 开始,需要加 1 37 | const day = pad(date.getDate()); 38 | const hours = pad(date.getHours()); 39 | const minutes = pad(date.getMinutes()); 40 | const seconds = pad(date.getSeconds()); 41 | 42 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 43 | }; 44 | 45 | /*** 46 | * 日期格式化方法 47 | * 48 | * @author DingDangDog 49 | * @param format 格式化后的日期格式,标准格式:YYYY-MM-dd HH:mm:ss。 50 | * @param date 待格式化的日期,可以是string或Date类型 51 | * @return 标准格式结果示例:2022-12-08 17:30:00 52 | */ 53 | export const dateFormater = (format: string, date: string | Date) => { 54 | date = new Date(date); 55 | 56 | const dataRegIndexs = [0, 1, 2, 3, 4, 5]; 57 | const dataRegKeys = ["Y+", "M+", "d+", "H+", "m+", "s+"]; 58 | const dataItem = [ 59 | date.getFullYear().toString(), 60 | date.getMonth() + 1 < 10 61 | ? "0" + (date.getMonth() + 1) 62 | : (date.getMonth() + 1).toString(), 63 | date.getDate() < 10 ? "0" + date.getDate() : date.getDate().toString(), 64 | date.getHours().toString(), 65 | date.getMinutes().toString(), 66 | date.getSeconds().toString(), 67 | ]; 68 | 69 | let ret; 70 | for (const index in dataRegIndexs) { 71 | ret = new RegExp("(" + dataRegKeys[index] + ")").exec(format); 72 | if (ret) { 73 | format = format.replace( 74 | ret[1], 75 | ret[1].length == 1 76 | ? dataItem[index] 77 | : dataItem[index].padStart(ret[1].length, "0") 78 | ); 79 | } 80 | } 81 | return format; 82 | }; 83 | 84 | export const toGithub = () => { 85 | window.open(`https://github.com/dingdangdog/cashbook`, "_blank"); 86 | }; 87 | 88 | export const toDocumentation = () => { 89 | window.open("https://doc.cashbook.oldmoon.top", "_blank"); 90 | }; 91 | -------------------------------------------------------------------------------- /utils/confirm.ts: -------------------------------------------------------------------------------- 1 | export interface ConfirmModel { 2 | title: string; 3 | content: string; 4 | confirmText?: string; 5 | cancelText?: string; 6 | closeText?: string; 7 | confirm: Function; 8 | cancel?: Function; 9 | close?: Function; 10 | } 11 | 12 | export const GlobalConfirmModels = ref([]); 13 | export const ThisConfirmModel = ref(); 14 | export const openConfirmDialogFlag = ref(false); 15 | 16 | export class Confirm { 17 | static open = (model: ConfirmModel) => { 18 | if (openConfirmDialogFlag.value && ThisConfirmModel.value) { 19 | // 如果是弹窗接弹窗,延迟100ms再打开 20 | openConfirmDialogFlag.value = false; 21 | setTimeout(() => { 22 | ThisConfirmModel.value = model; 23 | openConfirmDialogFlag.value = true; 24 | }, 100); 25 | } else { 26 | // 如果是第一个弹窗,直接打开 27 | ThisConfirmModel.value = model; 28 | openConfirmDialogFlag.value = true; 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const FlowTypes = [ 2 | { title: "支出", value: "支出" }, 3 | { title: "收入", value: "收入" }, 4 | { title: "不计收支", value: "不计收支" }, 5 | ]; 6 | -------------------------------------------------------------------------------- /utils/fileUtils.ts: -------------------------------------------------------------------------------- 1 | export const exportExcel = (fileName: string, title: any, data: any) => { 2 | 3 | } 4 | 5 | export const exportJson = (fileName: string, data: string) => { 6 | writeFile(fileName, data); 7 | } 8 | 9 | const writeFile = (fileName: string, content: any) => { 10 | const blob = new Blob([content], { type: 'application/octet-stream' }); 11 | const fileUrl = window.URL.createObjectURL(blob); 12 | const link = document.createElement("a"); 13 | link.style.display = "none"; 14 | link.href = fileUrl; 15 | link.setAttribute("download", fileName); 16 | document.body.appendChild(link); 17 | link.click(); 18 | } -------------------------------------------------------------------------------- /utils/flag.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export const showBookDialogFlag = ref({ 4 | visible: false, 5 | }); 6 | 7 | export const showFlowExcelImportDialog = ref(false); 8 | // 流水Json导入弹出框 9 | export const showFlowJsonImportDialog = ref(false); 10 | export const showFlowEditInvoiceDialog = ref(false); 11 | 12 | export const showFlowTableDialog = ref(false); 13 | 14 | export const showSetConvertDialog = ref(false); 15 | export const showChangePasswordDialog = ref(false); 16 | 17 | export const showFlowEditDialog = ref(false); 18 | export const showAutoMergeFlowsDialog = ref(false); 19 | export const showAutoDeduplicationFlowsDialog = ref(false); 20 | -------------------------------------------------------------------------------- /utils/flowConvert.ts: -------------------------------------------------------------------------------- 1 | import { typeRelationStore } from "./store"; 2 | 3 | // | 本软件类型 | 支付宝 | 微信 | 京东金融 | 备注 | 4 | // | -------- | ---- | -------- | ---- | ---- | 5 | // | 数码电器 | 数码电器 | | 数码电器/手机通讯/电脑办公 | 电子产品、家电等 | 6 | // | 充值缴费 | 充值缴费 | | 充值缴费 | 水电、话费等 | 7 | // | 美容美发 | 美容美发 | | 美妆个护 | 理发、护肤等 | 8 | // | 转账红包 | 转账红包 | 微信红包/转账 | | 转账、红包等 | 9 | // | 日用百货 | 日用百货 | | 清洁纸品/鞋服箱包 | 垃圾袋、卫生纸等 | 10 | // | 服饰装扮 | 服饰装扮 | | 服饰内衣/钟表眼镜 | 鞋服等 | 11 | // | 文化休闲 | 文化休闲/运动户外 | | 图书文娱/文体玩具 | 书籍、游戏、视频软件VIP等 | 12 | // | 餐饮美食 | 餐饮美食 | | 食品酒饮 | 吃好喝好 | 13 | // | 医疗健康 | 医疗健康 | | 医疗保健 | 药品、保养品等 | 14 | // | 亲友代付 | 亲友代付 | 亲属卡交易 | | 如题 | 15 | // | 家居家装 | 家居家装 | | | 家具、装修用品等 | 16 | // | 爱车养车 | 爱车养车 | | 汽车用品 | 保养、油气、过路费等 | 17 | // | 投资理财 | 收入/投资理财 | | 小金库 | 理财收益、签到红包等 | 18 | // | 教育培训 | 教育培训 | | 教育培训 | 网课、培训等 | 19 | // | 退款 | 退款 | 退款 | | | 20 | // | 微信交易 | | 扫二维码付款/二维码收款/商户消费 | | 这部分交易需要人工分类 | 21 | // | 其他 | 其他/商业服务/生活服务/借用借还 | | 其他网购/其他/网购/收发快递/白条 | 一些少见或未知类型,通常需要人工分类 | 22 | 23 | /** 24 | * 模板导入 25 | * @param row 26 | * @param indexMap 27 | */ 28 | export function templateConvert( 29 | row: any[], 30 | indexMap: Record 31 | ): Flow { 32 | const flow: Flow | any = {}; 33 | flow.day = row[indexMap["交易时间"]]; 34 | flow.flowType = String(row[indexMap["收/支"]]); 35 | flow.industryType = String(row[indexMap["交易分类"]]); 36 | flow.payType = String(row[indexMap["收/付款方式"]]); 37 | flow.money = row[indexMap["金额"]]; 38 | flow.attribution = String(row[indexMap["流水归属"]]); 39 | flow.name = String(row[indexMap["交易对方"]]); 40 | flow.description = String(row[indexMap["备注"]]); 41 | return flow; 42 | } 43 | 44 | /** 45 | * 支付宝 46 | * @param row 47 | * @param indexMap 48 | */ 49 | export function alipayConvert( 50 | row: any[], 51 | indexMap: Record 52 | ): Flow { 53 | const flow: Flow | any = {}; 54 | flow.day = row[indexMap["交易时间"]]; 55 | flow.flowType = String(row[indexMap["收/支"]]); 56 | // + '' 防止数据不是字符串导致报错 57 | flow.industryType = typeConvert(row[indexMap["交易分类"]]); 58 | flow.payType = "支付宝"; 59 | flow.money = row[indexMap["金额"]]; 60 | flow.name = String(row[indexMap["交易对方"]]); 61 | flow.description = row[indexMap["商品说明"]] + "-" + row[indexMap["备注"]]; 62 | return flow; 63 | } 64 | 65 | export function typeConvert(type: any): string { 66 | // 20240922 类型转换,如果没有匹配的类型则保留原类型 67 | // 20250116 新转换实现 68 | const ts = typeRelationStore.value.filter((t) => t.source == type); 69 | return ts.length > 0 ? ts[0].target : type; 70 | } 71 | 72 | /** 73 | * 微信支付 74 | * @param row 75 | * @param indexMap 76 | */ 77 | export function wxpayConvert( 78 | row: any[], 79 | indexMap: Record 80 | ): Flow { 81 | const flow: Flow | any = {}; 82 | flow.day = row[indexMap["交易时间"]]; 83 | flow.flowType = 84 | row[indexMap["收/支"]] == "/" ? "不计收支" : row[indexMap["收/支"]]; 85 | flow.industryType = String(typeConvert(row[indexMap["交易类型"]])); 86 | flow.payType = "微信"; 87 | flow.money = parseFloat(row[indexMap["金额(元)"]].replace("¥", "")); 88 | flow.name = String(row[indexMap["商品"]]); 89 | flow.description = 90 | row[indexMap["交易对方"]] + 91 | "-" + 92 | row[indexMap["支付方式"]] + 93 | "-" + 94 | row[indexMap["备注"]]; 95 | return flow; 96 | } 97 | 98 | /** 99 | * 京东金融 100 | * @param row 101 | * @param indexMap 102 | */ 103 | export function jdFinanceConvert( 104 | row: any[], 105 | indexMap: Record 106 | ): Flow { 107 | const flow: Flow | any = {}; 108 | flow.day = row[indexMap["交易时间"]]; 109 | flow.flowType = String(row[indexMap["收/支"]]); 110 | flow.industryType = typeConvert(row[indexMap["交易分类"]]); 111 | flow.payType = "京东金融"; 112 | 113 | // 京东的金额有特殊处理 114 | const jdMoney = String(row[indexMap["金额"]]); 115 | const match = jdMoney.match(/^(\d*\.?\d+)(.*)/); 116 | flow.money = match ? match[1] : jdMoney; 117 | const desc = match ? match[2] : ""; 118 | // console.log(money); // "980.27" 119 | // console.log(desc); // "(已全额退款)" 120 | flow.name = String(row[indexMap["交易说明"]]); 121 | flow.description = 122 | desc + 123 | row[indexMap["商户名称"]] + 124 | "-" + 125 | row[indexMap["收/付款方式"]] + 126 | "-" + 127 | row[indexMap["备注"]]; 128 | return flow; 129 | } 130 | -------------------------------------------------------------------------------- /utils/model.ts: -------------------------------------------------------------------------------- 1 | // 统一最外层包装类 2 | export interface Result { 3 | c: number; 4 | d: T; 5 | m?: string; 6 | } 7 | 8 | export interface Page { 9 | pageNum: number; 10 | pageSize: number; 11 | pages: number; 12 | total: number; 13 | totalOut: number; 14 | totalIn: number; 15 | notInOut: number; 16 | data: T[]; 17 | } 18 | 19 | // 分页查询参数 20 | export interface PageParam { 21 | pageNum: number; 22 | pageSize: number; 23 | } 24 | export interface UserInfo { 25 | id: number; 26 | name: string; 27 | username: string; 28 | createDate: Date; 29 | } 30 | export interface CommonOption {} 31 | export interface MonthAnalysis { 32 | month: string; 33 | outSum: string; // 总支出 34 | inSum: string; // 总收入 35 | zeroSum: string; // 总不计收支 36 | maxInType: string; // 最大收入类型 37 | maxInTypeSum: string; // 最大收入金额 38 | maxOutType: string; // 最大支出类型 39 | maxOutTypeSum: string; // 最大支出金额 40 | maxOut: Flow; // 最大单笔支出 41 | maxIn: Flow; // 最大单笔收入 42 | maxZero: Flow; // 最大单笔收入 43 | } 44 | 45 | /** 46 | * 创建流水的传输实体 47 | */ 48 | export interface CreateFlowDto { 49 | day?: string; 50 | flowType?: string; 51 | bookId?: number | string; 52 | type?: string; 53 | payType?: string; 54 | money?: number; 55 | name?: string; 56 | description?: string; 57 | } 58 | 59 | /** 60 | * 更新流水的传输实体 61 | */ 62 | export interface UpdateFlowDto { 63 | day?: string; 64 | bookId?: number | string; 65 | flowType?: string; 66 | type?: string; 67 | payType?: string; 68 | money?: number; 69 | name?: string; 70 | description?: string; 71 | } 72 | 73 | export class FlowQuery { 74 | pageNum?: number = 1; 75 | pageSize?: number = 20; 76 | id?: string | number; 77 | bookId?: string | number; 78 | startDay?: string; 79 | endDay?: string; 80 | flowType?: string; 81 | industryType?: string; 82 | payType?: string; 83 | name?: string; 84 | attribution?: string; 85 | description?: string; 86 | moneySort?: string; 87 | } 88 | 89 | export interface Server { 90 | version?: string; 91 | dataPath?: string; 92 | openRegister?: string; 93 | } 94 | 95 | export interface AdminLogin { 96 | account?: string; 97 | password?: string; 98 | } 99 | 100 | export interface CommonChartQuery { 101 | bookId?: string; 102 | flowType?: string; 103 | startDay?: string; 104 | endDay?: string; 105 | } 106 | export interface CommonChartData { 107 | type: string; // 数据标记 key,可能是日期、年月、支出类型、收入类型等,视具体使用场景而定 108 | inSum: number; // 收入 109 | outSum: number; // 支出 110 | zeroSum: number; // 不计收支 111 | } 112 | 113 | export interface Typer { 114 | bookId?: string; 115 | flowType?: string; 116 | type?: string; 117 | value?: string; 118 | oldValue?: string; 119 | } 120 | 121 | export interface CommonSelectOption { 122 | title: string; 123 | value: string; 124 | } 125 | -------------------------------------------------------------------------------- /utils/store.ts: -------------------------------------------------------------------------------- 1 | import type { UserInfo } from "./model"; 2 | 3 | // 全局用户信息存储 4 | export const GlobalUserInfo = ref(); 5 | 6 | export const typeRelationStore = ref([]); 7 | 8 | export const SystemConfig = ref(); 9 | -------------------------------------------------------------------------------- /utils/table.ts: -------------------------------------------------------------------------------- 1 | export interface SystemSetting { 2 | id: number; 3 | title: string; 4 | description: string; 5 | keywords: string; 6 | version: string; 7 | openRegister: boolean; 8 | createDate: Date; 9 | updateBy: Date; 10 | } 11 | 12 | // 用户表 13 | export interface User { 14 | id: number; 15 | username: string; 16 | password: string; 17 | name?: string; 18 | email: string; 19 | createDate: Date; 20 | } 21 | 22 | export interface Book { 23 | id: number; 24 | bookId: string; 25 | bookName: string; 26 | shareKey: string; 27 | userId: number; 28 | budget: number; 29 | createDate: Date; 30 | } 31 | 32 | export interface Flow { 33 | id?: number; 34 | userId?: number; 35 | bookId?: string; 36 | day?: string; 37 | flowType?: string; // 流水类型:收入、支出、不计收支 38 | industryType?: string; // 行业分类(支出类型/收入类型) 39 | payType?: string; // 支付方式/收款方式 40 | money?: number; 41 | name?: string; 42 | description?: string; 43 | invoice?: string; 44 | origin?: string; // 流水来源:谁谁-支付宝导入;谁谁手动输出 45 | attribution?: string; // 流水归属(谁的收入/支出) 46 | eliminate?: number; // 平账标志,0未平账;1已平账,-1忽略平账 47 | } 48 | 49 | // Budget 支出计划(预算) 50 | export interface Budget { 51 | id?: number; 52 | userId?: number; 53 | bookId?: string; 54 | month?: string; 55 | budget?: number; 56 | used?: number; 57 | } 58 | 59 | export interface FixedFlow { 60 | id?: number; 61 | userId?: number; 62 | bookId?: string; 63 | startMonth?: string; 64 | endMonth?: string; 65 | month?: string; 66 | money?: number; 67 | name?: string; 68 | description?: string; 69 | flowType?: string; // 流水类型:收入、支出、不计收支 70 | industryType?: string; // 行业分类(支出类型/收入类型) 71 | payType?: string; // 支付方式/收款方式 72 | attribution?: string; // 流水归属(谁的收入/支出) 73 | } 74 | 75 | export interface TypeRelation { 76 | id: number; 77 | userId: number; 78 | bookId: string; 79 | source: string; 80 | target: string; 81 | } 82 | --------------------------------------------------------------------------------