news 2026/9/10 12:20:47

公共艺术品资产与维护成本管理系统:Spring Boot + Vue 3 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
公共艺术品资产与维护成本管理系统:Spring Boot + Vue 3 实战

最近看到一条挺有意思的新闻话题:“Banksy works cost public almost £150k”。抛开艺术品争议不谈,这件事给技术人员留了一个很现实的思考:公共艺术品一旦进入城市,后续的清洁、修复、安保、保险甚至拆除,都会变成一笔笔公共支出。这些支出怎么登记、怎么统计、怎么防止重复花钱,光靠 Excel 和口头沟通肯定撑不住。

本文就围绕这个场景,从零到一搭建一套“公共艺术品资产与维护成本管理系统”。项目采用 Spring Boot + MyBatis-Plus + MySQL 做后端,Vue 3 + Element Plus 做前端,覆盖资产台账、维护工单、成本记录、年度统计等核心功能。无论你是做毕业设计、后端入门练习,还是想了解前后端分离项目的完整落地思路,都可以直接参考这套流程,并把代码复制到本地跑通。

1. 背景与核心概念

1.1 为什么公共艺术品需要一套管理系统

先看一个很常见的现象:城市里摆放的艺术装置、墙面壁画、路边的雕塑,大家平时只看到它作为“景观”存在,很少有人会追问它每年花了多少钱维护。但如果这些艺术品损坏、褪色、被涂鸦覆盖,就需要请专业人员清洁、修复;遇到安全隐患时,还要临时设置围挡、增加巡查;如果因为城市规划需要搬移,又会涉及运输、保险、场地恢复等成本。

这些费用分散在市政、文旅、园林、安保等多个部门,如果没有统一台账,很容易出现两个问题:

  • 成本不透明:不同年份、不同部门花了多少钱,没有历史沉淀。
  • 维护不及时:没有周期计划和巡检记录,小问题拖成大问题,最后不得不花更多钱修复。

所以,一套“公共艺术品资产管理系统”的核心价值,就是把每一件作品当成一项固定资产来管理,同时把与之相关的维护工单、成本明细记录下来,形成可查询、可统计、可追溯的数据资产。

1.2 系统的核心实体

1.2.1 资产台账

资产台账用来记录艺术品的基本信息,包括资产编号、作品名称、作者、所在位置、作品类型、当前状态、入库时间、所属部门等。这里的“资产编号”建议保持唯一,类似固定资产里的一物一码。

1.2.2 维护工单

当一件艺术品出现损坏、老化或者定期需要保养时,系统应该生成一张维护工单。工单里包含关联的艺术品、处理内容、紧急程度、当前状态、负责人、计划完成时间等信息。通过工单可以分析哪件作品维护频率最高,哪个位置容易出现异常。

1.2.3 成本记录

成本记录是整套系统里最敏感也最重要的数据。它记录的是每一笔与作品相关的真实费用,字段包括费用类型、金额、发生日期、支付部门、备注等。费用类型可以拆分为清洁、修复、拆除、安保、保险、运输等。

有了这些数据,我们才能回答类似“某一年哪一类艺术品的维护成本最高”这类问题。

1.3 核心业务流转

系统的核心流程可以拆成下面几步:

  1. 艺术品入库时,管理员在系统中录入资产台账。
  2. 巡检人员发现艺术品破损后,创建维护工单。
  3. 维护人员执行修复、清洁等工作,并登记对应的成本记录。
  4. 管理层按年度、月份、费用类型查看成本统计报表。

这套流程本质上是“资产档案 + 工单管理 + 成本归集”的组合,非常符合传统后台管理系统的典型形态。

2. 环境准备与版本说明

在开始写代码之前,先确认一下本地环境。不同版本之间会有细节差异,本文示例以常见组合为准,重点演示配置思路,你不需要严格追求最新版本。

建议环境如下:

  • JDK 8 或 JDK 17
  • Maven 3.6 或更高版本
  • MySQL 5.7 或 8.0
  • Node.js 14 或更高版本
  • IDE:IntelliJ IDEA / VS Code 均可

后端框架版本方面,本文使用 Spring Boot 2.7.18 + MyBatis-Plus 3.5.5。如果你使用 Spring Boot 3.x,需要注意依赖坐标中javax.servlet会变成jakarta.servlet,MySQL 驱动坐标也会从mysql:mysql-connector-java调整为com.mysql:mysql-connector-j

前端使用 Vue 3 + Vite + Element Plus + ECharts,这四个库都通过 npm 安装,版本只要保持较新即可,具体以安装时的 package.json 为准。

3. 数据库设计与 SQL 脚本

3.1 设计思路

这套系统的核心表有三张:artworkmaintenance_taskcost_record

  • artwork:艺术品资产表,存放作品的基本档案。
  • maintenance_task:维护工单表,记录某件作品被处理的任务。
  • cost_record:成本记录表,记录与作品相关的费用明细。

三张表的关系是:一件作品可以有多条维护工单,一条维护工单可以关联多条成本记录;成本记录也可以直接挂在作品下,不依赖工单,方便处理“无工单但有费用”的场景。

为了保证数据的可追溯性,所有表都加上create_timeupdate_time,并保留deleted字段,用于逻辑删除。逻辑删除可以避免误删数据,后面会在 MyBatis-Plus 里统一配置。

3.2 建表 SQL

下面是完整的建库建表脚本:

CREATE DATABASE IF NOT EXISTS public_art_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE public_art_system; CREATE TABLE artwork ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', code VARCHAR(32) NOT NULL COMMENT '资产编号', name VARCHAR(128) NOT NULL COMMENT '作品名称', artist VARCHAR(64) DEFAULT '' COMMENT '作者', location VARCHAR(128) DEFAULT '' COMMENT '所在位置', type TINYINT DEFAULT 1 COMMENT '作品类型:1-壁画 2-雕塑 3-装置 4-其他', status TINYINT DEFAULT 1 COMMENT '状态:1-在展 2-维护中 3-已移除', install_date DATE DEFAULT NULL COMMENT '入库日期', owner_dept VARCHAR(64) DEFAULT '' COMMENT '所属部门', created_by VARCHAR(32) DEFAULT '' COMMENT '创建人', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0-未删除 1-已删除', UNIQUE KEY uk_code (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='艺术品资产表'; CREATE TABLE maintenance_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', artwork_id BIGINT NOT NULL COMMENT '艺术品ID', task_no VARCHAR(32) NOT NULL COMMENT '工单编号', title VARCHAR(128) NOT NULL COMMENT '工单标题', description VARCHAR(500) DEFAULT '' COMMENT '问题描述', priority TINYINT DEFAULT 2 COMMENT '优先级:1-低 2-中 3-高', status TINYINT DEFAULT 1 COMMENT '状态:1-待处理 2-处理中 3-已完成 4-已取消', assignee VARCHAR(32) DEFAULT '' COMMENT '负责人', plan_finish_date DATE DEFAULT NULL COMMENT '计划完成日期', finish_time DATETIME DEFAULT NULL COMMENT '实际完成时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除', UNIQUE KEY uk_task_no (task_no), KEY idx_artwork_id (artwork_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维护工单表'; CREATE TABLE cost_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', artwork_id BIGINT NOT NULL COMMENT '艺术品ID', task_id BIGINT DEFAULT NULL COMMENT '关联工单ID,可以为空', cost_type VARCHAR(32) NOT NULL COMMENT '费用类型:清洁/修复/拆除/安保/保险/运输', amount DECIMAL(12, 2) NOT NULL COMMENT '金额', occurred_date DATE NOT NULL COMMENT '费用发生日期', pay_dept VARCHAR(64) DEFAULT '' COMMENT '支付部门', remark VARCHAR(255) DEFAULT '' COMMENT '备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除', KEY idx_artwork_id (artwork_id), KEY idx_task_id (task_id), KEY idx_occurred_date (occurred_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成本记录表';

字段设计时有两个地方要特别说明:

一是金额字段使用DECIMAL(12, 2),不要使用FLOATDOUBLE,否则做累计求和时可能出现精度误差。二是所有关键字段都建立索引,包括artwork_idoccurred_date,这样后续按日期范围统计成本时性能才可能有保障。

4. Spring Boot 后端实现

4.1 创建项目与 Maven 依赖

后端项目推荐用 IDEA 直接创建 Spring Initializr 项目,也可以手动建立 Maven 工程。核心依赖如下:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>public-art-system</artifactId> <version>1.0.0</version> <name>public-art-system</name> <description>公共艺术品资产与维护成本管理系统</description> <properties> <java.version>8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

这里用到了 MyBatis-Plus 的BaseMapper,它可以帮我们省去大量手写 SQL 的重复工作。如果你不想引入 MyBatis-Plus,也可以直接用 Spring Data JPA 或原生 MyBatis,核心实现思路都差不多。

4.2 application.yml 配置

src/main/resources下创建application.yml

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/public_art_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 id-type: auto

配置说明:

  • map-underscore-to-camel-case:开启驼峰映射,让数据库的install_date自动映射到 Java 字段installDate
  • logic-delete-field:配置逻辑删除字段,后续调用删除方法时,真正执行的其实是UPDATE artwork SET deleted = 1 WHERE id = ?
  • jackson.date-format:统一返回给前端的时间格式,避免出现时间戳。

如果你的数据库密码或端口不同,记得改成自己的配置。生产环境不要使用明文密码,建议通过环境变量或配置中心注入。

4.3 实体类与表映射

创建entity包,新建Artwork.java

package com.example.publicart.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; @Data @TableName("artwork") public class Artwork { @TableId(type = IdType.AUTO) private Long id; private String code; private String name; private String artist; private String location; private Integer type; private Integer status; private LocalDate installDate; private String ownerDept; private String createdBy; private LocalDateTime createTime; private LocalDateTime updateTime; @TableLogic private Integer deleted; }

@TableName指定表名,@TableId(type = IdType.AUTO)表示主键自增,@TableLogic标记逻辑删除字段。@Data是 Lombok 提供的注解,会自动生成 getter、setter 和 toString 方法。

再创建CostRecord.java

package com.example.publicart.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @Data @TableName("cost_record") public class CostRecord { @TableId(type = IdType.AUTO) private Long id; private Long artworkId; private Long taskId; private String costType; private BigDecimal amount; private LocalDate occurredDate; private String payDept; private String remark; private LocalDateTime createTime; @TableLogic private Integer deleted; }

注意amount使用BigDecimal,这是金额计算的标准做法。

4.4 Mapper 与 Service

创建mapper包:

package com.example.publicart.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.publicart.entity.Artwork; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ArtworkMapper extends BaseMapper<Artwork> { }

成本记录的 Mapper 同样写法:

package com.example.publicart.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.publicart.entity.CostRecord; import org.apache.ibatis.annotations.Mapper; @Mapper public interface CostRecordMapper extends BaseMapper<CostRecord> { }

Service 层可以直接使用 MyBatis-Plus 提供的IServiceServiceImpl。这里为了简化,我直接在 Controller 中注入 Mapper,实际项目建议还是补一层 Service,方便加事务和业务校验。

4.5 Controller 接口

创建controller包,统一接口前缀为/api。先写一个通用返回对象:

package com.example.publicart.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

然后编写艺术品管理接口:

package com.example.publicart.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.publicart.common.Result; import com.example.publicart.entity.Artwork; import com.example.publicart.mapper.ArtworkMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/artworks") public class ArtworkController { @Autowired private ArtworkMapper artworkMapper; @PostMapping public Result<String> create(@RequestBody Artwork artwork) { artworkMapper.insert(artwork); return Result.ok("新增成功"); } @GetMapping("/page") public Result<Page<Artwork>> page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String name, @RequestParam(required = false) Integer status) { QueryWrapper<Artwork> wrapper = new QueryWrapper<>(); wrapper.lambda() .like(name != null && !name.isEmpty(), Artwork::getName, name) .eq(status != null, Artwork::getStatus, status) .orderByDesc(Artwork::getCreateTime); Page<Artwork> result = artworkMapper.selectPage(new Page<>(page, size), wrapper); return Result.ok(result); } @PutMapping public Result<String> update(@RequestBody Artwork artwork) { artworkMapper.updateById(artwork); return Result.ok("更新成功"); } @DeleteMapping("/{id}") public Result<String> delete(@PathVariable Long id) { Artwork artwork = new Artwork(); artwork.setId(id); artworkMapper.deleteById(artwork); return Result.ok("删除成功"); } }

这段代码有几个细节值得说:

  • 使用QueryWrapper动态拼接条件。当name为空时,like条件不会生效;当status为空时,eq条件不会生效。
  • orderByDesc(Artwork::getCreateTime)让新增作品排在最前面。
  • 删除操作走的是逻辑删除,数据不会真的消失,这是为了避免误删资产档案。

4.6 成本统计接口

统计是成本管理的核心。下面提供一个按月份统计年度成本的接口:

package com.example.publicart.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.example.publicart.common.Result; import com.example.publicart.entity.CostRecord; import com.example.publicart.mapper.CostRecordMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/statistics") public class CostStatisticsController { @Autowired private CostRecordMapper costRecordMapper; @GetMapping("/cost") public Result<List<Map<String, Object>>> costStatistics(@RequestParam(required = false) Integer year) { Integer targetYear = year != null ? year : LocalDate.now().getYear(); QueryWrapper<CostRecord> wrapper = new QueryWrapper<>(); wrapper.select("DATE_FORMAT(occurred_date, '%Y-%m') AS month", "SUM(amount) AS total") .eq("DATE_FORMAT(occurred_date, '%Y')", targetYear) .groupBy("month") .orderByAsc("month"); List<Map<String, Object>> list = costRecordMapper.selectMaps(wrapper); return Result.ok(list); } }

在这个接口里,DATE_FORMAT(occurred_date, '%Y-%m')会把费用日期统一成“2025-01”这样的月份格式,再按月份分组求和。返回结果类似:

{ "code": 200, "message": "success", "data": [ { "month": "2025-01", "total": 3200.00 }, { "month": "2025-02", "total": 4800.00 } ] }

需要注意,selectMaps返回的字段名是数据库列别名。MySQL 中如果使用AS month,部分驱动会转成大写MONTH,前端拿到后要做兼容。更稳定的做法是统一在 SQL 中使用小写别名,或者在 Java 里做一次字段名转换。

4.7 跨域与时间格式配置

前端开发服务器默认是http://localhost:3000,后端是http://localhost:8080,两者不同源,所以需要开启跨域访问。新建一个配置类:

package com.example.publicart.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:3000") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600); } }

如果是线上部署,建议把allowedOrigins改为具体的域名,不要长期使用*,否则接口容易被任意网站跨域调用。

5. Vue 3 前端实现

5.1 项目初始化和依赖

前端工程可以用 Vite 快速初始化:

npm create vite@latest public-art-web -- --template vue cd public-art-web npm install npm install axios element-plus echarts

安装完成后,在main.js中引入 Element Plus:

import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' const app = createApp(App) app.use(ElementPlus) app.mount('#app')

5.2 封装 axios 请求

src/api/request.js中创建 axios 实例:

import axios from 'axios' const request = axios.create({ baseURL: 'http://localhost:8080', timeout: 10000 }) request.interceptors.response.use( response => { return response.data }, error => { console.error('接口异常:', error) return Promise.reject(error) } ) export default request

然后在src/api/artwork.js中封装接口:

import request from './request' export const getArtworkPage = (params) => { return request.get('/api/artworks/page', { params }) } export const createArtwork = (data) => { return request.post('/api/artworks', data) } export const updateArtwork = (data) => { return request.put('/api/artworks', data) } export const deleteArtwork = (id) => { return request.delete(`/api/artworks/${id}`) } export const getCostStatistics = (params) => { return request.get('/api/statistics/cost', { params }) }

5.3 艺术品列表页

src/views/ArtworkList.vue中,使用el-tableel-pagination展示数据。核心代码片段如下:

<template> <div> <el-form inline> <el-form-item label="作品名称"> <el-input v-model="query.name" placeholder="请输入作品名称" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="loadData">查询</el-button> </el-form-item> </el-form> <el-table :data="tableData" border stripe> <el-table-column prop="code" label="资产编号" width="120" /> <el-table-column prop="name" label="作品名称" /> <el-table-column prop="artist" label="作者" width="120" /> <el-table-column prop="location" label="所在位置" /> <el-table-column label="操作" width="150"> <template #default> <el-button type="danger" size="small" @click="handleDelete">删除</el-button> </template> </el-table-column> </el-table> <el-pagination background layout="total, prev, pager, next" :total="total" v-model:current-page="query.page" :page-size="query.size" @current-change="loadData" /> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import { getArtworkPage, deleteArtwork } from '../api/artwork' const tableData = ref([]) const total = ref(0) const query = reactive({ page: 1, size: 10, name: '' }) const loadData = async () => { const res = await getArtworkPage(query) tableData.value = res.data.records total.value = res.data.total } const handleDelete = async (row) => { await deleteArtwork(row.id) loadData() } onMounted(() => { loadData() }) </script>

注意代码中handleDelete里的row参数需要从模板作用域传入,这里为了简洁省略了row的传递,实际使用时应该写成@click="handleDelete(row)"

5.4 成本统计图

成本趋势图使用 ECharts。在src/views/CostStatistics.vue中:

<template> <div> <el-select v-model="year" placeholder="选择年份" @change="loadData"> <el-option label="2024" value="2024" /> <el-option label="2025" value="2025" /> </el-select> <div ref="chartRef" style="height: 400px; margin-top: 16px"></div> </div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from 'vue' import * as echarts from 'echarts' import { getCostStatistics } from '../api/artwork' const chartRef = ref(null) const year = ref('2025') let chartInstance = null const loadData = async () => { const res = await getCostStatistics({ year: year.value }) const months = res.data.map(item => item.month) const values = res.data.map(item => Number(item.total)) if (chartInstance) { chartInstance.setOption({ xAxis: { data: months }, series: [{ data: values }] }) } } onMounted(() => { chartInstance = echarts.init(chartRef.value) loadData() }) onBeforeUnmount(() => { if (chartInstance) { chartInstance.dispose() } }) </script>

这里只展示了核心思路,完整的 ECharts option 还需要配置标题、tooltip、yAxis 等,你可以按需补充。关键是理解“后端按月份聚合结果,前端直接渲染”的数据流。

6. 运行与验证

6.1 启动后端

在后端项目根目录执行:

mvn spring-boot:run

看到类似下面的日志说明启动成功:

Started PublicArtApplication in 3.12 seconds Tomcat started on port(s): 8080

如果数据库连接失败,优先检查application.yml中的用户名、密码、数据库名是否正确。

6.2 启动前端

public-art-web目录下执行:

npm run dev

默认访问地址是http://localhost:3000。打开页面后,可以先在艺术品列表页新增一条数据,再查看成本统计页。

6.3 接口验证示例

用 curl 测试新增作品接口:

curl -X POST http://localhost:8080/api/artworks \ -H "Content-Type: application/json" \ -d '{ "code": "ART-001", "name": "街头壁画示例", "artist": "Unknown", "location": "城市文化广场", "type": 1, "status": 1, "ownerDept": "城市管理部" }'

预期返回:

{ "code": 200, "message": "success", "data": "新增成功" }

查询成本统计:

curl "http://localhost:8080/api/statistics/cost?year=2025"

如果数据库中还没有成本记录,返回的data会是空数组,这是正常现象。

7. 常见问题与排查思路

在实际开发过程中,新手最常见的问题可以总结成下面这张表:

问题现象常见原因解决思路
启动时报数据库连接失败密码错误、数据库未启动、URL 写错先用 Navicat 等工具测试连接,再检查配置
接口返回 404Controller 路径或启动类扫描包不正确确认启动类在根包下,Controller 使用@RestController
时间字段返回乱码数据库字符集不是 utf8mb4建表时指定DEFAULT CHARSET=utf8mb4
前端请求接口跨域后端未配置跨域添加CorsConfig,允许前端地址访问
金额统计出现精度问题数据库字段用FLOATDOUBLE修改为DECIMAL(12, 2),Java 中使用BigDecimal
删除数据后仍然能查到逻辑删除字段配置不正确检查@TableLogiclogic-delete-value配置
分页查询不生效没有配置 MyBatis-Plus 分页插件添加MybatisPlusInterceptor并注册分页插件

如果是使用 Spring Boot 3.x 项目,还要额外注意引入mybatis-plus-spring-boot3-starter,而不是mybatis-plus-boot-starter,依赖坐标不同。

排查接口问题时,建议先把 MyBatis-Plus 的 SQL 日志打开。配置文件里已经设置了log-impl: org.apache.ibatis.logging.stdout.StdOutImpl,启动后在控制台直接观察打印的 SQL 语句,比猜测问题要高效得多。

8. 工程化最佳实践

8.1 资产管理规范

资产编号是系统的唯一索引,建议采用“部门编码 + 年份 + 流水号”的格式,例如WG-2025-001。这样即使后期系统切换,也能通过编号快速确认资产归属。

艺术品状态变化必须留下记录。比如作品从“在展”变为“维护中”,建议额外增加一张状态变更表,记录操作人、变更时间、变更原因。这套系统的artwork表里只有当前状态,长期来看不够完整,实际项目中可以扩展。

8.2 成本数据精度

金额计算必须坚持BigDecimal,不要用double。在 Java 中,0.1 + 0.2的 double 结果并不是精确的0.3,这可能影响成本统计的准确性。

数据库层面,建议在cost_record表上增加一个唯一约束或唯一索引,保证同一笔成本不会重复录入。更严格的方案是在应用层做幂等校验,例如按“艺术品 + 费用类型 + 费用日期 + 金额 + 支付部门”作为业务唯一键。

8.3 权限与审计

如果系统要上线,必须考虑权限控制。至少区分以下角色:

  • 管理员:负责资产新增、修改、删除。
  • 维护人员:负责工单处理。
  • 财务/管理层:只读成本和统计页面。

成本数据属于敏感数据,修改操作应该记录审计日志,包括操作人、操作时间、修改前后内容。技术上可以使用 Spring Security + Sa-Token,也可以先用拦截器做简单的登录鉴权,根据项目复杂度决定。

8.4 数据备份与合规

数据库建议开启定时备份。简单的方式是使用 mysqldump:

mysqldump -u root -p public_art_system > backup_$(date +%Y%m%d).sql

生产系统不要直接使用 root 账号连接应用,应单独创建最小权限账号:

CREATE USER 'art_app'@'%' IDENTIFIED BY 'your_strong_password'; GRANT SELECT, INSERT, UPDATE, DELETE ON public_art_system.* TO 'art_app'@'%'; FLUSH PRIVILEGES;

这样做即使应用方配置泄露,也不至于让攻击者直接删库或改表结构。

公共艺术品数据如果涉及公开财政支出,发布到线上时还要注意数据脱敏,不要随意关联操作人员身份证、手机号等敏感信息。涉及公共资金数据的展示,建议在显著位置注明数据来源,并对统计口径做清晰说明。

9. 总结与下一步学习路线

通过这个实战项目,我们完成了一套公共艺术品资产与维护成本管理系统的核心闭环。从数据库设计、Spring Boot 后端接口、成本统计,到 Vue 3 前端页面和 ECharts 可视化,整体流程覆盖了前后端分离开发中最常用的一套技术组合。

你学到的关键技术点包括:

  • 用 MyBatis-Plus 快速完成单表 CRUD 和分页查询。
  • QueryWrapper动态拼接查询条件。
  • DATE_FORMAT配合GROUP BY做按月份成本聚合。
  • 用逻辑删除避免资产数据误删。
  • 用 Vue 3 组合式 API 封装接口并渲染列表和图表。

下一步,你可以沿着这几个方向继续深入:

  • 给系统增加登录认证和角色权限,使用 Spring Security 或 Sa-Token。
  • 增加“维护工单”的完整流程,支持待办、审批和提醒。
  • 使用定时任务,每月自动生成成本统计报表并推送邮件。
  • 引入 Redis,缓存统计结果,减少高频查询对数据库的压力。

实际项目中,公共资产的管理还会涉及 GIS 地图定位、图片上传、存证归档等需求。建议先把这个基础版本跑通,再逐步迭代。如果你在做毕业设计或项目实训,也可以以本文为骨架,补充一个具体的业务场景,让系统更有完整性。

如果这篇文章对你有帮助,欢迎收藏备用。遇到问题也可以在评论区留言,我会根据实际经验帮你分析。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 21:00:05

【DeepSeek Harness 基石-Cordis元框架】入门指南与六大核心概念解析

文章目录 1. 项目概述 1.1 什么是 Cordis 1.2 项目状态 1.3 适用场景 1.4 设计哲学 1.5 Deepseek Harness与Cordis的关系 2. 快速开始 2.1 环境要求 2.2 安装 2.3 最简示例 2.4 使用脚手架创建项目 2.5 项目结构 2.6 使用 CLI 启动 3. 核心概念:Context(上下文) 3.1 什么是 …

作者头像 李华
网站建设 2026/9/2 23:45:01

开源原型模板:把产品评审变成可追踪的前端工程流程

这次我们来看一类非常容易被低估的开源项目&#xff1a;面向“产品构建 评审流程”的开源原型模板。它不是一个单一框架&#xff0c;而是一整套把“快速出原型”和“结构化评审”串起来的前端工程模板。产品经理可以拿它快速搭出可点击的交互稿&#xff0c;设计师可以在真实页…

作者头像 李华
网站建设 2026/9/2 8:31:06

RNN+AdaBoost建模无文档脏数据的工程实践

简介&#xff1a;本资源为2017年KDD Cup全球顶级数据挖掘竞赛的完整参赛项目复现包&#xff0c;面向数据科学学习者、算法工程师及高校科研人员&#xff0c;聚焦大规模时序预测与交通流量建模等典型工业级任务。压缩包共70个文件&#xff08;31.47MB&#xff09;&#xff0c;涵…

作者头像 李华
网站建设 2026/9/2 11:07:44

零代码开发:从选型搭建到商业化的真实边界

《不写一行代码年赚4亿美元》这种标题&#xff0c;放在哪里都能吸引一堆点击。但做技术的人看到之后&#xff0c;第一反应不是算收入&#xff0c;而是先问一句&#xff1a;零代码工具到底能不能承担这么高的预期&#xff1f;我自己的答案很清楚&#xff1a;能靠零代码搭出实用系…

作者头像 李华
网站建设 2026/9/4 8:51:03

联想2025秋招算法笔试真题解析:KMP、并查集与堆排序考点全盘点

每年八月底到十月初&#xff0c;秋招战线拉得最长的一批公司里&#xff0c;联想绝对排得上号。我这两年帮师弟师妹改简历、做模拟面试&#xff0c;前前后后接触了不少联想2025届秋招的算法岗笔试题&#xff0c;自己也把能找到的真题和面经整理了一遍。说实话&#xff0c;联想的…

作者头像 李华