Commit b7e53da7 authored by 刘正强's avatar 刘正强

谁上传就属谁:拜访报告签名强制=上传者(git 用户),校验脚本/hook/文档同步更新

parent d8770d0c
__pycache__/
*.pyc
*.pyo
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
1. 报告文件:`客户/<客户名>/YYYY-MM-DD_客户名_帛丝人员_拜访报告.md` 1. 报告文件:`客户/<客户名>/YYYY-MM-DD_客户名_帛丝人员_拜访报告.md`
2. 文件名中的"客户名"必须与所在文件夹名一致;日期用 `YYYY-MM-DD` 2. 文件名中的"客户名"必须与所在文件夹名一致;日期用 `YYYY-MM-DD`
3. 每个客户一个文件夹,不得混放其它客户报告。 3. 每个客户一个文件夹,不得混放其它客户报告。
4. **署名(谁上传就属谁)**:文件名中的"帛丝人员"与正文"拜访人(帛丝人员)"必须等于上传者本人(当前 git 用户名)。校验时用 `--owner <git用户>` 强制;不符即拒绝。
## 报告必填格式(必填门) ## 报告必填格式(必填门)
报告必须包含以下五节,每节必须填写**实际内容**(去除 HTML 注释 `<!-- -->` 与占位符 `<…>` 后仍有文字): 报告必须包含以下五节,每节必须填写**实际内容**(去除 HTML 注释 `<!-- -->` 与占位符 `<…>` 后仍有文字):
...@@ -19,14 +20,14 @@ ...@@ -19,14 +20,14 @@
- 四、问题清单及下一步计划 - 四、问题清单及下一步计划
- 五、其他补充 - 五、其他补充
> 缺节、空节、仅注释/占位符、命名/路径不符 → 一律判为不合规。 > 缺节、空节、仅注释/占位符、命名/路径不符、署名非本人 → 一律判为不合规。
## 准入动作(铁律) ## 准入动作(铁律)
1. 生成或修改任何 `*_拜访报告.md` 后,**必须**先运行校验: 1. 生成或修改任何 `*_拜访报告.md` 后,**必须**先运行校验(带署名校验时传 `--owner`,值取 `git config user.name`
```bash ```bash
python _validation/validate_report.py "<报告路径>" python _validation/validate_report.py "<报告路径>" --owner "<git用户名>"
``` ```
2. 校验失败 → 不得 commit / push,应列出缺哪节、缺什么,让用户补全后再提交。 2. 校验失败 → 不得 commit / push,应列出缺哪节、缺什么、署名是否本人,让用户补全后再提交。
3. 可整体扫描: 3. 可整体扫描:
```bash ```bash
python _validation/validate_report.py python _validation/validate_report.py
...@@ -35,5 +36,6 @@ ...@@ -35,5 +36,6 @@
5. 报告被视为受控对象:写入即留痕(git),如需改历史请走正常 git 提交,不删信息源。 5. 报告被视为受控对象:写入即留痕(git),如需改历史请走正常 git 提交,不删信息源。
## 属性默认值 ## 属性默认值
- 客户名、日期、拜访人(帛丝人员):从文件名解析(`日期_客户_人员`),不必在正文重复,但正文元数据行建议保留。 - 客户名、日期:从文件名解析(`日期_客户_人员`)。
- **拜访人(帛丝人员)=上传者本人(git 用户名),由 pre-push / 校验脚本自动比对,不由 AI 代填或推测。**
- 报告补充项(如项目负责人、系统、服务状态):为可选项,不作为必填门。 - 报告补充项(如项目负责人、系统、服务状态):为可选项,不作为必填门。
...@@ -44,7 +44,8 @@ laokehu-project-kb/ ...@@ -44,7 +44,8 @@ laokehu-project-kb/
- 文件:`YYYY-MM-DD_客户名_帛丝人员_拜访报告.md` - 文件:`YYYY-MM-DD_客户名_帛丝人员_拜访报告.md`
- 位置:`客户/<客户名>/`(客户名与文件名中的客户名必须一致) - 位置:`客户/<客户名>/`(客户名与文件名中的客户名必须一致)
- 示例:`客户/丰隆/2026-08-20_丰隆_戴堂亮_拜访报告.md` - 示例:`客户/丰隆/2026-08-20_丰隆_刘正强_拜访报告.md`
- **署名(谁上传就属谁)**:文件名里的"帛丝人员"以及正文里的"拜访人(帛丝人员)",**必须等于你本人(当前 git 用户名)**`pre-push` 会自动校验,署名不符会被拒绝上传,不允许替别人或让别人的名字上报。
## 校验方式 ## 校验方式
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
> 内部项目记录|<YYYY-MM-DD> > 内部项目记录|<YYYY-MM-DD>
> >
> 客户名称:<客户名> | 拜访人(帛丝人员):<姓名> | 日期:<YYYY-MM-DD> > 客户名称:<客户名> | 拜访人(帛丝人员):<你的姓名,且必须与 git 用户名一致> | 日期:<YYYY-MM-DD>
>
> 署名规则:谁上传就属谁 —— 文件名里的"帛丝人员"和上面这行的拜访人,必须等于当前 git 用户名本人,否则会被拒绝上传。
## 一、项目背景 ## 一、项目背景
......
...@@ -7,11 +7,13 @@ laokehu-project-kb 拜访报告校验器(必填门) ...@@ -7,11 +7,13 @@ laokehu-project-kb 拜访报告校验器(必填门)
1. 文件必须位于 客户/<客户名>/ 下,且为 *_拜访报告.md 1. 文件必须位于 客户/<客户名>/ 下,且为 *_拜访报告.md
2. 文件名必须符合命名约定:YYYY-MM-DD_客户名_帛丝人员_拜访报告.md,且"客户名"与所在文件夹名一致 2. 文件名必须符合命名约定:YYYY-MM-DD_客户名_帛丝人员_拜访报告.md,且"客户名"与所在文件夹名一致
3. 必须包含五大必填节标题,且每个必填节都有实际内容(去掉 HTML 注释和占位符后非空) 3. 必须包含五大必填节标题,且每个必填节都有实际内容(去掉 HTML 注释和占位符后非空)
4. (可选 --owner)署名归属:报告"拜访人"必须等于上传者(当前 git 用户)——谁上传就属谁
用法: 用法:
python _validation/validate_report.py <报告.md> 校验单个文件 python _validation/validate_report.py <报告.md> 校验单个文件
python _validation/validate_report.py --tree 扫描 客户/ 目录下所有报告,汇总结果(默认) python _validation/validate_report.py <报告.md> --owner <git用户> 校验单个文件并校验归属
python _validation/validate_report.py --stdin --name xxx.md 从 stdin 读 blob 内容校验(供服务器 hook 用) python _validation/validate_report.py --tree 扫描 客户/ 目录下所有报告,汇总结果(默认)
python _validation/validate_report.py --stdin --name xxx.md --owner <git用户> 从 stdin 读 blob 校验(服务器 hook 用)
可选:--quiet(只打印通过/失败),--json(结构化输出),--report-path <glob>(扫描子集) 可选:--quiet(只打印通过/失败),--json(结构化输出),--report-path <glob>(扫描子集)
退出码: 退出码:
...@@ -126,7 +128,26 @@ def check_path(path): ...@@ -126,7 +128,26 @@ def check_path(path):
return parts, [] return parts, []
def validate_file(path, quiet=False): # ---- 署名归属:谁上传就属谁 ---- (从文件名/正文取拜访人,与当前 git 用户比对)
OWNER_IN_TEXT = re.compile(r"拜访人(帛丝人员)\s*[::]\s*([^||\s ]+)")
def check_owner(filename, text, owner):
"""校验报告归属 = 上传者(当前 git 用户)。只校验文件名与正文中声明的拜访人。"""
if not owner:
return []
errors = []
m = FILENAME_RE.match(filename)
if m and m.group(3) != owner:
errors.append(f"文件署名与上传者不一致:文件名=“{m.group(3)}”,上传者(git 用户)=“{owner}”")
if text:
tm = OWNER_IN_TEXT.search(text)
if tm and tm.group(1).strip() != owner:
errors.append(f"正文拜访人与上传者不一致:正文=“{tm.group(1).strip()}”,上传者(git 用户)=“{owner}”")
return errors
def validate_file(path, quiet=False, owner=None):
path = Path(path) path = Path(path)
name = path.name name = path.name
if not path.exists(): if not path.exists():
...@@ -149,6 +170,7 @@ def validate_file(path, quiet=False): ...@@ -149,6 +170,7 @@ def validate_file(path, quiet=False):
if text is not None: if text is not None:
problems += check_content(text) problems += check_content(text)
problems += check_owner(name, text, owner)
ok = len(problems) == 0 ok = len(problems) == 0
if not quiet: if not quiet:
...@@ -176,6 +198,10 @@ def main(): ...@@ -176,6 +198,10 @@ def main():
args = sys.argv[1:] args = sys.argv[1:]
quiet = "--quiet" in args quiet = "--quiet" in args
as_json = "--json" in args as_json = "--json" in args
owner = None
if "--owner" in args:
i = args.index("--owner")
owner = args[i + 1]
report_glob = None report_glob = None
if "--report-path" in args: if "--report-path" in args:
i = args.index("--report-path") i = args.index("--report-path")
...@@ -188,6 +214,7 @@ def main(): ...@@ -188,6 +214,7 @@ def main():
name = args[i + 1] name = args[i + 1]
text = sys.stdin.read() text = sys.stdin.read()
problems = check_content(text) problems = check_content(text)
problems += check_owner(name or "", text, owner)
if name: if name:
m = FILENAME_RE.match(name) m = FILENAME_RE.match(name)
if not m: if not m:
...@@ -202,7 +229,7 @@ def main(): ...@@ -202,7 +229,7 @@ def main():
return 0 if ok else 1 return 0 if ok else 1
if args and not args[0].startswith("--"): if args and not args[0].startswith("--"):
ok, problems = validate_file(args[0], quiet=quiet) ok, problems = validate_file(args[0], quiet=quiet, owner=owner)
if as_json: if as_json:
print(json.dumps({"ok": ok, "file": args[0], "errors": problems}, ensure_ascii=False)) print(json.dumps({"ok": ok, "file": args[0], "errors": problems}, ensure_ascii=False))
return 0 if ok else 1 return 0 if ok else 1
......
...@@ -7,6 +7,9 @@ RC=0 ...@@ -7,6 +7,9 @@ RC=0
ROOT=$(git rev-parse --show-toplevel) || exit 1 ROOT=$(git rev-parse --show-toplevel) || exit 1
VALIDATOR="$ROOT/_validation/validate_report.py" VALIDATOR="$ROOT/_validation/validate_report.py"
# 谁上传就属谁:以当前 git 用户作为"拜访人"归属
PUSHER=$(git config user.name)
# 收集本次推送涉及的 .md 报告文件 # 收集本次推送涉及的 .md 报告文件
REPORTS="" REPORTS=""
...@@ -38,7 +41,7 @@ fi ...@@ -38,7 +41,7 @@ fi
echo "laokehu 校验:本次推送涉及 $(printf '%s\n' "$REPORTS" | sed '/^$/d' | wc -l | tr -d ' ') 份拜访报告,正在校验…" echo "laokehu 校验:本次推送涉及 $(printf '%s\n' "$REPORTS" | sed '/^$/d' | wc -l | tr -d ' ') 份拜访报告,正在校验…"
for f in $REPORTS; do for f in $REPORTS; do
if python "$VALIDATOR" "$ROOT/$f" >/dev/null 2>&1; then if python "$VALIDATOR" "$ROOT/$f" --owner "$PUSHER" >/dev/null 2>&1; then
: :
else else
echo "✗ 不合规:$f" echo "✗ 不合规:$f"
...@@ -51,5 +54,6 @@ if [ "$RC" -ne 0 ]; then ...@@ -51,5 +54,6 @@ if [ "$RC" -ne 0 ]; then
echo "提交被拒:有拜访报告未满足《必填门》格式要求,请按模板补全后再推送。" echo "提交被拒:有拜访报告未满足《必填门》格式要求,请按模板补全后再推送。"
echo " - 五必备节:一、项目背景 | 二、拜访事前准备 | 三、本次实际拜访结果 | 四、问题清单及下一步计划 | 五、其他补充" echo " - 五必备节:一、项目背景 | 二、拜访事前准备 | 三、本次实际拜访结果 | 四、问题清单及下一步计划 | 五、其他补充"
echo " - 命名:YYYY-MM-DD_客户名_帛丝人员_拜访报告.md(放置于 客户/<客户名>/ 下)" echo " - 命名:YYYY-MM-DD_客户名_帛丝人员_拜访报告.md(放置于 客户/<客户名>/ 下)"
echo " - 署名:拜访人必须是你本人(当前 git 用户:$PUSHER)——谁上传就属谁"
fi fi
exit $RC exit $RC
...@@ -20,10 +20,12 @@ while read old new ref; do ...@@ -20,10 +20,12 @@ while read old new ref; do
RANGE="$old..$new" RANGE="$old..$new"
fi fi
CHANGED=$(git -c core.quotepath=false diff --name-only --diff-filter=ACMRT "$RANGE" 2>/dev/null) CHANGED=$(git -c core.quotepath=false diff --name-only --diff-filter=ACMRT "$RANGE" 2>/dev/null)
# 谁上传就属谁:以本次提交头部的作者名作为"拜访人"归属
OWNER=$(git log -1 --format='%an' "$new" 2>/dev/null)
for p in $CHANGED; do for p in $CHANGED; do
case "$p" in case "$p" in
客户/*_拜访报告.md) 客户/*_拜访报告.md)
if git show "$new:$p" 2>/dev/null | "$PYTHON" "$VALIDATOR" --path "$p" --name "$(basename "$p")" -; then if git show "$new:$p" 2>/dev/null | "$PYTHON" "$VALIDATOR" --path "$p" --name "$(basename "$p")" --owner "$OWNER" -; then
: :
else else
echo "✗ [pre-receive] 不合规:$p" >&2 echo "✗ [pre-receive] 不合规:$p" >&2
...@@ -43,6 +45,7 @@ if [ "$RC" -ne 0 ]; then ...@@ -43,6 +45,7 @@ if [ "$RC" -ne 0 ]; then
printf '%s\n' "$BAD_PATHS" | sed '/^$/d' | sed 's/^/ - /' >&2 printf '%s\n' "$BAD_PATHS" | sed '/^$/d' | sed 's/^/ - /' >&2
echo "规则:五大必填节(项目背景/拜访事前准备/本次实际拜访结果/问题清单及下一步计划/其他补充)均须有实际内容;" >&2 echo "规则:五大必填节(项目背景/拜访事前准备/本次实际拜访结果/问题清单及下一步计划/其他补充)均须有实际内容;" >&2
echo "命名:YYYY-MM-DD_客户名_帛丝人员_拜访报告.md,位于 客户/<客户名>/ 下。" >&2 echo "命名:YYYY-MM-DD_客户名_帛丝人员_拜访报告.md,位于 客户/<客户名>/ 下。" >&2
echo "署名:拜访人必须等于提交者本人(谁上传就属谁)。" >&2
exit $RC exit $RC
fi fi
......
...@@ -10,6 +10,7 @@ import sys ...@@ -10,6 +10,7 @@ import sys
import json import json
FILENAME_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})_(.+)_(.+)_拜访报告\.md$") FILENAME_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})_(.+)_(.+)_拜访报告\.md$")
OWNER_IN_TEXT = re.compile(r"拜访人(帛丝人员)\s*[::]\s*([^||\s ]+)")
HTML_COMMENT = re.compile(r"<!--.*?-->", re.S) HTML_COMMENT = re.compile(r"<!--.*?-->", re.S)
PLACEHOLDER_TAG = re.compile(r"<[^>]*>|【[^】]*】|[[^]]*]|\(填写\)|(填写)|待填|填写|xxx", re.S) PLACEHOLDER_TAG = re.compile(r"<[^>]*>|【[^】]*】|[[^]]*]|\(填写\)|(填写)|待填|填写|xxx", re.S)
HEADING = re.compile(r"^\s*#{1,6}\s", re.M) HEADING = re.compile(r"^\s*#{1,6}\s", re.M)
...@@ -74,14 +75,31 @@ def check_content(text): ...@@ -74,14 +75,31 @@ def check_content(text):
return errors return errors
def check_owner(name, text, owner):
"""谁上传就属谁:文件名/正文声明的拜访人必须等于 owner。"""
if not owner:
return []
errors = []
m = FILENAME_RE.match(name)
if m and m.group(3) != owner:
errors.append(f"文件署名与上传者不一致:文件名=“{m.group(3)}”,上传者=“{owner}”")
tm = OWNER_IN_TEXT.search(text)
if tm and tm.group(1).strip() != owner:
errors.append(f"正文拜访人与上传者不一致:正文=“{tm.group(1).strip()}”,上传者=“{owner}”")
return errors
def main(): def main():
args = sys.argv[1:] args = sys.argv[1:]
name = None name = None
path = None path = None
owner = None
if "--name" in args: if "--name" in args:
name = args[args.index("--name") + 1] name = args[args.index("--name") + 1]
if "--path" in args: if "--path" in args:
path = args[args.index("--path") + 1] path = args[args.index("--path") + 1]
if "--owner" in args:
owner = args[args.index("--owner") + 1]
text = sys.stdin.read() text = sys.stdin.read()
...@@ -93,6 +111,7 @@ def main(): ...@@ -93,6 +111,7 @@ def main():
if not m: if not m:
problems.append(f"文件名不符合命名约定:{name}") problems.append(f"文件名不符合命名约定:{name}")
problems += check_content(text) problems += check_content(text)
problems += check_owner(name or "", text, owner)
ok = len(problems) == 0 ok = len(problems) == 0
if "--json" in args: if "--json" in args:
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
``` ```
YYYY-MM-DD_客户名_帛丝人员_拜访报告.md YYYY-MM-DD_客户名_帛丝人员_拜访报告.md
``` ```
例:`客户/丰隆/2026-08-20_丰隆_戴堂亮_拜访报告.md` 例:`客户/丰隆/2026-08-20_丰隆_刘正强_拜访报告.md`
2. 建议从 `_template/拜访报告_模板.md` 复制内容再填。 2. 建议从 `_template/拜访报告_模板.md` 复制内容再填。
3. 把五节里的 `<!-- 填写:… -->` **替换成真实内容**(删除注释,写入内容)。五节: 3. 把五节里的 `<!-- 填写:… -->` **替换成真实内容**(删除注释,写入内容)。五节:
- 一、项目背景 - 一、项目背景
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
- 四、问题清单及下一步计划 - 四、问题清单及下一步计划
- 五、其他补充 - 五、其他补充
4. 正文顶部元数据行:`客户名称 / 拜访人 / 日期` 填上。 4. 正文顶部元数据行:`客户名称 / 拜访人 / 日期` 填上。
5. **署名=你本人**:文件名里的"帛丝人员"以及正文"拜访人(帛丝人员)"必须写**你自己(当前 git 用户名)**。上传时脚本会自动取当前 git 用户比对,写到别人名字会被拒绝。想确认自己的 git 用户名:`git config user.name`。
## 三、提交与推送 ## 三、提交与推送
```bash ```bash
...@@ -33,13 +34,14 @@ git add 客户/ ...@@ -33,13 +34,14 @@ git add 客户/
git commit -m "拜访报告:<客户名> <日期>" git commit -m "拜访报告:<客户名> <日期>"
git push git push
``` ```
- 若格式不合规,push 会被拒绝,屏幕会提示缺哪节/哪项,按提示补全后重新 `git add && git commit && git push`。 - 若格式不合规,push 会被拒绝,屏幕会提示缺哪节/哪项/署名是否本人,按提示补全后重新 `git add && git commit && git push`。
- 也可提前自查: - 也可提前自查(带署名校验)
```bash ```bash
python _validation/validate_report.py "客户/<客户名>/<文件>.md" python _validation/validate_report.py "客户/<客户名>/<文件>.md" --owner "$(git config user.name)"
``` ```
## 四、常见问题 ## 四、常见问题
- **我用的 GitLab / 网页上传**:网页上传不经过本地 hook,无法自动拦截,我只能在你上传后扫描。建议尽量用 git 客户端 push。 - **我用的 GitLab / 网页上传**:网页上传不经过本地 hook,无法自动拦截,我只能在你上传后扫描。建议尽量用 git 客户端 push。
- **想提交但校验说"缺少必填节"**:确认 markdown 里五节标题一个不少,且每节下有真实文字(不是空的注释)。 - **想提交但校验说"缺少必填节"**:确认 markdown 里五节标题一个不少,且每节下有真实文字(不是空的注释)。
- **提示客户名不一致**:检查文件名里的客户名是否和所在文件夹名完全一致。 - **提示客户名不一致**:检查文件名里的客户名是否和所在文件夹名完全一致。
- **提示"署名与上传者不一致"**:说明文件里的拜访人(文件名或正文)写了别人,或你本机 git 用户名没设对。先 `git config user.name` 看你的用户名,再把报告署名改成你自己。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment