Address
304 North Cardinal St.
Dorchester Center, MA 02124
Work Hours
Monday to Friday: 7AM - 7PM
Weekend: 10AM - 5PM
Address
304 North Cardinal St.
Dorchester Center, MA 02124
Work Hours
Monday to Friday: 7AM - 7PM
Weekend: 10AM - 5PM
luckysheet server
是luckysheet
官方的一个后台。用来和luckyshee
t建立ws
连接、保存文档至数据库。spring boot
项目。直接下载官方代码,也很难直接运行。spring boot
项目导入luckysheet server
的办法。如果有更好的办法,请告知我。。luckysheet server
的文件那么多那么复杂。mysql安装
redis安装
idea安装以及新建一个spring boot web项目。(如果已经有项目就打开原来的项目)
数据库初始化。注意schema名称要和配置文件的一致。
CREATE SCHEMA `collsheet` ;
CREATE TABLE `luckysheet` (
`id` bigint(30) NOT NULL,
`block_id` varchar(200) NOT NULL,
`row_col` varchar(50) DEFAULT NULL,
`index` varchar(200) NOT NULL,
`list_id` varchar(200) NOT NULL,
`status` int(1) DEFAULT NULL,
`json_data` json DEFAULT NULL,
`order` int(3) DEFAULT NULL,
`is_delete` int(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `lib` (`list_id`,`index`,`block_id`),
KEY `order` (`order`),
KEY `status` (`status`),
KEY `is_delete` (`is_delete`)
) ENGINE=InnoDB DEFAULT CHARSET=armscii8;
INSERT INTO luckysheet VALUES (139400313311449087, 'fblock', '', '1', '1079500#-8803#7c45f52b7d01486d88bc53cb17dcd2c3', 1, '{"row":84,"name":"Sheet1","chart":[],"color":"","index":"1","order":0,"column":60,"config":{},"status":0,"celldata":[],"ch_width":4748,"rowsplit":[],"rh_height":1790,"scrollTop":0,"scrollLeft":0,"visibledatarow":[],"visibledatacolumn":[],"jfgird_select_save":[],"jfgrid_selection_range":{}}', 0, 0);
INSERT INTO luckysheet VALUES (139400313311449088, 'fblock', '', '2', '1079500#-8803#7c45f52b7d01486d88bc53cb17dcd2c3', 0, '{"row":84,"name":"Sheet2","chart":[],"color":"","index":"2","order":1,"column":60,"config":{},"status":0,"celldata":[],"ch_width":4748,"rowsplit":[],"rh_height":1790,"scrollTop":0,"scrollLeft":0,"visibledatarow":[],"visibledatacolumn":[],"jfgird_select_save":[],"jfgrid_selection_range":{}}', 1, 0);
INSERT INTO luckysheet VALUES (139400313311449089, 'fblock', '', '3', '1079500#-8803#7c45f52b7d01486d88bc53cb17dcd2c3', 0, '{"row":84,"name":"Sheet3","chart":[],"color":"","index":"3","order":2,"column":60,"config":{},"status":0,"celldata":[],"ch_width":4748,"rowsplit":[],"rh_height":1790,"scrollTop":0,"scrollLeft":0,"visibledatarow":[],"visibledatacolumn":[],"jfgird_select_save":[],"jfgrid_selection_range":{}}', 2, 0);
pom.xml
文件修改目的是引入luckysheet server
需要的依赖。
注意版本号。。个别依赖版本不一致会报错。
文件修改号后,需要等待idea下载好所有依赖。
<!--导入数据库驱动,不同数据库,驱动器不一样-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--引入 mybatis-spring-boot-starter 的依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!--引入 hutool-all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
<!--websokcet的starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--JSON解析-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
<!--为类添加getter和setter和构造函数等-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<!--google的gson工具-->
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
<scope>compile</scope>
</dependency>
<!--阿里的druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 给redis集群用的Lettuce的连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.3</version>
</dependency>
application.yml
文件修改主要是mysql
数据库配置、redis
配置、日志配置等
注意数据库配置有重复。。因为官方就是有重复的。。暂时还没有对重复进行去掉。
#服务器配置
server:
port: 8080
#数据库配置
spring:
application:
name: web
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
http:
encoding:
charset: UTF-8
enabled: true
datasource:
druid:
url: jdbc:mysql://127.0.0.1:3306/collsheet
username: root
password: 5587699$lgg
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 127.0.0.1
port: 6379
password:
timeout: 10000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
database: 0
db:
mysql:
druid:
url: jdbc:mysql://127.0.0.1:3306/collsheet
username: root
password: 5587699$lgg
driver-class-name: com.mysql.cj.jdbc.Driver
postgre:
druid:
url: jdbc:mysql://127.0.0.1:3306/collsheet
username: root
password: 5587699$lgg
driver-class-name: com.mysql.cj.jdbc.Driver
#日记配置
logging:
file:
path: my-log
pattern:
console: "%clr(%d{yyyy-MM-dd HH:mm:ss}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(-){faint} %clr([%14.14t]){faint} %clr(%-40.40logger.%13.13M){cyan} %clr(%3.3L) %clr(:){faint} %msg%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"
#redis通道名称
redis.channel: luckysheet.channel
#表格中块的大小,新增加时写入每一个sheet的第一个模块中
row_size: 500
col_size: 500
#使用服务器类型
servertype: tomcat
复制如下一大丢文件至你的项目
我全部丢到一个文件夹了。。不太合适。。最好分好类。
复制过来后,idea会有报错。。都是import错误。。。删掉这些报错的import错误。。然后交给idea自动import即可。
BaseHandle.java
BaseModel.java
ConfigerService.java
ConfigMergeModel.java
DataSourceConfig.java
DisabledTypeEnum.java
GridFileRedisCacheService.java
GridRecordDataModel.java
GzipHandle.java
IpAndPortUtil.java
IRecordDataInsertHandle.java
IRecordDataUpdataHandle.java
IRecordDelHandle.java
IRecordSelectHandle.java
JdbcTempleConfig.java
JfGridConfigModel.java
JfGridFileController.java
JfGridFileGetService.java
JfGridFileUtil.java
JfGridUpdateService.java
JsonUtil.java
LuckySheetGridModel.java
MSExcelUtil.java
MyStringUtil.java
MyURLUtil.java
MyWebSocketHandler.java
MyWebSocketInterceptor.java
OperationTypeEnum.java
Pako_GzipUtils.java
readme.txt
RecordDataInsertHandle.java
RecordDataUpdataHandle.java
RecordDelHandle.java
RecordSelectHandle.java
RedisCacheService.java
RedisConfig.java
RedisLock.java
RedisMessageListener.java
RedisMessageModel.java
RedisMessagePublish.java
RedisQueueService.java
ScheduleService.java
SheetOperationEnum.java
SnowFlake.java
SysConstant.java
TestUtil.java
TimeUtil.java
WebSocketConfig.java
WSUserModel.java
luckysheet server
项目也可以运行了。什么都不需要做了。后端用uuid工具生成一个uuid。
用该uuid向luckysheet表插入一条空白记录。插入语句参考如下。这个是初始化语句。目的是为了新建一个空白表格。一条记录表示一个空白sheet页面。
luckysheet server
用分块实现sheet的保存。第一个fblock
表示配置块,里面没有数据的。只有颜色等配置。
INSERT INTO luckysheet VALUES (139400313311449087, 'fblock', '', '1', '1079500#-8803#7c45f52b7d01486d88bc53cb17dcd2c3', 1, '{"row":84,"name":"Sheet1","chart":[],"color":"","index":"1","order":0,"column":60,"config":{},"status":0,"celldata":[],"ch_width":4748,"rowsplit":[],"rh_height":1790,"scrollTop":0,"scrollLeft":0,"visibledatarow":[],"visibledatacolumn":[],"jfgird_select_save":[],"jfgrid_selection_range":{}}', 0, 0);
传递该uuid到前端
前端的luckysheet配置如下
socket_url = "ws://" + window.location.host + "/websocket/luckysheet?t=" + user.name;
luckysheetOptions = {
container: 'luckysheet', //luckysheet为容器id
lang: 'zh', // 设定表格语言
showinfobar: false,
allowUpdate: true,
updateUrl: socket_url,
loadUrl: "/user/load",
gridKey: uuid,
}
注意loadUrl地址的controller文件。。可以根据需要使用里面的方法。里面有导入全部数据、导入sheet等。我修改为如下:(最末尾那一行)
@PostMapping("/load")
public String load(HttpServletRequest request, HttpServletResponse response, @RequestParam(defaultValue = "") String gridKey) {
//告诉浏览器,当前发送的是gzip格式的内容
response.setHeader("Content-Encoding", "gzip");
response.setContentType("text/html");
String resultStr="";
if(gridKey.trim().length()!=0){
try {
String _checkStr=check(request,gridKey.toString(),null,OperationTypeEnum.Read);
if(_checkStr.length()>0){
return null;
}
List<JSONObject> dbObject=null;
//dbObject=jfGridFileGetService.getDefaultByGridKey(gridKey);
dbObject=jfGridFileGetService.getAllSheetByGridKey(gridKey);
至此一切都可以正常使用了。
这个是sql语句有错误。定位到RecordDelHandle.java文件
原来的写法
/**
* 删除sheet(非物理删除)
*
* @param model
* @return
*/
@Override
public boolean updateDataForReDel(GridRecordDataModel model) {
try{
String sql1="update "+ JfGridConfigModel.TABLENAME+" set is_delete=? where list_id=? and index=? ";
log.info("updateSql1:"+sql1);
luckySheetJdbcTemplate.update(sql1,new Object[]{model.getIs_delete(),model.getList_id(),model.getIndex()});
return true;
}catch (Exception e){
log.error(e.getMessage());
return false;
}
}
主要原因是字段名称index在mysql是系统关键词。。直接如此引用必然报错。修改如下(增加表名用t.index代替)
/**
* 删除sheet(非物理删除)
*
* @param model
* @return
*/
@Override
public boolean updateDataForReDel(GridRecordDataModel model) {
try{
String sql1="update "+ JfGridConfigModel.TABLENAME+" t set is_delete=? where list_id=? and t.index=? ";
log.info("updateSql1:"+sql1);
luckySheetJdbcTemplate.update(sql1,new Object[]{model.getIs_delete(),model.getList_id(),model.getIndex()});
return true;
}catch (Exception e){
log.error(e.getMessage());
return false;
}
}
定位RecordDataUpdataHandle.java文件137行
if(v instanceof JSON){
updateSql.append("CAST('"+((JSON) v).toJSONString()+"' as JSON)");
}else{
//updateSql.append("""+v+""");
updateSql.append(v);
}
其中v是前端传递过来的sheet名称。按道理官方这么写没错。。但是不懂为什么v自身带有两个双引号。导致该sql语句是 “”v””。多了2个双引号。。所以数据库提示字符错误。
这个bug事实上有很大的影响。。不仅仅是无法修改行或列。。
这个bug没解决6.4的bug也没办法解决。。事实上影响到了所有表格cg( Operation_cg)操作(操作config)。
bug没解决前,提示sql语句存在sync错误。
定位到RecordDataUpdataHandle.java449行。
注释掉的是原本的报错的写法。。更换成如下写法即可。。主要是JSON_CONTAINS_PATH(t.json_data,'one',"$."+key+"")=0"
和`”update “+JfGridConfigModel.TABLENAME+” t 需要指定一个表的别名以及告诉JSON_CONTAINS_PATH函数一个正确的列。
同理还有这个类的497行。。一样的错误。。
//String createSql="update "+JfGridConfigModel.TABLENAME+" t set t.json_data=json_set(json_data,"$."+key+"",CAST('"+newObj.getString(key)+"' AS JSON)) where 1=1 " +condition+
// " and JSON_CONTAINS_PATH(jsontest,'one',"$."+key+"")=0";
String createSql="update "+JfGridConfigModel.TABLENAME+" t set t.json_data=json_set(json_data,"$."+key+"",CAST('"+newObj.getString(key)+"' AS JSON)) where 1=1 " +condition+
" and JSON_CONTAINS_PATH(t.json_data,'one',"$."+key+"")=0";
这个bug事实上影响也很大。。不仅仅是无法修改边框。甚至其他涉及config的都无法保存成功。
解决及发现这个bug花了数天时间。最终才定位问题所在。我感觉好像是我错了。。。但是按照我这个办法修改后,可以添加边框了。
根本原因是因为luckysheet的config类,比如边框、单元格大小等在后台都是用前端发过来的进行替换原本的。比如如下一个正确的config。后面那个json是前端进行更改发送给后端的。
注意看如下的config的属性。有的是对象,有的是数组。。后台用fastJson框架进行处理,需要转换为JSONObject或JSONArray对象。。
但是涉及到config类的保存方法中,把config的属性的类型定死为JSONObject了。。所以就会提示JSONArray无法转换为JSONObject。。。需要进行相应的判断。。
解决办法更加复杂了。。因为涉及到多个方法。。这些方法的参数类型都定死了为JSONObject了。。。。。
所以。。最最根本的原因是前端更改接口了!!!!后端没料到会传入JSONArray。
"config": {
"columnlen": {
"2": 260,
"3": 260
},
"borderInfo": [
{
"color": "#000",
"range": [
{
"row": [
0,
9
],
"top": 0,
"left": 0,
"width": 73,
"column": [
0,
4
],
"height": 19,
"top_move": 0,
"left_move": 0,
"row_focus": 0,
"width_move": 369,
"height_move": 199,
"column_focus": 0
}
],
"style": "1",
"rangeType": "range",
"borderType": "border-all"
}
]
},
{"t":"cg","v":{"2":251,"3":203},"i":"1","k":"columnlen"}
解决步骤:
JfGridUpdateService.java文件中:1168行
//JSONObject _v = null
Object _v = null;//需要替换的值
JfGridUpdateService.java文件中:1220行
//JSONObject _k = JfGridFileUtil.getObjectByObject(_config, k);
Object _k = _config.get(k);
IRecordDataUpdataUhanle.java文件中利用多态增加如下一个接口:
boolean updateJsonbForSetRootNull(JSONObject query, String word, JSONObject db, Integer position, String words);
boolean updateJsonbForSetRootNull(JSONObject query, String word, Object db, Integer position, String words);
RecordDataUpdataUhanle.java文件中利用多态实现接口:
public boolean updateJsonbForSetRootNull(JSONObject query, String word, Object db, Integer position, String words) {
return updateJsonbForInsertNull(query, word, db, position, words);
}
RecordDataUpdataUhanle.java文件中利用多态增加如下方法:
注意在515行处增加如下判断语句:
public boolean updateJsonbForInsertNull(JSONObject query, String word, Object db, Integer position, String words) {
if (db instanceof JSONObject){
updateSql.append("CAST('"+((JSONObject)db).toString(SerializerFeature.WriteMapNullValue)+"' as JSON)");
}
if (db instanceof JSONArray){
updateSql.append("CAST('"+((JSONArray)db).toString(SerializerFeature.WriteMapNullValue)+"' as JSON)");
}
updateSql.append(") where 1=1 "+condition);
log.info("updateSql:{}",updateSql.toString());
luckySheetJdbcTemplate.update(updateSql.toString(),arr.toArray());
}
### 6.5前端手动输入单一的单元格数值。会出现某个单元格数值不见了。
- 前端手动输入单一的单元格数值。会出现某个单元格数值不见了。这个单元格可能是之前手动输入的某个数值,也可能是之前用拖动生成的数值。
- 解决方法如下:
- ```java
定位JfGridUpdateService.java 1554行
//_result = recordDataUpdataHandle.updateJsonbForElementInsert(query, "celldata", _db, 0);
_result = recordDataUpdataHandle.updateJsonbForElementInsert(query, "celldata", _db, 999999999);
```
- 原因:这个方法访问如下方法:
- ```java
RecordDataUpdataHandle.updateJsonbForElementInsert(JSONObject query, String word, JSONObject db, Integer position)
```
- 最终都会执行这个sql语句:
- ```sql
update luckysheet t set t.json_data=json_set(t.json_data,"$.celldata[position]",CAST('{"r":0,"c":0,"v":{"ct":{"t":"n","fa":"General"},"v":1346,"m":"1346"}}' as JSON)) where 1=1 and t.list_id="78290eb1-c879-4ae3-9eb3-e11dab05da93" and t.index="1" and t.block_id="0_0"
```
- 如果传入的position为0。就会把celldata[0]的json给替换掉。如果position为999999999。就会插入新数据。
### 6.6 重复登录bug
- 这个不能说是bug。。只能说是考虑问题。。
- 2022年5月23日。。测试的时候发现。。如果一个用户可以在多个网页打开同一个项目。。经常会把该项目搞崩溃。。导致该项目无法访问。所以目前暂时定死了。。一个用户同时只能登录一个luckysheet。。
- ```java
MyWebSocketHandler.java 第52行。
修改该变量为public。
//private static final Hashtable<String, Hashtable<String, WSUserModel>> USER_SOCKET_SESSION_MAP;
public static final Hashtable<String, Hashtable<String, WSUserModel>> USER_SOCKET_SESSION_MAP;
```
- ```java
MyWebSocketInterceptor.java 第46行增加如下4行。只要该方法返回false,就不能连接ws。
如下通过判断是否有token登录。如果有token登录就拒绝登录。
String str = MyWebSocketHandler.USER_SOCKET_SESSION_MAP.toString();
if (str.indexOf(token) > -1){
return false;
}
```
### 6.7 双击表格的某个单元格,进入单元格编辑界面,然后马上退出编辑网页。。然后就不能再次进入这个项目了
- 原因:双击某个单元格的时候,前端向后端发送如下命令
```json
{
"t": "mv",
"i": "1",
"v": {
"op": "enterEdit",
"range": [
{
"left": 148,
"width": 73,
"top": 60,
"height": 19,
"left_move": 148,
"width_move": 73,
"top_move": 60,
"height_move": 19,
"row": [
3,
3
],
"column": [
2,
2
],
"row_focus": 3,
"column_focus": 2
}
]
}
}
```
- 注意,前端发送过来的是一个JSONObject对象。后端接收到这个命令后,什么都不做直接写入数据库
```json
updateSql:update luckysheet t set t.json_data=json_set(t.json_data,"$.jfgird_select_save",CAST('{"op":"enterEdit","range":[{"top_move":0,"top":0,"left":148,"column_focus":2,"width":73,"left_move":148,"column":[2,2],"width_move":73,"row":[0,0],"height_move":19,"row_focus":0,"height":19}]}' as JSON)) where 1=1 and t.list_id='e4777a8b-7d29-43d0-95d6-6b17863e3bdb' and t.index='1' and t.block_id='fblock'
```
- 注意如上sql语句。。把jfgird_select_save这个key对应的值写成了一个对象。。但是前端要求这个是一个数组。。所以前端会报错,,然后就不能进入这个文档了。。
- 简单来说就是某句代码向文档写入了脏数据。。导致前端无法识别这个文档。。然后就无法进入了。
- 解决办法:解决办法就是不写入整个收到的命令。而是提取该命令中的所需要的数据写入数据库。。真正需要的是v.range这个数组。
```java
定位JfGridUpdateService.java第1769行,更换为如下写法:
//Object db = bson.get("v");
Object db_wrap = bson.get("v");
if (!(db_wrap instanceof JSONObject)){
return;
}
Object db = ((JSONObject) db_wrap).get("range");
if (db == null){
return;
}
```
### 6.8 无法保存单元格回车信息
- 如果单元格的内容有回车(excel经常见)的情况下。会出现更新失败。
- 主要原因还是sql语句的符号错误。
- 错误提示如下:
```java
RecordDataUpdataHandle.DataListValue 188 : StatementCallback; SQL [update luckysheet t set t.json_data=json_set(t.json_data,"$.celldata[0].v",CAST('{"ct":{"s":[{"v":"nihaornnihao"}]
MysqlDataTruncation: Data truncation: Invalid JSON text in argument 1 to function cast_as_json: "Invalid encoding in string." at position 23
MyWebSocketHandler.handleMessage 138 : handleUpdate--error:更新失败
```
- 把完整的sql语句复制到mysql中执行。。报的错误是一样的。由此可见,错误的原因是sql语句符号错误。
- 因为仅仅是回车才出现的错误。所以推测是回车的转义问题。
- 注意到`('{"ct":{"s":[{"v":"nihaornnihao"}]`的`rn`仅有一个``。试着修改为`\n\r`。不在提示这个错误。
- 解决办法如下。解决思路就是用字符串替换`rn`为`\r\n`。注意顺手把"''"也替换为""。否则当单元格有‘的时候一样会报错。
```java
定位RecordDataUpdataHandle.java文件166行。
增加如下几行
//log.info("updateSql:"+updateSql.toString());
//luckySheetJdbcTemplate.update(updateSql.toString());
String v_str = v.toString()
.replace("\r","\\r")
.replace("\n","\\n")
.replace("'","");
v = JSON.parse(v_str);
```
### 6.9 合并单元格无法保存回车信息。
- 这个bug很少出现。
- 我在导入excel文件的时候才发现有这个bug。具体可以看导入excel文件篇。
- 某个单元格无法写入数据,更新失败。原因如上所述。当这个合并单元格有换行的时候。就会出现这个bug。
- 解决方法如下:注释为原本的写法。稍微改进了下。
```java
定位RecordDataUpdataHandle.java文件224行。
//updateSql.append("CAST('"+db.toString(SerializerFeature.WriteMapNullValue)+"' as JSON)");
String db_str = db.toString(SerializerFeature.WriteMapNullValue)
.replace("\r","\\r")
.replace("\n","\\n")
.replace("'","");
updateSql.append("CAST('"+db_str+"' as JSON)");
```