ForgeFlow-ERP/backend/app/api/routes/system_extension.py
2026-06-12 16:00:56 +08:00

360 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import delete, desc, select
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.miniapp import (
MiniAppAttendancePoint,
MiniAppNotice,
MiniAppNoticePoint,
MiniAppPersonRole,
MiniAppPersonnel,
)
from app.models.org import AiAssistantConfig, SystemConfig
from app.schemas.database import (
AiAssistantConfigRead,
AiAssistantConfigUpdate,
BroadcastMessageCreate,
BroadcastMessageRead,
SmartOperationReportConfigRead,
SmartOperationReportConfigUpdate,
SystemConfigRead,
SystemConfigUpdate,
)
from app.services.auth import AuthContext, require_authenticated_user, require_system_admin
from app.services.system_config import (
RAW_MATERIAL_LOT_PREFIX_CONFIG_CODE,
RAW_MATERIAL_LOT_PREFIX_DEFAULT,
SMART_OPERATION_REPORT_CONFIG_CODE,
get_smart_operation_report_enabled,
normalize_enabled_config_value,
upsert_smart_operation_report_config,
upsert_system_config,
)
router = APIRouter()
def _active_attendance_point_names(db: Session) -> list[str]:
return [
name
for name in db.scalars(
select(MiniAppAttendancePoint.name)
.where(MiniAppAttendancePoint.is_active.is_(True))
.order_by(MiniAppAttendancePoint.name.asc())
).all()
if str(name or "").strip()
]
def _notice_point_names(db: Session, notice_id: int) -> list[str]:
return [
name
for name in db.scalars(
select(MiniAppNoticePoint.attendance_point_name)
.where(MiniAppNoticePoint.notice_id == notice_id)
.order_by(MiniAppNoticePoint.attendance_point_name.asc())
).all()
if str(name or "").strip()
]
def _replace_notice_points(db: Session, notice_id: int, point_names: list[str] | None) -> None:
selected_names = [str(name or "").strip() for name in point_names or _active_attendance_point_names(db) if str(name or "").strip()]
selected_names = list(dict.fromkeys(selected_names))
if selected_names:
existing_names = set(
db.scalars(select(MiniAppAttendancePoint.name).where(MiniAppAttendancePoint.name.in_(selected_names))).all()
)
missing = [name for name in selected_names if name not in existing_names]
if missing:
raise HTTPException(status_code=400, detail=f"小程序考勤点不存在:{''.join(missing)}")
db.execute(delete(MiniAppNoticePoint).where(MiniAppNoticePoint.notice_id == notice_id))
for name in selected_names:
db.add(MiniAppNoticePoint(notice_id=notice_id, attendance_point_name=name))
def _broadcast_read(db: Session, row: MiniAppNotice) -> BroadcastMessageRead:
return BroadcastMessageRead(
broadcast_id=row.id,
title=row.title,
content=row.content,
attendance_point_names=_notice_point_names(db, row.id),
tone="INFO",
status="ACTIVE" if row.is_active else "PAUSED",
starts_at=None,
ends_at=None,
sort_no=row.sort_order,
created_by=row.created_by,
created_at=row.created_at,
updated_at=row.updated_at,
)
def _get_notice_creator_phone(db: Session) -> str:
admin_phone = db.scalar(
select(MiniAppPersonRole.phone)
.where(MiniAppPersonRole.role.in_(["admin", "manager"]))
.order_by(MiniAppPersonRole.role, MiniAppPersonRole.phone)
.limit(1)
)
if admin_phone:
return admin_phone
first_phone = db.scalar(select(MiniAppPersonnel.phone).order_by(MiniAppPersonnel.phone).limit(1))
if first_phone:
return first_phone
system_person = MiniAppPersonnel(
phone="ERP_SYSTEM",
name="ERP系统",
is_temporary=False,
temporary_expires_at=None,
)
db.add(system_person)
db.flush()
db.add(MiniAppPersonRole(phone=system_person.phone, role="manager"))
return system_person.phone
def _assistant_config_read(row: AiAssistantConfig | None) -> AiAssistantConfigRead:
if not row:
return AiAssistantConfigRead(
assistant_name="嘉恒工艺助手",
provider_name="",
api_base_url="",
api_key=None,
model_name="",
temperature=0.3,
system_prompt="你是宁波嘉恒智能五金 ERP 的企业助手,优先解释业务流程、字段含义、数据异常和操作步骤。",
enabled=False,
config_id=None,
api_key_configured=False,
updated_at=None,
)
return AiAssistantConfigRead(
config_id=row.id,
assistant_name=row.assistant_name,
provider_name=row.provider_name,
api_base_url=row.api_base_url,
api_key=None,
model_name=row.model_name,
temperature=float(row.temperature or 0.3),
system_prompt=row.system_prompt,
enabled=bool(row.enabled),
api_key_configured=bool(row.api_key),
updated_at=row.updated_at,
)
def _raw_material_lot_prefix_read(row: SystemConfig | None = None) -> SystemConfigRead:
if not row:
return SystemConfigRead(
config_code=RAW_MATERIAL_LOT_PREFIX_CONFIG_CODE,
config_name="原材料库存批次号前缀",
config_value=RAW_MATERIAL_LOT_PREFIX_DEFAULT,
remark="原材料库存批次号默认前缀,例如 YL0001",
updated_at=None,
)
return SystemConfigRead.model_validate(row)
def _smart_operation_report_config_read(row: SystemConfig | None, db: Session) -> SmartOperationReportConfigRead:
enabled = get_smart_operation_report_enabled(db) if row is None else normalize_enabled_config_value(row.config_value) == "开启"
return SmartOperationReportConfigRead(
config_code=SMART_OPERATION_REPORT_CONFIG_CODE,
config_name="对接智能报工小程序",
enabled=enabled,
config_value="开启" if enabled else "关闭",
remark=row.remark if row else "开启后显示工序报工并按小程序报工数据计算生产进度关闭后按ERP入库数据反推生产进度",
updated_at=row.updated_at if row else None,
)
def _active_broadcast_rows(db: Session, limit: int = 30) -> list[MiniAppNotice]:
return db.scalars(
select(MiniAppNotice)
.where(MiniAppNotice.is_active.is_(True))
.order_by(MiniAppNotice.sort_order.asc(), desc(MiniAppNotice.updated_at), desc(MiniAppNotice.id))
.limit(limit)
).all()
@router.get("/active-broadcast", response_model=BroadcastMessageRead | None)
def active_broadcast(
context: AuthContext = Depends(require_authenticated_user),
db: Session = Depends(get_db),
) -> BroadcastMessageRead | None:
_ = context
rows = _active_broadcast_rows(db, limit=1)
row = rows[0] if rows else None
return _broadcast_read(db, row) if row else None
@router.get("/active-broadcasts", response_model=list[BroadcastMessageRead])
def active_broadcasts(
limit: int = Query(default=30, ge=1, le=100),
context: AuthContext = Depends(require_authenticated_user),
db: Session = Depends(get_db),
) -> list[BroadcastMessageRead]:
_ = context
return [_broadcast_read(db, row) for row in _active_broadcast_rows(db, limit=limit)]
@router.get("/broadcasts", response_model=list[BroadcastMessageRead])
def list_broadcasts(
limit: int = Query(default=100, ge=1, le=300),
context: AuthContext = Depends(require_system_admin),
db: Session = Depends(get_db),
) -> list[BroadcastMessageRead]:
_ = context
rows = db.scalars(select(MiniAppNotice).order_by(MiniAppNotice.sort_order.asc(), desc(MiniAppNotice.updated_at)).limit(limit)).all()
return [_broadcast_read(db, row) for row in rows]
@router.post("/broadcasts", response_model=BroadcastMessageRead)
def create_broadcast(
payload: BroadcastMessageCreate,
context: AuthContext = Depends(require_system_admin),
db: Session = Depends(get_db),
) -> BroadcastMessageRead:
creator_phone = _get_notice_creator_phone(db)
row = MiniAppNotice(
title=payload.title,
content=payload.content,
sort_order=payload.sort_no,
is_active=payload.status == "ACTIVE",
created_by=creator_phone,
)
db.add(row)
db.flush()
_replace_notice_points(db, row.id, payload.attendance_point_names)
db.commit()
db.refresh(row)
return _broadcast_read(db, row)
@router.put("/broadcasts/{broadcast_id}", response_model=BroadcastMessageRead)
def update_broadcast(
broadcast_id: int,
payload: BroadcastMessageCreate,
context: AuthContext = Depends(require_system_admin),
db: Session = Depends(get_db),
) -> BroadcastMessageRead:
_ = context
row = db.get(MiniAppNotice, broadcast_id)
if not row:
raise HTTPException(status_code=404, detail="广播内容不存在")
row.title = payload.title
row.content = payload.content
row.sort_order = payload.sort_no
row.is_active = payload.status == "ACTIVE"
db.add(row)
_replace_notice_points(db, row.id, payload.attendance_point_names)
db.commit()
db.refresh(row)
return _broadcast_read(db, row)
@router.get("/assistant-config", response_model=AiAssistantConfigRead)
def get_assistant_config(
context: AuthContext = Depends(require_system_admin),
db: Session = Depends(get_db),
) -> AiAssistantConfigRead:
_ = context
row = db.scalars(select(AiAssistantConfig).order_by(desc(AiAssistantConfig.id)).limit(1)).first()
return _assistant_config_read(row)
@router.put("/assistant-config", response_model=AiAssistantConfigRead)
def save_assistant_config(
payload: AiAssistantConfigUpdate,
context: AuthContext = Depends(require_system_admin),
db: Session = Depends(get_db),
) -> AiAssistantConfigRead:
row = db.scalars(select(AiAssistantConfig).order_by(desc(AiAssistantConfig.id)).limit(1)).first()
if not row:
row = AiAssistantConfig(created_by=context.user.id)
row.assistant_name = payload.assistant_name
row.provider_name = payload.provider_name
row.api_base_url = payload.api_base_url
if payload.api_key is not None:
row.api_key = payload.api_key
row.model_name = payload.model_name
row.temperature = payload.temperature
row.system_prompt = payload.system_prompt
row.enabled = 1 if payload.enabled else 0
row.updated_by = context.user.id
db.add(row)
db.commit()
db.refresh(row)
return _assistant_config_read(row)
@router.get("/raw-material-lot-prefix", response_model=SystemConfigRead)
def get_raw_material_lot_prefix_config(
context: AuthContext = Depends(require_system_admin),
db: Session = Depends(get_db),
) -> SystemConfigRead:
_ = context
row = db.scalar(
select(SystemConfig).where(
SystemConfig.config_code == RAW_MATERIAL_LOT_PREFIX_CONFIG_CODE,
SystemConfig.status == "ACTIVE",
)
)
return _raw_material_lot_prefix_read(row)
@router.put("/raw-material-lot-prefix", response_model=SystemConfigRead)
def save_raw_material_lot_prefix_config(
payload: SystemConfigUpdate,
context: AuthContext = Depends(require_system_admin),
db: Session = Depends(get_db),
) -> SystemConfigRead:
row = upsert_system_config(
db,
config_code=RAW_MATERIAL_LOT_PREFIX_CONFIG_CODE,
config_name="原材料库存批次号前缀",
config_value=payload.config_value,
remark=payload.remark or "原材料库存批次号默认前缀,例如 YL0001",
updated_by=context.user.id,
)
db.commit()
db.refresh(row)
return _raw_material_lot_prefix_read(row)
@router.get("/smart-operation-report", response_model=SmartOperationReportConfigRead)
def get_smart_operation_report_config(
context: AuthContext = Depends(require_authenticated_user),
db: Session = Depends(get_db),
) -> SmartOperationReportConfigRead:
_ = context
row = db.scalar(
select(SystemConfig).where(
SystemConfig.config_code == SMART_OPERATION_REPORT_CONFIG_CODE,
SystemConfig.status == "ACTIVE",
)
)
return _smart_operation_report_config_read(row, db)
@router.put("/smart-operation-report", response_model=SmartOperationReportConfigRead)
def save_smart_operation_report_config(
payload: SmartOperationReportConfigUpdate,
context: AuthContext = Depends(require_system_admin),
db: Session = Depends(get_db),
) -> SmartOperationReportConfigRead:
row = upsert_smart_operation_report_config(
db,
enabled=payload.enabled,
updated_by=context.user.id,
remark=payload.remark,
)
db.commit()
db.refresh(row)
return _smart_operation_report_config_read(row, db)