diff --git a/backend/iot-device/src/main/java/com/iot/device/dto/ControlItem.java b/backend/iot-device/src/main/java/com/iot/device/dto/ControlItem.java index a491005..3f11bd8 100644 --- a/backend/iot-device/src/main/java/com/iot/device/dto/ControlItem.java +++ b/backend/iot-device/src/main/java/com/iot/device/dto/ControlItem.java @@ -53,4 +53,18 @@ public class ControlItem { @Schema(description = "当前值(按设备查询时填充)") private Object value; + + // ===== 语义扩展(客户端据此渲染,取代按名称猜测)===== + + @Schema(description = "功能语义标签(power/switch/backlight/indicator/brightness/color_temp/mode/scene/countdown/...)") + private String semantic; + + @Schema(description = "功能分组(switch/scene/mode/countdown/display/config/measure/status)") + private String grp; + + @Schema(description = "是否主开关(1是,0否)") + private Integer isPrimary; + + @Schema(description = "枚举中文名映射") + private java.util.Map enumLabels; } diff --git a/backend/iot-device/src/main/java/com/iot/device/entity/ProductAction.java b/backend/iot-device/src/main/java/com/iot/device/entity/ProductAction.java index 0aa3d7e..4dde2e2 100644 --- a/backend/iot-device/src/main/java/com/iot/device/entity/ProductAction.java +++ b/backend/iot-device/src/main/java/com/iot/device/entity/ProductAction.java @@ -29,4 +29,7 @@ public class ProductAction extends BaseEntity { @Schema(description = "输出参数(JSON)") private String output; + + @Schema(description = "完成模式(sync 同步完成 / ack_only 仅受理)") + private String completionMode; } diff --git a/backend/iot-device/src/main/java/com/iot/device/entity/ProductProperty.java b/backend/iot-device/src/main/java/com/iot/device/entity/ProductProperty.java index aaa431d..9fe980d 100644 --- a/backend/iot-device/src/main/java/com/iot/device/entity/ProductProperty.java +++ b/backend/iot-device/src/main/java/com/iot/device/entity/ProductProperty.java @@ -53,4 +53,24 @@ public class ProductProperty extends BaseEntity { @Schema(description = "排序") private Integer sort; + + // ===== 语义扩展(V19 建列 / V22 补列,此前库里有列但代码没用)===== + + @Schema(description = "功能语义标签(power/switch/backlight/indicator/brightness/color_temp/mode/scene/countdown/child_lock/temperature/humidity/battery/motion/contact/fault/raw)") + private String semantic; + + @Schema(description = "功能分组(switch/scene/mode/countdown/display/config/measure/status)") + private String grp; + + @Schema(description = "是否主开关(1是,0否)") + private Integer isPrimary; + + @Schema(description = "物理通道") + private String channel; + + @Schema(description = "完成模式(sync/ack_only)") + private String completionMode; + + @Schema(description = "枚举中文名(JSON对象)") + private String enumLabel; } diff --git a/backend/iot-device/src/main/java/com/iot/device/service/impl/ControlSchemaServiceImpl.java b/backend/iot-device/src/main/java/com/iot/device/service/impl/ControlSchemaServiceImpl.java index 20c32f7..bd30a94 100644 --- a/backend/iot-device/src/main/java/com/iot/device/service/impl/ControlSchemaServiceImpl.java +++ b/backend/iot-device/src/main/java/com/iot/device/service/impl/ControlSchemaServiceImpl.java @@ -17,9 +17,11 @@ import com.iot.device.mapper.ProductEventMapper; import com.iot.device.mapper.ProductMapper; import com.iot.device.mapper.ProductPropertyMapper; import com.iot.device.service.ControlSchemaService; +import com.iot.device.service.spec.SpecSemanticResolver; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -42,6 +44,7 @@ public class ControlSchemaServiceImpl implements ControlSchemaService { private final ProductEventMapper productEventMapper; private final DeviceMapper deviceMapper; private final ObjectMapper objectMapper; + private final SpecSemanticResolver semanticResolver; @Override public ControlSchema getProductSchema(Long productId) { @@ -134,8 +137,19 @@ public class ControlSchemaServiceImpl implements ControlSchemaService { return result; } + /** + * 回填当前值。 + * + *

key 语义统一:物模型 code 是平台无关的稳定标识(涂鸦 code), + * 而 dp_id 是涂鸦的数字 DP 别名。运行期上报的 dps 的 key 两者都可能出现 + * (APP 上报用数字 dpId,云端状态同步用 code),因此按 code 优先、dp_id 兜底匹配。

+ */ private void fillValues(List items, Map dps) { for (ControlItem item : items) { + if (item.getCode() != null && dps.containsKey(item.getCode())) { + item.setValue(dps.get(item.getCode())); + continue; + } if (item.getDpId() != null && dps.containsKey(item.getDpId())) { item.setValue(dps.get(item.getDpId())); } @@ -172,10 +186,44 @@ public class ControlSchemaServiceImpl implements ControlSchemaService { item.setEnumValues(parseEnum(p.getEnumList())); String access = p.getAccess() == null ? "" : p.getAccess().toLowerCase(Locale.ROOT); item.setWritable(access.contains("write")); + // 语义字段:物模型已落库则直接用;未落库(旧数据)则即时推导,保证客户端始终拿得到语义 + String semantic = p.getSemantic(); + String grp = p.getGrp(); + if (!StringUtils.hasText(semantic) || !StringUtils.hasText(grp)) { + SpecSemanticResolver.Semantic s = semanticResolver.resolve( + p.getCode(), p.getName(), p.getType(), p.getAccess()); + if (!StringUtils.hasText(semantic)) { + semantic = s.getSemantic(); + } + if (!StringUtils.hasText(grp)) { + grp = s.getGrp(); + } + // 可写性修正:状态量被误标 write 时强制只读 + if (semanticResolver.isReadOnly(semantic, grp, p.getCode())) { + item.setWritable(false); + } + } + item.setSemantic(semantic); + item.setGrp(grp); + item.setIsPrimary(p.getIsPrimary()); + item.setEnumLabels(parseEnumLabels(p.getEnumLabel())); item.setWidget(resolveWidget(item)); return item; } + /** 解析枚举中文名映射({"warm":"暖风"})。 */ + private Map parseEnumLabels(String json) { + if (json == null || json.isBlank()) { + return null; + } + try { + return objectMapper.readValue(json, new TypeReference>() { + }); + } catch (Exception e) { + return null; + } + } + private String resolveWidget(ControlItem item) { String type = item.getType() == null ? "" : item.getType().toLowerCase(Locale.ROOT); String name = item.getName() == null ? "" : item.getName().toLowerCase(Locale.ROOT); @@ -195,8 +243,28 @@ public class ControlSchemaServiceImpl implements ControlSchemaService { return "text"; } - /** 主开关:优先匹配 开关/电源/switch/power;否则唯一可写 bool */ + /** + * 主开关识别。 + * + *

重构:原实现按名称含 switch/power/开关/电源 猜测, + * 会把「背光开关」误判成主开关(JD2BTSA 误控背光的根因)。 + * 现改为优先使用物模型落库的 {@code is_primary} / {@code semantic}, + * 由 {@link SpecSemanticResolver} 单一权威判定;物模型无语义字段时才回退旧的启发式。

+ */ private String pickMainSwitch(List writable) { + // 1) 物模型已显式标记主开关 + for (ControlItem i : writable) { + if (i.getIsPrimary() != null && i.getIsPrimary() == 1) { + return i.getCode(); + } + } + // 2) 语义为 power + for (ControlItem i : writable) { + if (SpecSemanticResolver.POWER.equals(i.getSemantic()) && i.isWritable()) { + return i.getCode(); + } + } + // 3) 回退:旧启发式(仅在物模型无语义字段时才会走到) List bools = writable.stream() .filter(i -> "switch".equals(i.getWidget())) .toList(); diff --git a/backend/iot-device/src/main/java/com/iot/device/service/impl/ProductServiceImpl.java b/backend/iot-device/src/main/java/com/iot/device/service/impl/ProductServiceImpl.java index 0142d6a..c93b53f 100644 --- a/backend/iot-device/src/main/java/com/iot/device/service/impl/ProductServiceImpl.java +++ b/backend/iot-device/src/main/java/com/iot/device/service/impl/ProductServiceImpl.java @@ -3,6 +3,8 @@ package com.iot.device.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.iot.common.constants.PlatformType; + +import java.util.ArrayList; import com.iot.device.dto.ProductSpec; import com.iot.device.entity.Device; import com.iot.device.entity.Product; @@ -16,6 +18,7 @@ import com.iot.device.mapper.ProductMapper; import com.iot.device.mapper.ProductPropertyMapper; import com.iot.device.service.ProductService; import com.iot.device.service.TuyaCloudService; +import com.iot.device.service.spec.SpecSemanticResolver; import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -38,6 +41,7 @@ public class ProductServiceImpl implements ProductService { private final ProductActionMapper productActionMapper; private final DeviceMapper deviceMapper; private final TuyaCloudService tuyaCloudService; + private final SpecSemanticResolver semanticResolver; @Override @Transactional @@ -327,8 +331,19 @@ public class ProductServiceImpl implements ProductService { } } pp.setSort(sort++); + // 语义落库(SpecSemanticResolver 为唯一权威): + // 1) semantic / grp 决定客户端如何渲染、能否当主开关; + // 2) 修正 access —— 涂鸦 fnCodes 为空时兜底成"全部可写", + // 会把 scene_1~6 / relay_status / countdown 等状态量误标为可写。 + SpecSemanticResolver.Semantic sem = semanticResolver.resolve( + pp.getCode(), pp.getName(), pp.getType(), pp.getAccess()); + pp.setSemantic(sem.getSemantic()); + pp.setGrp(sem.getGrp()); + pp.setAccess(sem.isWritable() ? "write" : "read"); productPropertyMapper.insert(pp); } + // 选主开关并回填 is_primary(详情页大圆钮的唯一依据) + applyPrimary(p.getId()); p.setSpecVersion(p.getSpecVersion() == null ? 1 : p.getSpecVersion() + 1); productMapper.updateById(p); total++; @@ -336,6 +351,37 @@ public class ProductServiceImpl implements ProductService { return total; } + /** + * 依据语义规则选出产品的主开关功能点,并回填 is_primary。 + */ + private void applyPrimary(Long productId) { + List props = productPropertyMapper.selectList( + new LambdaQueryWrapper().eq(ProductProperty::getProductId, productId)); + if (props.isEmpty()) { + return; + } + List views = new ArrayList<>(); + for (ProductProperty pp : props) { + SpecSemanticResolver.Semantic sem = semanticResolver.resolve( + pp.getCode(), pp.getName(), pp.getType(), pp.getAccess()); + views.add(new SpecSemanticResolver.ProductPropertyView( + pp.getCode(), pp.getName(), pp.getType(), + pp.getSemantic() != null ? pp.getSemantic() : sem.getSemantic(), + pp.getGrp() != null ? pp.getGrp() : sem.getGrp(), + sem.isWritable(), pp.getSort())); + } + String primary = semanticResolver.pickPrimary(views); + for (ProductProperty pp : props) { + int want = (primary != null && primary.equals(pp.getCode())) ? 1 : 0; + if (pp.getIsPrimary() == null || pp.getIsPrimary() != want) { + ProductProperty upd = new ProductProperty(); + upd.setId(pp.getId()); + upd.setIsPrimary(want); + productPropertyMapper.updateById(upd); + } + } + } + private String mapTuyaType(String t) { if (t == null) { return "string"; diff --git a/backend/iot-device/src/main/java/com/iot/device/service/spec/SpecSemanticResolver.java b/backend/iot-device/src/main/java/com/iot/device/service/spec/SpecSemanticResolver.java new file mode 100644 index 0000000..7c81af3 --- /dev/null +++ b/backend/iot-device/src/main/java/com/iot/device/service/spec/SpecSemanticResolver.java @@ -0,0 +1,413 @@ +package com.iot.device.service.spec; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * 物模型语义解析器 —— 功能点语义规则的唯一权威。 + * + *

重构前,同一个"哪个功能点是主开关、哪些只是状态量"的问题有两份启发式实现:

+ *
    + *
  • Android 端 TuyaSdkService.switchDpScore / isNonPowerSwitchName + * (26 个中英排除词 + 100/90/60/40/10 五档打分)
  • + *
  • 后端 ControlSchemaServiceImpl.pickMainSwitch + * (名称含 switch/power/开关/电源)
  • + *
+ *

两份实现口径不一致,是 JD2BTSA「背光开关被当成主开关误控」以及 + * JD6BTSA「scene_1~6 / countdown / relay_status 等状态量被渲染成可写控件」的直接根因。 + * 现统一收敛到本类,客户端只消费物模型落库的 semantic / grp / is_primary 字段。

+ */ +@Component +public class SpecSemanticResolver { + + // ==================== 语义标签常量 ==================== + public static final String POWER = "power"; + public static final String SWITCH = "switch"; + public static final String BACKLIGHT = "backlight"; + public static final String INDICATOR = "indicator"; + public static final String BRIGHTNESS = "brightness"; + public static final String COLOR_TEMP = "color_temp"; + public static final String MODE = "mode"; + public static final String SCENE = "scene"; + public static final String COUNTDOWN = "countdown"; + public static final String CHILD_LOCK = "child_lock"; + public static final String TEMPERATURE = "temperature"; + public static final String HUMIDITY = "humidity"; + public static final String BATTERY = "battery"; + public static final String MOTION = "motion"; + public static final String CONTACT = "contact"; + public static final String FAULT = "fault"; + public static final String RAW = "raw"; + + // ==================== 功能分组常量 ==================== + public static final String GRP_SWITCH = "switch"; + public static final String GRP_SCENE = "scene"; + public static final String GRP_MODE = "mode"; + public static final String GRP_COUNTDOWN = "countdown"; + public static final String GRP_DISPLAY = "display"; + public static final String GRP_CONFIG = "config"; + public static final String GRP_MEASURE = "measure"; + public static final String GRP_STATUS = "status"; + + /** 非主控语义词表:命中即判定为状态量/配置项,永远不能被当作主开关。 */ + private static final Map NON_CONTROL = buildNonControl(); + + /** 功能语义词表:命中即给出功能语义。 */ + private static final Map FUNCTION = buildFunction(); + + /** + * 只读语义集合:即使物模型里被标成 write,也应视为只读状态量。 + * 根因:涂鸦物模型同步时用 fnCodes.contains(code) 判定可写, + * 而 fnCodes 为空时兜底成"全部可写",导致状态量被误标。 + */ + private static final Set READ_ONLY_SEMANTICS = Set.of(SCENE, FAULT); + + /** 明确不可作为主开关的分组。 */ + private static final Set NON_PRIMARY_GROUPS = Set.of( + GRP_SCENE, GRP_MODE, GRP_COUNTDOWN, GRP_DISPLAY, GRP_CONFIG, GRP_MEASURE, GRP_STATUS); + + /** + * 解析单个功能点的语义。 + * + * @param code 功能点编码(如 switch_1 / backlight_switch) + * @param name 功能点名称(如 开关 / 背光开关) + * @param type 数据类型(bool/int/enum/...) + * @param access 声明的访问权限(read/write/notify) + */ + public Semantic resolve(String code, String name, String type, String access) { + String c = norm(code); + String n = norm(name); + String both = (c + " " + n).trim(); + + String semantic = null; + String grp = matchGroup(c); + + // 1) 先判"非主控"(背光/指示灯/倒计时/状态量…)——最容易被误认成主开关 + for (Map.Entry e : NON_CONTROL.entrySet()) { + if (containsWord(both, e.getKey())) { + semantic = e.getValue(); + break; + } + } + // 2) 再判功能语义 + if (semantic == null) { + for (Map.Entry e : FUNCTION.entrySet()) { + if (containsWord(both, e.getKey())) { + semantic = e.getValue(); + break; + } + } + } + // 3) 开关类兜底 + if (semantic == null && isSwitchWord(both)) { + semantic = SWITCH; + } + if (semantic == null) { + semantic = RAW; + } + + // 分组兜底 + if (grp == null) { + grp = defaultGroup(semantic); + } + + // 可写性修正:声明 access 含 write,但语义属只读状态量 → 强制只读 + boolean declaredWritable = access != null && access.toLowerCase(Locale.ROOT).contains("write"); + boolean writable = declaredWritable && !isReadOnly(semantic, grp, code); + + return new Semantic(semantic, grp, writable, false); + } + /** + * 在一个产品的物模型范围内选出主开关(is_primary)。 + * + *

规则:

+ *
    + *
  1. 优先取语义为 power 且可写的功能点(sort 最小者);
  2. + *
  3. 其次取 switch 分组内 sort 最小的可写开关通道(多通道设备以首个为详情页主开关, + * 列表页仍按 grp 整组联动);
  4. + *
  5. 都没有则无主开关(客户端退化为普通控件列表)。
  6. + *
+ * + * @return 主开关的 code;无主开关返回 null + */ + public String pickPrimary(List props) { + if (props == null || props.isEmpty()) { + return null; + } + List sorted = new ArrayList<>(props); + sorted.sort((a, b) -> Integer.compare(nz(a.getSort()), nz(b.getSort()))); + + // 1) 明确标记为 power 的 + for (ProductPropertyView p : sorted) { + if (POWER.equals(p.getSemantic()) && p.isWritable()) { + return p.getCode(); + } + } + // 2) switch 分组内首个可写通道 + for (ProductPropertyView p : sorted) { + if (p.isWritable() + && !NON_PRIMARY_GROUPS.contains(p.getGrp()) + && (SWITCH.equals(p.getSemantic()) || GRP_SWITCH.equals(p.getGrp()))) { + return p.getCode(); + } + } + // 3) 兜底:全产品唯一可写布尔 + ProductPropertyView only = null; + int writableBoolCount = 0; + for (ProductPropertyView p : sorted) { + if (p.isWritable() && "bool".equalsIgnoreCase(p.getType())) { + writableBoolCount++; + if (only == null) { + only = p; + } + } + } + return (writableBoolCount == 1 && only != null) ? only.getCode() : null; + } + + /** + * 判断某语义/分组是否应视为只读状态量。 + */ + public boolean isReadOnly(String semantic, String grp, String code) { + if (semantic != null && READ_ONLY_SEMANTICS.contains(semantic)) { + return true; + } + if (grp != null && (GRP_STATUS.equals(grp) || GRP_SCENE.equals(grp))) { + return true; + } + // relay_status* 这类明确的状态上报前缀 + return code != null && code.toLowerCase(Locale.ROOT).startsWith("relay_status"); + } + + /** 判断某分组是否为「开关通道组」(设备列表整组联动的依据)。 */ + public boolean isSwitchGroup(String grp) { + return GRP_SWITCH.equals(grp); + } + + // ==================== 内部规则 ==================== + + private static Map buildNonControl() { + Map m = new LinkedHashMap<>(); + // 背光 / 指示灯 / 氛围 + m.put("背光", BACKLIGHT); + m.put("backlight", BACKLIGHT); + m.put("指示灯", INDICATOR); + m.put("指示", INDICATOR); + m.put("indicator", INDICATOR); + m.put("氛围灯", INDICATOR); + m.put("氛围", INDICATOR); + m.put("ambience", INDICATOR); + m.put("夜灯", INDICATOR); + m.put("night", INDICATOR); + m.put("灯效", INDICATOR); + m.put("彩灯", INDICATOR); + m.put("led", INDICATOR); + // 倒计时 / 定时 + m.put("倒计时", COUNTDOWN); + m.put("countdown", COUNTDOWN); + m.put("定时", COUNTDOWN); + m.put("timer", COUNTDOWN); + // 童锁 + m.put("童锁", CHILD_LOCK); + m.put("childlock", CHILD_LOCK); + m.put("child lock", CHILD_LOCK); + // 故障 / 报警 + m.put("故障", FAULT); + m.put("fault", FAULT); + m.put("报警", FAULT); + m.put("alarm", FAULT); + // 其余状态/配置量:归为原始语义,绝不作为主开关 + for (String w : List.of("状态", "status", "state", "剩余", "remaining", "累计", + "保温", "节能", "省电", "eco", "恢复", "出厂", "设置", "setting", "reset", + "锁定", "lock", "配网", "校准")) { + m.put(w, RAW); + } + return m; + } + + private static Map buildFunction() { + Map m = new LinkedHashMap<>(); + m.put("亮度", BRIGHTNESS); + m.put("brightness", BRIGHTNESS); + m.put("调光", BRIGHTNESS); + m.put("色温", COLOR_TEMP); + m.put("colortemp", COLOR_TEMP); + m.put("colortempvalue", COLOR_TEMP); + m.put("模式", MODE); + m.put("mode", MODE); + m.put("场景", SCENE); + m.put("scene", SCENE); + m.put("preset", SCENE); + m.put("温度", TEMPERATURE); + m.put("temperature", TEMPERATURE); + m.put("湿度", HUMIDITY); + m.put("humidity", HUMIDITY); + m.put("电量", BATTERY); + m.put("battery", BATTERY); + m.put("人体", MOTION); + m.put("motion", MOTION); + m.put("pir", MOTION); + m.put("门磁", CONTACT); + m.put("contact", CONTACT); + return m; + } + + /** 分组匹配:优先按编码前缀(switch_1 → switch)。 */ + private String matchGroup(String normCode) { + String c = normCode; + if (c.startsWith("switch")) { + return GRP_SWITCH; + } + if (c.startsWith("scene")) { + return GRP_SCENE; + } + if (c.startsWith("mode")) { + return GRP_MODE; + } + if (c.startsWith("countdown") || c.startsWith("timer")) { + return GRP_COUNTDOWN; + } + if (c.startsWith("relaystatus")) { + return GRP_STATUS; + } + if (c.startsWith("backlight")) { + return GRP_DISPLAY; + } + if (c.startsWith("battery")) { + return GRP_MEASURE; + } + return null; + } + + private String defaultGroup(String semantic) { + if (semantic == null) { + return null; + } + return switch (semantic) { + case SWITCH, POWER -> GRP_SWITCH; + case SCENE -> GRP_SCENE; + case MODE -> GRP_MODE; + case COUNTDOWN -> GRP_COUNTDOWN; + case BACKLIGHT, INDICATOR -> GRP_DISPLAY; + case TEMPERATURE, HUMIDITY, BATTERY -> GRP_MEASURE; + case FAULT -> GRP_STATUS; + default -> null; + }; + } + + /** 归一化:小写 + 去掉空格/下划线/连字符/点。 */ + private String norm(String s) { + if (s == null) { + return ""; + } + return s.trim().toLowerCase(Locale.ROOT) + .replace(" ", "").replace("_", "").replace("-", "").replace(".", ""); + } + + /** 词包含判断:归一化后的串里包含该词。 */ + private boolean containsWord(String normalizedBoth, String word) { + if (word == null || word.isEmpty()) { + return false; + } + String w = word.toLowerCase(Locale.ROOT).replace(" ", ""); + return normalizedBoth.contains(w); + } + + private boolean isSwitchWord(String both) { + return both.contains("开关") || both.contains("switch") || both.contains("电源") + || both.contains("power") || both.contains("onoff") || both.contains("通电") + || both.contains("断电") || both.contains("供电") || both.contains("relay"); + } + + private static int nz(Integer v) { + return v == null ? Integer.MAX_VALUE : v; + } + // ==================== 值对象 ==================== + + /** 语义解析结果。 */ + public static class Semantic { + private final String semantic; + private final String grp; + private final boolean writable; + private final boolean primary; + + public Semantic(String semantic, String grp, boolean writable, boolean primary) { + this.semantic = semantic; + this.grp = grp; + this.writable = writable; + this.primary = primary; + } + + public String getSemantic() { + return semantic; + } + + public String getGrp() { + return grp; + } + + public boolean isWritable() { + return writable; + } + + public boolean isPrimary() { + return primary; + } + } + + /** 物模型功能点视图(供主开关挑选,避免直接依赖实体层)。 */ + public static class ProductPropertyView { + private final String code; + private final String name; + private final String type; + private final String semantic; + private final String grp; + private final boolean writable; + private final Integer sort; + + public ProductPropertyView(String code, String name, String type, String semantic, + String grp, boolean writable, Integer sort) { + this.code = code; + this.name = name; + this.type = type; + this.semantic = semantic; + this.grp = grp; + this.writable = writable; + this.sort = sort; + } + + public String getCode() { + return code; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getSemantic() { + return semantic; + } + + public String getGrp() { + return grp; + } + + public boolean isWritable() { + return writable; + } + + public Integer getSort() { + return sort; + } + } +} \ No newline at end of file