360 lines
12 KiB
Python
360 lines
12 KiB
Python
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)
|