Spring Boot + Vue全栈开发:年度商业调研评选系统实战解析

Spring BootVue调研评选系统
于 2026-08-29 04:24:48 修改
·本内容遵循CC 4.0 BY-SA版权协议

每年到第四季度,很多商业媒体和行业协会都会启动“年度名册调研”“商业之王评选”这类活动,目的是把过去一年里在创新力、增长性、行业影响力上表现突出的企业和人物找出来。这类评选看似是一个品牌运营动作,背后却要处理大量企业申报数据、评审分数、权重计算和榜单发布逻辑。如果完全靠表格收集、人工汇总,不仅效率低,还容易出现数据口径不统一、评审过程难追溯的问题。本文就以一套“年度商业调研评选系统”为例,从数据库设计到后端接口,再到前端展示,完整拆解如何用 Spring Boot + Vue 实现一个可落地的调研评选管理平台。

这套系统可以支持企业在线申报、管理员审核、评委多维度打分、系统自动加权计算名次、最终生成名册并发布榜单。通过这套系统的开发,你不仅能掌握一个完整的全栈项目流程,还能理解调研业务里最常见的“申报—评审—计算—发布”闭环是如何通过代码实现的。无论你是刚接触 Spring Boot 的初学者,还是想找一个完整项目练手的后端开发者,这篇文章都比较合适。

1. 背景:年度商业调研的业务逻辑与技术挑战

1.1 年度名册调研是怎么运转的

先来还原一下真实的业务场景。一个“年度商业之王”名册调研活动,通常分成几个阶段:主办方发布调研通知,企业在线提交申报材料,主办方进行资格审核,然后邀请评审团从多个维度打分,再按既定权重计算总分,最后确定名册名单并对外公布。

这个流程里,最少涉及三类角色:

  • 企业用户:填写企业基本信息、提交年度业务数据、上传申报材料。
  • 主办方管理员:审核申报资料、配置评审维度、管理评审团、查看最终排名。
  • 评审专家:为每个通过审核的申报企业打分,并可以填写评审意见。

从技术角度看,这就是一个典型的多角色 CRUD + 复杂业务计算的系统。和普通管理系统不同的是,评选系统对“流程状态”和“计算结果”要求很高。企业不能越过了审核阶段就直接进入评审;评审分数一旦提交,不能随意修改;最终名册一定是按照规则计算出来的,而不是人为拍脑袋填进去的。

1.2 传统调研方式的痛点

过去很多小规模评选会采用“Excel 表格 + 邮件回收”的方式。调研通知发出去之后,企业把申报表填好回传;工作人员再手动整理成一张总表,然后分发给评委打分;打分结束后再回收、汇总、计算。这个过程至少有几个非常现实的问题:

第一,数据格式不统一。有的企业把营收填成“万元”,有的填成“元”,有的甚至直接填一个文字描述,后期清洗非常痛苦。第二,版本管理混乱。企业可能回传两次文件,评委也有初版、修订版,最后到底以哪个版本为准,经常要靠人工沟通确认。第三,计算过程不够透明。人工汇总容易出错,而且一旦参赛方对结果提出疑问,主办方很难快速拿出可追溯的评分明细。

数字化改造之后,这些问题都能通过系统化的流程约束规避掉:申报表单中字段类型固定,数据存储统一;每次提交都记录时间戳;评审分数落到数据库明细表中;排名计算交给程序执行,整个链路有迹可循。

1.3 系统需要解决的核心问题

结合上述背景,我们把系统要解决的问题收敛为下面几点:

  • 支持企业在线提交申报,并约束数据格式。
  • 支持主办方按年份创建调研活动,配置评分维度和权重。
  • 支持评委查看申报详情、打分、提交评审意见。
  • 支持系统根据评分规则自动计算企业总分并生成名册。
  • 支持最终榜单在前端页面上公开展示。

本项目的重点不在 UI 多精致,而在于把业务流程的关键节点用代码完整表达出来。理解这套逻辑之后,哪怕后续要换成别的评选场景,比如“年度产品调研”“年度技术团队评选”,也能很轻松地做迁移。

2. 技术选型与环境准备

2.1 技术栈选型

考虑到项目的学习成本以及日常开发中的普遍性,这里采用一套比较常见的组合:

  • 后端:Spring Boot
  • 持久层:MyBatis-Plus
  • 数据库:MySQL
  • 前端:Vue 3 + Element Plus
  • 构建工具:Maven、npm

Spring Boot 是目前 Java 后端开发使用率很高的框架,用它可以快速搭建独立运行的服务;MyBatis-Plus 在 MyBatis 基础上增强了单表 CRUD 能力,能减少模板代码;Vue 3 + Element Plus 则能让我们快速开发出美观且交互完整的后台和展示页面。

这里不引入过于复杂的前后端分离脚手架,保证代码简单直接,重点还是讲清楚业务实现逻辑。

2.2 环境版本说明

本文示例以以下环境为基础:

TEXT
JDK 1.8+
Maven 3.6+
MySQL 5.7+ / 8.0
Spring Boot 2.7.x
MyBatis-Plus 3.5.x
Node.js 16+ / npm 8+
Vue 3 + Vite

版本需要根据你的项目实际情况调整。如果你使用的是 Spring Boot 3.x,则需要 JDK 17 以上,个别依赖的坐标也需要同步升级。本文示例思路在所有版本上都适用,但具体依赖版本号建议以你自己的工程为准。

2.3 项目结构设计

我们采用前后端分离结构,后端工程目录如下:

TEXT
wise-survey-backend
├── pom.xml
├── src/main/java/com/example/survey
│ ├── SurveyApplication.java
│ ├── controller
│ │ ├── CompanyController.java
│ │ ├── ReviewController.java
│ │ └── RankingController.java
│ ├── entity
│ │ ├── Company.java
│ │ ├── SurveyApply.java
│ │ ├── ReviewScore.java
│ │ └── RankingResult.java
│ ├── mapper
│ │ ├── CompanyMapper.java
│ │ ├── SurveyApplyMapper.java
│ │ ├── ReviewScoreMapper.java
│ │ └── RankingResultMapper.java
│ ├── service
│ │ ├── CompanyService.java
│ │ ├── ReviewService.java
│ │ └── RankingService.java
│ └── config
│ └── CorsConfig.java
└── src/main/resources
├── application.yml
└── sql/init.sql

前端这里我们简化处理,使用 Vite 创建 Vue 3 项目,核心页面放在 src/views 目录下。

3. 数据库设计:核心表结构

数据库是整个评选系统的地基。设计得是否合理,直接影响后续开发的复杂度。针对上述业务场景,我们拆出四张核心表:企业信息表、调研申报表、评审打分表、名册结果表。

3.1 企业信息表 company

企业基本信息独立成表,好处是同一个企业可以参与多个年度的调研,不需要重复录入基础资料。

SQL
CREATE TABLE `company` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL COMMENT '企业名称',
`industry` varchar(100) DEFAULT NULL COMMENT '所属行业',
`area` varchar(100) DEFAULT NULL COMMENT '所在地区',
`credit_code` varchar(100) DEFAULT NULL COMMENT '统一社会信用代码',
`introduction` text COMMENT '企业简介',
`contact_name` varchar(50) DEFAULT NULL COMMENT '联系人',
`contact_phone` varchar(30) DEFAULT NULL COMMENT '联系电话',
`status` tinyint(4) DEFAULT '0' COMMENT '状态:0正常 1禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业信息表';

表中 credit_code 可以做成唯一索引,避免企业重复注册。industryarea 字段在后续做数据筛选和分类统计时会非常有用。

3.2 调研申报表 survey_apply

申报表是评选业务的核心单据,记录企业在某一年度的申报数据。为什么要把申报数据和企业基础信息分开?因为一家企业的基本信息相对固定,但每年的营收、净利润、员工数这些经营指标是变化的。

SQL
CREATE TABLE `survey_apply` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`company_id` bigint(20) NOT NULL COMMENT '企业ID',
`survey_year` varchar(10) NOT NULL COMMENT '调研年度,如2026',
`report_title` varchar(200) DEFAULT NULL COMMENT '申报标题',
`report_content` text COMMENT '申报内容描述',
`revenue` decimal(18,2) DEFAULT NULL COMMENT '年度营收(万元)',
`net_profit` decimal(18,2) DEFAULT NULL COMMENT '年度净利润(万元)',
`employee_count` int(11) DEFAULT NULL COMMENT '员工人数',
`attachment_url` varchar(500) DEFAULT NULL COMMENT '附件路径',
`status` tinyint(4) DEFAULT '0' COMMENT '状态:0待审核 1审核通过 2审核驳回',
`audit_remark` varchar(500) DEFAULT NULL COMMENT '审核意见',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_company_year` (`company_id`, `survey_year`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='调研申报表';

这里把营收和净利润统一成“万元”,是在数据入口就做了规范约束。实际业务中如果企业填写的单位不统一,计算排名时就会很麻烦。所以在后端接口里,前端表单需要做单位提示,后端也需要做字段校验。

3.3 评审打分表 review_score

评审打分是评选公平性的关键。设计评分表时,我们要考虑一个申报单会被多个评委打分,而每个评委又会从不同维度打分,所以这里采用“评委 + 申报单 + 维度”的联合维度来存储一条记录。

SQL
CREATE TABLE `review_score` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`apply_id` bigint(20) NOT NULL COMMENT '申报单ID',
`reviewer_id` bigint(20) NOT NULL COMMENT '评审人员ID',
`dimension_code` varchar(50) NOT NULL COMMENT '评分维度编码',
`score` decimal(5,2) DEFAULT NULL COMMENT '维度得分',
`comment` varchar(500) DEFAULT NULL COMMENT '评审意见',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_review` (`apply_id`, `reviewer_id`, `dimension_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评审打分表';

注意这里加了唯一索引 uk_review,防止同一个评委在同一个维度下对同一个企业重复打分。实际开发中,评委有可能修改打分,我们一般会通过“先删除后插入”或“按唯一索引更新”的方式处理。

维度本身可以做成配置表,这里为了控制示例规模,我们在代码中定义一个固定的维度枚举:创新力、增长性、行业影响力。

3.4 名册结果表 ranking_result

名册结果表保存最终排名结果。每个年度的榜单可以按类型拆分,比如“商业之王主榜”“行业影响力榜”等。为了简单起见,我们用 ranking_type 字段来区分。

SQL
CREATE TABLE `ranking_result` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`survey_year` varchar(10) NOT NULL COMMENT '调研年度',
`company_id` bigint(20) NOT NULL COMMENT '企业ID',
`company_name` varchar(200) NOT NULL COMMENT '企业名称(冗余存储)',
`rank_no` int(11) DEFAULT NULL COMMENT '名次',
`ranking_type` varchar(50) DEFAULT 'MAIN' COMMENT '榜单类型',
`total_score` decimal(10,2) DEFAULT NULL COMMENT '总分',
`publish_status` tinyint(4) DEFAULT '0' COMMENT '发布状态:0未发布 1已发布',
`publish_time` datetime DEFAULT NULL COMMENT '发布时间',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_year_type` (`survey_year`, `ranking_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='名册结果表';

为什么要把 company_name 冗余存一份?因为榜单展示时如果每次都去关联企业表,会多一次查询;而且榜单发布之后,企业名称如果被修改,理论上不应该影响已经发布的历史榜单,所以这里做了冗余。

4. 后端核心接口实现(Spring Boot)

4.1 创建项目与依赖配置

我们先创建 Spring Boot 工程,然后在 pom.xml 中加入 Web、MySQL、MyBatis-Plus、Lombok 等依赖。

XML
<?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>wise-survey-backend</artifactId>
<version>1.0.0</version>
<name>wise-survey-backend</name>
<description>年度商业调研评选系统后端</description>
 
<properties>
<java.version>1.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.3.1</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>
</dependencies>
</project>

如果使用 Spring Boot 3.x,需要把 mysql-connector-java 换成 com.mysql:mysql-connector-j,同时 MyBatis-Plus 也需要使用支持 Spring Boot 3 的版本。

配置文件放在 src/main/resources/application.yml

YAML
server:
port: 8080
 
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wise_survey?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
 
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

map-underscore-to-camel-case 可以让数据库的下划线字段自动映射为 Java 的驼峰属性,比如 company_id 映射为 companyId

4.2 实体类与 Mapper

以企业实体为例,创建 entity/Company.java

JAVA
package com.example.survey.entity;
 
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
 
import java.time.LocalDateTime;
 
@Data
@TableName("company")
public class Company {
 
@TableId(type = IdType.AUTO)
private Long id;
 
private String name;
 
private String industry;
 
private String area;
 
private String creditCode;
 
private String introduction;
 
private String contactName;
 
private String contactPhone;
 
private Integer status;
 
private LocalDateTime createTime;
 
private LocalDateTime updateTime;
}

其他实体类的写法类似,这里就不再逐一贴出完整代码了。创建 Mapper 接口只需要继承 BaseMapper<T>,例如:

JAVA
package com.example.survey.mapper;
 
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.survey.entity.Company;
 
public interface CompanyMapper extends BaseMapper<Company> {
}

在启动类上加上 @MapperScan 告诉 MyBatis 去哪里扫描 Mapper:

JAVA
package com.example.survey;
 
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
@MapperScan("com.example.survey.mapper")
public class SurveyApplication {
 
public static void main(String[] args) {
SpringApplication.run(SurveyApplication.class, args);
}
}

4.3 企业申报接口

企业申报是整套系统的入口。正常情况下,企业用户先填写企业信息,再填写年度申报数据。这里我们为了演示方便,在同一个接口中完成两个动作:如果企业不存在,就自动创建企业;然后再创建申报单。

创建 CompanyService.java

JAVA
package com.example.survey.service;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.survey.entity.Company;
import com.example.survey.entity.SurveyApply;
import com.example.survey.mapper.CompanyMapper;
import com.example.survey.mapper.SurveyApplyMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import java.time.LocalDateTime;
 
@Service
public class CompanyService {
 
@Autowired
private CompanyMapper companyMapper;
 
@Autowired
private SurveyApplyMapper surveyApplyMapper;
 
@Transactional(rollbackFor = Exception.class)
public Long apply(Company company, SurveyApply apply) {
// 1. 检查企业信用代码是否已存在
Company exist = companyMapper.selectOne(new LambdaQueryWrapper<Company>()
.eq(Company::getCreditCode, company.getCreditCode()));
if (exist == null) {
company.setStatus(0);
company.setCreateTime(LocalDateTime.now());
companyMapper.insert(company);
} else {
company.setId(exist.getId());
}
 
// 2. 创建申报单
apply.setCompanyId(company.getId());
apply.setStatus(0);
apply.setCreateTime(LocalDateTime.now());
surveyApplyMapper.insert(apply);
return apply.getId();
}
}

这里有一个比较重要的设计点:申报和创建企业必须放在同一个事务中。如果企业创建成功但申报单插入失败,事务回滚后,企业信息也不会残留,避免产生“只有企业没有申报单”的脏数据。

对应的 Controller:

JAVA
package com.example.survey.controller;
 
import com.example.survey.entity.Company;
import com.example.survey.entity.SurveyApply;
import com.example.survey.service.CompanyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
 
import java.util.HashMap;
import java.util.Map;
 
@RestController
@RequestMapping("/api/company")
public class CompanyController {
 
@Autowired
private CompanyService companyService;
 
@PostMapping("/apply")
public Map<String, Object> apply(@RequestBody Map<String, Object> params) {
Company company = new Company();
company.setName((String) params.get("name"));
company.setIndustry((String) params.get("industry"));
company.setArea((String) params.get("area"));
company.setCreditCode((String) params.get("creditCode"));
company.setIntroduction((String) params.get("introduction"));
company.setContactName((String) params.get("contactName"));
company.setContactPhone((String) params.get("contactPhone"));
 
SurveyApply apply = new SurveyApply();
apply.setSurveyYear((String) params.get("surveyYear"));
apply.setReportTitle((String) params.get("reportTitle"));
apply.setReportContent((String) params.get("reportContent"));
apply.setRevenue(new java.math.BigDecimal(params.get("revenue").toString()));
apply.setNetProfit(new java.math.BigDecimal(params.get("netProfit").toString()));
apply.setEmployeeCount(Integer.valueOf(params.get("employeeCount").toString()));
 
Long applyId = companyService.apply(company, apply);
 
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("applyId", applyId);
return result;
}
}

严格来说,真实的项目应该使用 DTO 来接收参数,避免 Map 方式过于灵活导致参数不可控。这里为了压缩篇幅用了 Map,但正式开发中建议定义专门的 CompanyApplyDTO,并用 @Validated 做参数校验。

4.4 评审打分接口

评审打分是评委登录系统后,对某个申报单进行打分。一个申报单下有多个维度分数,我们需要设计一个批量提交接口。这里处理逻辑采用“先删除该评委在该申报单下的历史打分,再批量插入新打分”,这样可以保证打分数据的一致性。

创建 ReviewService.java

JAVA
package com.example.survey.service;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.survey.entity.ReviewScore;
import com.example.survey.mapper.ReviewScoreMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import java.util.List;
 
@Service
public class ReviewService {
 
@Autowired
private ReviewScoreMapper reviewScoreMapper;
 
@Transactional(rollbackFor = Exception.class)
public void submitScore(Long applyId, Long reviewerId, List<ReviewScore> scoreList) {
// 1. 删除历史打分
reviewScoreMapper.delete(new LambdaQueryWrapper<ReviewScore>()
.eq(ReviewScore::getApplyId, applyId)
.eq(ReviewScore::getReviewerId, reviewerId));
 
// 2. 重新插入
for (ReviewScore score : scoreList) {
score.setApplyId(applyId);
score.setReviewerId(reviewerId);
score.setCreateTime(java.time.LocalDateTime.now());
reviewScoreMapper.insert(score);
}
}
}

这个接口虽然逻辑简单,但有一个容易被忽视的问题:如果 scoreList 为空,我们需要让接口直接返回,而不是删除历史数据之后不插入任何记录。实际开发中,可以在删除前做一次判断,如果传过来的打分列表为空,直接抛出业务异常。

评审维度的分数建议在接口层做范围校验,比如“创新力”满分 100 分,最小值 0,不能让评委提交一个 150 分。这里给出一个简单的校验片段:

JAVA
for (ReviewScore score : scoreList) {
if (score.getScore() == null || score.getScore() < 0 || score.getScore() > 100) {
throw new IllegalArgumentException("评分必须在0到100之间");
}
}

4.5 名册生成与榜单查询

名册生成是这个系统里最核心的计算逻辑。一笔申报单的总分计算,需要把所有评委在三个维度上的打分聚合起来。为了避免复杂 SQL 写起来难以理解,我们这里用 Java 代码来处理聚合逻辑:先查询所有申报单,再逐单查询评分明细,加权汇总后排序。

创建 RankingService.java

JAVA
package com.example.survey.service;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.survey.entity.RankingResult;
import com.example.survey.entity.ReviewScore;
import com.example.survey.entity.SurveyApply;
import com.example.survey.mapper.RankingResultMapper;
import com.example.survey.mapper.ReviewScoreMapper;
import com.example.survey.mapper.SurveyApplyMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
 
@Service
public class RankingService {
 
// 评分维度权重:创新力0.4,增长性0.35,行业影响力0.25
private static final Map<String, Double> WEIGHT_MAP = new HashMap<>();
 
static {
WEIGHT_MAP.put("INNOVATION", 0.4);
WEIGHT_MAP.put("GROWTH", 0.35);
WEIGHT_MAP.put("INFLUENCE", 0.25);
}
 
@Autowired
private SurveyApplyMapper surveyApplyMapper;
 
@Autowired
private ReviewScoreMapper reviewScoreMapper;
 
@Autowired
private RankingResultMapper rankingResultMapper;
 
public void generateRanking(String surveyYear, String rankingType) {
// 1. 查询该年度所有审核通过的申报单
List<SurveyApply> applyList = surveyApplyMapper.selectList(
new LambdaQueryWrapper<SurveyApply>()
.eq(SurveyApply::getSurveyYear, surveyYear)
.eq(SurveyApply::getStatus, 1));
 
List<RankingDto> rankingList = new ArrayList<>();
 
// 2. 逐个申报单计算总分
for (SurveyApply apply : applyList) {
List<ReviewScore> scores = reviewScoreMapper.selectList(
new LambdaQueryWrapper<ReviewScore>()
.eq(ReviewScore::getApplyId, apply.getId()));
 
if (scores.isEmpty()) {
continue;
}
 
// 按维度分组,计算每个维度的平均分
Map<String, List<ReviewScore>> grouped = scores.stream()
.collect(Collectors.groupingBy(ReviewScore::getDimensionCode));
 
BigDecimal totalScore = BigDecimal.ZERO;
for (Map.Entry<String, List<ReviewScore>> entry : grouped.entrySet()) {
BigDecimal dimensionAvg = entry.getValue().stream()
.map(ReviewScore::getScore)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(entry.getValue().size()), 2, RoundingMode.HALF_UP);
 
Double weight = WEIGHT_MAP.getOrDefault(entry.getKey(), 0.0);
totalScore = totalScore.add(dimensionAvg.multiply(BigDecimal.valueOf(weight)));
}
 
RankingDto dto = new RankingDto();
dto.setApplyId(apply.getId());
dto.setCompanyId(apply.getCompanyId());
dto.setTotalScore(totalScore.setScale(2, RoundingMode.HALF_UP));
rankingList.add(dto);
}
 
// 3. 按总分降序排名
rankingList.sort((a, b) -> b.getTotalScore().compareTo(a.getTotalScore()));
 
// 4. 保存名册结果
// 先清空今年的历史结果,避免重复生成
rankingResultMapper.delete(new LambdaQueryWrapper<RankingResult>()
.eq(RankingResult::getSurveyYear, surveyYear)
.eq(RankingResult::getRankingType, rankingType));
 
int rank = 1;
for (RankingDto dto : rankingList) {
RankingResult result = new RankingResult();
result.setSurveyYear(surveyYear);
result.setCompanyId(dto.getCompanyId());
result.setCompanyName(getCompanyName(dto.getCompanyId()));
result.setRankNo(rank++);
result.setRankingType(rankingType);
result.setTotalScore(dto.getTotalScore());
result.setPublishStatus(0);
rankingResultMapper.insert(result);
}
}
 
private String getCompanyName(Long companyId) {
// 这里简化为直接查询企业表,正常可以批量查询优化
return null;
}
}

在上面的示例里,getCompanyName 方法留了空实现。真实项目中,建议在生成名册前先批量查询企业信息,放进一个 Map<Long, String> 中,避免循环单条查询数据库造成 N+1 问题。这一点在实际项目中特别重要,因为榜单可能涉及几十甚至上百家企业,逐条查会有明显性能损耗。

名册生成完成后,榜单查询就非常简单了。只需要按年度和榜单类型查询,并按照 rank_no 升序返回。

5. 前端页面实现(Vue 3 简版)

5.1 榜单展示页面

前端我们不追求完整后台管理系统,而是先把两个核心场景打通:一个是评委打分页面,一个是名册榜单展示页面。

先看榜单页面。创建一个 src/views/RankingPage.vue

VUE
<template>
<div class="ranking-page">
<h2>{{ surveyYear }} 年度商业之王名册</h2>
<el-table :data="rankingList" stripe border>
<el-table-column prop="rankNo" label="名次" width="80" />
<el-table-column prop="companyName" label="企业名称" />
<el-table-column prop="totalScore" label="总分" width="120" />
</el-table>
</div>
</template>
 
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
 
const surveyYear = ref('2026')
const rankingList = ref([])
 
onMounted(() => {
fetchRanking()
})
 
async function fetchRanking() {
const { data } = await axios.get('/api/ranking/list', {
params: { surveyYear: surveyYear.value, rankingType: 'MAIN' }
})
rankingList.value = data
}
</script>

由于前后端端口不同,开发环境下需要在 Vite 中配置代理。在 vite.config.js 中加入:

JS
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
 
export default defineConfig({
plugins: [vue()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})

这样前端请求 /api/ranking/list 时,Vite 会把请求转发到后端 8080 端口,避免出现跨域问题。

5.2 评审打分页面

评委打分页面思路:评委选择一个待评申报单,页面展示申报的基本信息,下方针对多个评分维度输入分数,点击提交后调用后端接口。

VUE
<template>
<div>
<h2>评审打分</h2>
<el-form label-width="100px">
<el-form-item label="申报标题">
<el-input v-model="apply.reportTitle" disabled />
</el-form-item>
<el-form-item label="创新力">
<el-input-number v-model="scoreMap.INNOVATION" :min="0" :max="100" />
</el-form-item>
<el-form-item label="增长性">
<el-input-number v-model="scoreMap.GROWTH" :min="0" :max="100" />
</el-form-item>
<el-form-item label="行业影响力">
<el-input-number v-model="scoreMap.INFLUENCE" :min="0" :max="100" />
</el-form-item>
<el-button type="primary" @click="submitScore">提交打分</el-button>
</el-form>
</div>
</template>
 
<script setup>
import { reactive, ref } from 'vue'
import axios from 'axios'
 
const apply = ref({
reportTitle: 'XX企业2026年度申报材料'
})
 
const scoreMap = reactive({
INNOVATION: 80,
GROWTH: 75,
INFLUENCE: 85
})
 
async function submitScore() {
const scoreList = [
{ dimensionCode: 'INNOVATION', score: scoreMap.INNOVATION },
{ dimensionCode: 'GROWTH', score: scoreMap.GROWTH },
{ dimensionCode: 'INFLUENCE', score: scoreMap.INFLUENCE }
]
await axios.post('/api/review/submit', {
applyId: 1,
reviewerId: 1,
scoreList
})
}
</script>

这只是一个演示页面。真正的业务中,评审专家需要先查看企业申报附件、财务报表等内容,页面交互会更复杂,但核心提交逻辑是一样的。

6. 系统运行与验证

6.1 初始化数据库

先在 MySQL 中创建数据库,并执行前面给出的建表 SQL:

BASH
mysql -uroot -p
# 输入密码后执行
CREATE DATABASE wise_survey DEFAULT CHARACTER SET utf8mb4;
USE wise_survey;
SOURCE /path/to/init.sql;

6.2 启动后端服务

wise-survey-backend 目录下执行:

BASH
mvn spring-boot:run

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

TEXT
Tomcat started on port(s): 8080 (http)
Started SurveyApplication in 3.2 seconds

6.3 调用接口验证

使用 curl 调用企业申报接口:

BASH
curl -X POST http://localhost:8080/api/company/apply \
-H "Content-Type: application/json" \
-d '{
"name": "示例科技有限公司",
"industry": "信息技术",
"area": "北京",
"creditCode": "91110000MA000001X0",
"introduction": "一家专注于企业服务的科技公司",
"contactName": "张三",
"contactPhone": "13800000000",
"surveyYear": "2026",
"reportTitle": "2026年度商业创新实践案例",
"reportContent": "本公司在过去一年中完成多个核心产品升级",
"revenue": 12000,
"netProfit": 1500,
"employeeCount": 300
}'

返回结果:

JSON
{
"success": true,
"applyId": 1
}

接着调用评审打分接口:

BASH
curl -X POST http://localhost:8080/api/review/submit \
-H "Content-Type: application/json" \
-d '{
"applyId": 1,
"reviewerId": 1,
"scoreList": [
{ "dimensionCode": "INNOVATION", "score": 90 },
{ "dimensionCode": "GROWTH", "score": 85 },
{ "dimensionCode": "INFLUENCE", "score": 88 }
]
}'

打完分后,调用名册生成接口,再访问榜单查询接口,应该能看到该企业出现在名册中。

6.4 前端访问

进入前端项目目录,执行:

BASH
npm install
npm run dev

浏览器访问 http://localhost:5173,打开榜单页面即可看到后端返回的排名数据。

7. 常见问题与排查思路

在开发这套评选系统过程中,最常遇到的问题集中表现在下面几个方面:

问题现象 常见原因 解决思路
后端启动报数据库连接失败 MySQL 未启动、连接串错误或密码不对 检查 MySQL 服务和 application.yml 配置
接口请求返回 404 Controller 路径与前端请求路径不一致 核对 @RequestMapping 和前端 URL
跨域请求被拦截 前后端端口不一致且未配置代理 配置 Vite proxy 或后端 CorsConfig
中文乱码 数据库字符集或连接参数不是 UTF-8 建库使用 utf8mb4,url 添加 characterEncoding=utf8
MyBatis-Plus 查询全部数据 条件构造器没生效 检查 LambdaQueryWrapper 是否真正执行 eq 条件
名册生成后没有数据 申报单状态不是审核通过,或没有打分数据 确认申报单 status=1 且已有评分记录

第一个常见的坑是 CORS。虽然我们在 Vite 里配置了代理,但如果你直接使用 Postman 或让前端不走代理,跨域问题就会暴露。一个简单的后端全局配置如下:

JAVA
package com.example.survey.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("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
}

第二个常见问题是评审分数的精度。数据库中使用 decimal(5,2),但 Java 端如果使用 Double 类型,很容易出现精度丢失。因此实体类中关于金额、分数的字段,建议统一使用 BigDecimal

第三个坑是名册生成时忘记清理历史结果。如果不先删除上一轮生成的结果,直接插入新数据会导致榜单重复、名次错乱。所以生成名册的第一步,一定是先按“年度 + 榜单类型”删除旧数据。

8. 工程实践与上线建议

8.1 权限与安全

本文为了演示没有引入完整的认证授权体系,但真实上线时必须做好权限控制。企业用户只能维护自己的申报单,评委只能查看分配给自己的申报单,管理员才拥有审核和生成名册的权限。

在 Spring Boot 工程中,可以引入 Spring Security 或 Sa-Token 实现登录认证和角色授权。接口层面至少要做到三件事:身份认证、角色校验、越权访问控制。

例如,评审打分接口不能只接收 reviewerId 就信任它,应该从当前登录上下文中获取实际用户,判断该用户是否为有效评委,并且该申报单是否分配给他。否则任何普通用户都可以构造请求替别人打分。

8.2 数据一致性与幂等

申报接口要考虑重复提交问题。企业用户可能因为网络原因连续点了两次提交按钮,导致生成两条申报单。解决方案有两个方向:一是前端提交按钮做 loading 状态,禁止重复点击;二是后端在 survey_apply 表中增加一个企业申报年度唯一约束,同一个企业在同一年度只能有一条申报记录。

如果是已有系统的改造,建议在接口层做一次校验:

JAVA
Long count = surveyApplyMapper.selectCount(new LambdaQueryWrapper<SurveyApply>()
.eq(SurveyApply::getCompanyId, companyId)
.eq(SurveyApply::getSurveyYear, year));
if (count > 0) {
throw new RuntimeException("该企业本年度已经提交过申报");
}

8.3 评分公平性算法

当评委人数较多时,直接使用所有评委的平均分,很容易受到极端分数影响。比如某位评委对某企业给出了远超其他评委的高分,就会拉高该企业的总分。这种情况下,可以考虑“去掉一个最高分和一个最低分,再计算平均分”的算法。

RankingService 中,可以对每个维度分组后的分数集合做一次排序,然后去掉首尾元素:

JAVA
List<BigDecimal> sortedScores = list.stream()
.map(ReviewScore::getScore)
.sorted()
.collect(Collectors.toList());
 
if (sortedScores.size() > 2) {
sortedScores = sortedScores.subList(1, sortedScores.size() - 1);
}
 
BigDecimal avg = sortedScores.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(sortedScores.size()), 2, RoundingMode.HALF_UP);

这种去极值平均法在各类评审系统中很常见,能有效抑制个别异常评分对结果的影响。

8.4 生产环境建议

上线之前,建议从下面几个角度再检查一遍系统:

  • 配置管理:数据库密码、密钥等信息不要明文写在配置文件里,可以使用环境变量或配置中心。
  • 日志记录:申报提交、审核、打分、名册生成等关键操作都要打印操作人和操作时间,便于后续审计。
  • 数据备份:每天定时备份数据库,尤其是名册结果生成前后,必须确保可以回滚。
  • 导出能力:榜单最终可能需要生成 PDF 或 Excel 用于线下发布,可以提前规划导出接口。
  • 性能优化:当申报单数量较大时,名册生成中的逐单查询评分列表会产生 N+1 查询,建议改为一次性查出所有评分数据,再在内存中分组计算。

把年度调研评选系统做扎实的核心,不是堆砌多炫酷的界面,也不是追求多复杂的算法,而是保证流程严谨、数据可追溯、计算可复现。申报、审核、评审、计算、发布,每个环节都有明确的状态和记录,这套系统才真正具备上线价值。如果你正准备在公司内部搭建类似的调研平台,建议先按这篇文章把数据表和核心接口梳理一遍,再根据自己的业务规模去扩展权限、导出和消息通知功能。

Spring Boot + Vue3 完整开发全栈项目附资料.zip
Spring Boot + Vue3 全栈开发详解及实践指南》在现代Web开发领域,Spring BootVue3已经成为构建高效、可扩展应用的热门选择。
花纵酒
2919
Spring Boot + Vue全栈开发实战随书源码附件
综上所述,"Spring Boot + Vue全栈开发实战"项目涉及的技术点包括Spring Boot的自动配置、Web开发、数据库操作、安全框架,以及Vue.js的组件化开发、状态管理、HTTP请求等
风起风来
821
Spring Boot Vue前后端分离开发实战.pdf
综上所述,文档《Spring Boot Vue前后端分离开发实战.pdf》涵盖了前后端分离的开发模式、Spring BootVue.js的基础知识和使用、开发环境的搭建、前后端交互的实现、项目配置与启动
亚历山大伟
6742
# Spring Boot + Vue 全栈开发实战
该栏目下的文章为学习 王 松的《Spring Boot + Vue全栈开发实战》而做的笔记
3207257-Spring Boot+Vue全栈开发实战.pdf
《3207257-Spring Boot+Vue全栈开发实战.pdf》这份文档便是一本专注于讲解如何使用Spring BootVue.js进行全栈开发实战教程。
慕容峻才
489
Vue Spring Boot前后端分离开发实战》源码Vue+Spring Boot前后端分离开发实战教学课件(PPT)
这本《Vue Spring Boot前后端分离开发实战》的源码提供了深入学习和实践这一技术栈的机会。以下是对其中涉及知识点的详细说明1.
1808
Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端
**学习与进阶** 作为一名刚入行的开发者,你可以通过这个项目学习到如何集成多种技术,理解全栈开发流程,同时提升Spring Boot后端开发Vue.js前端以及微信小程序的实战技能。
热心网友!
2778
Spring Boot + Vue 全栈开发实战》官方案例.zip
本文档提供《Spring Boot + Vue 全栈开发实战》一书的官方示例代码,涵盖多个Spring Boot基础项目配置,展示Web服务搭建、环境管理及属性配置等核心功能,适合前后端分离架构的学习
JJJ69
6
spring boot+vue+springcloud全栈开发实战
本文介绍了Spring BootVueSpring Cloud的组合使用,实现全栈开发Spring Boot简化Java应用配置,Vue构建交互式前端界面,Spring Cloud提供微服务架构支持。
DataEase登顶Gitee年度BI赛道架构解析与二次开发实践
开源BI工具正在改变企业数据可视化的落地方式,其核心价值在于降低使用门槛并保持灵活的扩展能力。DataEase作为一款基于元数据驱动的开源BI平台,采用前后端分离架构,后端以Spring Boot为核心,前端基于Vue 3与ECharts构建,通过数据源层、数据集层、图表层、仪表板层和权限层的清晰抽象,实现了从数据接入到看板展示的全链路闭环。该工具支持MySQL、ClickHouse、Hive等多种数据源,并提供行级数据权限与定时同步能力,适合运营、财务等非专业团队快速搭建分析看板。凭借活跃的社区迭代和Gi
微信投票小程序SaaS系统搭建多租户设计与微信生态对接实战
SaaS多租户架构是企业级应用降本增效的关键模式,通过共享基础设施与租户隔离机制,实现一套代码服务多个客户。其原理在于将数据存储、权限控制、业务逻辑在租户维度进行划分,既保证数据安全又降低运维成本。在本地生活服务领域,微信投票小程序成为商家数字化运营的常见入口,餐饮、教育、零售等行业的拉票、评选活动高频发生。基于多租户设计的SaaS投票系统,能帮助平台方快速开通独立活动、独立后台,并整合微信登录、分享拉票、订阅消息等生态能力。这类系统兼顾规则配置、防刷策略与数据一致性,适合具备工程化交付能力的团队落地,同时
开源BI工具DataEase数据可视化与仪表板实战指南
数据可视化已成为企业数字化运营的基础能力,而开源BI工具正是降低这一能力门槛的关键。其核心原理在于通过连接数据库等数据源,以拖拽方式快速生成图表和仪表板,实现从数据到洞察的直接转化。技术价值体现在缩短报表开发周期、降低工具采购成本,同时支持私有化部署与灵活权限管控,尤其适合中小团队及业务部门自助分析场景。在实际应用中,用户可通过在线安装包、Docker Compose或源码编译快速部署,完成数据源接入、数据集管理和仪表板设计,并能通过iframe嵌入与二次开发深度整合进现有系统。本文以DataEase为例,
企业级技术交付全生命周期服务方法论
本文提出‘圣殿骑士’企业级技术交付全生命周期服务方法论,聚焦开发、架构、管理、培训与解决方案五大环节的深度咬合。通过以终为始的逆向工程思维、工具链强制统一(Terraform/Prometheus/Loki)、可验证交付物设计、C4架构建模、自动化治理引擎及721实操培训等关键技术实践,解决传统交付中割裂、不可测、难传承等核心痛点。案例覆盖新能源车企BMS系统升级,验证毫秒级延迟、分钟级故障定位与热升级等硬性指标。
ciyinzhi8788
312
CnOpenData 线上招聘网站信息数据
CnOpenData线上招聘网站信息数据库整合七大招聘平台数据,覆盖2014年至今的岗位、企业、薪资、技能要求等结构化字段,支持劳动力市场趋势分析、人岗匹配研究及中小企业就业研究。数据具多源性、长时序、高频更新与文本挖掘潜力,广泛应用于劳动经济学、教育政策与企业HR决策。
CnOpenData_wj
1486