操作步骤(可复制执行)
下文用 TS 代表时间戳;先执行 TS=$(date +%Y%m%d_%H%M%S) && echo $TS,后续命令引用同一 TS。
步骤 1|定位补丁点
# 1) 把容器内的 app.jar 复制到宿主机(228 MB,秒级)
docker cp dataease:/opt/apps/app.jar /tmp/app.jar.work
md5sum /tmp/app.jar.work # 记下来,等下核对
# 2) 确认目标 chunk 存在(版本升级后文件名中的版本号会变)
unzip -l /tmp/app.jar.work | grep 'static/assets/chunk/locale-' | head
# 3) 解出目标文件(★ 保留路径,不能用 -j,回写时要靠这个相对路径)
mkdir -p /tmp/patchwork && cd /tmp/patchwork
unzip -o -q /tmp/app.jar.work "BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js"
# 4) ★ 确认目标串唯一出现 1 次(不是 1 就必须停下来人工检查)
grep -c -o 'setLinkToken(e){e!==this.linkToken&&(this.visitorPermissions=0),this.linkToken=e}' \
BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js
若换了大版本,用 grep -l setLinkToken *.js 反查是哪个 chunk 定义了这个方法(修正环境为 locale-3.0.0-dataease.js)。
步骤 2|备份(三个都要做)
# ① compose 文件
cp -a /opt/dataease3.0/docker-compose.yml /root/docker-compose.yml.bak-$TS
# ② 原始 app.jar(回滚的唯一依据)
docker cp dataease:/opt/apps/app.jar /root/app.jar.v3.1.0.orig-$TS
# ③ 记录校验值
md5sum /root/app.jar.v3.1.0.orig-$TS /opt/dataease3.0/docker-compose.yml
原环境实测:app.jar md5 = af7acda865ec8eeccd991b7562e00048(228,897,023 字节)、compose md5 = 4e353f9ec13e376d8845df6086296a92。
步骤 3|制作补丁 jar
cd /tmp/patchwork
# ① 字节级替换(用 python 而非 sed,避免转义地狱;断言保证只替换 1 处)
python3 - <<'PYEOF'
p = 'BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js'
b = open(p, 'rb').read()
old = b'setLinkToken(e){e!==this.linkToken&&(this.visitorPermissions=0),this.linkToken=e}'
new = (b'setLinkToken(e){try{e&&(document.cookie="DE-LINK-TOKEN="+e+"; path=/; SameSite=Lax")}'
b'catch(t){}e!==this.linkToken&&(this.visitorPermissions=0),this.linkToken=e}')
assert b.count(old) == 1, '目标串出现次数异常,终止'
b2 = b.replace(old, new)
open(p, 'wb').write(b2)
print('size: %d -> %d (delta %+d)' % (len(b), len(b2), len(b2) - len(b)))
PYEOF
# 期望输出:size: 197856 -> 197935 (delta +79)
# ② 生成待发布的补丁副本(从原始备份复制,保证基线干净)
mkdir -p /opt/dataease3.0/patch
cp -a /root/app.jar.v3.1.0.orig-$TS /opt/dataease3.0/patch/app.jar
# ③ ★ 只替换 jar 内那一个条目(不要整体解压重打包!)
zip -X /opt/dataease3.0/patch/app.jar \
"BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js"
zip 会打印一行 zip warning: Local Entry CRC does not match CD —— 属正常现象(原 jar 该条目用了 streamed 写法),不影响结果,继续即可。
步骤 4|校验补丁 jar(不做这步不要上线)
cd /opt/dataease3.0/patch
# ① 条目总数、名称、压缩方法必须与原件完全一致
unzip -v app.jar | awk 'NR>3 && NF>=8 {print $2"|"substr($0,index($0,$8))}' | sort > /tmp/after.txt
unzip -v /root/app.jar.v3.1.0.orig-$TS | awk 'NR>3 && NF>=8 {print $2"|"substr($0,index($0,$8))}' | sort > /tmp/before.txt
diff /tmp/before.txt /tmp/after.txt && echo "✅ 条目与压缩方法完全一致"
# ② STORED(未压缩)条目数必须不变 —— Spring Boot 硬性要求,少了会启动失败
awk -F'|' '$1=="Stored"' /tmp/after.txt | wc -l # v3.1.0 应为 503
# ③ 包内补丁确实写入
unzip -p app.jar "BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js" \
| grep -c 'DE-LINK-TOKEN="+e+; path=/; SameSite=Lax' # 应为 1
# ④ 单条目完整性测试
unzip -t app.jar "BOOT-INF/classes/static/assets/chunk/locale-3.0.0-dataease.js"
# ⑤ 补丁包总校验值(本次实测 a09982e1577f314c240abfa2ee10b43b)
md5sum app.jar
本次实测结果:2262 个条目、仅 1 条 CRC 变化、503 个 STORED 不变 → 打包安全。
步骤 5|部署(挂载 + 重建容器)
编辑 /opt/dataease3.0/docker-compose.yml,在 dataease.volumes: 首行插入(建议连注释一起留档):
volumes:
# === [PATCH-20261008] 分享链接/嵌入静态资源 401 修复,官方 v3.1.0 CommunityTokenFilter 回归 ===
# 补丁:前端 setLinkToken 处补写 DE-LINK-TOKEN Cookie。回滚方式:删除下面一行后重新 up -d
# 原始备份:/root/app.jar.v3.1.0.orig-<TS>(md5 af7acda865ec8eeccd991b7562e00048)
# ⚠ 升级 DataEase 时必须先移除本行与 /opt/dataease3.0/patch/app.jar,否则会挂载旧版 jar
- ${DE_BASE}/dataease3.0/patch/app.jar:/opt/apps/app.jar
cd /opt/dataease3.0
# ① 先校验 compose 语法与变量插值(不启动任何东西)
docker compose -f docker-compose.yml -f docker-compose-mysql.yml config > /dev/null && echo "compose OK"
# ② 重建容器(配置变更 → 只重建 dataease,mysql-de 保持 Running)
docker compose -f docker-compose.yml -f docker-compose-mysql.yml up -d
# ③ 确认挂载生效 + 容器内 jar 就是补丁包
docker inspect dataease --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' | grep app.jar
docker exec dataease md5sum /opt/apps/app.jar # 应等于步骤 4 ⑤ 的值
步骤 6|验证(三层,缺一不可)
# ① 应用就绪(约 30 秒)
for i in $(seq 1 60); do
[ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8100/de2api/dekey)" = "200" ] && \
{ echo "就绪(约 $((i*3)) 秒)"; break; }; sleep 3
done
# ② 服务端提供的前端 chunk 已是补丁版(大小应为 197935)
curl -s -o /dev/null -w '/assets/chunk/locale-3.0.0-dataease.js => %{http_code} size=%{size_download}\n' \
http://127.0.0.1:8100/assets/chunk/locale-3.0.0-dataease.js
# ③ 无 Cookie 时仍 401 —— 这是**预期**的,说明服务端过滤逻辑未被改动,修复完全靠前端写 Cookie
curl -s -o /dev/null -w 'jpg(无 Cookie) => %{http_code}\n' \
http://127.0.0.1:8100/de2api/static-resource/7404398862594478080.jpg
第四层:浏览器实测(唯一能证明"真的修好了"的方式)。用无头浏览器打开分享页,检查 Cookie、资源状态码与真实渲染:
// verify.js —— 运行:NODE_PATH=<playwright 路径> node verify.js
const { chromium } = require('playwright');
const BASE = 'http://192.168.1.243:8100';
(async () => {
const b = await chromium.launch({ headless: true, channel: 'chrome' });
const ctx = await b.newContext();
const p = await ctx.newPage();
p.on('response', (r) => r.url().includes('static-resource') &&
console.log(r.status(), r.url().replace(BASE, ''), r.headers()['content-type'], r.headers()['content-length']));
await p.goto(BASE + '/#/de-link/LBU2yjBI', { waitUntil: 'domcontentloaded', timeout: 60000 });
await p.waitForTimeout(15000);
console.log('COOKIES:', JSON.stringify(await ctx.cookies()));
console.log('IMG:', JSON.stringify(await p.evaluate(() =>
Array.from(document.querySelectorAll('img')).map(i => ({ src: i.src, w: i.naturalWidth, h: i.naturalHeight })))));
await b.close();
})();