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)