统一英文角色并调整钉钉权限与风险预警文案
This commit is contained in:
parent
5bdb2852e5
commit
724a5a6453
@ -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`,并执行数据库迁移。
|
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`
|
健康检查:`GET http://127.0.0.1:18474/api/v1/health`
|
||||||
|
|
||||||
|
|||||||
@ -156,7 +156,7 @@ class ConversationService:
|
|||||||
assistant_message = ConversationMessage(
|
assistant_message = ConversationMessage(
|
||||||
conversation_id=conversation.id,
|
conversation_id=conversation.id,
|
||||||
role="assistant",
|
role="assistant",
|
||||||
content="已根据当前描述完成风险预估,下面是本轮分析结果。你可以继续补充产品、供应商或标准信息。",
|
content="已根据当前描述完成风险预警分析,下面是本轮分析结果。你可以继续补充产品、供应商或标准信息。",
|
||||||
structured_data=(
|
structured_data=(
|
||||||
{"riskResult": risk_result.model_dump(mode="json", by_alias=False)}
|
{"riskResult": risk_result.model_dump(mode="json", by_alias=False)}
|
||||||
if risk_result
|
if risk_result
|
||||||
@ -320,7 +320,7 @@ class ConversationService:
|
|||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": (
|
"content": (
|
||||||
"你是蝶雅产品风险预估助手。请基于用户当前问题和历史上下文回答,"
|
"你是蝶雅产品风险预警助手。请基于用户当前问题和历史上下文回答,"
|
||||||
"持续理解产品品类、规格、包材、供应商、加工厂和开发阶段。"
|
"持续理解产品品类、规格、包材、供应商、加工厂和开发阶段。"
|
||||||
"回答要具体、简洁、可执行;不要声称已经完成 RAG、OCR 或语音识别。"
|
"回答要具体、简洁、可执行;不要声称已经完成 RAG、OCR 或语音识别。"
|
||||||
),
|
),
|
||||||
|
|||||||
@ -125,10 +125,8 @@ class DingTalkAuthService:
|
|||||||
role = self.map_role(
|
role = self.map_role(
|
||||||
str(detail.get("dept_name") or detail.get("deptName") or ""),
|
str(detail.get("dept_name") or detail.get("deptName") or ""),
|
||||||
str(detail.get("title") or detail.get("position") 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:
|
if user is None:
|
||||||
user = UserAccount(
|
user = UserAccount(
|
||||||
emp_no=f"dt_{user_id}"[:20],
|
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
|
user.dingtalk_dept_id = int(dept_id) if dept_id is not None else None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
user.dingtalk_dept_id = None
|
user.dingtalk_dept_id = None
|
||||||
user.dingtalk_is_dept_manager = bool(
|
user.dingtalk_is_dept_manager = self._is_dingtalk_manager(detail)
|
||||||
detail.get("is_admin")
|
|
||||||
or detail.get("is_dept_manager")
|
|
||||||
or user.dingtalk_is_dept_manager
|
|
||||||
)
|
|
||||||
role_codes = detail.get("role_list") or user.dingtalk_role_codes
|
role_codes = detail.get("role_list") or user.dingtalk_role_codes
|
||||||
if isinstance(role_codes, (list, dict)):
|
if isinstance(role_codes, (list, dict)):
|
||||||
role_codes = json.dumps(role_codes, ensure_ascii=False)
|
role_codes = json.dumps(role_codes, ensure_ascii=False)
|
||||||
@ -170,15 +164,35 @@ class DingTalkAuthService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def map_role(dept: str, title: str, is_admin: bool = False) -> str:
|
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()
|
return "admin" if is_admin else "user"
|
||||||
if is_admin or "it" in text or "信息" in text or "系统" in text:
|
|
||||||
return "admin"
|
@staticmethod
|
||||||
if "审核" in text or "review" in text:
|
def _is_dingtalk_manager(detail: dict) -> bool:
|
||||||
return "reviewer"
|
"""Read administrator/department-leader flags from DingTalk API variants."""
|
||||||
if "产品经理" in text or "product manager" in text or (
|
|
||||||
"产品" in dept and ("经理" in title or "主管" in title)
|
direct = (
|
||||||
):
|
detail.get("admin"),
|
||||||
return "product_manager"
|
detail.get("is_admin"),
|
||||||
return "普通用户"
|
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
|
||||||
|
|||||||
@ -89,7 +89,7 @@ class LLMClient:
|
|||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": (
|
"content": (
|
||||||
"你是蝶雅产品风险预估系统的语义理解模块。"
|
"你是蝶雅产品风险预警系统的语义理解模块。"
|
||||||
"只输出合法 JSON,不要输出 Markdown。请输出 json object。抽取用户输入中的意图和风险评估参数。"
|
"只输出合法 JSON,不要输出 Markdown。请输出 json object。抽取用户输入中的意图和风险评估参数。"
|
||||||
"intent 只能是 greet、general、assessment、supplier、material、cases、"
|
"intent 只能是 greet、general、assessment、supplier、material、cases、"
|
||||||
"standard、compliance、solution 之一。"
|
"standard、compliance、solution 之一。"
|
||||||
@ -142,7 +142,7 @@ class LLMClient:
|
|||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": (
|
"content": (
|
||||||
"你是蝶雅产品风险预估系统的OCR字段抽取模块,只输出合法JSON对象。"
|
"你是蝶雅产品风险预警系统的OCR字段抽取模块,只输出合法JSON对象。"
|
||||||
"只能提取输入文字明确出现的信息,不得推测或补造。输出字段:product_name、product_category、"
|
"只能提取输入文字明确出现的信息,不得推测或补造。输出字段:product_name、product_category、"
|
||||||
"supplier、factory、material_type、stage_new、description、defect_level、occurred_on、confidence、"
|
"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。"
|
"measurements。缺失标量用null。defect_level只能是A、B、C或null;occurred_on使用YYYY-MM-DD或null。"
|
||||||
@ -199,7 +199,7 @@ class LLMClient:
|
|||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": (
|
"content": (
|
||||||
"你是蝶雅产品风险预估系统的预防方案生成模块。"
|
"你是蝶雅产品风险预警系统的预防方案生成模块。"
|
||||||
"请根据提供的风险评估数据生成可执行的质量预防建议。"
|
"请根据提供的风险评估数据生成可执行的质量预防建议。"
|
||||||
"只输出合法 json object,不要输出 Markdown,格式必须是 "
|
"只输出合法 json object,不要输出 Markdown,格式必须是 "
|
||||||
'{"suggestions":["建议1","建议2"]}。'
|
'{"suggestions":["建议1","建议2"]}。'
|
||||||
|
|||||||
@ -258,8 +258,8 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes:
|
|||||||
standard_cards = report_data["standard_cards"]
|
standard_cards = report_data["standard_cards"]
|
||||||
section_shift = 1 if material_cards else 0
|
section_shift = 1 if material_cards else 0
|
||||||
story = [
|
story = [
|
||||||
Paragraph("新品风险预估报告", title),
|
Paragraph("新品风险预警报告", title),
|
||||||
Paragraph(f"蝶雅产品部 · 风险预估系统 · 自动生成 · {generated_at}", subtitle),
|
Paragraph(f"蝶雅产品部 · 风险预警系统 · 自动生成 · {generated_at}", subtitle),
|
||||||
Paragraph(
|
Paragraph(
|
||||||
f"产品/评估对象:{result.subject_id} 供应商:{supplier_name or '未指定'} 报告编号:{report_number}",
|
f"产品/评估对象:{result.subject_id} 供应商:{supplier_name or '未指定'} 报告编号:{report_number}",
|
||||||
body,
|
body,
|
||||||
@ -451,7 +451,7 @@ def build_risk_report_pdf(result: RiskAssessmentResult) -> bytes:
|
|||||||
leftMargin=18 * mm,
|
leftMargin=18 * mm,
|
||||||
topMargin=16 * mm,
|
topMargin=16 * mm,
|
||||||
bottomMargin=16 * mm,
|
bottomMargin=16 * mm,
|
||||||
title="蝶雅新品风险预估报告",
|
title="蝶雅新品风险预警报告",
|
||||||
)
|
)
|
||||||
doc.build(story)
|
doc.build(story)
|
||||||
return output.getvalue()
|
return output.getvalue()
|
||||||
@ -471,12 +471,12 @@ def build_risk_report_docx(result: RiskAssessmentResult) -> bytes:
|
|||||||
normal.font.size = Pt(9)
|
normal.font.size = Pt(9)
|
||||||
title = document.add_paragraph()
|
title = document.add_paragraph()
|
||||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
run = title.add_run("新品风险预估报告")
|
run = title.add_run("新品风险预警报告")
|
||||||
run.bold = True
|
run.bold = True
|
||||||
run.font.size = Pt(20)
|
run.font.size = Pt(20)
|
||||||
subtitle = document.add_paragraph()
|
subtitle = document.add_paragraph()
|
||||||
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
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)
|
report_data = aggregate_report_data(result)
|
||||||
standards = report_data["standards"]
|
standards = report_data["standards"]
|
||||||
similar_cases = report_data["similar_cases"]
|
similar_cases = report_data["similar_cases"]
|
||||||
|
|||||||
31
migrations/versions/20260911_0020_english_user_role.py
Normal file
31
migrations/versions/20260911_0020_english_user_role.py
Normal file
@ -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'")
|
||||||
|
)
|
||||||
@ -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
|
||||||
@ -47,7 +47,7 @@ def main() -> None:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--role",
|
"--role",
|
||||||
default="admin",
|
default="admin",
|
||||||
choices=("普通用户", "dev", "procurement", "qc", "ops", "reviewer", "manager", "admin"),
|
choices=("user", "product_manager", "reviewer", "admin"),
|
||||||
)
|
)
|
||||||
parser.add_argument("--email")
|
parser.add_argument("--email")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
@ -2,9 +2,15 @@ from app.services.dingtalk_auth import DingTalkAuthService
|
|||||||
|
|
||||||
|
|
||||||
def test_dingtalk_role_mapping() -> None:
|
def test_dingtalk_role_mapping() -> None:
|
||||||
assert DingTalkAuthService.map_role("产品部", "产品经理") == "product_manager"
|
assert DingTalkAuthService.map_role("产品部", "产品经理") == "user"
|
||||||
assert DingTalkAuthService.map_role("品控部", "质量主管") == "普通用户"
|
assert DingTalkAuthService.map_role("品控部", "质量主管") == "user"
|
||||||
assert DingTalkAuthService.map_role("采购部", "采购专员") == "普通用户"
|
assert DingTalkAuthService.map_role("采购部", "采购专员") == "user"
|
||||||
assert DingTalkAuthService.map_role("", "审核员") == "reviewer"
|
assert DingTalkAuthService.map_role("", "审核员") == "user"
|
||||||
assert DingTalkAuthService.map_role("", "", is_admin=True) == "admin"
|
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": []})
|
||||||
|
|||||||
@ -11,7 +11,7 @@ def test_conversation_message_data_reads_persisted_risk_result() -> None:
|
|||||||
id=10,
|
id=10,
|
||||||
conversation_id=3,
|
conversation_id=3,
|
||||||
role="assistant",
|
role="assistant",
|
||||||
content="已完成风险预估。",
|
content="已完成风险预警。",
|
||||||
structured_data={
|
structured_data={
|
||||||
"riskResult": {
|
"riskResult": {
|
||||||
"subjectId": "软管新品",
|
"subjectId": "软管新品",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user