99 lines
5.2 KiB
Markdown
99 lines
5.2 KiB
Markdown
# 自动编号 → 手动固定编号修复(删段重排根治)
|
|
|
|
## 适用场景
|
|
他人(如屠佳青)用修订模式整段删除了一个**自动编号列表项**(段落 pPr 含 `<w:numPr>`,且段落标记 del=True),导致 OnlyOffice **markup 修订视图**把后续列表项渲染成"旧号新号"双编号:
|
|
|
|
```
|
|
(1)报名服务 ← 正常
|
|
(2)笔试服务[删除线] ← 被删,仍占编号位
|
|
(3)(2)面试服务 ← 双号!自动引擎按"接受后会变(2)"提前显示
|
|
(4)(3)项目管理 ← 双号!
|
|
```
|
|
|
|
Maggie/Doro 平时看 markup 视图,要求"修订视图下编号稳定显示 (1)(2)(3)(4)"。
|
|
根治办法:把这一组列表项从自动编号转成**手动文本编号**——文本是字面量,渲染器原样输出,不再经过自动编号引擎重排。
|
|
|
|
## 关键认知(动手前必须确认)
|
|
1. **被删项的删除是他人修订 → 绝不动其正文**(只在段首加编号 run,不碰 del 内容)。
|
|
2. **全文先确认没有对这些子项编号的交叉引用**(如"按上述第3项""见(4)")。本例"具体服务内容详见 附件一:服务报价单"是文字列举,非编号引用 → 安全。若存在引用,转手动编号后引用文字需同步核对。
|
|
3. 用 `docker exec <nc容器> find ... ` 从 Nextcloud **拉当前交付版**作修复源,`md5sum` 比对确认本地副本没过时。
|
|
|
|
## 可复用代码(2026-06-16 验证通过)
|
|
```python
|
|
import zipfile, shutil, os
|
|
from lxml import etree
|
|
NS='http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
|
def q(t): return f'{{{NS}}}{t}'
|
|
def ln(el): return etree.QName(el).localname
|
|
|
|
src='交付版.docx'; out='FIXED.docx'
|
|
work='work.docx'; shutil.copy(src, work)
|
|
root=etree.fromstring(zipfile.ZipFile(work).read('word/document.xml'))
|
|
|
|
# 1. 按正文开头定位目标段(按你的合同改这些前缀)
|
|
segs={}
|
|
for p in root.iter(q('p')):
|
|
t=''.join((x.text or '') for x in p.iter() if ln(x) in ('t','delText'))
|
|
if t.startswith('报名服务:'): segs['报名']=p
|
|
elif t.startswith('笔试服务:提供'): segs['笔试']=p # 被删的那项
|
|
elif t.startswith('面试服务:'): segs['面试']=p
|
|
elif t.startswith('项目管理:整个项目'): segs['项目管理']=p
|
|
|
|
def make_rpr(): # 字体/字号照搬本段原文(本例宋体sz=24)。务必与目标段一致
|
|
rpr=etree.SubElement(etree.Element(q('tmp')), q('rPr'))
|
|
rf=etree.SubElement(rpr, q('rFonts'))
|
|
for a in ('ascii','hAnsi','cs'): rf.set(q(a),'宋体;SimSun')
|
|
etree.SubElement(rpr, q('sz')).set(q('val'),'24')
|
|
etree.SubElement(rpr, q('szCs')).set(q('val'),'24')
|
|
return rpr
|
|
|
|
def make_run(text):
|
|
r=etree.Element(q('r')); r.append(make_rpr())
|
|
t=etree.SubElement(r, q('t')); t.text=text
|
|
t.set('{http://www.w3.org/XML/1998/namespace}space','preserve')
|
|
return r
|
|
|
|
def remove_numpr(p):
|
|
ppr=p.find(q('pPr'))
|
|
if ppr is not None:
|
|
np=ppr.find(q('numPr'))
|
|
if np is not None: ppr.remove(np)
|
|
|
|
def insert_first(p, node): # 插到 pPr 之后、第一个内容元素之前
|
|
idx=len(p)
|
|
for i,c in enumerate(p):
|
|
if ln(c) in ('r','ins','del','hyperlink'): idx=i; break
|
|
p.insert(idx, node)
|
|
|
|
# 2. 普通项:明文编号 run
|
|
for key,label in [('报名','(1)'),('面试','(3)'),('项目管理','(4)')]:
|
|
remove_numpr(segs[key]); insert_first(segs[key], make_run(label))
|
|
|
|
# 3. 被删项:编号 run 必须包进【他人的】<w:del>(复制其 author/date)
|
|
p=segs['笔试']; remove_numpr(p)
|
|
ex=p.find(q('del')) # 已有的他人 del(屠佳青)
|
|
nd=etree.Element(q('del'))
|
|
nd.set(q('author'), ex.get(q('author'))) # 照抄他人 author
|
|
nd.set(q('date'), ex.get(q('date')))
|
|
nd.set(q('id'),'99001') # 不冲突的大 id
|
|
r=etree.SubElement(nd, q('r')); r.append(make_rpr())
|
|
dt=etree.SubElement(r, q('delText')); dt.text='(2)'
|
|
dt.set('{http://www.w3.org/XML/1998/namespace}space','preserve')
|
|
insert_first(p, nd)
|
|
|
|
# 4. 写回(只换 document.xml,其余条目原样复制)
|
|
new_doc=etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True)
|
|
with zipfile.ZipFile(work) as zin, zipfile.ZipFile(out,'w',zipfile.ZIP_DEFLATED) as zout:
|
|
for it in zin.namelist():
|
|
zout.writestr(it, new_doc if it=='word/document.xml' else zin.read(it))
|
|
```
|
|
|
|
## 交付前必验(vision 不可用时降级四查,缺一不可)
|
|
1. **逐段 markup diff vs 交付源** → 只有目标 N 段不同,其余零改动(本例 346 段只动 4 段)。
|
|
2. **pdftotext 渲染层数编号链** → `onlyoffice-render.sh out.docx && pdftotext -f1 -l1 out.pdf -` 确认 1.2 下严格 (1)(2)(3)(4) 无双号。
|
|
3. **编号 run rPr == 本段正文 run rPr**(字体宋体、sz=24 一致)。
|
|
4. **接受修订后视图编号链连续** + `python-docx Document(out)` 可打开(XML 合法)。验证被删项的编号确实在 `<del author=他人>` 里、他人原 del 内容(id 不变)一字未动。
|
|
|
|
## 上传
|
|
`docker cp` 覆盖 `任务交付/` 同名文件 → `chown www-data` → `occ files:scan --path` → 清 OnlyOffice 缓存(`rm -rf .../App_Data/cache/files/*`)让 Maggie 打开看到新版。`md5sum` 比对容器内==本地确认上传成功。
|