From 724a5a6453a05e2f08f889ffebb12546e067a0fc Mon Sep 17 00:00:00 2001 From: kx_guo Date: Fri, 11 Sep 2026 10:17:51 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=8B=B1=E6=96=87=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=B9=B6=E8=B0=83=E6=95=B4=E9=92=89=E9=92=89=E6=9D=83?= =?UTF-8?q?=E9=99=90=E4=B8=8E=E9=A3=8E=E9=99=A9=E9=A2=84=E8=AD=A6=E6=96=87?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- app/services/conversations.py | 4 +- app/services/dingtalk_auth.py | 52 ++++++++++++------- app/services/llm_client.py | 6 +-- app/services/reports.py | 10 ++-- .../20260911_0020_english_user_role.py | 31 +++++++++++ .../20260911_0021_dingtalk_two_role_policy.py | 31 +++++++++++ scripts/create_user.py | 2 +- tests/test_auth.py | 16 ++++-- tests/test_conversations.py | 2 +- 10 files changed, 121 insertions(+), 37 deletions(-) create mode 100644 migrations/versions/20260911_0020_english_user_role.py create mode 100644 migrations/versions/20260911_0021_dingtalk_two_role_policy.py diff --git a/README.md b/README.md index d3631d0..f475e47 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ python scripts/create_user.py --emp-no admin001 --name 管理员 --role admin -- OCR 接口:`POST /api/v1/ocr/recognize` 接收最多 5 个 JPG、PNG 或 PDF 文件,图片调用百度 WebImage,PDF 调用 PaddleOCR-VL 文档解析;返回可编辑字段、检测数值、置信度及标准库判定。`POST /api/v1/ocr/judge` 用于人工修正检测值后重新判定,`GET /api/v1/ocr/files/{file_id}` 通过登录鉴权读取原始附件。启用前配置 `BAIDU_OCR_API_KEY` 和 `BAIDU_OCR_SECRET_KEY`,并执行数据库迁移。 -角色采用管理员 `admin`、产品经理 `product_manager`、审核员 `reviewer`、普通用户 `普通用户` 四类。模型权重由管理员通过 `GET/PATCH /api/v1/model-config` 维护,七维权重合计必须为 100%,修改仅对新评估生效。 +角色采用管理员 `admin`、产品经理 `product_manager`、审核员 `reviewer`、普通用户 `user` 四类。模型权重由管理员通过 `GET/PATCH /api/v1/model-config` 维护,七维权重合计必须为 100%,修改仅对新评估生效。 + +钉钉免登用户按两级规则同步:钉钉管理员或部门主管映射为 `admin`(已包含审核权限),其他钉钉用户统一映射为 `user`。`product_manager` 和 `reviewer` 仅保留给非钉钉账号或后续独立用户管理流程使用。 健康检查:`GET http://127.0.0.1:18474/api/v1/health` diff --git a/app/services/conversations.py b/app/services/conversations.py index f8f523c..30f1138 100644 --- a/app/services/conversations.py +++ b/app/services/conversations.py @@ -156,7 +156,7 @@ class ConversationService: assistant_message = ConversationMessage( conversation_id=conversation.id, role="assistant", - content="已根据当前描述完成风险预估,下面是本轮分析结果。你可以继续补充产品、供应商或标准信息。", + content="已根据当前描述完成风险预警分析,下面是本轮分析结果。你可以继续补充产品、供应商或标准信息。", structured_data=( {"riskResult": risk_result.model_dump(mode="json", by_alias=False)} if risk_result @@ -320,7 +320,7 @@ class ConversationService: { "role": "system", "content": ( - "你是蝶雅产品风险预估助手。请基于用户当前问题和历史上下文回答," + "你是蝶雅产品风险预警助手。请基于用户当前问题和历史上下文回答," "持续理解产品品类、规格、包材、供应商、加工厂和开发阶段。" "回答要具体、简洁、可执行;不要声称已经完成 RAG、OCR 或语音识别。" ), diff --git a/app/services/dingtalk_auth.py b/app/services/dingtalk_auth.py index 4ba1b02..75c6f94 100644 --- a/app/services/dingtalk_auth.py +++ b/app/services/dingtalk_auth.py @@ -125,10 +125,8 @@ class DingTalkAuthService: role = self.map_role( str(detail.get("dept_name") or detail.get("deptName") or ""), str(detail.get("title") or detail.get("position") or ""), - bool(detail.get("is_admin") or detail.get("is_dept_manager")), + self._is_dingtalk_manager(detail), ) - if user is not None and role == "普通用户" and user.role != "普通用户": - role = user.role if user is None: user = UserAccount( emp_no=f"dt_{user_id}"[:20], @@ -153,11 +151,7 @@ class DingTalkAuthService: user.dingtalk_dept_id = int(dept_id) if dept_id is not None else None except (TypeError, ValueError): user.dingtalk_dept_id = None - user.dingtalk_is_dept_manager = bool( - detail.get("is_admin") - or detail.get("is_dept_manager") - or user.dingtalk_is_dept_manager - ) + user.dingtalk_is_dept_manager = self._is_dingtalk_manager(detail) role_codes = detail.get("role_list") or user.dingtalk_role_codes if isinstance(role_codes, (list, dict)): role_codes = json.dumps(role_codes, ensure_ascii=False) @@ -170,15 +164,35 @@ class DingTalkAuthService: @staticmethod def map_role(dept: str, title: str, is_admin: bool = False) -> str: - """Map DingTalk organization snapshots to the four-role administration model.""" + """Map DingTalk users to admin (including review) or regular user.""" - text = f"{dept} {title}".lower() - if is_admin or "it" in text or "信息" in text or "系统" in text: - return "admin" - if "审核" in text or "review" in text: - return "reviewer" - if "产品经理" in text or "product manager" in text or ( - "产品" in dept and ("经理" in title or "主管" in title) - ): - return "product_manager" - return "普通用户" + return "admin" if is_admin else "user" + + @staticmethod + def _is_dingtalk_manager(detail: dict) -> bool: + """Read administrator/department-leader flags from DingTalk API variants.""" + + direct = ( + detail.get("admin"), + detail.get("is_admin"), + detail.get("isAdmin"), + detail.get("is_dept_manager"), + detail.get("isDeptManager"), + ) + if any(value is True or str(value).lower() in {"1", "true"} for value in direct if value is not None): + return True + leaders = detail.get("leader_in_dept") or detail.get("leaderInDept") or [] + if isinstance(leaders, dict): + leaders = [leaders] + if isinstance(leaders, list): + return any( + item is True + or str(item).lower() in {"1", "true"} + or isinstance(item, dict) + and any( + value is True or str(value).lower() in {"1", "true"} + for value in item.values() + ) + for item in leaders + ) + return False diff --git a/app/services/llm_client.py b/app/services/llm_client.py index c33f055..df26f3c 100644 --- a/app/services/llm_client.py +++ b/app/services/llm_client.py @@ -89,7 +89,7 @@ class LLMClient: { "role": "system", "content": ( - "你是蝶雅产品风险预估系统的语义理解模块。" + "你是蝶雅产品风险预警系统的语义理解模块。" "只输出合法 JSON,不要输出 Markdown。请输出 json object。抽取用户输入中的意图和风险评估参数。" "intent 只能是 greet、general、assessment、supplier、material、cases、" "standard、compliance、solution 之一。" @@ -142,7 +142,7 @@ class LLMClient: { "role": "system", "content": ( - "你是蝶雅产品风险预估系统的OCR字段抽取模块,只输出合法JSON对象。" + "你是蝶雅产品风险预警系统的OCR字段抽取模块,只输出合法JSON对象。" "只能提取输入文字明确出现的信息,不得推测或补造。输出字段:product_name、product_category、" "supplier、factory、material_type、stage_new、description、defect_level、occurred_on、confidence、" "measurements。缺失标量用null。defect_level只能是A、B、C或null;occurred_on使用YYYY-MM-DD或null。" @@ -199,7 +199,7 @@ class LLMClient: { "role": "system", "content": ( - "你是蝶雅产品风险预估系统的预防方案生成模块。" + "你是蝶雅产品风险预警系统的预防方案生成模块。" "请根据提供的风险评估数据生成可执行的质量预防建议。" "只输出合法 json object,不要输出 Markdown,格式必须是 " '{"suggestions":["建议1","建议2"]}。' diff --git a/app/services/reports.py b/app/services/reports.py index def24b9..beecf76 100644 --- a/app/services/reports.py +++ b/app/services/reports.py @@ -258,8 +258,8 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes: standard_cards = report_data["standard_cards"] section_shift = 1 if material_cards else 0 story = [ - Paragraph("新品风险预估报告", title), - Paragraph(f"蝶雅产品部 · 风险预估系统 · 自动生成 · {generated_at}", subtitle), + Paragraph("新品风险预警报告", title), + Paragraph(f"蝶雅产品部 · 风险预警系统 · 自动生成 · {generated_at}", subtitle), Paragraph( f"产品/评估对象:{result.subject_id}  供应商:{supplier_name or '未指定'}  报告编号:{report_number}", body, @@ -451,7 +451,7 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes: leftMargin=18 * mm, topMargin=16 * mm, bottomMargin=16 * mm, - title="蝶雅新品风险预估报告", + title="蝶雅新品风险预警报告", ) doc.build(story) return output.getvalue() @@ -471,12 +471,12 @@ def build_risk_report_docx(result: RiskAssessmentResult) -> bytes: normal.font.size = Pt(9) title = document.add_paragraph() title.alignment = WD_ALIGN_PARAGRAPH.CENTER - run = title.add_run("新品风险预估报告") + run = title.add_run("新品风险预警报告") run.bold = True run.font.size = Pt(20) subtitle = document.add_paragraph() subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER - subtitle.add_run(f"蝶雅产品部 · 风险预估系统 · 自动生成 · {result.subject_id}").font.size = Pt(9) + subtitle.add_run(f"蝶雅产品部 · 风险预警系统 · 自动生成 · {result.subject_id}").font.size = Pt(9) report_data = aggregate_report_data(result) standards = report_data["standards"] similar_cases = report_data["similar_cases"] diff --git a/migrations/versions/20260911_0020_english_user_role.py b/migrations/versions/20260911_0020_english_user_role.py new file mode 100644 index 0000000..8df2149 --- /dev/null +++ b/migrations/versions/20260911_0020_english_user_role.py @@ -0,0 +1,31 @@ +"""Normalize the final Chinese system role code to English.""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "20260911_0020" +down_revision: str | None = "20260910_0019" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + connection = op.get_bind() + connection.execute( + sa.text("UPDATE user_accounts SET role = 'user', updated_at = now() WHERE role = '普通用户'") + ) + connection.execute( + sa.text("UPDATE role_permissions SET role = 'user', updated_at = now() WHERE role = '普通用户'") + ) + + +def downgrade() -> None: + connection = op.get_bind() + connection.execute( + sa.text("UPDATE user_accounts SET role = '普通用户', updated_at = now() WHERE role = 'user'") + ) + connection.execute( + sa.text("UPDATE role_permissions SET role = '普通用户', updated_at = now() WHERE role = 'user'") + ) diff --git a/migrations/versions/20260911_0021_dingtalk_two_role_policy.py b/migrations/versions/20260911_0021_dingtalk_two_role_policy.py new file mode 100644 index 0000000..699411e --- /dev/null +++ b/migrations/versions/20260911_0021_dingtalk_two_role_policy.py @@ -0,0 +1,31 @@ +"""Apply the admin/user policy to existing real DingTalk accounts.""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "20260911_0021" +down_revision: str | None = "20260911_0020" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.get_bind().execute( + sa.text( + """ + UPDATE user_accounts + SET role = CASE WHEN dingtalk_is_dept_manager THEN 'admin' ELSE 'user' END, + updated_at = now() + WHERE dingtalk_id IS NOT NULL + AND dingtalk_id NOT LIKE 'local-%' + """ + ) + ) + + +def downgrade() -> None: + # The previous product_manager/reviewer assignment cannot be reconstructed + # reliably; keep the current safe role instead of guessing on downgrade. + pass diff --git a/scripts/create_user.py b/scripts/create_user.py index f4a1b03..e3b9c5e 100644 --- a/scripts/create_user.py +++ b/scripts/create_user.py @@ -47,7 +47,7 @@ def main() -> None: parser.add_argument( "--role", default="admin", - choices=("普通用户", "dev", "procurement", "qc", "ops", "reviewer", "manager", "admin"), + choices=("user", "product_manager", "reviewer", "admin"), ) parser.add_argument("--email") args = parser.parse_args() diff --git a/tests/test_auth.py b/tests/test_auth.py index b91d328..babf21b 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -2,9 +2,15 @@ from app.services.dingtalk_auth import DingTalkAuthService def test_dingtalk_role_mapping() -> None: - assert DingTalkAuthService.map_role("产品部", "产品经理") == "product_manager" - assert DingTalkAuthService.map_role("品控部", "质量主管") == "普通用户" - assert DingTalkAuthService.map_role("采购部", "采购专员") == "普通用户" - assert DingTalkAuthService.map_role("", "审核员") == "reviewer" + assert DingTalkAuthService.map_role("产品部", "产品经理") == "user" + assert DingTalkAuthService.map_role("品控部", "质量主管") == "user" + assert DingTalkAuthService.map_role("采购部", "采购专员") == "user" + assert DingTalkAuthService.map_role("", "审核员") == "user" assert DingTalkAuthService.map_role("", "", is_admin=True) == "admin" - assert DingTalkAuthService.map_role("未知部门", "普通员工") == "普通用户" + assert DingTalkAuthService.map_role("未知部门", "普通员工") == "user" + + +def test_dingtalk_manager_flag_variants() -> None: + assert DingTalkAuthService._is_dingtalk_manager({"admin": True}) + assert DingTalkAuthService._is_dingtalk_manager({"leader_in_dept": [{"leader": True}]}) + assert not DingTalkAuthService._is_dingtalk_manager({"admin": False, "leader_in_dept": []}) diff --git a/tests/test_conversations.py b/tests/test_conversations.py index 1f9a615..a0fb1fe 100644 --- a/tests/test_conversations.py +++ b/tests/test_conversations.py @@ -11,7 +11,7 @@ def test_conversation_message_data_reads_persisted_risk_result() -> None: id=10, conversation_id=3, role="assistant", - content="已完成风险预估。", + content="已完成风险预警。", structured_data={ "riskResult": { "subjectId": "软管新品",