refactor(core): complete model and util move

This commit is contained in:
caoqianming 2026-04-15 16:08:58 +08:00
parent b34c898089
commit a1e2536844
27 changed files with 51 additions and 595 deletions

View File

@ -18,9 +18,9 @@ use std::{
use tokio::task::JoinHandle;
use tokio::sync::RwLock;
use uuid::Uuid;
use plc_platform_core::model::{PointSubscriptionInfo, ScanMode};
use crate::{
model::{PointSubscriptionInfo, ScanMode},
telemetry::PointMonitorInfo,
};
@ -1057,7 +1057,7 @@ impl ConnectionManager {
quality: crate::telemetry::PointQuality::Good,
protocol: "opcua".to_string(),
timestamp: Some(Utc::now()),
scan_mode: crate::model::ScanMode::Poll,
scan_mode: ScanMode::Poll,
},
)) {
tracing::warn!(

View File

@ -138,7 +138,10 @@ async fn unit_task(state: AppState, store: Arc<ControlRuntimeStore>, unit_id: Uu
// Wait run_time_sec. run_time_sec == 0 means run without a time limit
// (relies on acc_time_sec to eventually stop). Treat as a very long phase.
let secs = if unit.run_time_sec > 0 { unit.run_time_sec } else { i32::MAX };
let unit_for_wait = crate::model::ControlUnit { run_time_sec: secs, ..unit.clone() };
let unit_for_wait = plc_platform_core::model::ControlUnit {
run_time_sec: secs,
..unit.clone()
};
if !wait_phase(&state, &store, &unit_for_wait, &all_roles, &notify, &mut fault_tick).await {
continue;
}
@ -228,7 +231,7 @@ async fn unit_task(state: AppState, store: Arc<ControlRuntimeStore>, unit_id: Uu
async fn wait_phase(
state: &AppState,
store: &ControlRuntimeStore,
unit: &crate::model::ControlUnit,
unit: &plc_platform_core::model::ControlUnit,
all_roles: &[(Uuid, HashMap<String, EquipmentRolePoint>)],
notify: &Arc<Notify>,
fault_tick: &mut tokio::time::Interval,
@ -279,7 +282,7 @@ async fn push_ws(state: &AppState, runtime: &UnitRuntime) {
async fn check_fault_comm(
state: &AppState,
runtime: &mut UnitRuntime,
unit: &crate::model::ControlUnit,
unit: &plc_platform_core::model::ControlUnit,
all_roles: &[(Uuid, HashMap<String, EquipmentRolePoint>)],
) -> bool {
let monitor = state
@ -431,7 +434,7 @@ async fn load_equipment_maps(state: &AppState, unit_id: Uuid) -> Result<EquipMap
fn build_equipment_maps(
unit_id: Uuid,
equipment_list: &[crate::model::Equipment],
equipment_list: &[plc_platform_core::model::Equipment],
mut role_points_by_equipment: HashMap<Uuid, Vec<EquipmentRolePoint>>,
) -> EquipMaps {
let mut kind_roles: HashMap<String, HashMap<String, EquipmentRolePoint>> = HashMap::new();
@ -496,7 +499,7 @@ fn find_cmd(
#[cfg(test)]
mod tests {
use super::build_equipment_maps;
use crate::model::Equipment;
use plc_platform_core::model::Equipment;
use crate::service::EquipmentRolePoint;
use chrono::Utc;
use std::collections::HashMap;

View File

@ -170,7 +170,7 @@ pub async fn patch_signal(state: &AppState, point_id: Uuid, value_on: bool) {
source_id: Uuid::nil(),
point_id,
client_handle: 0,
scan_mode: crate::model::ScanMode::Poll,
scan_mode: plc_platform_core::model::ScanMode::Poll,
timestamp: Some(chrono::Utc::now()),
quality: PointQuality::Good,
value: Some(value),

View File

@ -1,6 +1,7 @@
use std::collections::HashMap;
use tokio::sync::mpsc;
use uuid::Uuid;
use plc_platform_core::model::EventRecord;
const CONTROL_EVENT_CHANNEL_CAPACITY: usize = 1024;
const TELEMETRY_EVENT_CHANNEL_CAPACITY: usize = 4096;
@ -448,7 +449,7 @@ async fn persist_event_if_needed(
return;
};
let inserted = sqlx::query_as::<_, crate::model::EventRecord>(
let inserted = sqlx::query_as::<_, EventRecord>(
r#"
INSERT INTO event (event_type, level, unit_id, equipment_id, source_id, message, payload)
VALUES ($1, $2, $3, $4, $5, $6, $7)

View File

@ -50,14 +50,14 @@ pub struct GetUnitListQuery {
#[derive(serde::Serialize)]
pub struct UnitEquipmentItem {
#[serde(flatten)]
pub equipment: crate::model::Equipment,
pub equipment: plc_platform_core::model::Equipment,
pub role_points: Vec<crate::handler::equipment::SignalRolePoint>,
}
#[derive(serde::Serialize)]
pub struct UnitWithRuntime {
#[serde(flatten)]
pub unit: crate::model::ControlUnit,
pub unit: plc_platform_core::model::ControlUnit,
pub runtime: Option<crate::control::runtime::UnitRuntime>,
pub equipments: Vec<UnitEquipmentItem>,
}
@ -250,21 +250,21 @@ pub async fn get_unit(
#[derive(serde::Serialize)]
pub struct PointDetail {
#[serde(flatten)]
pub point: crate::model::Point,
pub point: plc_platform_core::model::Point,
pub point_monitor: Option<crate::telemetry::PointMonitorInfo>,
}
#[derive(serde::Serialize)]
pub struct EquipmentDetail {
#[serde(flatten)]
pub equipment: crate::model::Equipment,
pub equipment: plc_platform_core::model::Equipment,
pub points: Vec<PointDetail>,
}
#[derive(serde::Serialize)]
pub struct UnitDetail {
#[serde(flatten)]
pub unit: crate::model::ControlUnit,
pub unit: plc_platform_core::model::ControlUnit,
pub runtime: Option<crate::control::runtime::UnitRuntime>,
pub equipments: Vec<EquipmentDetail>,
}

View File

@ -3,7 +3,7 @@ use axum::{
response::IntoResponse,
};
use crate::util::response::ApiErr;
use plc_platform_core::util::response::ApiErr;
pub async fn get_api_md() -> Result<impl IntoResponse, ApiErr> {
let content = tokio::fs::read_to_string("API.md")

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;
use validator::Validate;
use crate::util::{
use plc_platform_core::util::{
pagination::{PaginatedResponse, PaginationParams},
response::ApiErr,
};
@ -44,7 +44,7 @@ pub struct SignalRolePoint {
#[derive(Serialize)]
pub struct EquipmentListItem {
#[serde(flatten)]
pub equipment: crate::model::Equipment,
pub equipment: plc_platform_core::model::Equipment,
pub point_count: i64,
pub role_points: Vec<SignalRolePoint>,
}

View File

@ -20,7 +20,7 @@ use tokio::{
time::{Duration, interval},
};
use crate::util::response::ApiErr;
use plc_platform_core::util::response::ApiErr;
const LOG_DIR: &str = "./logs";
const DEFAULT_TAIL_LINES: usize = 200;

View File

@ -5,8 +5,8 @@ use sqlx::types::Json as SqlxJson;
use uuid::Uuid;
use validator::Validate;
use crate::model::Page;
use crate::util::response::ApiErr;
use plc_platform_core::model::Page;
use plc_platform_core::util::response::ApiErr;
use crate::AppState;
#[derive(Deserialize, Validate)]

View File

@ -11,7 +11,7 @@ use std::collections::{HashMap, HashSet};
use uuid::Uuid;
use validator::Validate;
use crate::util::{
use plc_platform_core::util::{
pagination::{PaginatedResponse, PaginationParams},
response::ApiErr,
};
@ -56,7 +56,7 @@ pub struct GetPointHistoryQuery {
#[derive(Serialize)]
pub struct PointHistoryItem {
#[serde(serialize_with = "crate::util::datetime::option_utc_to_local_str")]
#[serde(serialize_with = "plc_platform_core::util::datetime::option_utc_to_local_str")]
pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
pub quality: crate::telemetry::PointQuality,
pub value: Option<crate::telemetry::DataValue>,

View File

@ -11,7 +11,7 @@ use opcua::types::ReferenceTypeId;
use opcua::client::Session;
use std::collections::{HashMap, VecDeque};
use crate::util::response::ApiErr;
use plc_platform_core::util::response::ApiErr;
use crate::{AppState, model::{Node, Source}};
use anyhow::{Context};
@ -61,7 +61,7 @@ pub struct SourceWithStatus {
pub source: SourcePublic,
pub is_connected: bool,
pub last_error: Option<String>,
#[serde(serialize_with = "crate::util::datetime::option_utc_to_local_str")]
#[serde(serialize_with = "plc_platform_core::util::datetime::option_utc_to_local_str")]
pub last_time: Option<DateTime<Utc>>,
}
@ -74,9 +74,9 @@ pub struct SourcePublic {
pub security_policy: Option<String>,
pub security_mode: Option<String>,
pub enabled: bool,
#[serde(serialize_with = "crate::util::datetime::utc_to_local_str")]
#[serde(serialize_with = "plc_platform_core::util::datetime::utc_to_local_str")]
pub created_at: DateTime<Utc>,
#[serde(serialize_with = "crate::util::datetime::utc_to_local_str")]
#[serde(serialize_with = "plc_platform_core::util::datetime::utc_to_local_str")]
pub updated_at: DateTime<Utc>,
}

View File

@ -3,7 +3,10 @@ use serde::Deserialize;
use uuid::Uuid;
use validator::Validate;
use crate::util::{response::ApiErr, pagination::{PaginatedResponse, PaginationParams}};
use plc_platform_core::util::{
pagination::{PaginatedResponse, PaginationParams},
response::ApiErr,
};
use crate::{AppState};
/// 获取所有标签

View File

@ -7,10 +7,8 @@ mod db;
mod event;
mod handler;
mod middleware;
mod model;
mod service;
mod telemetry;
mod util;
mod websocket;
use axum::{
routing::{get, post, put},
@ -48,8 +46,9 @@ pub struct AppState {
#[tokio::main]
async fn main() {
dotenv::dotenv().ok();
util::log::init_logger();
let _single_instance = match util::single_instance::try_acquire("PLCControl.Gateway") {
plc_platform_core::util::log::init_logger();
let _single_instance =
match plc_platform_core::util::single_instance::try_acquire("PLCControl.Gateway") {
Ok(guard) => guard,
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
tracing::warn!("Another PLC Control instance is already running");

View File

@ -1,178 +0,0 @@
use crate::util::datetime::utc_to_local_str;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::types::Json;
use sqlx::FromRow;
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ScanMode {
Poll,
Subscribe,
}
impl ScanMode {
pub fn as_str(&self) -> &'static str {
match self {
ScanMode::Poll => "poll",
ScanMode::Subscribe => "subscribe",
}
}
}
impl From<ScanMode> for String {
fn from(mode: ScanMode) -> Self {
mode.as_str().to_string()
}
}
impl std::fmt::Display for ScanMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl std::str::FromStr for ScanMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"poll" => Ok(ScanMode::Poll),
"subscribe" => Ok(ScanMode::Subscribe),
_ => Err(format!("Invalid scan mode: {}", s)),
}
}
}
#[derive(Debug, Serialize, Deserialize, FromRow, Clone)]
pub struct Source {
pub id: Uuid,
pub name: String,
pub protocol: String, // opcua, modbus
pub endpoint: String,
pub security_policy: Option<String>,
pub security_mode: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
pub enabled: bool,
#[serde(serialize_with = "utc_to_local_str")]
pub created_at: DateTime<Utc>,
#[serde(serialize_with = "utc_to_local_str")]
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
#[allow(dead_code)]
pub struct Node {
pub id: Uuid,
pub source_id: Uuid,
pub external_id: String, // ns=2;s=Temperature
// comment fixed
pub namespace_uri: Option<String>,
pub namespace_index: Option<i32>,
pub identifier_type: Option<String>, // i/s/g/b
pub identifier: Option<String>,
pub browse_name: String,
pub display_name: Option<String>,
pub node_class: String, // Object/Variable/Method coil/input topic
pub parent_id: Option<Uuid>,
#[serde(serialize_with = "utc_to_local_str")]
pub created_at: DateTime<Utc>,
#[serde(serialize_with = "utc_to_local_str")]
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
#[allow(dead_code)]
pub struct Point {
pub id: Uuid,
pub node_id: Uuid,
pub name: String,
pub description: Option<String>,
pub unit: Option<String>,
pub tag_id: Option<Uuid>,
pub equipment_id: Option<Uuid>,
pub signal_role: Option<String>,
#[serde(serialize_with = "utc_to_local_str")]
pub created_at: DateTime<Utc>,
#[serde(serialize_with = "utc_to_local_str")]
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone)]
pub struct PointSubscriptionInfo {
pub point_id: Uuid,
pub external_id: String,
}
#[derive(Debug, Serialize, Deserialize, FromRow, Clone)]
pub struct Tag {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
#[serde(serialize_with = "utc_to_local_str")]
pub created_at: DateTime<Utc>,
#[serde(serialize_with = "utc_to_local_str")]
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow, Clone)]
pub struct Equipment {
pub id: Uuid,
pub unit_id: Option<Uuid>,
pub code: String,
pub name: String,
pub kind: Option<String>,
pub description: Option<String>,
#[serde(serialize_with = "utc_to_local_str")]
pub created_at: DateTime<Utc>,
#[serde(serialize_with = "utc_to_local_str")]
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow, Clone)]
pub struct ControlUnit {
pub id: Uuid,
pub code: String,
pub name: String,
pub description: Option<String>,
pub enabled: bool,
pub run_time_sec: i32,
pub stop_time_sec: i32,
pub acc_time_sec: i32,
pub bl_time_sec: i32,
pub require_manual_ack_after_fault: bool,
#[serde(serialize_with = "utc_to_local_str")]
pub created_at: DateTime<Utc>,
#[serde(serialize_with = "utc_to_local_str")]
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow, Clone)]
pub struct EventRecord {
pub id: Uuid,
pub event_type: String,
pub level: String,
pub unit_id: Option<Uuid>,
pub equipment_id: Option<Uuid>,
pub source_id: Option<Uuid>,
pub message: String,
pub payload: Option<Json<serde_json::Value>>,
#[serde(serialize_with = "utc_to_local_str")]
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow, Clone)]
pub struct Page {
pub id: Uuid,
pub name: String,
pub data: Json<HashMap<String, Uuid>>,
#[serde(serialize_with = "utc_to_local_str")]
pub created_at: DateTime<Utc>,
#[serde(serialize_with = "utc_to_local_str")]
pub updated_at: DateTime<Utc>,
}

View File

@ -1,4 +1,4 @@
use crate::model::{ControlUnit, EventRecord};
use plc_platform_core::model::{ControlUnit, EventRecord};
use sqlx::{PgPool, QueryBuilder, Row};
use uuid::Uuid;
@ -337,7 +337,7 @@ pub async fn get_all_enabled_units(pool: &PgPool) -> Result<Vec<ControlUnit>, sq
pub async fn get_equipment_by_unit_ids(
pool: &PgPool,
unit_ids: &[Uuid],
) -> Result<Vec<crate::model::Equipment>, sqlx::Error> {
) -> Result<Vec<plc_platform_core::model::Equipment>, sqlx::Error> {
if unit_ids.is_empty() {
return Ok(vec![]);
}
@ -345,7 +345,7 @@ pub async fn get_equipment_by_unit_ids(
"SELECT * FROM equipment WHERE unit_id = ANY($1) ORDER BY {}",
equipment_order_clause_with_unit()
);
sqlx::query_as::<_, crate::model::Equipment>(&sql)
sqlx::query_as::<_, plc_platform_core::model::Equipment>(&sql)
.bind(unit_ids)
.fetch_all(pool)
.await
@ -354,12 +354,12 @@ pub async fn get_equipment_by_unit_ids(
pub async fn get_equipment_by_unit_id(
pool: &PgPool,
unit_id: Uuid,
) -> Result<Vec<crate::model::Equipment>, sqlx::Error> {
) -> Result<Vec<plc_platform_core::model::Equipment>, sqlx::Error> {
let sql = format!(
"SELECT * FROM equipment WHERE unit_id = $1 ORDER BY {}",
unit_order_clause()
);
sqlx::query_as::<_, crate::model::Equipment>(&sql)
sqlx::query_as::<_, plc_platform_core::model::Equipment>(&sql)
.bind(unit_id)
.fetch_all(pool)
.await
@ -368,11 +368,11 @@ pub async fn get_equipment_by_unit_id(
pub async fn get_points_by_equipment_ids(
pool: &PgPool,
equipment_ids: &[Uuid],
) -> Result<Vec<crate::model::Point>, sqlx::Error> {
) -> Result<Vec<plc_platform_core::model::Point>, sqlx::Error> {
if equipment_ids.is_empty() {
return Ok(vec![]);
}
sqlx::query_as::<_, crate::model::Point>(
sqlx::query_as::<_, plc_platform_core::model::Point>(
r#"SELECT * FROM point WHERE equipment_id = ANY($1) ORDER BY equipment_id, created_at"#,
)
.bind(equipment_ids)

View File

@ -1,4 +1,4 @@
use crate::model::{Point, PointSubscriptionInfo};
use plc_platform_core::model::{Point, PointSubscriptionInfo};
use sqlx::{query_as, PgPool, Row};
use std::collections::HashMap;

View File

@ -1,4 +1,4 @@
use crate::model::Source;
use plc_platform_core::model::Source;
use sqlx::{query_as, PgPool};
pub async fn get_enabled_source(

View File

@ -1,4 +1,4 @@
use crate::model::{Point, Tag};
use plc_platform_core::model::{Point, Tag};
use sqlx::{query_as, PgPool};
pub async fn get_tags_count(pool: &PgPool) -> Result<i64, sqlx::Error> {

View File

@ -1,9 +1,8 @@
use chrono::{DateTime, Utc};
use plc_platform_core::model::ScanMode;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::model::ScanMode;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ValueType {
@ -80,14 +79,14 @@ pub struct PointMonitorInfo {
pub point_id: Uuid,
pub client_handle: u32,
pub scan_mode: ScanMode,
#[serde(serialize_with = "crate::util::datetime::option_utc_to_local_str")]
#[serde(serialize_with = "plc_platform_core::util::datetime::option_utc_to_local_str")]
pub timestamp: Option<DateTime<Utc>>,
pub quality: PointQuality,
pub value: Option<DataValue>,
pub value_type: Option<ValueType>,
pub value_text: Option<String>,
pub old_value: Option<DataValue>,
#[serde(serialize_with = "crate::util::datetime::option_utc_to_local_str")]
#[serde(serialize_with = "plc_platform_core::util::datetime::option_utc_to_local_str")]
pub old_timestamp: Option<DateTime<Utc>>,
pub value_changed: bool,
}

View File

@ -1,6 +0,0 @@
pub mod datetime;
pub mod log;
pub mod pagination;
pub mod response;
pub mod single_instance;
pub mod validator;

View File

@ -1,24 +0,0 @@
use chrono::{DateTime, Local, Utc};
use serde::Serializer;
pub fn utc_to_local_string(date: &DateTime<Utc>) -> String {
date.with_timezone(&Local).format("%Y-%m-%d %H:%M:%S%.3f").to_string()
}
pub fn utc_to_local_str<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let formatted = utc_to_local_string(date);
serializer.serialize_str(&formatted)
}
pub fn option_utc_to_local_str<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match date {
Some(d) => utc_to_local_str(d, serializer),
None => serializer.serialize_none(),
}
}

View File

@ -1,37 +0,0 @@
use std::sync::OnceLock;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use tracing_appender::{rolling, non_blocking};
use time::UtcOffset;
static LOG_GUARD: OnceLock<non_blocking::WorkerGuard> = OnceLock::new();
pub fn init_logger() {
std::fs::create_dir_all("./logs").ok();
let file_appender = rolling::daily("./logs", "app.log");
let (file_writer, guard) = non_blocking(file_appender);
LOG_GUARD.set(guard).ok();
let timer = fmt::time::OffsetTime::new(
UtcOffset::from_hms(8, 0, 0).unwrap(),
time::format_description::well_known::Rfc3339,
);
tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(
fmt::layer()
.compact()
.with_timer(timer.clone())
.with_writer(std::io::stdout),
)
.with(
fmt::layer()
.json()
.with_timer(timer)
.with_writer(file_writer)
.with_ansi(false)
.with_current_span(false)
.with_span_list(false),
)
.init();
}

View File

@ -1,61 +0,0 @@
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use validator::Validate;
/// 分页响应结构
#[derive(Serialize)]
pub struct PaginatedResponse<T> {
pub data: Vec<T>,
pub total: i64,
pub page: u32,
pub page_size: i32,
pub total_pages: u32,
}
impl<T> PaginatedResponse<T> {
/// 创建分页响应
pub fn new(data: Vec<T>, total: i64, page: u32, page_size: i32) -> Self {
let total_pages = if page_size > 0 {
((total as f64) / (page_size as f64)).ceil() as u32
} else {
0
};
Self {
data,
total,
page,
page_size,
total_pages,
}
}
}
/// 分页查询参数
#[serde_as]
#[derive(Debug, Deserialize, Validate)]
pub struct PaginationParams {
#[validate(range(min = 1))]
#[serde_as(as = "serde_with::DisplayFromStr")]
#[serde(default = "default_page")]
pub page: u32,
#[validate(range(min = -1, max = 100))]
#[serde_as(as = "serde_with::DisplayFromStr")]
#[serde(default = "default_page_size")]
pub page_size: i32,
}
fn default_page() -> u32 {
1
}
fn default_page_size() -> i32 {
20
}
impl PaginationParams {
/// 计算偏移量
pub fn offset(&self) -> u32 {
(self.page - 1) * self.page_size.max(0) as u32
}
}

View File

@ -1,144 +0,0 @@
use anyhow::Error;
use axum::{
Json,
http::StatusCode,
response::IntoResponse,
extract::rejection::{
QueryRejection,
PathRejection,
JsonRejection,
FormRejection,
},
};
use serde::Serialize;
use serde_json::Value;
use sqlx::Error as SqlxError;
#[derive(Debug, Serialize)]
pub struct ErrResp {
pub err_code: i32,
pub err_msg: String,
pub err_detail: Option<Value>,
}
impl ErrResp {
pub fn new(err_code: i32, err_msg: impl Into<String>, detail: Option<Value>) -> Self {
Self {
err_code,
err_msg: err_msg.into(),
err_detail: detail,
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum ApiErr {
Unauthorized(String, Option<Value>),
Forbidden(String, Option<Value>),
BadRequest(String, Option<Value>),
NotFound(String, Option<Value>),
Internal(String, Option<Value>),
}
impl IntoResponse for ApiErr {
fn into_response(self) -> axum::response::Response {
match self {
ApiErr::Unauthorized(msg, detail) => {
(StatusCode::UNAUTHORIZED, Json(ErrResp::new(401, msg, detail))).into_response()
}
ApiErr::Forbidden(msg, detail) => {
(StatusCode::FORBIDDEN, Json(ErrResp::new(403, msg, detail))).into_response()
}
ApiErr::BadRequest(msg, detail) => {
(StatusCode::BAD_REQUEST, Json(ErrResp::new(400, msg, detail))).into_response()
}
ApiErr::NotFound(msg, detail) => {
(StatusCode::NOT_FOUND, Json(ErrResp::new(404, msg, detail))).into_response()
}
ApiErr::Internal(msg, detail) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrResp::new(500, msg, detail)),
)
.into_response(),
}
}
}
impl From<Error> for ApiErr {
fn from(err: Error) -> Self {
tracing::error!("Error: {:?}; root_cause: {}", err, err.root_cause());
ApiErr::Internal("internal server error".to_string(), None)
}
}
impl From<SqlxError> for ApiErr {
fn from(err: SqlxError) -> Self {
match err {
SqlxError::RowNotFound => {
ApiErr::NotFound("Resource not found".into(), None)
}
SqlxError::Database(db_err) => {
if db_err.code().as_deref() == Some("23505") {
ApiErr::BadRequest("数据已存在".into(), None)
} else {
tracing::error!("Database error: {}", db_err);
ApiErr::Internal("Database error".into(), None)
}
}
_ => {
tracing::error!("Database error: {}", err);
ApiErr::Internal("Database error".into(), None)
}
}
}
}
impl From<QueryRejection> for ApiErr {
fn from(rejection: QueryRejection) -> Self {
tracing::warn!("Query parameter error: {}", rejection);
ApiErr::BadRequest(
"Invalid query parameters".to_string(),
Some(serde_json::json!({
"detail": rejection.to_string()
}))
)
}
}
impl From<PathRejection> for ApiErr {
fn from(rejection: PathRejection) -> Self {
tracing::warn!("Path parameter error: {}", rejection);
ApiErr::BadRequest(
"Invalid path parameter".to_string(),
Some(serde_json::json!({
"detail": rejection.to_string()
}))
)
}
}
impl From<JsonRejection> for ApiErr {
fn from(rejection: JsonRejection) -> Self {
tracing::warn!("JSON parsing error: {}", rejection);
ApiErr::BadRequest(
"Invalid JSON format".to_string(),
Some(serde_json::json!({
"detail": rejection.to_string()
}))
)
}
}
impl From<FormRejection> for ApiErr {
fn from(rejection: FormRejection) -> Self {
tracing::warn!("Form data error: {}", rejection);
ApiErr::BadRequest(
"Invalid form data".to_string(),
Some(serde_json::json!({
"detail": rejection.to_string()
}))
)
}
}

View File

@ -1,65 +0,0 @@
use fs2::FileExt;
use std::{
fs::{File, OpenOptions},
io,
path::PathBuf,
};
pub fn try_acquire(name: &str) -> io::Result<SingleInstanceGuard> {
SingleInstanceGuard::acquire(name)
}
pub struct SingleInstanceGuard {
_file: File,
}
impl SingleInstanceGuard {
fn acquire(name: &str) -> io::Result<Self> {
let lock_path = lock_file_path(name);
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(lock_path)?;
if let Err(err) = file.try_lock_exclusive() {
if err.kind() == io::ErrorKind::WouldBlock {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"another PLC Control instance is already running",
));
}
return Err(io::Error::new(
err.kind(),
format!("failed to lock single-instance file: {}", err),
));
}
Ok(Self { _file: file })
}
}
fn lock_file_path(name: &str) -> PathBuf {
let sanitized: String = name
.chars()
.map(|ch| match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
_ => '_',
})
.collect();
std::env::temp_dir().join(format!("{}.lock", sanitized))
}
#[cfg(test)]
mod tests {
use super::lock_file_path;
#[test]
fn lock_path_is_sanitized() {
let path = lock_file_path("PLCControl/Gateway:test");
let path_str = path.to_string_lossy();
assert!(path_str.ends_with("PLCControl_Gateway_test.lock"));
}
}

View File

@ -1,34 +0,0 @@
use crate::util::response::ApiErr;
use serde_json::{json, Value};
use validator::ValidationErrors;
impl From<ValidationErrors> for ApiErr {
fn from(errors: ValidationErrors) -> Self {
// 构建详细的错误信息
let mut error_details = serde_json::Map::new();
let mut first_error_msg = String::from("请求参数验证失败");
for (field, field_errors) in errors.field_errors() {
let error_list: Vec<String> = field_errors
.iter()
.map(|e| {
e.message.as_ref()
.map(|m| m.to_string())
.unwrap_or_else(|| e.code.to_string())
})
.collect();
error_details.insert(field.to_string(), json!(error_list));
// 获取第一个字段的第一个错误信息
if first_error_msg == "请求参数验证失败" && !error_list.is_empty() {
if let Some(msg) = field_errors[0].message.as_ref() {
first_error_msg = format!("{}: {}", field, msg);
} else {
first_error_msg = format!("{}: {}", field, field_errors[0].code);
}
}
}
ApiErr::BadRequest(first_error_msg, Some(Value::Object(error_details)))
}
}

View File

@ -17,7 +17,7 @@ use uuid::Uuid;
pub enum WsMessage {
PointNewValue(crate::telemetry::PointMonitorInfo),
PointSetValueBatchResult(crate::connection::BatchSetPointValueRes),
EventCreated(crate::model::EventRecord),
EventCreated(plc_platform_core::model::EventRecord),
UnitRuntimeChanged(crate::control::runtime::UnitRuntime),
}