Personal Finance Management Platform Built with Spring Boot, Vue, and uni-app

Architecture Overview

The platform is split into three layers:

  • Back end: Spring Boot 2.7, MyBatis-Plus 3.5, MySQL 8
  • Admin Portal: Vue 3 + Element-Plus
  • Mobile Client: uni-app compiled to WeChat Mini-Program and H5

Backend Implementation

Domain Model

@Data
@TableName("t_wallet")
public class Wallet {
    private Long id;
    private Long userId;
    private String name;
    private BigDecimal balance;
    private LocalDateTime createdAt;
}

@Data
@TableName("t_transaction")
public class Transaction {
    private Long id;
    private Long walletId;
    private BigDecimal amount;
    private String category;
    private String note;
    private LocalDateTime occurredAt;
}

REST Endpoints

@RestController
@RequestMapping("/api/wallet")
@RequiredArgsConstructor
public class WalletController {
    private final WalletService walletService;

    @GetMapping
    public List<Wallet> list(HttpServletRequest req) {
        Long userId = AuthHolder.getUserId(req);
        return walletService.findByUser(userId);
    }

    @PostMapping
    public Wallet create(@RequestBody WalletDto dto, HttpServletRequest req) {
        Long userId = AuthHolder.getUserId(req);
        return walletService.add(userId, dto);
    }

    @GetMapping("/{id}/stats")
    public WalletStats stats(@PathVariable Long id) {
        return walletService.monthlyStats(id);
    }
}

Service Layer

@Service
@RequiredArgsConstructor
public class WalletService {
    private final WalletMapper walletMapper;
    private final TransactionMapper txnMapper;

    public Wallet add(Long userId, WalletDto dto) {
        Wallet w = new Wallet();
        w.setUserId(userId);
        w.setName(dto.getName());
        w.setBalance(BigDecimal.ZERO);
        w.setCreatedAt(LocalDateTime.now());
        walletMapper.insert(w);
        return w;
    }

    public WalletStats monthlyStats(Long walletId) {
        LocalDateTime start = YearMonth.now().atDay(1).atStartOfDay();
        LocalDateTime end = YearMonth.now().atEndOfMonth().atTime(23, 59, 59);

        BigDecimal income = txnMapper.sumByType(walletId, "INCOME", start, end);
        BigDecimal expense = txnMapper.sumByType(walletId, "EXPENSE", start, end);

        return new WalletStats(income, expense);
    }
}

Seucrity Filter

@Component
public class JwtFilter extends OncePerRequestFilter {
    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain)
                                    throws ServletException, IOException {
        String header = req.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (jwtUtil.validate(token)) {
                Long userId = jwtUtil.getUserId(token);
                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(userId, null, List.of());
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        chain.doFilter(req, res);
    }
}

Admin Portal (Vue)

Pinia Store

import { defineStore } from 'pinia'
import axios from '@/plugins/axios'

export const useWalletStore = defineStore('wallet', {
  state: () => ({
    list: []
  }),
  actions: {
    async fetch() {
      const { data } = await axios.get('/api/wallet')
      this.list = data
    },
    async add(payload) {
      const { data } = await axios.post('/api/wallet', payload)
      this.list.unshift(data)
    }
  }
})

Component Example

<template>
  <el-table :data="walletStore.list">
    <el-table-column prop="name" label="Wallet" />
    <el-table-column label="Balance">
      <template #default="{ row }">
        ¥{{ row.balance.toFixed(2) }}
      </template>
    </el-table-column>
    <el-table-column label="Action">
      <template #default="{ row }">
        <el-button @click="showStats(row.id)">Stats</el-button>
      </template>
    </el-table-column>
  </el-table>
</template>

<script setup>
import { useWalletStore } from '@/stores/wallet'
import { onMounted } from 'vue'

const walletStore = useWalletStore()
onMounted(() => walletStore.fetch())
</script>

Mobile Client (uni-app)

<template>
  <view class="container">
    <view v-for="w in wallets" :key="w.id" class="card">
      <text>{{ w.name }}</text>
      <text>¥{{ w.balance }}</text>
    </view>
    <button @click="addWallet">+ Add Wallet</button>
  </view>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { get, post } from '@/utils/request'

const wallets = ref([])

onMounted(async () => {
  wallets.value = await get('/api/wallet')
})

async function addWallet() {
  const name = prompt('Wallet name')
  if (name) {
    const res = await post('/api/wallet', { name })
    wallets.value.unshift(res)
  }
}
</script>

Deployment

  1. Database:``` CREATE DATABASE finance CHARACTER SET utf8mb4; -- run schema.sql and data.sql

  2. Backend:``` mvn clean package -DskipTests java -jar target/finance-0.0.1-SNAPSHOT.jar
    --spring.profiles.active=prod
    --server.port=8080

  3. Admin Portal:``` cd vue-admin npm ci npm run build

    serve dist/ via nginx

  4. Mobile:``` cd uni-app npm ci

    HBuilderX → 发行 → 小程序-微信(或H5)

    
    

Testing

Scenario Input Expected Result
Login success alice / 123456 JWT token Pass
Login fail alice / wrong 401 Pass
Add wallet name=Cash 201 + id Pass
List wallets GET /api/wallet array Pass

Database Schema

CREATE TABLE t_wallet (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  name VARCHAR(50) NOT NULL,
  balance DECIMAL(12,2) DEFAULT 0,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE t_transaction (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  wallet_id BIGINT NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  category VARCHAR(20) NOT NULL,
  note VARCHAR(255),
  occurred_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Tags: Spring Boot Vue 3 uni-app mybatis-plus JWT

Posted on Sun, 11 Oct 2026 16:15:31 +0000 by mlschutz