| @@ -22,7 +22,9 @@ SOURCES += \ | |||
| src/domain/hmi_model.cpp \ | |||
| src/domain/control_logic_model.cpp \ | |||
| src/domain/project_model.cpp \ | |||
| src/domain/runtime_state.cpp | |||
| src/domain/runtime_state.cpp \ | |||
| src/services/project_service.cpp \ | |||
| src/infrastructure/json_project_storage.cpp | |||
| HEADERS += \ | |||
| src/ui/main_window.h \ | |||
| @@ -31,7 +33,10 @@ HEADERS += \ | |||
| src/domain/hmi_model.h \ | |||
| src/domain/control_logic_model.h \ | |||
| src/domain/project_model.h \ | |||
| src/domain/runtime_state.h | |||
| src/domain/runtime_state.h \ | |||
| src/domain/project_storage.h \ | |||
| src/services/project_service.h \ | |||
| src/infrastructure/json_project_storage.h | |||
| FORMS += \ | |||
| src/ui/main_window.ui | |||
| @@ -0,0 +1,63 @@ | |||
| #pragma once | |||
| #include "project_model.h" | |||
| #include <string> | |||
| namespace integrated_platform::domain { | |||
| // 工程文件存储失败原因 | |||
| enum class ProjectStorageError | |||
| { | |||
| None, // 无错误 | |||
| FileOpenFailed, // 文件打开失败 | |||
| FileReadFailed, // 文件读取失败 | |||
| FileWriteFailed, // 文件写入失败 | |||
| InvalidJson, // JSON 格式无效 | |||
| MissingField, // 缺少必要字段 | |||
| InvalidField, // 字段值无效 | |||
| UnsupportedVersion, // 工程版本不受支持 | |||
| InvalidProject // 工程内容无效 | |||
| }; | |||
| // 工程保存结果 | |||
| struct ProjectSaveResult | |||
| { | |||
| bool succeeded = false; // 保存是否成功 | |||
| ProjectStorageError error = ProjectStorageError::None; // 保存失败原因 | |||
| std::string message; // 结果描述信息 | |||
| }; | |||
| // 工程加载结果 | |||
| struct ProjectLoadResult | |||
| { | |||
| bool succeeded = false; // 加载是否成功 | |||
| Project project; // 加载得到的工程 | |||
| ProjectStorageError error = ProjectStorageError::None; // 加载失败原因 | |||
| std::string message; // 结果描述信息 | |||
| }; | |||
| // 工程文件存储边界 | |||
| class ProjectStorage | |||
| { | |||
| public: | |||
| virtual ~ProjectStorage() = default; | |||
| /** | |||
| * @brief 将工程保存到指定文件 | |||
| * @param project 需要保存的有效工程 | |||
| * @param file_path UTF-8 编码的目标文件路径 | |||
| * @return 工程保存结果 | |||
| */ | |||
| virtual ProjectSaveResult save( | |||
| const Project &project, const std::string &file_path) = 0; | |||
| /** | |||
| * @brief 从指定文件加载工程 | |||
| * @param file_path UTF-8 编码的工程文件路径 | |||
| * @return 工程加载结果 | |||
| */ | |||
| virtual ProjectLoadResult load(const std::string &file_path) = 0; | |||
| }; | |||
| } // namespace integrated_platform::domain | |||
| @@ -1 +0,0 @@ | |||
| @@ -0,0 +1,16 @@ | |||
| #pragma once | |||
| #include "domain/project_storage.h" | |||
| namespace integrated_platform::infrastructure { | |||
| // 使用版本化 JSON 文件保存和加载工程 | |||
| class JsonProjectStorage final : public domain::ProjectStorage | |||
| { | |||
| public: | |||
| domain::ProjectSaveResult save( | |||
| const domain::Project &project, const std::string &file_path) override; | |||
| domain::ProjectLoadResult load(const std::string &file_path) override; | |||
| }; | |||
| } // namespace integrated_platform::infrastructure | |||
| @@ -1 +0,0 @@ | |||
| @@ -0,0 +1,181 @@ | |||
| #include "project_service.h" | |||
| #include <algorithm> | |||
| #include <chrono> | |||
| #include <cctype> | |||
| #include <random> | |||
| #include <sstream> | |||
| #include <utility> | |||
| namespace integrated_platform::services { | |||
| namespace { | |||
| // 使用高精度时间戳和随机值生成工程标识 | |||
| std::string generateProjectId() | |||
| { | |||
| const auto timestamp = static_cast<unsigned long long>( | |||
| std::chrono::high_resolution_clock::now().time_since_epoch().count()); | |||
| std::random_device random_device; | |||
| const auto random_value = static_cast<unsigned long long>(random_device()); | |||
| std::ostringstream stream; | |||
| stream << "project-" << std::hex << timestamp << '-' << random_value; | |||
| return stream.str(); | |||
| } | |||
| } // namespace | |||
| ProjectService::ProjectService(domain::ProjectStorage &storage) | |||
| : storage_(storage), | |||
| project_(makeNewProject("Untitled")) | |||
| { | |||
| } | |||
| const domain::Project &ProjectService::project() const | |||
| { | |||
| return project_; | |||
| } | |||
| domain::Project &ProjectService::editProject() | |||
| { | |||
| modified_ = true; | |||
| return project_; | |||
| } | |||
| const std::string &ProjectService::currentFilePath() const | |||
| { | |||
| return current_file_path_; | |||
| } | |||
| bool ProjectService::hasCurrentFile() const | |||
| { | |||
| return !current_file_path_.empty(); | |||
| } | |||
| bool ProjectService::isModified() const | |||
| { | |||
| return modified_; | |||
| } | |||
| ProjectOperationResult ProjectService::createNewProject(const std::string &name) | |||
| { | |||
| if (isBlank(name)) | |||
| { | |||
| return {false, | |||
| ProjectServiceError::InvalidProjectName, | |||
| domain::ProjectStorageError::None, | |||
| "project name must not be empty"}; | |||
| } | |||
| // 新工程创建成功后清除原文件关联,并标记为待保存 | |||
| project_ = makeNewProject(name); | |||
| current_file_path_.clear(); | |||
| modified_ = true; | |||
| return {true, ProjectServiceError::None, domain::ProjectStorageError::None, {}}; | |||
| } | |||
| ProjectOperationResult ProjectService::save() | |||
| { | |||
| if (!hasCurrentFile()) | |||
| { | |||
| return {false, | |||
| ProjectServiceError::FilePathRequired, | |||
| domain::ProjectStorageError::None, | |||
| "project file path is required"}; | |||
| } | |||
| return saveAs(current_file_path_); | |||
| } | |||
| ProjectOperationResult ProjectService::saveAs(const std::string &file_path) | |||
| { | |||
| if (isBlank(file_path)) | |||
| { | |||
| return {false, | |||
| ProjectServiceError::FilePathRequired, | |||
| domain::ProjectStorageError::None, | |||
| "project file path is required"}; | |||
| } | |||
| std::string validation_error; | |||
| if (!project_.validate(&validation_error)) | |||
| { | |||
| return {false, | |||
| ProjectServiceError::InvalidProject, | |||
| domain::ProjectStorageError::InvalidProject, | |||
| validation_error}; | |||
| } | |||
| // 先完成领域校验,避免将非法工程交给存储层 | |||
| const domain::ProjectSaveResult result = storage_.save(project_, file_path); | |||
| if (!result.succeeded) | |||
| { | |||
| return storageFailure(result.error, result.message); | |||
| } | |||
| // 只有存储成功后才更新文件关联和未保存状态 | |||
| current_file_path_ = file_path; | |||
| modified_ = false; | |||
| return {true, ProjectServiceError::None, domain::ProjectStorageError::None, {}}; | |||
| } | |||
| ProjectOperationResult ProjectService::load(const std::string &file_path) | |||
| { | |||
| if (isBlank(file_path)) | |||
| { | |||
| return {false, | |||
| ProjectServiceError::FilePathRequired, | |||
| domain::ProjectStorageError::None, | |||
| "project file path is required"}; | |||
| } | |||
| domain::ProjectLoadResult result = storage_.load(file_path); | |||
| if (!result.succeeded) | |||
| { | |||
| return storageFailure(result.error, result.message); | |||
| } | |||
| std::string validation_error; | |||
| if (!result.project.validate(&validation_error)) | |||
| { | |||
| return {false, | |||
| ProjectServiceError::InvalidProject, | |||
| domain::ProjectStorageError::InvalidProject, | |||
| validation_error}; | |||
| } | |||
| // 文件读取和领域校验均成功后,才替换当前工程状态 | |||
| project_ = std::move(result.project); | |||
| current_file_path_ = file_path; | |||
| modified_ = false; | |||
| return {true, ProjectServiceError::None, domain::ProjectStorageError::None, {}}; | |||
| } | |||
| domain::Project ProjectService::makeNewProject(const std::string &name) | |||
| { | |||
| domain::Project project; | |||
| project.metadata.id = generateProjectId(); | |||
| project.metadata.name = name; | |||
| project.metadata.formatVersion = "1.0"; | |||
| return project; | |||
| } | |||
| bool ProjectService::isBlank(const std::string &value) | |||
| { | |||
| return value.empty() | |||
| || std::all_of( | |||
| value.cbegin(), | |||
| value.cend(), | |||
| [](unsigned char character) | |||
| { | |||
| return std::isspace(character) != 0; | |||
| }); | |||
| } | |||
| ProjectOperationResult ProjectService::storageFailure( | |||
| domain::ProjectStorageError error, const std::string &message) | |||
| { | |||
| return {false, ProjectServiceError::StorageFailure, error, message}; | |||
| } | |||
| } // namespace integrated_platform::services | |||
| @@ -0,0 +1,80 @@ | |||
| #pragma once | |||
| #include "domain/project_storage.h" | |||
| #include <string> | |||
| namespace integrated_platform::services { | |||
| // 工程管理操作失败原因 | |||
| enum class ProjectServiceError | |||
| { | |||
| None, // 无错误 | |||
| InvalidProjectName, // 工程名称为空或仅包含空白字符 | |||
| FilePathRequired, // 未提供工程文件路径 | |||
| InvalidProject, // 当前工程不符合领域规则 | |||
| StorageFailure // 工程文件存储操作失败 | |||
| }; | |||
| // 工程管理操作结果 | |||
| struct ProjectOperationResult | |||
| { | |||
| bool succeeded = false; // 操作是否成功 | |||
| ProjectServiceError error = ProjectServiceError::None; // 服务层错误原因 | |||
| domain::ProjectStorageError storageError = domain::ProjectStorageError::None; // 存储层错误原因 | |||
| std::string message; // 面向调用方的结果描述 | |||
| }; | |||
| // 编排当前工程的新建、保存和加载流程 | |||
| class ProjectService | |||
| { | |||
| public: | |||
| // 使用外部提供的工程存储实现创建服务 | |||
| explicit ProjectService(domain::ProjectStorage &storage); | |||
| const domain::Project &project() const; | |||
| domain::Project &editProject(); | |||
| const std::string ¤tFilePath() const; | |||
| bool hasCurrentFile() const; | |||
| bool isModified() const; | |||
| /** | |||
| * @brief 创建未保存的新工程 | |||
| * @param name 工程名称,不得为空或仅包含空白字符 | |||
| * @return 新建操作结果 | |||
| */ | |||
| ProjectOperationResult createNewProject(const std::string &name); | |||
| /** | |||
| * @brief 保存到当前工程文件 | |||
| * @return 保存操作结果,当前工程没有文件路径时返回失败 | |||
| */ | |||
| ProjectOperationResult save(); | |||
| /** | |||
| * @brief 将当前工程保存到指定文件 | |||
| * @param file_path UTF-8 编码的目标文件路径,不得为空或仅包含空白字符 | |||
| * @return 保存操作结果 | |||
| */ | |||
| ProjectOperationResult saveAs(const std::string &file_path); | |||
| /** | |||
| * @brief 从指定文件加载工程 | |||
| * @param file_path UTF-8 编码的工程文件路径,不得为空或仅包含空白字符 | |||
| * @return 加载操作结果,失败时不替换当前工程 | |||
| */ | |||
| ProjectOperationResult load(const std::string &file_path); | |||
| private: | |||
| static domain::Project makeNewProject(const std::string &name); | |||
| static bool isBlank(const std::string &value); | |||
| static ProjectOperationResult storageFailure( | |||
| domain::ProjectStorageError error, const std::string &message); | |||
| domain::ProjectStorage &storage_; // 非拥有的工程存储依赖 | |||
| domain::Project project_; // 当前编辑中的工程 | |||
| std::string current_file_path_; // 当前工程对应的文件路径 | |||
| bool modified_ = true; // 当前工程是否有未保存修改 | |||
| }; | |||
| } // namespace integrated_platform::services | |||
| @@ -0,0 +1,237 @@ | |||
| #include "domain/project_storage.h" | |||
| #include "infrastructure/json_project_storage.h" | |||
| #include "services/project_service.h" | |||
| #include <QFile> | |||
| #include <QTemporaryDir> | |||
| #include <exception> | |||
| #include <iostream> | |||
| #include <stdexcept> | |||
| #include <string> | |||
| namespace domain = integrated_platform::domain; | |||
| namespace infrastructure = integrated_platform::infrastructure; | |||
| namespace services = integrated_platform::services; | |||
| namespace { | |||
| void require(bool condition, const std::string &message) | |||
| { | |||
| if (!condition) | |||
| { | |||
| throw std::runtime_error(message); | |||
| } | |||
| } | |||
| domain::Project makeExampleProject() | |||
| { | |||
| domain::HmiControl start_button; | |||
| start_button.id = "start-button"; | |||
| start_button.type = domain::HmiControlType::Button; | |||
| start_button.bounds = {10, 20, 120, 48}; | |||
| start_button.text = "Start"; | |||
| start_button.binding = domain::RegisterAddress{domain::RegisterArea::M, 0}; | |||
| start_button.properties.emplace("color", "green"); | |||
| domain::HmiPage page; | |||
| page.id = "main-page"; | |||
| page.name = "Main"; | |||
| page.controls.push_back(start_button); | |||
| domain::LogicNode contact; | |||
| contact.id = "start-contact"; | |||
| contact.config = domain::ContactNodeConfig{ | |||
| domain::RegisterAddress{domain::RegisterArea::M, 0}, | |||
| domain::ContactMode::NormallyOpen}; | |||
| domain::LogicNode compare; | |||
| compare.id = "temperature-check"; | |||
| compare.config = domain::CompareNodeConfig{ | |||
| domain::RegisterAddress{domain::RegisterArea::D, 2}, | |||
| domain::ComparisonOperator::GreaterThanOrEqual, | |||
| static_cast<std::int16_t>(100)}; | |||
| domain::LogicNode coil; | |||
| coil.id = "run-coil"; | |||
| coil.config = domain::CoilNodeConfig{ | |||
| domain::RegisterAddress{domain::RegisterArea::M, 1}, | |||
| domain::CoilMode::Set}; | |||
| domain::ControlLogic logic; | |||
| logic.id = "start-logic"; | |||
| logic.name = "Start logic"; | |||
| logic.enabled = false; | |||
| logic.nodes = {contact, compare, coil}; | |||
| logic.connections.push_back({contact.id, compare.id}); | |||
| logic.connections.push_back({compare.id, coil.id}); | |||
| domain::Project project; | |||
| project.metadata = {"example-project", "Example project", "1.0"}; | |||
| project.hmiPages.push_back(page); | |||
| project.controlLogics.push_back(logic); | |||
| return project; | |||
| } | |||
| void writeText(const QString &path, const QByteArray &content) | |||
| { | |||
| QFile file(path); | |||
| require(file.open(QIODevice::WriteOnly), "test file must be writable"); | |||
| require(file.write(content) == content.size(), "test file must be written completely"); | |||
| } | |||
| QByteArray readBytes(const QString &path) | |||
| { | |||
| QFile file(path); | |||
| require(file.open(QIODevice::ReadOnly), "saved project must be readable"); | |||
| return file.readAll(); | |||
| } | |||
| void testEmptyProjectRoundTrip() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| infrastructure::JsonProjectStorage storage; | |||
| services::ProjectService service(storage); | |||
| require(service.createNewProject("Empty project").succeeded, | |||
| "empty project creation must succeed"); | |||
| const QString path = directory.filePath("empty.json"); | |||
| require(service.saveAs(path.toStdString()).succeeded, | |||
| "empty project save must succeed"); | |||
| require(!service.isModified(), "saved project must not be marked modified"); | |||
| require(service.load(path.toStdString()).succeeded, | |||
| "empty project load must succeed"); | |||
| require(service.project().metadata.name == "Empty project", | |||
| "empty project name must survive round trip"); | |||
| require(service.project().hmiPages.empty(), "empty project must have no HMI pages"); | |||
| require(service.project().controlLogics.empty(), | |||
| "empty project must have no control logics"); | |||
| } | |||
| void testExampleProjectRoundTrip() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| infrastructure::JsonProjectStorage storage; | |||
| services::ProjectService service(storage); | |||
| service.editProject() = makeExampleProject(); | |||
| const QString first_path = directory.filePath("example.json"); | |||
| const QString second_path = directory.filePath("example-copy.json"); | |||
| require(service.saveAs(first_path.toStdString()).succeeded, | |||
| "example project save must succeed"); | |||
| require(service.load(first_path.toStdString()).succeeded, | |||
| "example project load must succeed"); | |||
| const domain::Project &project = service.project(); | |||
| require(project.metadata.id == "example-project", "project id must survive round trip"); | |||
| require(project.hmiPages.size() == 1, "HMI page count must survive round trip"); | |||
| require(project.hmiPages.front().controls.front().binding->area() | |||
| == domain::RegisterArea::M, | |||
| "HMI M binding must survive round trip"); | |||
| require(project.hmiPages.front().controls.front().properties.at("color") == "green", | |||
| "HMI properties must survive round trip"); | |||
| require(project.controlLogics.size() == 1, | |||
| "control logic count must survive round trip"); | |||
| require(!project.controlLogics.front().enabled, | |||
| "control logic enabled state must survive round trip"); | |||
| require(project.controlLogics.front().nodes.size() == 3, | |||
| "logic node count must survive round trip"); | |||
| const auto &compare = std::get<domain::CompareNodeConfig>( | |||
| project.controlLogics.front().nodes.at(1).config); | |||
| require(compare.address.index() == 2 && compare.value == 100, | |||
| "comparison configuration must survive round trip"); | |||
| require(service.saveAs(second_path.toStdString()).succeeded, | |||
| "save as must succeed after load"); | |||
| require(readBytes(first_path) == readBytes(second_path), | |||
| "save and save as must produce stable JSON"); | |||
| } | |||
| void testInvalidFiles() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| infrastructure::JsonProjectStorage storage; | |||
| services::ProjectService service(storage); | |||
| service.editProject().metadata.name = "Current project"; | |||
| const QString invalid_json = directory.filePath("invalid-json.json"); | |||
| const QString missing_field = directory.filePath("missing-field.json"); | |||
| const QString unsupported_version = directory.filePath("unsupported-version.json"); | |||
| writeText(invalid_json, "{"); | |||
| auto result = service.load(invalid_json.toStdString()); | |||
| require(!result.succeeded | |||
| && result.storageError == domain::ProjectStorageError::InvalidJson, | |||
| "invalid JSON must be rejected"); | |||
| require(service.project().metadata.name == "Current project", | |||
| "invalid load must keep current project"); | |||
| writeText(missing_field, R"({"formatVersion":"1.0"})"); | |||
| result = service.load(missing_field.toStdString()); | |||
| require(!result.succeeded | |||
| && result.storageError == domain::ProjectStorageError::MissingField, | |||
| "missing fields must be rejected"); | |||
| writeText(unsupported_version, R"({"formatVersion":"2.0"})"); | |||
| result = service.load(unsupported_version.toStdString()); | |||
| require(!result.succeeded | |||
| && result.storageError == domain::ProjectStorageError::UnsupportedVersion, | |||
| "unsupported versions must be rejected"); | |||
| } | |||
| void testServiceStateAndSaveErrors() | |||
| { | |||
| QTemporaryDir directory; | |||
| require(directory.isValid(), "temporary directory must be valid"); | |||
| infrastructure::JsonProjectStorage storage; | |||
| services::ProjectService service(storage); | |||
| require(service.save().error == services::ProjectServiceError::FilePathRequired, | |||
| "save without a current path must be rejected"); | |||
| require(service.createNewProject(" ").error | |||
| == services::ProjectServiceError::InvalidProjectName, | |||
| "blank project names must be rejected"); | |||
| const QString path = directory.filePath("state.json"); | |||
| service.editProject().metadata.name = "State project"; | |||
| require(service.saveAs(path.toStdString()).succeeded, | |||
| "state project save must succeed"); | |||
| service.editProject().metadata.name = "Changed project"; | |||
| require(service.isModified(), "editing the project must mark it modified"); | |||
| require(service.save().succeeded, "save must use the current file path"); | |||
| require(!service.isModified(), "successful save must clear modified state"); | |||
| const QString failed_path = directory.filePath("missing/subdir/state.json"); | |||
| require(!service.saveAs(failed_path.toStdString()).succeeded, | |||
| "save to an unavailable path must fail"); | |||
| require(service.currentFilePath() == path.toStdString(), | |||
| "failed save as must keep the previous current path"); | |||
| } | |||
| } // namespace | |||
| int main() | |||
| { | |||
| try | |||
| { | |||
| testEmptyProjectRoundTrip(); | |||
| testExampleProjectRoundTrip(); | |||
| testInvalidFiles(); | |||
| testServiceStateAndSaveErrors(); | |||
| } | |||
| catch (const std::exception &error) | |||
| { | |||
| std::cerr << "project management tests failed: " << error.what() << '\n'; | |||
| return 1; | |||
| } | |||
| std::cout << "project management tests passed\n"; | |||
| return 0; | |||
| } | |||
| @@ -0,0 +1,31 @@ | |||
| TEMPLATE = app | |||
| TARGET = project_management_tests | |||
| CONFIG += console c++17 testcase warn_on | |||
| CONFIG -= app_bundle | |||
| QT += core | |||
| INCLUDEPATH += ../src | |||
| SOURCES += \ | |||
| project_management_tests.cpp \ | |||
| ../src/domain/register_address.cpp \ | |||
| ../src/domain/register_repository.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/domain/project_model.cpp \ | |||
| ../src/domain/runtime_state.cpp \ | |||
| ../src/services/project_service.cpp \ | |||
| ../src/infrastructure/json_project_storage.cpp | |||
| HEADERS += \ | |||
| ../src/domain/register_address.h \ | |||
| ../src/domain/register_repository.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/domain/project_model.h \ | |||
| ../src/domain/runtime_state.h \ | |||
| ../src/domain/project_storage.h \ | |||
| ../src/services/project_service.h \ | |||
| ../src/infrastructure/json_project_storage.h | |||
| @@ -1,8 +1,8 @@ | |||
| # Current Handoff | |||
| - Goal: 完成综合平台编程器的核心数据模型与接口。 | |||
| - Goal: 完成综合平台编程器的工程管理和后续编辑功能。 | |||
| - Branch: `main`。 | |||
| - Current status: 已完成开发顺序第 3 步,核心领域模型、寄存器仓库接口和运行模式边界已定义。 | |||
| - Current status: 已完成开发顺序第 4 步,工程管理服务、版本化 JSON 存储和核心往返测试已完成。 | |||
| - Changed files: | |||
| - `app/integrated_platform.pro` | |||
| - `app/src/main.cpp` | |||
| @@ -29,8 +29,15 @@ | |||
| - `app/src/domain/project_model.cpp` | |||
| - `app/src/domain/runtime_state.h` | |||
| - `app/src/domain/runtime_state.cpp` | |||
| - `app/src/domain/project_storage.h` | |||
| - `app/src/services/project_service.h` | |||
| - `app/src/services/project_service.cpp` | |||
| - `app/src/infrastructure/json_project_storage.h` | |||
| - `app/src/infrastructure/json_project_storage.cpp` | |||
| - `app/tests/domain_tests.pro` | |||
| - `app/tests/domain_tests.cpp` | |||
| - `app/tests/project_management_tests.pro` | |||
| - `app/tests/project_management_tests.cpp` | |||
| - Decisions made: | |||
| - Qt 应用源码位于 `app/`,构建输出位于被忽略的 `build/`。 | |||
| - 使用 qmake、Qt Widgets、Qt SerialBus 和 MinGW 8.1。 | |||
| @@ -40,6 +47,9 @@ | |||
| - 领域模型仅使用 C++17 标准库,不依赖 Qt UI、串口或 Modbus。 | |||
| - M 地址只承载布尔值,D 地址只承载带符号 16 位值,地址范围统一为 `0~4000`。 | |||
| - `RegisterRepository` 统一虚拟寄存器和真实 PLC 缓存的访问契约。 | |||
| - `ProjectStorage` 是领域层的工程文件存储契约,具体 JSON 和 Qt 文件 API 位于基础设施层。 | |||
| - `ProjectService` 负责工程路径、修改状态和新建保存加载流程;加载成功后才替换当前工程。 | |||
| - 工程文件格式版本当前为 `1.0`,保存使用 `QSaveFile` 原子提交。 | |||
| - 控制逻辑节点使用 `std::variant` 组合触点、线圈和数值比较的独立配置类型。 | |||
| - 当前控制逻辑范围不包含定时器;不得因为设备手册或旧开发计划提前加入未确认功能。 | |||
| - 新增逻辑节点时增加独立配置类型,不向通用节点结构堆叠无关可选字段。 | |||
| @@ -54,6 +64,8 @@ | |||
| - 在 `build/core-model/` 重新执行 qmake 和 `mingw32-make -j2`,应用构建成功。 | |||
| - 在 `build/domain-tests/` 构建并运行领域测试,输出 `domain tests passed`。 | |||
| - 控制逻辑节点配置重构后再次构建应用和领域测试,均通过。 | |||
| - Remaining work: 进入开发顺序第 4 步,实现工程新建、保存、另存为和加载。 | |||
| - 在 `build/project-management-tests/` 构建并运行工程管理测试,输出 `project management tests passed`。 | |||
| - 工程管理加入应用后重新执行 Qt 应用构建,构建成功。 | |||
| - Remaining work: 进入开发顺序第 5 步,完成主界面和状态管理。 | |||
| - Known risks / blockers: 无。 | |||
| - Suggested next command: 基于 `Project` 领域模型设计版本化的工程文件 DTO 与存储接口。 | |||
| - Suggested next command: 在主窗口注入 `ProjectService`,接入新建、打开、保存和另存为操作。 | |||
| @@ -56,11 +56,16 @@ main.cpp -> UI + Services + Infrastructure | |||
| | --- | --- | | |||
| | `register_address.*` | 定义 M/D 地址值对象,统一校验项目地址范围 `0~4000`。 | | |||
| | `register_repository.*` | 定义 M 位和 D 字的统一读写接口,并提供离线内存实现。 | | |||
| | `project_storage.*` | 定义工程文件保存和加载的领域存储契约,不依赖具体文件格式。 | | |||
| | `hmi_model.*` | 定义 HMI 页面、控件、位置、扩展属性和寄存器绑定。 | | |||
| | `control_logic_model.*` | 使用独立配置类型定义触点、线圈和数值比较节点,并校验地址区域与连接引用。 | | |||
| | `project_model.*` | 聚合工程元数据、HMI 页面和控制逻辑,并校验工程内标识唯一性。 | | |||
| | `runtime_state.*` | 定义编辑态、离线运行态、真机运行态及合法切换规则。 | | |||
| 工程管理由 `services/ProjectService` 编排新建、保存、另存为和加载。JSON 文件转换和原子写入 | |||
| 由 `infrastructure/JsonProjectStorage` 实现。加载必须先完成文件解析、版本检查和领域校验,成功 | |||
| 后才能替换当前工程;保存失败或加载失败不得修改当前工程路径和内存内容。 | |||
| `RegisterRepository` 是 HMI、逻辑执行器和运行服务唯一可见的寄存器边界。离线运行使用 | |||
| `VirtualRegisterRepository`;真机运行后续注入由基础设施层实现的 PLC 缓存仓库。仓库读取的是 | |||
| 最近一次有效值,写入成功只表示请求被仓库接受;真实 PLC 的异步确认由服务层负责反馈。 | |||
| @@ -30,6 +30,7 @@ | |||
| - 实现新建、保存、另存为和加载工程。 | |||
| - 保存 HMI 配置、控制逻辑、M/D 绑定和必要的工程版本信息。 | |||
| - 使用服务层编排工程操作,基础设施层负责版本化 JSON 转换和原子文件写入。 | |||
| - 对无效文件、缺失字段和不兼容版本给出明确错误。 | |||
| 完成标准:空工程和示例工程可以保存后重新加载,数据保持一致。 | |||