#include "json_project_storage.h" #include #include #include #include #include #include #include #include #include #include namespace { // 当前读写实现支持的工程文件格式版本 constexpr const char *kCurrentFormatVersion = "1.0"; // 保存解析过程中遇到的第一个错误,避免后续失败覆盖根因 struct ParseState { ProjectStorageError error = ProjectStorageError::None; std::string message; // 记录首次解析错误并返回 false,便于解析函数直接向上传播失败 bool fail(ProjectStorageError new_error, const std::string &new_message) { if (error == ProjectStorageError::None) { error = new_error; message = new_message; } return false; } }; // 将领域层使用的 UTF-8 字符串转换为 Qt 字符串 QString fromUtf8(const std::string &value) { return QString::fromUtf8(value.data(), static_cast(value.size())); } // 将 Qt 字符串转换为领域层使用的 UTF-8 字符串 std::string toUtf8(const QString &value) { const QByteArray bytes = value.toUtf8(); return std::string(bytes.constData(), static_cast(bytes.size())); } // 拼接带上下文的字段路径,用于生成可定位的错误信息 std::string fieldPath(const std::string &context, const char *field) { return context + '.' + field; } // 读取必填 JSON 字段,字段不存在时记录 MissingField 错误 bool readValue( const QJsonObject &object, const char *field, const std::string &context, QJsonValue *value, ParseState *state) { const QString key = QString::fromLatin1(field); if (!object.contains(key)) { return state->fail( ProjectStorageError::MissingField, "missing required field " + fieldPath(context, field)); } *value = object.value(key); return true; } // 读取必填字符串字段并转换为 UTF-8 标准字符串 bool readString( const QJsonObject &object, const char *field, const std::string &context, std::string *value, ParseState *state) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isString()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " must be a string"); } *value = toUtf8(json_value.toString()); return true; } // 读取必填布尔字段并校验 JSON 类型 bool readBool( const QJsonObject &object, const char *field, const std::string &context, bool *value, ParseState *state) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isBool()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " must be a boolean"); } *value = json_value.toBool(); return true; } // 读取指定闭区间内的整数,拒绝小数、非有限值和越界值 bool readInt( const QJsonObject &object, const char *field, const std::string &context, int minimum, int maximum, int *value, ParseState *state) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isDouble()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " must be an integer"); } // Qt JSON 统一用 double 表示数值,需要额外确认它能无损转换为 int const double number = json_value.toDouble(); if (!std::isfinite(number) || std::floor(number) != number || number < minimum || number > maximum) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " is outside the supported integer range"); } *value = static_cast(number); return true; } // 读取必填对象字段并校验 JSON 类型 bool readObject( const QJsonObject &object, const char *field, const std::string &context, QJsonObject *value, ParseState *state) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isObject()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " must be an object"); } *value = json_value.toObject(); return true; } // 读取必填数组字段并校验 JSON 类型 bool readArray( const QJsonObject &object, const char *field, const std::string &context, QJsonArray *value, ParseState *state) { QJsonValue json_value; if (!readValue(object, field, context, &json_value, state)) { return false; } if (!json_value.isArray()) { return state->fail( ProjectStorageError::InvalidField, fieldPath(context, field) + " must be an array"); } *value = json_value.toArray(); return true; } // 将寄存器地址序列化为包含区域和原始索引的 JSON 对象 QJsonObject serializeAddress(const RegisterAddress &address) { QJsonObject object; object.insert(QStringLiteral("area"), address.area() == RegisterArea::M ? QStringLiteral("M") : QStringLiteral("D")); object.insert(QStringLiteral("index"), address.index()); return object; } // 解析寄存器地址并校验区域名称及项目允许的索引范围 bool parseAddress( const QJsonObject &object, const std::string &context, RegisterAddress *address, ParseState *state) { std::string area_text; int index = 0; if (!readString(object, "area", context, &area_text, state) || !readInt( object, "index", context, RegisterAddress::kMinimumIndex, RegisterAddress::kMaximumIndex, &index, state)) { return false; } RegisterArea area = RegisterArea::M; if (area_text == "M") { area = RegisterArea::M; } else if (area_text == "D") { area = RegisterArea::D; } else { return state->fail( ProjectStorageError::InvalidField, context + ".area must be M or D"); } *address = RegisterAddress{area, index}; return true; } // 将 HMI 控件类型枚举转换为工程文件中的稳定字符串 QString hmiControlTypeName(HmiControlType type) { switch (type) { case HmiControlType::Button: { return QStringLiteral("button"); } case HmiControlType::Indicator: { return QStringLiteral("indicator"); } case HmiControlType::NumericDisplay: { return QStringLiteral("numericDisplay"); } case HmiControlType::NumericInput: { return QStringLiteral("numericInput"); } case HmiControlType::Label: { return QStringLiteral("label"); } default: { return {}; } } } // 将工程文件中的控件类型字符串转换为 HMI 控件类型枚举 bool parseHmiControlType( const std::string &value, HmiControlType *type, ParseState *state) { if (value == "button") { *type = HmiControlType::Button; } else if (value == "indicator") { *type = HmiControlType::Indicator; } else if (value == "numericDisplay") { *type = HmiControlType::NumericDisplay; } else if (value == "numericInput") { *type = HmiControlType::NumericInput; } else if (value == "label") { *type = HmiControlType::Label; } else { return state->fail( ProjectStorageError::InvalidField, "unsupported HMI control type " + value); } return true; } // 将 HMI 控件矩形区域序列化为 JSON 对象 QJsonObject serializeBounds(const HmiRect &bounds) { QJsonObject object; object.insert(QStringLiteral("x"), bounds.x); object.insert(QStringLiteral("y"), bounds.y); object.insert(QStringLiteral("width"), bounds.width); object.insert(QStringLiteral("height"), bounds.height); return object; } // 解析控件矩形区域,允许任意坐标但要求宽高为正数 bool parseBounds( const QJsonObject &object, const std::string &context, HmiRect *bounds, ParseState *state) { return readInt( object, "x", context, std::numeric_limits::min(), std::numeric_limits::max(), &bounds->x, state) && readInt( object, "y", context, std::numeric_limits::min(), std::numeric_limits::max(), &bounds->y, state) && readInt( object, "width", context, 1, std::numeric_limits::max(), &bounds->width, state) && readInt( object, "height", context, 1, std::numeric_limits::max(), &bounds->height, state); } // 将 HMI 控件的字符串扩展属性序列化为 JSON 对象 QJsonObject serializeProperties(const std::map &properties) { QJsonObject object; for (const auto &property : properties) { object.insert(fromUtf8(property.first), fromUtf8(property.second)); } return object; } // 解析 HMI 控件扩展属性,并确保所有属性值都是字符串 bool parseProperties( const QJsonObject &object, std::map *properties, ParseState *state) { for (auto current = object.constBegin(); current != object.constEnd(); ++current) { if (!current.value().isString()) { return state->fail( ProjectStorageError::InvalidField, "HMI control property values must be strings"); } properties->emplace(toUtf8(current.key()), toUtf8(current.value().toString())); } return true; } // 将单个 HMI 控件及其可选寄存器绑定序列化为 JSON 对象 QJsonObject serializeHmiControl(const HmiControl &control) { QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(control.id)); object.insert(QStringLiteral("type"), hmiControlTypeName(control.type)); object.insert(QStringLiteral("bounds"), serializeBounds(control.bounds)); object.insert(QStringLiteral("text"), fromUtf8(control.text)); // 使用 JSON null 明确表示控件没有寄存器绑定 if (control.binding.has_value()) { object.insert(QStringLiteral("binding"), serializeAddress(*control.binding)); } else { object.insert(QStringLiteral("binding"), QJsonValue::Null); } object.insert(QStringLiteral("properties"), serializeProperties(control.properties)); return object; } // 解析单个 HMI 控件,并逐层校验类型、区域、绑定和扩展属性 bool parseHmiControl( const QJsonObject &object, const std::string &context, HmiControl *control, ParseState *state) { std::string type_text; QJsonObject bounds; QJsonObject properties; QJsonValue binding; if (!readString(object, "id", context, &control->id, state) || !readString(object, "type", context, &type_text, state) || !readObject(object, "bounds", context, &bounds, state) || !readString(object, "text", context, &control->text, state) || !readValue(object, "binding", context, &binding, state) || !readObject(object, "properties", context, &properties, state)) { return false; } if (!parseHmiControlType(type_text, &control->type, state) || !parseBounds(bounds, context + ".bounds", &control->bounds, state) || !parseProperties(properties, &control->properties, state)) { return false; } // binding 允许为 null,其余非空值必须是合法的寄存器地址对象 if (binding.isNull()) { control->binding.reset(); return true; } if (!binding.isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".binding must be an object or null"); } RegisterAddress address{RegisterArea::M, 0}; if (!parseAddress(binding.toObject(), context + ".binding", &address, state)) { return false; } control->binding = address; return true; } // 将 HMI 页面及其全部控件序列化为 JSON 对象 QJsonObject serializeHmiPage(const HmiPage &page) { QJsonArray controls; for (const HmiControl &control : page.controls) { controls.append(serializeHmiControl(control)); } QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(page.id)); object.insert(QStringLiteral("name"), fromUtf8(page.name)); object.insert(QStringLiteral("width"), page.width); object.insert(QStringLiteral("height"), page.height); object.insert(QStringLiteral("controls"), controls); return object; } // 解析 HMI 页面基础信息,并按数组顺序构造页面控件 bool parseHmiPage( const QJsonObject &object, const std::string &context, HmiPage *page, ParseState *state) { QJsonArray controls; if (!readString(object, "id", context, &page->id, state) || !readString(object, "name", context, &page->name, state) || !readInt(object, "width", context, 1, std::numeric_limits::max(), &page->width, state) || !readInt(object, "height", context, 1, std::numeric_limits::max(), &page->height, state) || !readArray(object, "controls", context, &controls, state)) { return false; } // 预留准确容量,避免逐项加入控件时重复扩容 page->controls.reserve(static_cast(controls.size())); for (int index = 0; index < controls.size(); ++index) { if (!controls.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".controls items must be objects"); } HmiControl control; if (!parseHmiControl( controls.at(index).toObject(), context + ".controls[" + std::to_string(index) + ']', &control, state)) { return false; } page->controls.push_back(std::move(control)); } return true; } // 将触点模式枚举转换为工程文件中的稳定字符串 QString contactModeName(ContactMode mode) { return mode == ContactMode::NormallyOpen ? QStringLiteral("normallyOpen") : QStringLiteral("normallyClosed"); } // 将工程文件中的触点模式字符串转换为枚举 bool parseContactMode( const std::string &value, ContactMode *mode, ParseState *state) { if (value == "normallyOpen") { *mode = ContactMode::NormallyOpen; } else if (value == "normallyClosed") { *mode = ContactMode::NormallyClosed; } else { return state->fail( ProjectStorageError::InvalidField, "unsupported contact mode " + value); } return true; } // 将线圈模式枚举转换为工程文件中的稳定字符串 QString coilModeName(CoilMode mode) { switch (mode) { case CoilMode::Normal: { return QStringLiteral("normal"); } case CoilMode::Set: { return QStringLiteral("set"); } case CoilMode::Reset: { return QStringLiteral("reset"); } default: { return {}; } } } // 将工程文件中的线圈模式字符串转换为枚举 bool parseCoilMode(const std::string &value, CoilMode *mode, ParseState *state) { if (value == "normal") { *mode = CoilMode::Normal; } else if (value == "set") { *mode = CoilMode::Set; } else if (value == "reset") { *mode = CoilMode::Reset; } else { return state->fail( ProjectStorageError::InvalidField, "unsupported coil mode " + value); } return true; } // 将比较运算符枚举转换为工程文件中的稳定字符串 QString comparisonName(ComparisonOperator comparison) { switch (comparison) { case ComparisonOperator::Equal: { return QStringLiteral("equal"); } case ComparisonOperator::NotEqual: { return QStringLiteral("notEqual"); } case ComparisonOperator::LessThan: { return QStringLiteral("lessThan"); } case ComparisonOperator::LessThanOrEqual: { return QStringLiteral("lessThanOrEqual"); } case ComparisonOperator::GreaterThan: { return QStringLiteral("greaterThan"); } case ComparisonOperator::GreaterThanOrEqual: { return QStringLiteral("greaterThanOrEqual"); } default: { return {}; } } } // 将工程文件中的比较运算符字符串转换为枚举 bool parseComparison( const std::string &value, ComparisonOperator *comparison, ParseState *state) { if (value == "equal") { *comparison = ComparisonOperator::Equal; } else if (value == "notEqual") { *comparison = ComparisonOperator::NotEqual; } else if (value == "lessThan") { *comparison = ComparisonOperator::LessThan; } else if (value == "lessThanOrEqual") { *comparison = ComparisonOperator::LessThanOrEqual; } else if (value == "greaterThan") { *comparison = ComparisonOperator::GreaterThan; } else if (value == "greaterThanOrEqual") { *comparison = ComparisonOperator::GreaterThanOrEqual; } else { return state->fail( ProjectStorageError::InvalidField, "unsupported comparison operator " + value); } return true; } // 将触点节点配置序列化,并写入用于反序列化分派的类型标记 QJsonObject serializeNodeConfig(const ContactNodeConfig &config) { QJsonObject object; object.insert(QStringLiteral("type"), QStringLiteral("contact")); object.insert(QStringLiteral("address"), serializeAddress(config.address)); object.insert(QStringLiteral("mode"), contactModeName(config.mode)); return object; } // 将线圈节点配置序列化,并写入用于反序列化分派的类型标记 QJsonObject serializeNodeConfig(const CoilNodeConfig &config) { QJsonObject object; object.insert(QStringLiteral("type"), QStringLiteral("coil")); object.insert(QStringLiteral("address"), serializeAddress(config.address)); object.insert(QStringLiteral("mode"), coilModeName(config.mode)); return object; } // 将数值比较节点配置序列化,并保留有符号 16 位比较常量 QJsonObject serializeNodeConfig(const CompareNodeConfig &config) { QJsonObject object; object.insert(QStringLiteral("type"), QStringLiteral("compare")); object.insert(QStringLiteral("address"), serializeAddress(config.address)); object.insert(QStringLiteral("comparison"), comparisonName(config.comparison)); object.insert(QStringLiteral("value"), config.value); return object; } // 将逻辑节点序列化,并根据 variant 中的实际配置类型选择对应重载 QJsonObject serializeLogicNode(const LogicNode &node) { QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(node.id)); object.insert(QStringLiteral("configured"), node.configured); // std::visit 将不同节点配置统一转换为 config JSON 对象 object.insert( QStringLiteral("config"), std::visit( [](const auto &config) { return serializeNodeConfig(config); }, node.config)); return object; } // 根据 type 字段解析具体节点配置,并写入 LogicNodeConfig 变体 bool parseNodeConfig( const QJsonObject &object, const std::string &context, LogicNodeConfig *config, ParseState *state) { std::string type; QJsonObject address_object; if (!readString(object, "type", context, &type, state) || !readObject(object, "address", context, &address_object, state)) { return false; } RegisterAddress address{RegisterArea::M, 0}; if (!parseAddress(address_object, context + ".address", &address, state)) { return false; } // 每种节点只读取自身需要的字段,避免无关配置进入领域模型 if (type == "contact") { std::string mode_text; ContactMode mode = ContactMode::NormallyOpen; if (!readString(object, "mode", context, &mode_text, state) || !parseContactMode(mode_text, &mode, state)) { return false; } *config = ContactNodeConfig{address, mode}; return true; } if (type == "coil") { std::string mode_text; CoilMode mode = CoilMode::Normal; if (!readString(object, "mode", context, &mode_text, state) || !parseCoilMode(mode_text, &mode, state)) { return false; } *config = CoilNodeConfig{address, mode}; return true; } if (type == "compare") { std::string comparison_text; int value = 0; ComparisonOperator comparison = ComparisonOperator::Equal; if (!readString(object, "comparison", context, &comparison_text, state) || !readInt( object, "value", context, std::numeric_limits::min(), std::numeric_limits::max(), &value, state) || !parseComparison(comparison_text, &comparison, state)) { return false; } // 先按 int 校验范围,再安全收窄为领域模型要求的 int16_t *config = CompareNodeConfig{ address, comparison, static_cast(value)}; return true; } return state->fail( ProjectStorageError::InvalidField, "unsupported logic node type " + type); } // 解析逻辑节点标识及其多态配置 bool parseLogicNode( const QJsonObject &object, const std::string &context, LogicNode *node, ParseState *state) { QJsonObject config; if (!readString(object, "id", context, &node->id, state) || !readBool(object, "configured", context, &node->configured, state) || !readObject(object, "config", context, &config, state) || !parseNodeConfig(config, context + ".config", &node->config, state)) { return false; } return true; } QString expressionKindText(ConditionExpressionKind kind) { switch (kind) { case ConditionExpressionKind::Node: return QStringLiteral("node"); case ConditionExpressionKind::Series: return QStringLiteral("series"); case ConditionExpressionKind::Parallel: return QStringLiteral("parallel"); } return {}; } QJsonObject serializeConditionExpression(const ConditionExpression &expression) { QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(expression.id)); object.insert(QStringLiteral("kind"), expressionKindText(expression.kind)); if (expression.kind == ConditionExpressionKind::Node) { object.insert(QStringLiteral("node"), serializeLogicNode(*expression.node)); } else { QJsonArray children; for (const ConditionExpression &child : expression.children) { children.append(serializeConditionExpression(child)); } object.insert(QStringLiteral("children"), children); } return object; } bool parseConditionExpression( const QJsonObject &object, const std::string &context, ConditionExpression *expression, ParseState *state) { std::string kind; if (!readString(object, "id", context, &expression->id, state) || !readString(object, "kind", context, &kind, state)) { return false; } if (kind == "node") { QJsonObject node; if (!readObject(object, "node", context, &node, state)) { return false; } LogicNode parsed_node; if (!parseLogicNode(node, context + ".node", &parsed_node, state)) { return false; } expression->kind = ConditionExpressionKind::Node; expression->node = std::move(parsed_node); return true; } if (kind != "series" && kind != "parallel") { return state->fail( ProjectStorageError::InvalidField, context + ".kind must be node, series or parallel"); } QJsonArray children; if (!readArray(object, "children", context, &children, state)) { return false; } expression->kind = kind == "series" ? ConditionExpressionKind::Series : ConditionExpressionKind::Parallel; expression->children.reserve(static_cast(children.size())); for (int index = 0; index < children.size(); ++index) { if (!children.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".children items must be objects"); } ConditionExpression child; if (!parseConditionExpression( children.at(index).toObject(), context + ".children[" + std::to_string(index) + ']', &child, state)) { return false; } expression->children.push_back(std::move(child)); } return true; } QJsonObject serializeLadderRung(const LadderRung &rung) { QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(rung.id)); object.insert(QStringLiteral("name"), fromUtf8(rung.name)); object.insert( QStringLiteral("condition"), rung.condition.has_value() ? QJsonValue(serializeConditionExpression(*rung.condition)) : QJsonValue(QJsonValue::Null)); object.insert( QStringLiteral("output"), rung.output.has_value() ? QJsonValue(serializeLogicNode(*rung.output)) : QJsonValue(QJsonValue::Null)); return object; } bool parseLadderRung( const QJsonObject &object, const std::string &context, LadderRung *rung, ParseState *state) { QJsonValue condition; QJsonValue output; if (!readString(object, "id", context, &rung->id, state) || !readString(object, "name", context, &rung->name, state) || !readValue(object, "output", context, &output, state)) { return false; } if (!readValue(object, "condition", context, &condition, state)) { return false; } if (condition.isNull()) { rung->condition.reset(); } else if (!condition.isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".condition must be an object or null"); } else { ConditionExpression parsed_condition; if (!parseConditionExpression( condition.toObject(), context + ".condition", &parsed_condition, state)) { return false; } rung->condition = std::move(parsed_condition); } if (output.isNull()) { rung->output.reset(); return true; } if (!output.isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".output must be an object or null"); } LogicNode node; if (!parseLogicNode(output.toObject(), context + ".output", &node, state)) { return false; } rung->output = std::move(node); return true; } QJsonObject serializeControlLogic(const ControlLogic &logic) { QJsonArray rungs; for (const LadderRung &rung : logic.rungs) { rungs.append(serializeLadderRung(rung)); } QJsonObject object; object.insert(QStringLiteral("id"), fromUtf8(logic.id)); object.insert(QStringLiteral("name"), fromUtf8(logic.name)); object.insert(QStringLiteral("enabled"), logic.enabled); object.insert(QStringLiteral("rungs"), rungs); return object; } bool parseControlLogic( const QJsonObject &object, const std::string &context, ControlLogic *logic, ParseState *state) { QJsonArray rungs; if (!readString(object, "id", context, &logic->id, state) || !readString(object, "name", context, &logic->name, state) || !readBool(object, "enabled", context, &logic->enabled, state) || !readArray(object, "rungs", context, &rungs, state)) { return false; } logic->rungs.reserve(static_cast(rungs.size())); for (int index = 0; index < rungs.size(); ++index) { if (!rungs.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, context + ".rungs items must be objects"); } LadderRung rung; if (!parseLadderRung( rungs.at(index).toObject(), context + ".rungs[" + std::to_string(index) + ']', &rung, state)) { return false; } logic->rungs.push_back(std::move(rung)); } return true; } // 将完整领域工程聚合为工程文件的顶层 JSON 对象 QJsonObject serializeProject(const Project &project) { QJsonArray pages; for (const HmiPage &page : project.hmiPages) { pages.append(serializeHmiPage(page)); } QJsonArray logics; for (const ControlLogic &logic : project.controlLogics) { logics.append(serializeControlLogic(logic)); } QJsonObject object; object.insert(QStringLiteral("formatVersion"), fromUtf8(project.metadata.formatVersion)); object.insert(QStringLiteral("id"), fromUtf8(project.metadata.id)); object.insert(QStringLiteral("name"), fromUtf8(project.metadata.name)); object.insert(QStringLiteral("hmiPages"), pages); object.insert(QStringLiteral("controlLogics"), logics); return object; } // 从顶层 JSON 对象解析完整工程,并在解析子对象前检查格式版本 bool parseProject( const QJsonObject &object, Project *project, ParseState *state) { QJsonArray pages; QJsonArray logics; if (!readString( object, "formatVersion", "project", &project->metadata.formatVersion, state)) { return false; } // 不尝试猜测其他版本的结构,避免按当前格式错误解释数据 if (project->metadata.formatVersion != kCurrentFormatVersion) { return state->fail( ProjectStorageError::UnsupportedVersion, "unsupported project format version " + project->metadata.formatVersion); } if (!readString(object, "id", "project", &project->metadata.id, state) || !readString(object, "name", "project", &project->metadata.name, state) || !readArray(object, "hmiPages", "project", &pages, state) || !readArray(object, "controlLogics", "project", &logics, state)) { return false; } // 逐层解析时持续传递字段路径,任何失败都保留最初的具体位置 project->hmiPages.reserve(static_cast(pages.size())); for (int index = 0; index < pages.size(); ++index) { if (!pages.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, "project.hmiPages items must be objects"); } HmiPage page; if (!parseHmiPage( pages.at(index).toObject(), "project.hmiPages[" + std::to_string(index) + ']', &page, state)) { return false; } project->hmiPages.push_back(std::move(page)); } project->controlLogics.reserve(static_cast(logics.size())); for (int index = 0; index < logics.size(); ++index) { if (!logics.at(index).isObject()) { return state->fail( ProjectStorageError::InvalidField, "project.controlLogics items must be objects"); } ControlLogic logic; if (!parseControlLogic( logics.at(index).toObject(), "project.controlLogics[" + std::to_string(index) + ']', &logic, state)) { return false; } project->controlLogics.push_back(std::move(logic)); } return true; } // 将 Qt 文件错误转换为领域层统一的工程保存失败结果 ProjectSaveResult saveFailure( ProjectStorageError error, const QString &message) { return {false, error, toUtf8(message)}; } } // namespace // 校验工程后将其序列化,并通过 QSaveFile 原子写入目标文件 ProjectSaveResult JsonProjectStorage::save( const Project &project, const std::string &file_path) { // 写文件前先执行领域校验,防止持久化内部关系不合法的工程 std::string validation_error; if (!project.validate(&validation_error)) { return {false, ProjectStorageError::InvalidProject, validation_error}; } if (project.metadata.formatVersion != kCurrentFormatVersion) { return {false, ProjectStorageError::UnsupportedVersion, "unsupported project format version " + project.metadata.formatVersion}; } // QSaveFile 先写临时文件,仅在 commit 成功后替换目标文件 QSaveFile file(fromUtf8(file_path)); if (!file.open(QIODevice::WriteOnly)) { return saveFailure(ProjectStorageError::FileOpenFailed, file.errorString()); } const QByteArray data = QJsonDocument(serializeProject(project)).toJson( QJsonDocument::Indented); // 短写入也视为失败,并取消临时文件提交 if (file.write(data) != data.size()) { file.cancelWriting(); return saveFailure(ProjectStorageError::FileWriteFailed, file.errorString()); } if (!file.commit()) { return saveFailure(ProjectStorageError::FileWriteFailed, file.errorString()); } return {true, ProjectStorageError::None, {}}; } // 读取并解析工程文件,全部校验通过后才返回新的领域工程 ProjectLoadResult JsonProjectStorage::load(const std::string &file_path) { QFile file(fromUtf8(file_path)); if (!file.open(QIODevice::ReadOnly)) { return {false, {}, ProjectStorageError::FileOpenFailed, toUtf8(file.errorString())}; } const QByteArray data = file.readAll(); if (file.error() != QFileDevice::NoError) { return {false, {}, ProjectStorageError::FileReadFailed, toUtf8(file.errorString())}; } // 顶层必须是 JSON 对象,数组或标量不能表示完整工程 QJsonParseError parse_error; const QJsonDocument document = QJsonDocument::fromJson(data, &parse_error); if (parse_error.error != QJsonParseError::NoError || !document.isObject()) { const QString message = parse_error.error == QJsonParseError::NoError ? QStringLiteral("project JSON root must be an object") : parse_error.errorString(); return {false, {}, ProjectStorageError::InvalidJson, toUtf8(message)}; } // 先在局部对象中完成结构解析,失败时不会暴露半成品工程 Project project; ParseState state; if (!parseProject(document.object(), &project, &state)) { return {false, {}, state.error, state.message}; } // JSON 字段合法不代表业务关系合法,还需执行领域层整体校验 std::string validation_error; if (!project.validate(&validation_error)) { return {false, {}, ProjectStorageError::InvalidProject, validation_error}; } return {true, std::move(project), ProjectStorageError::None, {}}; }