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

发布时间:2026/10/17 7:03:11
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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT操作日志表;注意上述表结构是简化版本生产环境还需要补充索引、约束、软删除字段如deleted、字段注释等细节。比如p_tool中的description字段建议设置更长一些避免描述过多被截断。5. 后端核心接口实现5.1 统一响应结构与公共类写接口之前先定义一个统一响应结构方便前端处理。// 文件路径src/main/java/com/company/pmarket/common/Result.java package com.company.pmarket.common; public class ResultT { 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 ResultT ok(T data) { return new Result(200, success, data); } public static T ResultT 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 ResultPageResultTool page( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, RequestParam(required false) String keyword, RequestParam(required false) Long categoryId) { // 简化逻辑真实项目请注入 Page PageResultTool result toolService.pagePublishedTools(page, size, keyword, categoryId); return Result.ok(result); } /** * 获取工具详情同时增加浏览量 */ GetMapping(/{id}) public ResultTool detail(PathVariable Long id) { Tool tool toolService.getDetail(id); return Result.ok(tool); } }分页查询的 SQL 可以这样写注意只查询已发布的工具避免用户看到未审核的内容。!-- 文件路径src/main/resources/mapper/ToolMapper.xml -- select idselectPublishedPage resultTypecom.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 testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND category_id #{categoryId} /if ORDER BY create_time DESC LIMIT #{offset}, #{size} /select5.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 classtool-list el-card shadownever el-form :inlinetrue el-form-item label关键词 el-input v-modelquery.keyword placeholder工具名称或描述 clearable / /el-form-item el-form-item label分类 el-select v-modelquery.categoryId clearable placeholder全部分类 el-option v-foritem in categories :keyitem.id :labelitem.name :valueitem.id / /el-select /el-form-item el-form-item el-button typeprimary clickloadTools查询/el-button /el-form-item /el-form el-table :datatools v-loadingloading border el-table-column propname label工具名称 min-width140 / el-table-column propdescription label描述 min-width220 show-overflow-tooltip / el-table-column propusageType label使用方式 width110 / el-table-column propviewCount label浏览量 width90 / el-table-column propapplyCount label申请量 width90 / el-table-column label操作 width120 fixedright template #default{ row } el-button typeprimary link clickgoDetail(row.id)查看/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequery.page v-model:page-sizequery.size :totaltotal layouttotal, prev, pager, next current-changeloadTools / /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) /script6.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?page1size10keywordExcel预期返回结果类似{ code: 200, message: success, data: { total: 1, records: [ { id: 1, name: Excel 批量导出工具, description: 支持从数据库读取数据并导出为 Excel 文件, status: PUBLISHED, usageType: DOWNLOAD, accessLevel: RESTRICTED } ] } }7.3 启动前端服务如果使用 Vue 3 Vitenpm 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 后增加useUnicodetruecharacterEncodingutf8mb4分页查询总数不对动态 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 开始改一改代码里的包名你就能拥有一个真正属于自己团队的生产力市场原型。这里面的每一步都有值得优化的空间而这恰恰是这类工程项目的乐趣所在。