refactor(core): complete model and util move
This commit is contained in:
parent
b34c898089
commit
a1e2536844
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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, ¬ify, &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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
/// 获取所有标签
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
178
src/model.rs
178
src/model.rs
|
|
@ -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>,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
pub mod datetime;
|
||||
pub mod log;
|
||||
pub mod pagination;
|
||||
pub mod response;
|
||||
pub mod single_instance;
|
||||
pub mod validator;
|
||||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue