ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot+MySQL+Vue实战:构建企业内部生产力市场平台

Spring Boot+MySQL+Vue实战:构建企业内部生产力市场平台

团队里每天都上演着类似的对话:运营同学问“有没有一键生成活动海报的工具”,后端说“脚本在我电脑上,你等下发你”,测试说“这个造数工具上次是张三写的,代码不知道在哪个仓库”,新来的实习生则默默用 Excel 手工折腾了一整天。这些零散、重复、口口相传的内部工具,其实都在指向同一个问题——缺少一个统一的“生产力市场”。

本文不会去谈多么宏大的商业概念,而是从后端开发和工程落地的角度,把“生产力市场”拆解成一个可以真正落地实现的企业内部应用与工具分发平台。我们会围绕 Spring Boot + MySQL + Vue 这套常见技术栈,梳理需求、设计数据库、实现核心接口、处理权限和审计,并给出从零到一的实战代码。不管你是准备在公司内部搭建一个工具平台,还是想通过一个综合项目练习全栈开发能力,这篇内容都值得仔细看一遍。

1. 什么是“生产力市场”

1.1 概念与定位

“生产力市场”并不是一个严格意义上的商业术语,更多是对一类平台化产品形态的概括:把一个组织内部散落的脚本、工具、API、低代码组件、自动化流程统一收拢起来,以类似“应用商店”的方式提供检索、申请、获取、使用和评价的能力。

理解这个概念,只需要抓住三个关键词:

  • 统一入口:所有内部工具都有固定的访问路径,不再依赖同事之间的口头传播。
  • 标准化发布:工具作者按规范提交说明、版本、依赖信息和运行方式,而不是扔一个压缩包在群里。
  • 可管控消费:使用方申请工具需要经过审批,下载和调用行为有记录,敏感工具可以限定使用者范围。

从技术角度看,它本质上是一个“私有内部的软件资产管理平台”。但与传统的 CMDB(配置管理数据库)不同,它更强调工具的使用体验和生命周期管理,而不是单纯记录资产清单。

1.2 为什么要做这样一个平台

很多团队一开始都觉得“自己做一个小工具列表就行了”,但实际使用两三个月后,问题会逐渐暴露出来。

第一个问题是工具的可发现性差。工具一旦超过二十个,靠 README 文档或共享文件夹根本无法维护,用户不知道有什么工具可用,也不知道该问谁。

第二个问题是使用方式不统一。有人提供命令行脚本,有人提供 Web 页面,有人提供 REST API,使用者需要花费大量时间理解不同的使用习惯。

第三个问题是权限和审计缺失。内部工具往往涉及业务数据、服务器操作或批量变更,如果不控制授权范围,一旦被误用或滥用,风险非常高。而这些风险在“发一个脚本文件”的模式里是完全不可控的。

第四个问题是重复建设。因为没有统一的登记机制,A 团队可能花了三天写了一个 B 团队已经写过的导出工具,资源浪费非常明显。

所以说,“生产力市场”真正要解决的不仅仅是技术问题,更是组织协作和效能管理问题。

1.3 它与普通软件系统的区别

理解“生产力市场”,还需要把它和普通业务系统区分开。

普通业务系统是“为用户提供固定功能”,例如 OA 系统提供审批流、财务系统提供报销单,用户是功能的被动使用者。而“生产力市场”是“为工具的供给方和消费方搭建连接”,它的用户同时包含工具开发者和工具使用者,平台本身并不直接产生生产力,而是通过促进工具流通来放大生产力。

这种定位决定了它在设计上的特殊性:既要有内容管理(工具信息、版本、文档),也要有交易式流程(申请、审批、授权、回收),还要有运营侧的数据分析(哪些工具最热、哪些工具长期无人使用)。

2. 需求分析与功能模块设计

2.1 用户角色与核心痛点

先明确平台参与的角色,这是需求分析的起点。

角色核心诉求痛点
工具消费者快速找到可用工具并顺利使用不知道有什么工具、申请流程复杂、文档不清楚
工具提供者发布工具并触达目标用户发布流程繁琐、无法统计使用量、没人反馈问题
审批管理员控制工具可见性和使用范围缺乏审计记录、无法批量处理申请、难以回收权限
平台运维保障平台稳定和数据安全审核链路长、存储混乱、敏感工具存在泄露风险

2.2 核心功能模块划分

一个可落地的 MVP(最小可行产品)版本,建议把功能控制在以下范围:

  • 工具管理:工具的分类、创建、编辑、上下架、版本管理。
  • 分类与检索:按分类浏览、按关键词搜索、按标签筛选。
  • 申请与审批:用户申请工具使用权限,管理员审批,审批通过后自动开通访问权限。
  • 下载与调用:支持下载离线包、复制安装命令、获取 API 地址。
  • 操作审计:记录申请、审批、下载、调用等关键行为。
  • 评价反馈:允许用户对工具评分和提交问题反馈。

在第一个版本里,不建议一上来就做计费、积分、推荐算法这类偏运营的功能。先把“发布—检索—申请—审批—使用”这条主链路跑通,后面再逐步丰富。

2.3 核心业务状态机

工具本身有状态,申请单也有状态,这两条状态流需要先想清楚。

工具状态流转:

草稿 -> 待审核 -> 已发布 -> 已下架 ↘ 审核驳回

申请单状态流转:

待审批 -> 已通过 -> 已生效 -> 已回收 ↘ 已驳回

在代码和数据库设计中,建议使用状态字段加状态变更时间的方式进行记录,方便追溯历史。

3. 技术选型与项目结构

3.1 技术栈选择

“生产力市场”对技术栈没有特殊要求,完全可以采用团队最熟悉的一套。本文以常见的企业级组合为例:

  • 后端:Java 8 / Java 17 + Spring Boot
  • 数据库:MySQL 8.x
  • 缓存:Redis(用于热点数据缓存和接口限流,非必需)
  • 前端:Vue 3 + Element Plus
  • 构建工具:Maven
  • 接口文档:Spring Doc OpenAPI(可选)

版本说明:本文示例以 Spring Boot 2.7.x / 3.x 常见的依赖坐标为例,实际开发中请根据你们公司的脚手架版本统一调整,不要强行对齐。关键是理解实现思路,而不是照抄版本号。

3.2 项目目录结构

后端建议按模块分包,避免把所有类都塞进 controller 和 service 两层。

com.company.pmarket ├── PmarketApplication.java ├── common │ ├── Result.java │ ├── PageResult.java │ └── exception ├── config │ ├── WebMvcConfig.java │ └── MybatisPlusConfig.java ├── controller │ ├── ToolController.java │ ├── ApplyController.java │ └── AuditController.java ├── service │ ├── ToolService.java │ ├── ToolServiceImpl.java │ └── ApplyService.java ├── mapper │ ├── ToolMapper.java │ └── ApplyRecordMapper.java ├── entity │ ├── Tool.java │ └── ApplyRecord.java └── dto ├── ToolCreateDTO.java └── ApplyCreateDTO.java

上面的结构参考了常见的分层架构,但没必要拘泥于这种划分,根据团队规范调整即可。

4. 数据库设计与核心表结构

4.1 设计思路

数据库设计是本项目最核心的部分之一。我的建议是:不要先写代码,先建模。

建模需要注意几个关键点:

  • 工具信息与工具版本分离,因为一个工具可能对应多个版本。
  • 申请记录独立建表,方便审计和统计分析。
  • 用户与工具的授权关系通过申请单派生,不额外建“用户工具绑定表”,因为这个关系本质上是申请审批后的结果。

4.2 核心表 DDL

下面给出主要表的 MySQL DDL。

-- 用户表 CREATE TABLE `p_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(64) NOT NULL COMMENT '登录名', `password` varchar(128) NOT NULL COMMENT '加密密码', `display_name` varchar(64) DEFAULT NULL COMMENT '显示名', `email` varchar(128) DEFAULT NULL, `role` varchar(32) NOT NULL DEFAULT 'USER' COMMENT '角色:ADMIN/AUDITOR/USER', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 工具分类表 CREATE TABLE `p_category` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(64) NOT NULL COMMENT '分类名称', `sort_order` int NOT NULL DEFAULT 0, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工具分类表'; -- 工具主表 CREATE TABLE `p_tool` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL COMMENT '工具名称', `description` varchar(1024) DEFAULT NULL COMMENT '工具描述', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `owner_id` bigint NOT NULL COMMENT '工具负责人', `status` varchar(32) NOT NULL DEFAULT 'DRAFT' COMMENT '状态:DRAFT/PENDING/PUBLISHED/REJECTED/OFFLINE', `usage_type` varchar(32) NOT NULL DEFAULT 'DOWNLOAD' COMMENT '使用方式:DOWNLOAD/API/COMMAND', `access_level` varchar(32) NOT NULL DEFAULT 'PUBLIC' COMMENT '可见范围:PUBLIC/RESTRICTED', `download_url` varchar(512) DEFAULT NULL COMMENT '下载地址', `api_endpoint` varchar(256) DEFAULT NULL COMMENT 'API地址', `doc_url` varchar(512) DEFAULT NULL, `view_count` bigint NOT NULL DEFAULT 0, `apply_count` bigint NOT NULL DEFAULT 0, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工具主表'; -- 工具版本表 CREATE TABLE `p_tool_version` ( `id` bigint NOT NULL AUTO_INCREMENT, `tool_id` bigint NOT NULL, `version` varchar(32) NOT NULL, `release_note` varchar(512) DEFAULT NULL, `file_path` varchar(512) DEFAULT NULL COMMENT '文件存储路径', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_tool` (`tool_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工具版本表'; -- 申请审批表 CREATE TABLE `p_apply_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `tool_id` bigint NOT NULL COMMENT '工具ID', `user_id` bigint NOT NULL COMMENT '申请人ID', `apply_type` varchar(32) NOT NULL DEFAULT 'USE' COMMENT '申请类型:USE/ADMIN', `status` varchar(32) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING/APPROVED/REJECTED/RECYCLED', `apply_reason` varchar(512) DEFAULT NULL, `auditor_id` bigint DEFAULT NULL COMMENT '审批人ID', `audit_comment` varchar(512) DEFAULT NULL, `apply_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `audit_time` datetime DEFAULT NULL, `expire_time` datetime DEFAULT NULL COMMENT '授权过期时间', PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_tool` (`tool_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='申请审批表'; -- 操作日志表 CREATE TABLE `p_audit_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT NULL, `action` varchar(64) NOT NULL COMMENT '操作类型', `target_type` varchar(64) DEFAULT NULL, `target_id` bigint DEFAULT NULL, `detail` varchar(1024) DEFAULT NULL, `ip` varchar(64) DEFAULT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_target` (`target_type`, `target_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';

注意:上述表结构是简化版本,生产环境还需要补充索引、约束、软删除字段(如deleted)、字段注释等细节。比如p_tool中的description字段,建议设置更长一些,避免描述过多被截断。

5. 后端核心接口实现

5.1 统一响应结构与公共类

写接口之前,先定义一个统一响应结构,方便前端处理。

// 文件路径:src/main/java/com/company/pmarket/common/Result.java package com.company.pmarket.common; public class Result<T> { private int code; private String message; private T data; public Result(int code, String message, T data) { this.code = code; this.message = message; this.data = data; } public static <T> Result<T> ok(T data) { return new Result<>(200, "success", data); } public static <T> Result<T> fail(int code, String message) { return new Result<>(code, message, null); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } }

5.2 工具发布接口

工具创建和发布是平台提供给“工具提供者”的核心入口。这里只展示核心的 Service 实现逻辑,完整代码需要结合你的持久层框架。

// 文件路径:src/main/java/com/company/pmarket/service/ToolServiceImpl.java package com.company.pmarket.service; import com.company.pmarket.entity.Tool; import com.company.pmarket.mapper.ToolMapper; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.LocalDateTime; @Service public class ToolServiceImpl implements ToolService { @Resource private ToolMapper toolMapper; @Override public Long createTool(Tool tool) { // 1. 补充默认字段 tool.setStatus("DRAFT"); tool.setViewCount(0); tool.setApplyCount(0); tool.setCreateTime(LocalDateTime.now()); tool.setUpdateTime(LocalDateTime.now()); // 2. 校验工具名称和负责人 if (tool.getName() == null || tool.getName().trim().isEmpty()) { throw new IllegalArgumentException("工具名称不能为空"); } if (tool.getOwnerId() == null) { throw new IllegalArgumentException("工具负责人不能为空"); } // 3. 保存 toolMapper.insert(tool); return tool.getId(); } @Override public void submitAudit(Long toolId, Long operatorId) { Tool tool = toolMapper.selectById(toolId); if (tool == null) { throw new IllegalArgumentException("工具不存在"); } // 只有工具负责人或管理员才能提交审核 if (!tool.getOwnerId().equals(operatorId)) { throw new SecurityException("只有工具负责人可以提交审核"); } Tool update = new Tool(); update.setId(toolId); update.setStatus("PENDING"); update.setUpdateTime(LocalDateTime.now()); toolMapper.updateById(update); } }

这个类演示了两件事:创建工具时初始化状态;提交审核时校验操作人身份。很多内部系统容易省略“当前登录用户是谁”的校验,这是不够严谨的,后面在最佳实践部分我会展开说。

5.3 工具检索与详情

检索接口是平台最常见的高频接口,建议做好分页和参数校验。

// 文件路径:src/main/java/com/company/pmarket/controller/ToolController.java package com.company.pmarket.controller; import com.company.pmarket.common.PageResult; import com.company.pmarket.common.Result; import com.company.pmarket.entity.Tool; import com.company.pmarket.service.ToolService; 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 javax.annotation.Resource; @RestController @RequestMapping("/api/tools") public class ToolController { @Resource private ToolService toolService; /** * 分页查询已发布的工具 */ @GetMapping public Result<PageResult<Tool>> page( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId) { // 简化逻辑,真实项目请注入 Page PageResult<Tool> result = toolService.pagePublishedTools(page, size, keyword, categoryId); return Result.ok(result); } /** * 获取工具详情,同时增加浏览量 */ @GetMapping("/{id}") public Result<Tool> detail(@PathVariable Long id) { Tool tool = toolService.getDetail(id); return Result.ok(tool); } }

分页查询的 SQL 可以这样写,注意只查询已发布的工具,避免用户看到未审核的内容。

<!-- 文件路径:src/main/resources/mapper/ToolMapper.xml --> <select id="selectPublishedPage" resultType="com.company.pmarket.entity.Tool"> SELECT id, name, description, category_id, owner_id, status, usage_type, access_level, download_url, api_endpoint, doc_url, view_count, apply_count, create_time, update_time FROM p_tool WHERE status = 'PUBLISHED' <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> ORDER BY create_time DESC LIMIT #{offset}, #{size} </select>

5.4 申请与审批接口

申请和审批是平台权限管控的关键链路。

// 文件路径:src/main/java/com/company/pmarket/service/ApplyService.java package com.company.pmarket.service; public interface ApplyService { /** * 用户申请使用工具 */ Long applyUseTool(Long userId, Long toolId, String reason); /** * 管理员审批 */ void audit(Long applyId, Long auditorId, boolean approved, String comment); }

实现类核心逻辑:

// 文件路径:src/main/java/com/company/pmarket/service/ApplyServiceImpl.java package com.company.pmarket.service; import com.company.pmarket.entity.ApplyRecord; import com.company.pmarket.entity.Tool; import com.company.pmarket.mapper.ApplyRecordMapper; import com.company.pmarket.mapper.ToolMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; @Service public class ApplyServiceImpl implements ApplyService { @Resource private ApplyRecordMapper applyRecordMapper; @Resource private ToolMapper toolMapper; @Override public Long applyUseTool(Long userId, Long toolId, String reason) { Tool tool = toolMapper.selectById(toolId); if (tool == null) { throw new IllegalArgumentException("工具不存在"); } if (!"PUBLISHED".equals(tool.getStatus())) { throw new IllegalStateException("工具未发布,无法申请"); } // 幂等校验:同一个用户同一工具只能存在一条待审批申请 int pendingCount = applyRecordMapper.countPending(userId, toolId); if (pendingCount > 0) { throw new IllegalStateException("该工具已有待审批的申请,请勿重复提交"); } ApplyRecord record = new ApplyRecord(); record.setToolId(toolId); record.setUserId(userId); record.setApplyReason(reason); record.setStatus("PENDING"); record.setApplyTime(LocalDateTime.now()); applyRecordMapper.insert(record); // 工具申请量加一 toolMapper.increaseApplyCount(toolId); return record.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void audit(Long applyId, Long auditorId, boolean approved, String comment) { ApplyRecord record = applyRecordMapper.selectById(applyId); if (record == null) { throw new IllegalArgumentException("申请记录不存在"); } if (!"PENDING".equals(record.getStatus())) { throw new IllegalStateException("该申请已处理,不能重复审批"); } record.setAuditorId(auditorId); record.setAuditComment(comment); record.setAuditTime(LocalDateTime.now()); record.setStatus(approved ? "APPROVED" : "REJECTED"); applyRecordMapper.updateById(record); } }

这里有两个细节值得注意:申请时加了幂等校验,防止用户恶意重复提交;审批操作加了@Transactional,保证状态更新过程中出现异常时可以回滚。审批通过后,如果工具是 API 类型,还可以在事务里调用权限系统为用户开通调用凭证,这里就不展开细节了。

5.5 下载鉴权中间价

下载接口需要校验当前用户是否已经获得授权。可以用一个简单的注解 + 拦截器实现。

先定义一个注解:

// 文件路径:src/main/java/com/company/pmarket/common/annotation/RequireToolAuth.java package com.company.pmarket.common.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireToolAuth { /** * 工具ID参数名,默认从请求参数中获取 */ String toolIdParam() default "toolId"; }

然后在拦截器中校验:

// 文件路径:src/main/java/com/company/pmarket/config/ToolAuthInterceptor.java package com.company.pmarket.config; import com.company.pmarket.service.ApplyService; import org.springframework.web.servlet.HandlerInterceptor; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class ToolAuthInterceptor implements HandlerInterceptor { @Resource private ApplyService applyService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求中解析 toolId String toolId = request.getParameter("toolId"); if (toolId == null) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return false; } Long userId = (Long) request.getAttribute("userId"); if (userId == null) { // 真实项目里从 Token 或 Session 中解析 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } boolean hasAuth = applyService.checkUserToolPermission(userId, Long.valueOf(toolId)); if (!hasAuth) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return false; } return true; } }

这个拦截器只是演示思路,真实项目中还需要结合用户认证框架统一处理。

6. 前端展示与联调

6.1 前端页面设计

前端部分可以用 Vue 快速实现两个核心页面:工具列表页和工具详情页。

这里给一个工具列表页的核心代码片段,只展示模板部分:

<!-- 文件路径:src/views/ToolList.vue --> <template> <div class="tool-list"> <el-card shadow="never"> <el-form :inline="true"> <el-form-item label="关键词"> <el-input v-model="query.keyword" placeholder="工具名称或描述" clearable /> </el-form-item> <el-form-item label="分类"> <el-select v-model="query.categoryId" clearable placeholder="全部分类"> <el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="loadTools">查询</el-button> </el-form-item> </el-form> <el-table :data="tools" v-loading="loading" border> <el-table-column prop="name" label="工具名称" min-width="140" /> <el-table-column prop="description" label="描述" min-width="220" show-overflow-tooltip /> <el-table-column prop="usageType" label="使用方式" width="110" /> <el-table-column prop="viewCount" label="浏览量" width="90" /> <el-table-column prop="applyCount" label="申请量" width="90" /> <el-table-column label="操作" width="120" fixed="right"> <template #default="{ row }"> <el-button type="primary" link @click="goDetail(row.id)">查看</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" layout="total, prev, pager, next" @current-change="loadTools" /> </el-card> </div> </template>

对应的脚本部分:

<script setup> import { reactive, ref, onMounted } from 'vue' import { getTools } from '@/api/tool' import { useRouter } from 'vue-router' const router = useRouter() const loading = ref(false) const tools = ref([]) const total = ref(0) const categories = ref([]) const query = reactive({ page: 1, size: 10, keyword: '', categoryId: undefined }) const loadTools = async () => { loading.value = true try { const data = await getTools(query) tools.value = data.records total.value = data.total } finally { loading.value = false } } const goDetail = (id) => { router.push(`/tools/${id}`) } onMounted(loadTools) </script>

6.2 前后端接口约定的注意事项

前后端联调时最容易出问题的不是业务逻辑,而是接口约定不清晰。建议在项目初期就约定好几个规范。

第一,分页参数统一使用page和size,响应体中的分页字段统一为total、records,不要一会儿pageNum一会儿currentPage。

第二,所有接口返回Result包装结构,前端在 axios 拦截器里统一处理后端返回的code,避免每个页面重复写错误弹出逻辑。

第三,涉及时间字段时,明确是返回时间戳还是格式化后的字符串,建议后端返回标准格式字符串,前端不做本地转换。

7. 本地运行与验证

7.1 初始化数据

首次启动前需要创建数据库并执行初始化数据脚本。可以在数据库工具中执行前面的 DDL,然后插入一条测试数据。

INSERT INTO `p_category` (`name`, `sort_order`) VALUES ('数据工具', 1); INSERT INTO `p_category` (`name`, `sort_order`) VALUES ('自动化脚本', 2); INSERT INTO `p_user` (`username`, `password`, `display_name`, `role`) VALUES ('admin', '$2a$10$e0MYzXyjpJSfkqVvL9YZXuM2XyM.7pG9X6yA9JQm2aXe2vF5iRqJW', '管理员', 'ADMIN'); INSERT INTO `p_tool` (`name`, `description`, `category_id`, `owner_id`, `status`, `usage_type`, `access_level`) VALUES ('Excel 批量导出工具', '支持从数据库读取数据并导出为 Excel 文件', 1, 1, 'PUBLISHED', 'DOWNLOAD', 'RESTRICTED');

密码字段的示例哈希是占位内容,实际开发中需要通过BCryptPasswordEncoder生成真实密码。

7.2 启动后端服务

在项目根目录执行:

mvn spring-boot:run

启动成功后,访问接口文档地址,例如:

http://localhost:8080/swagger-ui.html

然后可以用 curl 模拟接口请求:

curl -X GET 'http://localhost:8080/api/tools?page=1&size=10&keyword=Excel'

预期返回结果类似:

{ "code": 200, "message": "success", "data": { "total": 1, "records": [ { "id": 1, "name": "Excel 批量导出工具", "description": "支持从数据库读取数据并导出为 Excel 文件", "status": "PUBLISHED", "usageType": "DOWNLOAD", "accessLevel": "RESTRICTED" } ] } }

7.3 启动前端服务

如果使用 Vue 3 + Vite:

npm install npm run dev

然后访问http://localhost:5173,配置请求代理,把/api转发到后端服务。Vite 的代理配置可以参考:

// 文件路径:vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

8. 常见问题与排查思路

下面整理一些你在搭建“生产力市场”平台时可能会遇到的高频问题。

问题现象常见原因解决思路
工具列表一直加载不出来后端服务未启动或接口路径错误先直接访问后端接口,确认返回正常,再检查前端代理配置
提交申请提示“工具不存在”toolId 传递错误或工具已被删除检查请求参数和前端路由传参,确认数据库记录存在
审批通过后用户仍无法下载下载接口权限校验逻辑未生效检查拦截器是否注册到 WebMvcConfig,确认注解扫描范围正确
同一个工具提交多条申请缺少幂等校验在申请前查询待审批记录,存在则拦截重复提交
工具状态一直是 DRAFT提交审核后接口报错或状态更新失败查看后端日志,确认事务是否回滚,检查状态流转条件
中文乱码数据库连接串未指定编码在 JDBC URL 后增加useUnicode=true&characterEncoding=utf8mb4
分页查询总数不对动态 SQL 中统计语句未跟随查询条件统计 SQL 和列表 SQL 使用相同的 where 条件

如果遇到 “Failed to configure a DataSource” 这类启动错误,优先检查 MySQL 服务是否启动、数据库连接参数是否正确、驱动依赖是否引入。这类问题大多数不是代码逻辑问题,而是环境配置问题。

9. 安全、权限与工程最佳实践

9.1 权限模型建议

对于“生产力市场”这类平台,不要把所有权限逻辑都写死在业务代码里,建议采用“角色 + 资源”的抽象模型。

基础的权限点至少包括:

  • tool:create:创建工具
  • tool:audit:审核工具
  • tool:offline:下架工具
  • apply:create:发起申请
  • apply:audit:审批申请
  • tool:download:下载受限工具

后端在接口入口统一鉴权,而不是在 Service 内部到处判断角色。这样一来新增权限点时,只需要维护权限配置,不需要改业务方法。如果公司内部已经有统一权限中心,也可以通过 API 接入,平台侧只需要保存用户 ID 和申请关系即可。

9.2 操作日志与审计

内部工具市场比普通业务系统更依赖审计能力,原因很简单:工具可能触发批量数据操作,一旦出现问题需要回溯到具体的人和操作。

日志记录建议采用异步方式,避免影响主流程性能。可以监听 Spring 的事件机制,在业务代码里发布操作事件:

// 文件路径:src/main/java/com/company/pmarket/service/ApplyServiceImpl.java applicationEventPublisher.publishEvent( new AuditEvent(this, userId, "APPLY_CREATE", "ToolApply", applyId, "用户申请使用工具") );

然后定义监听器,把日志写入p_audit_log表。这样做的好处是业务代码和日志解耦,日志写入失败不会影响主流程。

实际项目中还应该区分“业务审计日志”和“系统运行日志”。业务审计日志记录谁在什么时间做了什么操作;系统运行日志记录异常堆栈、慢 SQL、接口调用量等。两者用途不同,不要混在一个文件里。

9.3 文件存储与版本管理

工具包文件不建议直接存储在应用服务器的本地磁盘,否则服务部署多实例时会出现文件不一致的问题。建议使用对象存储服务或公司内部的分布式文件服务。

版本管理方面,工具历史版本不能随意删除,至少保留最近三个版本,方便用户回退。下载 URL 建议设置有效期,避免长期有效链接导致文件被爬取或泄露。

9.4 数据与接口安全

平台涉及内部工具分发,以下安全措施是必须考虑的。

第一,所有接口必须经过认证,不能存在“匿名可访问”的接口。Spring Boot 项目可以统一配置拦截器或使用 Spring Security。

第二,下载接口必须做授权校验,对RESTRICTED级别的工具重点保护。校验时不仅判断是否审批通过,还要看授权是否过期。

第三,对申请和审批接口做频率限制,防止批量刷单。Redis 的INCR配合过期时间可以快速实现一个简单限流器。

第四,定期清理长期未使用的授权记录。例如超过 90 天未使用的工具权限,可以自动回收或者通知管理员回收。

9.5 开发规范与可维护性建议

给团队内部平台写代码,和给外部客户写交付项目,在工程要求上是不同的。内部平台往往迭代快,但不能因此牺牲可维护性。

建议从第一天开始就执行以下规范:

  • 所有接口必须有清晰的命名,动词统一,例如POST /api/tools创建工具,POST /api/tools/{id}/submit-audit提交审核。
  • 状态流转必须集中管理,避免到处硬编码字符串。可以定义一个枚举类ToolStatusEnum,统一管理 DRAFT、PENDING、PUBLISHED、REJECTED、OFFLINE。
  • 异常信息要可读,不能把底层 SQL 异常直接返回给前端。全局异常处理器统一转换。
  • 关键业务方法必须写单元测试,尤其是申请、审批、权限校验这类核心链路。
  • 表和字段注释必须完整,团队协作时这一条能节省大量沟通成本。

10. 结语与下一步实践建议

搭建一个“生产力市场”平台,表面看是工具列表加申请审批,实际深入进去后,你会发现难点全在边界情况:一个工具应该由谁负责下架?员工离职后他的工具怎么转移?某个工具突然被大量申请意味着什么?审批通过了但使用者发现文档过期怎么办?这些问题没有标准答案,但一个好的平台设计至少应该为这些问题预留扩展空间。

如果你是从零开始,我建议采用最小闭环的方式推进:

第一周先完成工具登记和检索功能,让团队能把现有工具录进来。第二周加上申请审批流程,指定一个管理员负责审核。第三周接入下载鉴权和操作日志,让每一次使用都有迹可循。之后再根据团队反馈迭代评价、推荐、自动回收等高级能力。

不要想着一次把所有功能做完。工具市场的价值在于持续运营,不在于第一版包含多少功能。先把一个可用的闭环跑起来,让第一批工具提供者和使用者在真实工作中用起来,后续的优化方向自然会浮现出来。

如果你正在公司里推进类似的事情,也欢迎把文中这套表结构、接口设计和工程实践作为参考基础。从复制 DDL 开始,改一改代码里的包名,你就能拥有一个真正属于自己团队的生产力市场原型。这里面的每一步都有值得优化的空间,而这恰恰是这类工程项目的乐趣所在。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进