SpringBoot整合WebScoket显示进度条
1.问题描述
对于大文件上传解析,若直接上传,会超时,可使用WebSocket长链接方式实时显示文件的上传状态,实际上是从文件上传到内容解析完成存入数据库的过程,各个阶段的进度可自定义。
本文使用SpringBoot+WebSocket+vue2.0+Element+nginx实现文件实时上传显示进度条,上传的截图如下:

2.解决方案
1)导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2)开启WebSocket的支持,并把该类注入到spring容器中
package com.zxh.example.config; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter; //开启WebSocket的支持,并把该类注入到spring容器中
@Configuration
public class WebSocketConfig { @Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
} }
3)编写WebSocket服务
package com.zxh.example.service; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap; /**
* @author zhengkai.blog.csdn.net
*/
@ServerEndpoint("/wsServer/{userId}")
@Component
@Slf4j
public class WebSocketServer { /**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userId = ""; /**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
webSocketMap.put(userId, this);
//加入set中
} else {
webSocketMap.put(userId, this);
//加入set中
addOnlineCount();
//在线数加1
} log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount()); try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("用户:" + userId + ",网络异常!!!!!!");
}
} /**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
//从set中删除
subOnlineCount();
}
log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
} /**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("用户消息:" + userId + ",报文:" + message);
//可以群发消息
//消息保存到数据库、redis
if (StringUtils.isNotBlank(message)) {
try {
//解析发送的报文
JSONObject jsonObject = JSON.parseObject(message);
//追加发送人(防止串改)
jsonObject.put("fromUserId", this.userId);
String toUserId = jsonObject.getString("toUserId");
//传送给对应toUserId用户的websocket
if (StringUtils.isNotBlank(toUserId) && webSocketMap.containsKey(toUserId)) {
webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString());
} else {
log.error("请求的userId:" + toUserId + "不在该服务器上");
//否则不在这个服务器上,发送到mysql或者redis
}
} catch (Exception e) {
e.printStackTrace();
}
}
} /**
* 出现错误
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
} /**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
} /**
* 发送自定义消息
*/
public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
log.info("发送消息到:" + userId + ",报文:" + message);
if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) {
webSocketMap.get(userId).sendMessage(message);
} else {
log.error("用户" + userId + ",不在线!");
}
} public static synchronized int getOnlineCount() {
return onlineCount;
} public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
} public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
4)编写文件上传的controller
package com.zxh.example.controller; import com.zxh.example.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; @RestController
@RequestMapping("/api")
@Slf4j
public class TestController { @Autowired
private TestService testService; @PostMapping("/upload")
public String upload(MultipartFile file) {
return testService.upload(file);
} }
5)编写文件上传的实现类,实时解析文件并发送通知
package com.zxh.example.service; import cn.afterturn.easypoi.handler.inter.IReadHandler;
import com.zxh.example.entity.User;
import com.zxh.example.util.ExcelUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; @Service
@Slf4j
public class TestService { public String upload(MultipartFile file) {
Integer[] percent = {1};
sendMessage(percent[0]);
Integer percentMax1 = 20;
Integer percentMax2 = 100;
// 读取Excel中的数据到list集合中
List<User> list = new ArrayList<>();
//解析excel,解析1%~20%
ExcelUtils.importExcelBySax(file, User.class, 2, new IReadHandler<User>() {
@Override
public void handler(User o) {
list.add(o);
//每读取指定行,推送1
if (list.size() % 10000 == 0 && percent[0] < percentMax1) {
percent[0]++;
sendMessage(percent[0]);
}
} @Override
public void doAfterAll() {
//解析成功
percent[0] = percentMax1;
sendMessage(percent[0]);
}
}); //模拟数据插入,每1000条发送一次消息 21%~100%
Integer maxSize = 1000;
Integer queryCnt = list.size() % maxSize == 0 ? list.size() / maxSize : (list.size() / maxSize) + 1;
Integer sendCnt = 10;
for (int i = 0; i < queryCnt; i++) {
Integer endIndex = (i + 1) * maxSize;
if (endIndex > list.size()) {
endIndex = list.size();
}
//集合截取
List<User> tempList = new ArrayList<>(list.subList(i * maxSize, endIndex));
//模拟数据查询 if (queryCnt % sendCnt == 0 && percent[0] < percentMax2) {
percent[0]++;
sendMessage(percent[0]);
}
}
percent[0] = percentMax2;
sendMessage(percent[0]);
return "success";
} /**
* 自定义封装的发送方法
* @param msg
*/
private void sendMessage(Integer msg) {
try {
WebSocketServer.sendInfo(msg.toString(), "111");
} catch (IOException e) {
log.error("消息发送异常:" + e.getMessage());
e.printStackTrace();
}
}
}
6)编写全局的global.js,可在全局使用,方便各个页面都能获取到消息
export default {
//websocket
webSocket: {},
setWs: function (ws) {
this.webSocket = ws
},
wsUrl: `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/wsServer/`,
}
7)在main.js中注入global.js中的方法
import global from './global'
Vue.prototype.global = global
8)在Vue的App.vue创建webscoketd对象,并注册到全局
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
socket: null
}
},
mounted() {
this.initWs()
},
methods: {
//初始化
initWs() {
if (typeof (WebSocket) === "undefined") {
alert("您的浏览器不支持socket")
} else {
// 实例化socket 111是固定的用户id,正式环境直接获取当前登录用户id
this.socket = new WebSocket(this.global.wsUrl + '111')
this.global.setWs(this.socket)
// 监听socket连接
this.socket.onopen = () => {
console.log("socket连接成功")
}
// 监听socket错误信息
this.socket.onerror = () => {
console.error("连接错误")
}
//监听socket消息
this.socket.onmessage = (msg) => {
// console.log(msg)
}
// 监听socket关闭信息
this.socket.onclose = (e) => {
console.error("socket已经关闭")
console.error(e)
}
}
},
},
}
</script>
<style>
#app {
height: 100%;
}
</style>
9)在vue.config.js配置协议,转发到后台服务(本地开发)
module.exports = {
devServer: {
host: '0.0.0.0',
// //设置端口号
port: 8006,
//自动打开浏览器
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
},
//websocket配置,正式环境设置nginx代理
'/wsServer': {
target: 'http://localhost:8080'
},
},
},
}
10)编写上传文件的页面
<template>
<div>
<el-button type="primary" icon="el-icon-upload" @click="handleUpload" style="margin-left: 10px;">导入
</el-button>
<el-upload ref="importUpload" :auto-upload="false" :show-file-list="false" :on-change="postFile"
style="display: inline" action="#">
<el-button id="uploadButton1" style="display: none" slot="trigger" />
</el-upload>
<el-dialog title="上传进度" :visible.sync="uploadDialog" width="30%" @close="closeDialog"
:close-on-click-modal="false">
<p>
<div class="time-content">已用时间:{{timesStr}}</div>
</p>
<el-progress :percentage="percentMsg" :text-inside="true" :stroke-width="23"></el-progress>
<div class="status-content">
<p v-if="importStatus == 1">
<span class="status-content-icon-span">上传中,请稍后......</span>
</p>
<p v-if="importStatus == 2"><i class="el-icon-success"></i>
<span class="status-content-icon-span">上传成功</span>
</p>
<p v-if="importStatus == 3"><i class="el-icon-error"></i>
<span class="status-content-icon-span">上传失败</span>
</p>
</div>
</el-dialog>
</div>
</template> <script>
import {
user
} from "@/api/user"; let that
export default {
data() {
return {
uploadDialog: false,
websocket: "",
percentMsg: 0,
times: 0,
timesStr: '00:00',
timesId: null,
importStatus: 0, //上传状态,0未上传,1上传中,2上传成功,3上传失败
}
},
created() {
that = this
},
watch: {
'percentMsg': function (val) {
if (val === 100 && this.timesId) {
clearInterval(this.timesId)
}
},
'importStatus': function (val) {
if (val === 3 && this.timesId) {
clearInterval(this.timesId)
}
}
},
mounted() {
this.getSystemWs()
},
methods: {
getSystemWs() {
this.global.webSocket.onmessage = res => {
if (res && res.data) {
this.percentMsg = Number(res.data)
} else {
this.importStatus = 3
}
}
},
//上传开始计时
startUpload() {
this.timesId = setInterval(function () {
let timesStr = that.timesStr
that.times++
let m = parseInt(that.times / 60)
let s = that.times % 60
if (that.times != 0 && s % 60 == 0) {
m = that.times / 60
s = 0
}
if (m < 10) {
timesStr = '0' + m
} else {
timesStr = m
}
timesStr += ":"
if (s < 10) {
timesStr = timesStr + '0'
}
timesStr = timesStr + s
that.timesStr = timesStr
}, 1000);
}, handleUpload() {
const uploadObj1 = document.getElementById("uploadButton1");
uploadObj1.click();
}, beforeUpload(file) {
if (file.type == "" || file.type == null || file.type == undefined) {
const FileExt = file.name.replace(/.+\./, "").toLowerCase();
if (
FileExt == "xls" ||
FileExt == "xlsx" ||
FileExt == "XLS" ||
FileExt == "XLSX"
) {
return true;
} else {
this.$message.error("上传文件必须是Excel格式!");
return false;
}
}
return true;
},
postFile(file) {
this.percentMsg = 0
this.startUpload()
var fileData = new FormData();
fileData.append("file", file.raw);
let headers = {
"Content-Type": "multipart/form-data"
};
this.uploadDialog = true;
user.upload(fileData).then(res => {
if (res == 'success') {
this.importStatus = 2
} else {
this.importStatus = 3
}
});
},
closeDialog() {
if (this.timesId) {
clearInterval(this.timesId)
}
this.percentMsg = 0
this.times = 0
this.timesStr = '00:00'
if (this.importStatus == 2) {
this.getList()
}
this.importStatus = 0
},
},
}
</script> <style>
.time-content {
text-align: right;
width: 100%;
} .status-content {
margin-top: 40px;
width: 100%;
text-align: center;
} .status-content .el-icon-success {
font-size: 30px;
vertical-align: -20%;
color: #67C23A;
} .status-content .el-icon-error {
font-size: 30px;
vertical-align: -20%;
color: #ee3838;
} .status-content .el-icon-warning {
font-size: 30px;
vertical-align: -20%;
color: #E6A23C;
} .status-content-icon-span {
margin-left: 10px;
}
</style>
3.注意事项
3.1nginx代理配置
11)在上线时是需要使用nginx代理的,故需使用nginx代理前端的WebSocket
在nginx.conf做如下配置:
...
#请求体大小
client_max_body_size 20M;
... server {
listen 81;
server_name localhost; location / {
root html;
try_files $uri $uri/ /index.html;
} location ~^/api/ {
proxy_pass http://127.0.0.1:8080;
proxy_read_timeout 600s; #默认是60s,若不配置则超过60s会出现504状态码
} #websocket代理配置
location ~^/wsServer/ {
proxy_pass http://127.0.0.1:8080;
# 开启nginx对websocket的支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 36000s; #10小时未传输数据则关闭连接
}
...
默认情况下,如果代理服务器在60秒内未传输任何数据,则连接将关闭。请求体的大小根据实际情况修改。若不配置,则上传文件超过默认值1MB时就会出现413错误状态码。
3.2多节点问题
在单节点服务时,上述即可满足需求,但多节点服务时,通过nginx代理,若连接和请求都在同一台服务器时,可正常使用,但也会出现和A服务器连接了WebSocket,但在导入时请求的是B服务器的情况,此时B服务器并不会发送消息给前端,导致导入时不显示进度。此时就需要使用分布式的通知方式,下面使用redis的发布订阅功能进行消息的通知。
1)导入redis依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2)创建redis消息实体,
package com.zxh.model; import lombok.Data;
import lombok.experimental.Accessors; import java.util.List; /**
* redis发布订阅的消息实体
*/
@Data
@Accessors(chain = true)
public class RedisMessage {
//消息类型,1全部广播,2个人信息
private Integer category;
//消息
private String message;
//要发送的用户组
private List<String> userList;
}
方便消息的封装。
2)创建业务处理类,监听redis消息发布
主要用于监听消息的发布,收到消息时进行相关业务的处理。
package com.zxh.common.listener; import com.alibaba.fastjson.JSON;
import com.zxh.common.util.CollectionUtil;
import com.zxh.model.RedisMessage;
import com.zxh.server.WebSocketServer;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component; import java.io.IOException; /**
* redis消息订阅-业务处理
*/
@Component
@Slf4j
public class RedisMessageListener implements MessageListener { //重写onMessage,处理相关发布订阅的业务
@SneakyThrows
@Override
public void onMessage(Message message, byte[] bytes) {
String body = new String(message.getBody(), "UTF-8");
RedisMessage redisMessage = JSON.parseObject(body, RedisMessage.class); if (redisMessage != null) {
Integer category = redisMessage.getCategory();
//个人信息
if (category == 2) {
//根据用户id消息
if (CollectionUtil.isNotEmpty(redisMessage.getUserList())) {
redisMessage.getUserList().stream().forEach(userId -> {
try {
WebSocketServer.sendInfo(redisMessage.getMessage(),userId);
} catch (IOException e) {
e.printStackTrace();
}
});
} else {
log.warn("无用户信息,发送信息失败");
}
} else if (category == 1) {
}
}
}
}
3)配置redis发布订阅
package com.zxh.configure; import com.zxh.common.SystemConst;
import com.zxh.common.listener.RedisMessageListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; /**
* redis发布订阅配置
*/ @Configuration
@EnableCaching
public class RedisPubSubConfig { Logger logger = LoggerFactory.getLogger(this.getClass()); /**
* 配置 交换机消息,添加多个 messageListener参数,配置不同的交换机
*
* @param connectionFactory
* @param listenerAdapter
* @return
*/
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, new PatternTopic("channel:test1"));
return container;
} /**
* 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
*
* @param listener 业务处理类
* @return
*/
@Bean
MessageListenerAdapter listenerAdapter(RedisMessageListener listener) {
logger.info("redis消息监听器加载成功--------->>>>>>");
// onMessage 就是方法名,基于反射调用
return new MessageListenerAdapter(listener, "onMessage");
} @Bean
StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
} }
4)调用redis的发布功能
修改TestService的sendMessage的方法,把使用WebSocket发送信息改为把消息发布到redis中。
@Service
@Slf4j
public class TestService {
..... @Autowired
private StringRedisTemplate stringRedisTemplate; private void sendMessage(Integer msg) {
List<String> userList = Arrays.asList("1111");//使用redis的发布订阅发送消息
RedisMessage redisMessage = new RedisMessage().setCategory(2);
redisMessage.setMessage(msg.toString()).setUserList(userList);
stringRedisTemplate.convertAndSend("channel:test1", JSON.toJSONString(redisMessage));
} }
redis发布后,监听器监听到有消息时,使用WebSocket进行消息推送。每台服务器都会推送,只有服务连接成功的一台服务器才能通知到前台成功
SpringBoot整合WebScoket显示进度条的更多相关文章
- 上传媒体文件--添加显示进度条 layui的upload控件
上传媒体文件--添加显示进度条 layui的upload控件 详细上传功能请参考博客:上传文件--媒体文件+获取上传文件的属性信息 layui的upload控件 - じ逐梦 - 博客园 (cnbl ...
- 任务栏显示进度条 CreateComObject(CLSID_TaskbarList) as ITaskbarList4
http://www.cnblogs.com/jxsoft/archive/2011/06/02/2067712.html //在任务栏显示 进度条 unit Unit9; interface use ...
- Asp.Net实现无刷新文件上传并显示进度条(非服务器控件实现)(转)
Asp.Net实现无刷新文件上传并显示进度条(非服务器控件实现) 相信通过Asp.Net的服务器控件上传文件在简单不过了,通过AjaxToolkit控件实现上传进度也不是什么难事,为什么还要自己辛辛苦 ...
- WPF下载远程文件,并显示进度条和百分比
WPF下载远程文件,并显示进度条和百分比 1.xaml <ProgressBar HorizontalAlignment="Left" Height="10&quo ...
- (委托事件处理)关于多线程执行显示进度条的实例(转)&&线程间操作无效: 从不是创建控件“rtxtEntryNO”的线程访问它。
关于多线程执行显示进度条的实例! 之前回答了一篇关于怎么在线程中操作进度条的帖子,估计有人看的不是很明白今天没事,写了一个小小的实例,很简单,就2个文件权当抛砖引玉,希望有更好解决方案的人发表一下意见 ...
- android标题栏(titlebar)显示进度条
在后台线程中执行各种操作(网络连接.大数据存储)的时候,我们希望让客户能看到后台有操作在进行,那么既能有效的提示用户,又不占用当前操作空间,最好的方法就是在标题栏有个进度条. [代码] [Java]代 ...
- ProgressIndicator显示进度条以及一些文字信息
//ProgressIndicator可以显示进度条以及一些文字信息,不过这个属性一般都在cs文件中操作. private void PhoneApplicationPage_Loaded(objec ...
- VC下载文件 + 显示进度条
在codeproject里找了许久,发现这样一个VC下载文件并显示进度条的源码,于是添加了些中文注释: 1.下载线程函数: UINT DownloadFile(LPVOID pParam) { CWn ...
- VC下载文件显示进度条
VC下载文件显示进度条 逗比汪星人2009-09-18上传 by Koma http://blog.csd.net/wangningyu http://download.csdn.net/deta ...
- C# WinFrom 导入Excel文件,显示进度条
因为WINForm程序是在64位上运行如果使用另外一种快速的读取Excel的方法会报“未在本地计算机上注册“Microsoft.Jet.OLEDB.12.0”提供程序” 所以我就换了现在这种读取有点慢 ...
随机推荐
- java 通配符的使用
简介 java 通配符 是java写库的人必备的技能树 感觉有点模糊 jdk 版本要求较高 否则可能出现变异不通过的情况 最好用最新版本(jdk14) 虽然java 核心编程要求的是jdk1.8 但是 ...
- POLIR-Society-Organization-Lawsuits: (2020)粤0303民初16184号判决书
(2020)粤0303民初16184号判决书 深圳市罗湖区人民法院 送达公告页:https://guanwang.szlhfy.gov.cn/news/14209.cshtml 送达公告列表页(第16 ...
- Rider设置代码换行格式
打开设置,选择编程语言 At end of line (K&R style) 是侧括号换行,其他可自行尝试 else 相关
- [ROI 2023] 峰值 (Day 1)
\(\mathbf{Part. -1}\) 翻译自 ROI 2023 D1T3. 如果对于所有 \(1 \le j < i\),都有 \(a_j < a_i\),则称 \(a_i\) 为峰 ...
- python正则表达式re.match以及re.search函数
re.match 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match()就返回none. 例子1: #!/usr/bin/python import re print(re. ...
- 2.4 rt-thread实操-使用scons向工程中添加分组和文件
1.我的工程目录当前是这样 2. 现在想向工程中添加hello分组和hello.c.hello.h文件,并使用soncs自动生成到工程hello分组中,并可通过menuconfig去配置宏,进行打开是 ...
- ProfibusDP转DeviceNet欧姆龙 PLC 联合霍尼韦尔液位传感器实现食品生产线智能化升级案例
案例背景 食品加工企业的生产过程对卫生.高效和精准控制要求极高.某食品饮料生产厂,其灌装生产线部分设备如液位传感器.温度控制器采用 Profibus DP 协议,而生产线的中央控制系统使用基于 Dev ...
- MyEMS开源能源管理系统核心代码解读011
本期解读: 总览页面:myems/myems-web/src/components/MyEMS/dashboard/Dashboard.js 这段代码是一个React组件,用于构建一个能源管理系统的总 ...
- 不要太信任Cursor,这位网友被删库了。。。
刷X 的时候看到一个帖子,一网友让AI 去操作数据库,可能用户前期没描述清楚,以及没有添加严格的Rule, 导致删库了..... 用户提问:"你刚刚删除了我数据库里所有的数据?!" ...
- [题解]P4116 Qtree3
P3925 aaa被续 题目描述没看懂(雾) 简单解释一下: 对于节点\(u\),将子树\(u\)中的权值从大到小排序,记"权值乘排名之和"为\(u\)的贡献. 输出总贡献. 下文 ...