1. 需求分析与方案选型:为什么高校固定资产管理需要一套独立系统
1.1 高校资产管理的真实痛点
干了多年高校信息化相关项目,我最深的体会是:固定资产管理这事儿,看着不起眼,真做起来全是坑。高校的资产体量跟普通企业完全不是一个量级——教学楼、实验设备、办公家具、图书资料、电脑耗材,数量动辄几万条,而且分布在各院系、各实验室、各行政处室,资产责任人、存放地点、使用状态随时在变。
我接触过某高校的后勤老师,他之前用Excel管全校资产,台账几万行,每次年终盘点都要拉上一帮学生助理手工核对,贴标签贴到怀疑人生。设备借走了没登记、报废了没销账、存放地点搬了没更新,这些都是常态。更要命的是,上级部门来检查时要做资产年报,数据对不上,只能连夜改表。
所以高校固定资产管理系统的核心价值,不是"把Excel搬到网页上",而是解决几个真问题:资产全生命周期可追溯、责任到人、流程有审批记录、账实相符。这套系统本质上是给学校资产处、各院系资产管理员、普通教职工三方用的协同工具。
1.2 技术选型:SpringBoot+Vue3+MyBatis的组合逻辑
先聊聊为什么是这个技术组合。前后端分离现在已经是Java后端项目的主流形态了,尤其是高校信息化这种场景,甲方通常有自己的私有化部署要求,对技术栈有明确偏好,Java系依然是国内高校系统选型的大头。
后端选SpringBoot,理由很简单:生态成熟、招人容易、部署简单。SpringBoot内置Tomcat,一个jar包就能跑起来,配合约定的配置方式,开发效率比传统SSH那套高太多了。而且高校信息中心一般都有Java运维经验,后续交接维护不费劲。
MyBatis作为持久层框架,很多人会纠结它跟JPA选哪个。我的观点很明确:这种业务系统,SQL复杂、报表多、查询条件动态变化,MyBatis的灵活性和可控性是JPA比不了的。JPA自动建表虽然省事,但一旦业务复杂起来,联动查询、统计报表写起来反而别扭。MyBatis让你对SQL有完全的控制权,排查慢查询也直观。
前端Vue3也没什么悬念。Vue3的组合式API(Composition API)配合响应式系统,在管理后台这类中后台项目里写起来非常顺手。搭配Element Plus组件库,表格、表单、弹窗、树形控件现成的都有,短时间内能把界面做得像模像样。而且Vite构建速度比Webpack快一个量级,开发体验是真的舒服。
数据库选MySQL,这是最稳妥的答案。高校场景数据量虽然大,但固定资产这种业务撑死几十万条记录,MySQL完全扛得住。开源、免费、运维资料多,随便一个运维都能搞定,没必要上Oracle或者PostgreSQL给自己找麻烦。
1.3 模块划分与核心数据流
这套系统的模块划分,我是按实际用户角色和使用场景来拆的,不是按教科书乱分。核心模块包括以下几个:
| 模块 | 功能定位 | 主要使用者 |
|---|---|---|
| 资产台账管理 | 资产新增、编辑、查询、详情 | 资产管理员、普通教职工 |
| 资产领用/归还 | 申请、审批、出入库记录 | 教职工发起,管理员审批 |
| 资产维修/报废 | 维修登记、报废申请与审批 | 教职工、资产处 |
| 盘点管理 | 生成盘点任务、扫码盘点、差异核对 | 资产管理员 |
| 系统管理 | 用户、角色、部门、字典管理 | 系统管理员 |
| 报表统计 | 资产分类统计、部门统计、使用状态统计 | 管理层 |
数据流的主线是:资产从采购入库开始,进入台账,然后经历领用、调拨、维修、归还、报废这些状态变更,每一步都产生一条流转记录。只要这个主链路的数据完整,期末出年报、出报表就是按条件查一查的事。
2. 环境准备与项目搭建:从零把骨架跑起来
2.1 开发环境与版本选择
先列一套我实测过的环境组合,照着装不会翻车:
- JDK:1.8或11。建议直接用JDK 8,虽然老,但是SpringBoot 2.x用它最稳,很多高校服务器上也只装了JDK 8
- Maven:3.6+,用来管理后端依赖
- Node.js:16以上,Vue3的项目要求Node版本不能太低
- MySQL:5.7或8.0,推荐8.0,字符集记得用utf8mb4
- IDEA:后端用IDEA,前端用VS Code,两个都装吧
- Navicat或DBeaver:可视化操作数据库
SpringBoot版本建议用2.7.x,这个版本稳定且资料多。别追新用SpringBoot 3,3的版本要求JDK 17,跟很多高校现有环境不兼容,而且一些旧依赖兼容性会有问题。Vue3配合Element Plus,用Vite构建。
注意:前端Node版本太老会报错,装完Node后用
node -v确认一下。另外npm如果拉依赖慢,配上国内的镜像源,我这里不展开具体地址,自己搜一下就行。
2.2 后端项目骨架搭建
后端我习惯用IDEA的Spring Initializr直接生成,或者从GitHub找模板改。核心依赖就这几个:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency>数据库连接池我用Druid,原因就是好用:自带监控页面、SQL执行统计、防SQL注入,生产环境排查问题很有用。JWT用来做登录认证,这个后面细说。
application.yml配置里,有几个关键项值得注意。mapper-locations要指向Mapper XML文件的位置,map-underscore-to-camel-case设为true,这样数据库字段asset_name能自动映射到Java属性的assetName,省去大量手动映射:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/asset_db?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.asset.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpllog-impl这个配置建议开发阶段开着,能把SQL打到控制台,调试Mapper语句很方便。上线之前再关掉,不然日志文件增长很快。
2.3 数据库设计:资产表、流转表与用户表
表结构是整个系统的地基,地基建歪了,后面全得返工。我按三大块设计:系统权限相关、资产核心相关、流程记录相关。
用户相关三张表:用户表、角色表、用户角色关联表。我直接做RBAC(基于角色的访问控制),不做更复杂的权限模型,够用就行。角色就三种:系统管理员、资产管理员、普通教职工。用户表字段大概是user_id、username、password、real_name、dept_id、phone、status。
部门表是单独一张,因为高校的组织架构是树形的,学院下面有系、系下面有教研室。用parent_id做树的递归结构。
资产的核心是资产信息表,这是全系统字段最多的表:
CREATE TABLE `asset_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `asset_code` varchar(50) NOT NULL COMMENT '资产编号', `asset_name` varchar(200) NOT NULL COMMENT '资产名称', `category_id` bigint DEFAULT NULL COMMENT '资产分类ID', `specification` varchar(200) DEFAULT NULL COMMENT '规格型号', `sn` varchar(100) DEFAULT NULL COMMENT '序列号', `price` decimal(12,2) DEFAULT NULL COMMENT '资产原值', `purchase_date` date DEFAULT NULL COMMENT '购置日期', `dept_id` bigint DEFAULT NULL COMMENT '使用部门', `location` varchar(200) DEFAULT NULL COMMENT '存放地点', `keeper_id` bigint DEFAULT NULL COMMENT '资产责任人', `status` tinyint DEFAULT '1' COMMENT '1在库 2领用 3维修 4报废 5调拨', `source_type` varchar(20) DEFAULT NULL COMMENT '来源:采购/捐赠/自建', `remark` varchar(500) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_asset_code` (`asset_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;资产编号一定要唯一,这相当于资产的身份ID,后续所有流程都通过asset_code关联。我用的是"年份+分类代码+流水号"的规则,比如2024-DQ-000123。
资产变动记录表是另一张核心表,用来记录每一次状态的变更。这个表是个"流水账",只追加不修改,每一条记录就是资产的一份履历:
CREATE TABLE `asset_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `asset_id` bigint NOT NULL, `record_type` varchar(20) NOT NULL COMMENT '类型:IN入库/OUT领用/RETURN归还/REPAIR维修/SCRAP报废/TRANSFER调拨', `operator_id` bigint DEFAULT NULL COMMENT '操作人', `target_dept_id` bigint DEFAULT NULL COMMENT '目标部门', `target_user_id` bigint DEFAULT NULL COMMENT '领用人', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_asset_id` (`asset_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;流程表我做了申请单和审批记录,领用、维修、报废各是一张申请表加一张审批记录表。当然也可以做一张通用流程表,加个流程类型字段区分,两种都行。我偏向拆成多张表,因为不同流程的字段差异还是很大的,领用要填用途和使用期限,报废要填残值和报废原因,混在一起写起来很别扭。
3. 核心功能实现与实操细节:从登录到资产全流程
3.1 登录认证:JWT无状态方案
登录这块我选的JWT方案,没有用Session,原因就是前后端分离项目,接口和页面通常不在同一个域名下,Session的跨域处理很麻烦。JWT是Token放请求头里,服务器不需要存会话状态,天然支持跨域。
具体实现是这样的:用户登录时,后端校验账号密码,通过后生成一个JWT,把userId、username、角色信息放进去,签名放到Token里,返回给前端。前端把Token存在localStorage里,每次请求在Axios拦截器里带上Authorization: Bearer xxx。后端用一个拦截器,放行登录接口和静态资源,其他接口统一校验Token。
public class JwtUtil { private static final String SECRET = "your-secret-key-change-in-production"; public static String generateToken(User user) { Algorithm algorithm = Algorithm.HMAC256(SECRET); return JWT.create() .withClaim("userId", user.getId()) .withClaim("username", user.getUsername()) .withClaim("role", user.getRoleCode()) .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 8)) .sign(algorithm); } public static DecodedJWT verify(String token) { return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token); } }Token有效期我设8小时,高校教职工一天工作时间差不多这个时长,到期重新登录。如果要求更严,可以在有效期上做"续期"机制,但业务系统没必要弄这么复杂。
权限控制这块,我用的不是Spring Security,那套东西配置起来太重了,学习成本高。自己写拦截器加自定义注解就够了:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RequirePermission { String value(); }配合拦截器在请求进来时解析JWT里的角色,然后判断当前用户是否有权限访问该接口。系统管理员全部放行,资产管理员能操作资产管理接口,普通用户只能操作自己的申请和查询。
注意:JWT的密钥写死在代码里风险很高,生产环境务必改成配置项或者环境变量。另外务必要做Token过期校验,JWT本身是无状态的,丢失了无法撤销。如果你的安全等级要求高,可以考虑Redis存Token黑名单,但一般高校内部系统没必要。
3.2 资产台账:查询列表与Excel导入导出
台账模块是系统的门面,用户打开系统第一个界面就是它。这个模块做得好不好,直接决定用户愿不愿意用。我的经验是:列表页的查询条件要多、要细,默认排序要合理,表格列要可以自定义显示,导入导出功能必须有。
列表查询我用的MyBatis动态SQL,查询条件封装到一个Query对象里。前端传什么条件就拼接什么SQL:
<select id="selectAssetList" resultType="com.example.asset.entity.AssetInfo"> SELECT a.*, d.dept_name, c.category_name, u.real_name AS keeper_name FROM asset_info a LEFT JOIN sys_dept d ON a.dept_id = d.id LEFT JOIN asset_category c ON a.category_id = c.id LEFT JOIN sys_user u ON a.keeper_id = u.id <where> <if test="query.assetName != null and query.assetName != ''"> AND a.asset_name LIKE CONCAT('%', #{query.assetName}, '%') </if> <if test="query.deptId != null"> AND a.dept_id = #{query.deptId} </if> <if test="query.categoryId != null"> AND a.category_id = #{query.categoryId} </if> <if test="query.status != null"> AND a.status = #{query.status} </if> <if test="query.priceMin != null"> AND a.price >= #{query.priceMin} </if> <if test="query.priceMax != null"> AND a.price <= #{query.priceMax} </if> </where> ORDER BY a.create_time DESC </select>分页用的PageHelper插件,这个插件把分页逻辑封装得非常好,调用PageHelper.startPage(pageNum, pageSize),紧接着的查询自动加LIMIT,返回的List可以被PageInfo包装,拿到总数和页码信息:
public PageResult<AssetInfoVO> pageAssetList(AssetQuery query, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<AssetInfoVO> assetList = assetMapper.selectAssetList(query); PageInfo<AssetInfoVO> pageInfo = new PageInfo<>(assetList); return PageResult.of(pageInfo.getList(), pageInfo.getTotal()); }Excel导入导出,我用的阿里EasyExcel,比Apache POI好用得多。EasyExcel的API简单,内存占用还小。导入就是定义个实体类,加@ExcelProperty("资产名称")注解,然后一行代码读取:
List<AssetImportDTO> list = EasyExcel.read(inputStream) .head(AssetImportDTO.class) .sheet() .doReadSync();然后逐条在校验后插入。导出就是查全量数据,用EasyExcel写出去,设置响应头的ContentType为Excel格式,前端浏览器自动下载。
这里要注意几个坑:
- 导入模板的字段校验必须做,比如资产编号重复、价格字段不能为负。我会先整批校验,有错误就汇总返回,不逐条报错,不然用户得改十几次
- 导入Excel时金额类字段最好用字符串接收再转BigDecimal,避免科学计数法的问题
- 导出几万条数据时,EasyExcel的
write是流式的,不会OOM,但注意设置响应头的文件名要做URL编码,否则中文名会乱码
3.3 领用归还与报废:流程审批的完整闭环
资产领用这个场景,我举个例子:某学院老师要借一台投影仪,他登录系统,填一张领用申请单,选择资产编号(或者直接搜到这台设备),填写领用事由和预计归还日期,提交。流程流转到该学院资产管理员待办列表,管理员审核信息后点击通过,系统随即把资产状态改为"领用",同时写一条领用记录到asset_record表。老师那边界面就能看到"审核通过"。
关键点在于:资产状态变更和流程记录要在一个事务里完成。我用的方式是Service层加@Transactional注解,先更新资产状态,再插入记录,任何一个失败就回滚,保证数据一致。这是整个系统最核心的事务要求,不能马虎。
数据库层面,我是怎么设计状态流转的?资产状态是冗余在asset_info.status字段上的,不靠流程表去算。这样查询列表时不用关联流程表就能拿到状态,性能好很多。但代价就是状态可能不一致,比如流程走到了报废,但资产状态没更新。所以代码上必须严格封装状态变更的逻辑,不允许直接改status字段。我直接在Mapper层屏蔽了直接更新status的SQL,业务只能通过AssetService.changeStatus()这个入口去改,保证一致性。
维修流程会复杂一点。资产报修后,如果确定修不好走报废,报废申请要填报废原因、残值、证明人。报废审批这个流程我设计了二级审批:院系资产管理员初审,资产处终审。二级审批需要在申请表上记录first_approval_id、first_approval_time、second_approval_id、second_approval_time这几个字段,审批状态机是"待初审→待终审→通过/驳回"。
3.4 权限管理:RBAC模型的落地
RBAC方案落地起来很简单,三张表:sys_user、sys_role、sys_user_role,再加一个sys_menu和sys_role_menu做菜单权限。用户登录后,后端查出他有哪些角色,每个角色对应哪些菜单和接口权限,返回一个权限标识列表。
前端侧,路由加载那块做了一个动态路由的逻辑。没有用静态路由配置,而是登录后请求后端菜单接口,拿到当前用户可见的菜单,动态addRoute到Vue Router里。这样用户刷到系统首页,菜单就是根据权限渲染的。
菜单表是关键字段是path、component、parent_id、visible、sort。Vue3里component字段存的是组件路径字符串,动态加载时用import.meta.glob批量注册和映射。
const modules = import.meta.glob('../../views/**/*.vue') function loadView(viewPath) { return modules[`../../views/${viewPath}.vue`] }这套玩法要注意:生产环境构建时Vite会把所有匹配的组件都打进包里,因为import.meta.glob是静态分析的,会导致打包体积变大。我处理的办法是菜单组件按目录拆,尽量精简,别把不相关的页面大量塞进来。
3.5 报表统计:按分类、部门、状态聚合
报表统计模块做的是一张仪表盘加三张统计表。仪表盘显示资产总数量、总原值、本月新增、待处理审批这些数字,加一个状态分布饼图。图表用的ECharts,Vue3下配合vue-echarts组件,配置熟悉的同学上手很快。
聚合统计的SQL是正则的GROUP BY,我把几个核心统计做了成Mapper方法:
<!-- 按资产分类统计数量和原值 --> SELECT c.category_name, COUNT(a.id) AS asset_count, SUM(a.price) AS total_price FROM asset_info a LEFT JOIN asset_category c ON a.category_id = c.id GROUP BY a.category_id, c.category_name ORDER BY total_price DESC这里有个实际经验:统计SQL里对大表做GROUP BY,索引没建对会很慢。asset_info的category_id、dept_id、status这几个常用过滤和分组字段,我建了联合索引idx_category_dept_status(category_id, dept_id, status)。这个索引能同时支持按分类统计和按部门统计,效果不错。
4. 前后端联调与常见问题:实战中踩过的坑
4.1 跨域与Axios封装
前后端分离第一个拦路虎就是跨域。后端我在配置类里写了一个CorsFilter,指定允许的来源、方法和请求头。开发阶段用的是前后端的代理,在vite.config.js里配置proxy,把/api开头的请求转发到后端8080端口,这样前端访问走的是同源路径,不触发跨域。生产环境用Nginx做反向代理,前端静态资源和后端API同一个域名,同样不存在跨域问题。
// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })Axios封装这块我做了三层:统一请求函数、请求响应拦截器、错误处理。拦截器里自动加Token,响应码401自动跳登录页,业务错误码统一弹Message提示。这套封装所有页面公用,后面加新模块基本不用改。
4.2 MyBatis动态SQL与多表联查的坑
我踩过最大的坑,是分页插件跟多表LEFT JOIN联查的SQL一起用时,PageHelper自动生成的COUNT查询可能会出错。比如我的列表SQL里如果包含GROUP BY或者DISTINCT,PageHelper的自动count一般没问题,但如果子查询里面用了limit,pageHelper解析count就不准了。
解决的办法是手动写count查询:
<select id="countAssetList" resultType="long"> SELECT COUNT(1) FROM asset_info a <where> <!-- 和主查询同样的条件 --> </where> </select>然后代码里不依赖PageHelper自动count:
long total = assetMapper.countAssetList(query);还有一个联查的坑:字段名冲突。资产表有id,用户表有id,直接SELECT a.*, u.id AS user_id, u.real_name才不会把两个id搞混。所以我联查的SQL一律给每个非主表的字段起别名,避免映射进Entity的id属性时值错乱。
4.3 前端组件复用与状态管理
Vue3写管理后台,我强烈建议把资产选择器、部门树选择器、状态标签这些高频组件抽出来。资产选择器我用的是一个弹窗式组件,支持搜索和分页选择,所有需要选资产的表单(领用申请、维修申请、调拨申请)都能复用。部门树选择器用的是Element Plus的el-tree,封装成组件后,在表格筛选和表单里都能用。
状态管理这个项目用的Pinia。虽然大部分操作是接口请求,但有几个全局数据需要共享:用户信息、权限列表、菜单列表、部门树。这些在一旦登录后加载一次存Pinia里,页面之间切换不需要重复请求。
4.4 常见问题速查表
| 问题 | 现象 | 排查思路 |
|---|---|---|
| 跨域报错 | 浏览器控制台CORS错误 | 先确认代理配置,再有后端CorsFilter允许的来源是否匹配 |
| Token失效 | 请求返回401 | 检查JWT过期时间,检查前端是否在拦截器中清Token并跳登录页 |
| 分页数据不对 | 总量与实际不符 | 检查count查询是否独立,检查PageHelper startPage后是否紧接着就是查询 |
| 中文乱码 | 页面或Excel出现问号 | 数据库连接串加characterEncoding=utf8mb4,Excel模板用模板字符串 |
| 导入失败 | 数据未入库 | 看字段校验错误汇总,检查Excel表头与注解字段是否一致 |
| 生成报表慢 | 统计接口响应超时 | 看执行计划,建组合索引,避免大表全扫 |
| Mapper报错 | XML方法找不到 | 确认namespace和id是否匹配,XML文件是否在配置的mapper-locations路径下 |
5. 源码结构设计与二次开发建议
5.1 后端分层与包结构
这套系统的后端包结构,我按标准的分层架构整理:
com.example.asset ├── controller ├── service │ └── impl ├── mapper ├── entity / dto / vo ├── config ├── common │ ├── result │ ├── exception │ └── utils └── annotationController层只做参数接收、鉴权注解、返回结果封装。Service层做业务逻辑和事务控制。Mapper层只写SQL。Entity对应表结构,DTO做接收参数,VO做返回封装。前端传来的参数我不会直接用Entity接收,因为Query对象字段和Entity字段经常不一致,混在一起容易出错。
返回结果统一用Result对象包装:
public class Result<T> { private Integer code; private String message; private T data; }code为200表示成功,401表示未登录,500表示服务器异常。前端拦截器统一处理。
5.2 可复用的设计模式
梳理这套源码,我认为最值得参考的设计模式有这三个:
一个是事务模板模式。所有状态变更操作(领用、归还、报废、调拨)都走同一个AssetStateService.changeState()方法,内部统一加事务、写资产记录表、更新资产状态。业务代码里不散落事务注解,要改状态维护逻辑只改这一个入口就行。
第二个是消息提醒机制。资产审批通过或驳回时,系统向申请人发送站内消息。我用了一张消息表存待办通知,用户在首页看到红点提醒。这套机制用观察者模式实现,审批完成后发布一条消息事件,MessageReceiver异步写库。异步用的是Spring的@Async,没上消息队列,这种场景用不着。
第三个是字典管理。资产分类、状态、来源这些字段如果直接硬编码,后面改起来很痛苦。我做了字典表,前端用字典接口获取数据源渲染下拉框和标签。新增资产分类时,前端不需要改代码,字典表里加一行就行。
5.3 后续功能扩展方向
这套系统做完了基础版之后,有几块可以继续扩展:
第一是条码/二维码的应用。每个资产生成二维码标签,打印出来贴到设备上。盘点时用手机扫码枪或者App扫码,直接带出资产信息并标记"已盘到"。这个扩展的性价比特别高,能直接缩短盘点周期,是我最推荐做的二期功能。
第二是资产折旧计算。高校资产年报对于固定资产折旧有要求,目前系统里只有原值没有净值和月折旧额。可以扩展财务逻辑,设置折旧规则后定期跑批,生成折旧明细表。
第三是与采购系统的对接。资产入库数据如果是从采购审批流流转过来的,可以省去手工录入的工作,还能保证账实一致性。针对第三方系统的对接,后端预留了接口层,用开放接口的方式让别人调用。
写在最后的几点体会
这套系统做完,我自己最深的感触是:固定资产管理系统业务逻辑本身不复杂,难的是把规则理清楚、和用户对齐预期。我一开始把统计报表做得特别复杂,图表套图表,后来实际使用中大家看得最多还是那几张基础表和待办任务。做这类管理系统,用户要的是"顺手"和"不出错",而不是花哨。
再分享一个小技巧:开发阶段多用真实数据测试。我拿某高校批量导入了近一年的资产数据做测试,才发现多表联查的SQL慢得离谱,优化之后快了几十倍。如果你拿假数据测试,往往发现不了性能问题。
另外一个经验是,MyBatis的Mapper XML里SQL语句一定要写规范,格式化好。代码评审的时候,大部分人不会逐行读Java代码,但SQL基本都会仔细看,因为查询逻辑就是业务逻辑,写得好不好直接决定系统后期好不好维护。
如果你也要做类似项目,建议先把数据库表结构设计好、把状态机流转图理清楚再动手写代码。表结构定了,后面就是往里面填业务逻辑的事。