dev #1
@ -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`
|
||||
|
||||
|
||||
@ -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 或语音识别。"
|
||||
),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"]}。'
|
||||
|
||||
@ -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"]
|
||||
|
||||
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(
|
||||
"--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()
|
||||
|
||||
@ -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": []})
|
||||
|
||||
@ -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": "软管新品",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user