update 导入接口添加TID校验、EPC校验、更改导入模式。查询接口增加多条异常提示。

master
yinq 2 months ago
parent 9f6d1c4223
commit c37f105361

@ -20,6 +20,11 @@ public class ApiConstants {
*/
public static final Long PASS_TIME = 21600000L;
/**
* TID\EPC
*/
public static final Integer TE_LENGTH = 24;
/**
*
*/
@ -30,6 +35,18 @@ public class ApiConstants {
*/
public static final String OUT_CALLBACK_URL = "https://www.kdocs.cn/chatflow/api/v2/func/webhook/2w7TqqdGMqmLwdfQY3wFidcBF4K";
/**
*
*/
//“新增”模式需要 TID和 EPC查重确保新添加的内容唯一性建立新的记录;
public static final String MODE_ADD = "新增";
//“更新”模式则无需查重TID按照导入表格中的TID去查找相关记录并逐条更新TID对应的其他记录如EPC等只更新导入表格中含有的信息没有的信息保持原状而不是替换为空白内容
public static final String MODE_UPDATE = "更新";
//“新增(不查重 EPC)”模式只需要 TID查重建立新记录;
public static final String MODE_ADD_NO_CHECK = "新增不查重EPC";
//“更新(不查重 EPC)”模式只需要 TID 查重,按照导入表格中的 TID 去查找相关记录并逐条更新TID对应的其他记录如 EPC(不查重 EPC)等
public static final String MODE_UPDATE_NO_CHECK = "更新不查重EPC";
/**
*
*/
@ -55,4 +72,12 @@ public class ApiConstants {
public static final String DESCRIPTION = "情况说明";
public static final String STATUS_IMPORTING = "导入中";
public static final String STATUS_SYSTEM_ERROR = "系统错误";
public static final String IMPORT_SUCCESS = "导入成功";
/** 搜索状态 */
public static final String SELECT_STATUS = "搜索状态";
/** 导出状态 */
public static final String EXPORT_STATUS = "导出状态";
}

@ -114,9 +114,22 @@ public class HwTagRecord extends BaseEntity
*
*/
private String endDate;
/**
* TID
*/
private String selectTID;
/**
* EPC
*/
private String selectEPC;
/**
*
*/
private Integer queryResultsNumber;
@Override
public String toString() {
return "HwTagRecord{" +

@ -1,6 +1,8 @@
package hw.tagApi.service.mapper;
import java.util.List;
import java.util.Set;
import hw.tagApi.service.domain.HwTagRecord;
/**
@ -82,4 +84,19 @@ public interface HwTagRecordMapper
*/
public HwTagRecord selectHwTagRecordByTxId(String tId);
/**
* TID
*
* @param tids TID
* @return TID
*/
public Set<String> selectExistingTids(List<String> tids);
/**
* EPC
*
* @param epcs EPC
* @return EPC
*/
public Set<String> selectExistingEpcs(List<String> epcs);
}

@ -1,6 +1,8 @@
package hw.tagApi.service.service;
import java.util.List;
import java.util.Set;
import hw.tagApi.service.domain.HwTagRecord;
/**
@ -74,6 +76,12 @@ public interface IHwTagRecordService
public void batchInsertHwTagRecord(List<HwTagRecord> batchList);
/**
*
*
* @param selectTagRecord HwTagRecord
* @return HwTagRecord
*/
public HwTagRecord selectHwTagRecordData(HwTagRecord selectTagRecord);
/**
@ -83,4 +91,19 @@ public interface IHwTagRecordService
*/
public HwTagRecord selectHwTagRecordByTID(String tId);
/**
* TID
*
* @param tids TID
* @return TID
*/
public Set<String> selectExistingTids(List<String> tids);
/**
* EPC
*
* @param epcs EPC
* @return EPC
*/
public Set<String> selectExistingEpcs(List<String> epcs);
}

@ -1,7 +1,7 @@
package hw.tagApi.service.service.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import hw.tagApi.common.utils.DateUtils;
import hw.tagApi.common.utils.uuid.IdGenerator;
import org.springframework.beans.factory.annotation.Autowired;
@ -72,6 +72,15 @@ public class HwTagRecordServiceImpl implements IHwTagRecordService
return hwTagRecordMapper.updateHwTagRecord(hwTagRecord);
}
/**
* hwTagRecord
* @param hwTagRecord hwTagRecord
*/
public void initTagRecord(HwTagRecord hwTagRecord){
hwTagRecord.setRId(IdGenerator.nextId());
hwTagRecord.setCreateTime(DateUtils.getNowDate());
}
/**
*
*
@ -116,12 +125,19 @@ public class HwTagRecordServiceImpl implements IHwTagRecordService
hwTagRecordMapper.batchInsertHwTagRecord(batchList);
}
/**
*
*
* @param selectTagRecord HwTagRecord
* @return HwTagRecord
*/
@Override
public HwTagRecord selectHwTagRecordData(HwTagRecord selectTagRecord) {
List<HwTagRecord> tagRecordList = hwTagRecordMapper.selectHwTagRecordList(selectTagRecord);
if (!tagRecordList.isEmpty()){
return tagRecordList.get(0);
HwTagRecord tagRecord = tagRecordList.get(0);
tagRecord.setQueryResultsNumber(tagRecordList.size());
return tagRecord;
}
return null;
}
@ -137,11 +153,30 @@ public class HwTagRecordServiceImpl implements IHwTagRecordService
}
/**
* hwTagRecord
* @param hwTagRecord hwTagRecord
* TID
*
* @param tids TID
* @return TID
*/
public void initTagRecord(HwTagRecord hwTagRecord){
hwTagRecord.setRId(IdGenerator.nextId());
hwTagRecord.setCreateTime(DateUtils.getNowDate());
@Override
public Set<String> selectExistingTids(List<String> tids) {
if (tids == null || tids.isEmpty()) {
return new HashSet<>();
}
return new HashSet<>(hwTagRecordMapper.selectExistingTids(tids));
}
/**
* EPC
*
* @param epcs EPC
* @return EPC
*/
@Override
public Set<String> selectExistingEpcs(List<String> epcs) {
if (epcs == null || epcs.isEmpty()) {
return new HashSet<>();
}
return new HashSet<>(hwTagRecordMapper.selectExistingEpcs(epcs));
}
}

@ -1,11 +1,13 @@
package hw.tagApi.service.service.impl;
import hw.tagApi.common.exception.ServiceException;
import hw.tagApi.common.utils.uuid.IdGenerator;
import hw.tagApi.service.service.IHwTagRecordService;
import hw.tagApi.service.service.IKDocsService;
import hw.tagApi.service.utils.TagExcelUtil;
import hw.tagApi.service.utils.httpClientUtils;
import java.rmi.server.ServerCloneException;
import java.time.Duration;
import org.springframework.stereotype.Service;
@ -15,6 +17,7 @@ import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import hw.tagApi.service.constant.ApiConstants;
import hw.tagApi.service.domain.HwTagRecord;
@ -26,6 +29,8 @@ import hw.tagApi.service.domain.ApiContent;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import static hw.tagApi.service.utils.TagExcelUtil.startsWithCheck;
/**
*
*
@ -76,7 +81,7 @@ public class KDocsServiceImpl implements IKDocsService {
} catch (Exception e) {
log.error("导入任务执行失败: {}", e.getMessage(), e);
Map<String, Object> errorFields = new HashMap<>();
errorFields.put(ApiConstants.IMPORT_STATUS, "系统错误");
errorFields.put(ApiConstants.IMPORT_STATUS, "导入失败");
errorFields.put(ApiConstants.DESCRIPTION, "导入任务执行失败: " + e.getMessage());
resContent.setFields(errorFields);
}
@ -127,9 +132,16 @@ public class KDocsServiceImpl implements IKDocsService {
String fileUrl = String.valueOf(fields.get("文件链接"));
String fileName = String.valueOf(fields.getOrDefault("文件名", ""));
String TIDCheck = String.valueOf(fields.getOrDefault("TID校验", ""));
int charCount = TIDCheck.length();
String EPCCheck = String.valueOf(fields.getOrDefault("EPC校验", ""));
String EPCNumberCheck = String.valueOf(fields.getOrDefault("EPC位数校验", "0"));
int EPCNumberCheckLength = Integer.parseInt(EPCNumberCheck);
int tidCharCount = TIDCheck.length();
int epcCharCount = EPCCheck.length();
List<HwTagRecord> batchList = new ArrayList<>();
Map<String, Object> resFields = new HashMap<>();
List<String> errorMessages = new ArrayList<>();
int successCount = 0;
int failCount = 0;
apiContent.setFields(resFields);
try {
log.info("开始处理文件URL: {}", fileUrl);
@ -157,72 +169,138 @@ public class KDocsServiceImpl implements IKDocsService {
List<Map<String, String>> tagList = (List<Map<String, String>>) excelResult.get("tagList");
// 批量处理标签数据
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (Map<String, String> tagData : tagList) {
String tid = tagData.get("TID").replace("-", "");
String epc = tagData.get("EPC").replace("-", "");
if (tid.length() != 24) {
String errorMsg = String.format("TID长度校验失败预期长度24实际长度%dTID值%s", tid.length(), tid);
log.error("TID长度校验异常 - {}", errorMsg);
resFields.put(ApiConstants.IMPORT_STATUS, "导入失败");
resFields.put(ApiConstants.DESCRIPTION, "TID长度不符合要求" + errorMsg);
return apiContent;
}
if (charCount > 0 && !startsWithCheck(tid, TIDCheck)) {
String errorMsg = String.format("TID校验失败TID校验%s实际TID%s", TIDCheck, tid);
log.error("TID校验异常 - {}", errorMsg);
resFields.put(ApiConstants.IMPORT_STATUS, "导入失败");
resFields.put(ApiConstants.DESCRIPTION, "TID校验不符合要求" + errorMsg);
return apiContent;
}
HwTagRecord record = new HwTagRecord();
record.setOrderCode(orderInfo.get("orderNo"));
record.setBatchNumber(orderInfo.get("batchNo"));
record.setOperatorId(orderInfo.get("operatorId"));
record.setProcessingTime(sdf.parse(orderInfo.get("processTime")));
record.setTotalQuantity(Long.valueOf(orderInfo.get("totalCount")));
// 设置标签数据
record.setModelCode(model);
record.setTagSequence(tagData.get("序号"));
record.setTId(tid);
record.setEpc(epc);
record.setPassword(tagData.get("密码"));
record.setTestResult(tagData.get("测试结果"));
record.setTestValue(tagData.get("测试值"));
record.setReferenceValue(tagData.get("参考值"));
record.setTestingTime(sdf.parse(tagData.get("测试时间")));
record.setFileName(fileName);
// 达到批量大小时执行插入
if ("更新".equals(importMode)) {
tagRecordService.updateHwTagRecordByTID(record);
continue;
}
//校验tId唯一
HwTagRecord tagRecord = tagRecordService.selectHwTagRecordByTID(record.getTId());
if (tagRecord != null) {
continue;
}
record.setRId(IdGenerator.nextId());
// 添加到批处理列表
batchList.add(record);
if (batchList.size() >= BATCH_SIZE) {
tagRecordService.batchInsertHwTagRecord(batchList);
log.info("批量插入{}条数据成功", batchList.size());
batchList.clear();
// 如果不是更新模式先批量查询所有TID
Set<String> existingTids = new HashSet<>();
boolean tIdFlag = ApiConstants.MODE_ADD.equals(importMode) || ApiConstants.MODE_ADD_NO_CHECK.equals(importMode);
if (tIdFlag) {
List<String> allTids = tagList.stream()
.map(tag -> tag.get("TID").replace("-", ""))
.collect(Collectors.toList());
existingTids = tagRecordService.selectExistingTids(allTids);
}
// 如果不是更新模式批量查询所有EPC
Set<String> existingEpcs = new HashSet<>();
boolean epcCheckFlag = ApiConstants.MODE_ADD.equals(importMode) || ApiConstants.MODE_UPDATE.equals(importMode);
if (epcCheckFlag) {
List<String> allEpcs = tagList.stream()
.map(tag -> tag.get("EPC").replace("-", ""))
.collect(Collectors.toList());
existingEpcs = tagRecordService.selectExistingEpcs(allEpcs);
}
for (Map<String, String> tagData : tagList) {
try {
String tid = tagData.get("TID").replace("-", "");
String epc = tagData.get("EPC").replace("-", "");
if (tid.length() != ApiConstants.TE_LENGTH || (EPCNumberCheckLength > 0 && epc.length() != EPCNumberCheckLength)) {
StringBuilder builder = new StringBuilder();
if (tid.length() != ApiConstants.TE_LENGTH) {
String errorMsg = String.format("TID长度校验失败预期长度24实际长度%dTID值%s", tid.length(), tid);
builder.append(errorMsg);
log.error("异常 - {}", errorMsg);
}
if (EPCNumberCheckLength > 0 && epc.length() != EPCNumberCheckLength) {
String errorMsg = String.format("EPC长度校验失败预期长度24实际长度%dEPC值%s", epc.length(), epc);
builder.append(errorMsg);
log.error("EPC长度校验异常 - {}", errorMsg);
}
errorMessages.add(builder.toString());
failCount++;
continue;
}
if (tidCharCount > 0 && !startsWithCheck(tid, TIDCheck)) {
String errorMsg = String.format("TID校验失败TID校验%s实际TID%s", TIDCheck, tid);
log.error("TID校验异常 - {}", errorMsg);
errorMessages.add(errorMsg);
failCount++;
continue;
}
if (epcCharCount > 0 && !startsWithCheck(epc, EPCCheck)) {
String errorMsg = String.format("EPC校验失败EPC校验%s实际EPC%s", EPCCheck, epc);
log.error("EPC校验异常 - {}", errorMsg);
errorMessages.add(errorMsg);
failCount++;
continue;
}
HwTagRecord record = new HwTagRecord();
record.setOrderCode(orderInfo.get("orderNo"));
record.setBatchNumber(orderInfo.get("batchNo"));
record.setOperatorId(orderInfo.get("operatorId"));
record.setProcessingTime(sdf.parse(orderInfo.get("processTime")));
record.setTotalQuantity(Long.valueOf(orderInfo.get("totalCount")));
// 设置标签数据
record.setModelCode(model);
record.setTagSequence(tagData.get("序号"));
record.setTId(tid);
record.setEpc(epc);
record.setPassword(tagData.get("密码"));
record.setTestResult(tagData.get("测试结果"));
record.setTestValue(tagData.get("测试值"));
record.setReferenceValue(tagData.get("参考值"));
record.setTestingTime(sdf.parse(tagData.get("测试时间")));
record.setFileName(fileName);
//校验tId唯一
if (tIdFlag && existingTids.contains(tid)) {
String errorMsg = String.format("TID已存在%s", tid);
log.error(errorMsg);
errorMessages.add(errorMsg);
failCount++;
continue;
}
//校验EPC唯一
if (epcCheckFlag && existingEpcs.contains(epc)) {
String errorMsg = String.format("EPC已存在%s", epc);
log.error(errorMsg);
errorMessages.add(errorMsg);
failCount++;
continue;
}
// 更新方法
if ((ApiConstants.MODE_UPDATE.equals(importMode) || ApiConstants.MODE_UPDATE_NO_CHECK.equals(importMode)) && failCount == 0) {
tagRecordService.updateHwTagRecordByTID(record);
successCount++;
continue;
}
record.setRId(IdGenerator.nextId());
// 添加到批处理列表
batchList.add(record);
// 达到批量大小时执行插入
if (batchList.size() >= BATCH_SIZE && failCount == 0) {
tagRecordService.batchInsertHwTagRecord(batchList);
log.info("批量插入{}条数据成功", batchList.size());
successCount += batchList.size();
batchList.clear();
}
} catch (Exception e) {
String errorMsg = String.format("处理数据失败:%s", e.getMessage());
log.error(errorMsg);
errorMessages.add(errorMsg);
failCount++;
}
}
// 处理剩余的数据
if (!batchList.isEmpty()) {
if (!batchList.isEmpty() && failCount == 0) {
tagRecordService.batchInsertHwTagRecord(batchList);
log.info("批量插入剩余{}条数据成功", batchList.size());
successCount += batchList.size();
batchList.clear();
}
resFields.put(ApiConstants.IMPORT_STATUS, "导入完成");
// 设置导入结果
if (failCount > 0) {
throw new ServiceException(String.format(importMode + "失败:%d条。失败原因%s",
failCount, String.join("; ", errorMessages)));
} else {
resFields.put(ApiConstants.IMPORT_STATUS, "导入成功");
resFields.put(ApiConstants.DESCRIPTION, String.format("成功导入%d条数据", successCount));
}
}
} catch (Exception e) {
log.error("处理文件失败: {}", e.getMessage(), e);
resFields.put(ApiConstants.IMPORT_STATUS, "系统错误");
resFields.put(ApiConstants.IMPORT_STATUS, "导入失败");
resFields.put(ApiConstants.DESCRIPTION, e.getMessage());
}
@ -275,8 +353,8 @@ public class KDocsServiceImpl implements IKDocsService {
// 检查查询条件
if (!fields.containsKey("TID") && !fields.containsKey("EPC")) {
log.warn("查询条件错误 - ID: {}, 缺少必要的查询参数(TID或EPC)", queryId);
response.getFields().put("搜索状态", "查询条件错误");
response.getFields().put("情况说明", "查询条件必须包含TID或EPC");
response.getFields().put(ApiConstants.SELECT_STATUS, "系统错误");
response.getFields().put(ApiConstants.DESCRIPTION, "查询条件必须包含TID或EPC");
result.add(response);
continue;
}
@ -310,7 +388,12 @@ public class KDocsServiceImpl implements IKDocsService {
if (data != null) {
log.info("查询成功 - ID: {}, TID: {}, EPC: {}", queryId, data.getTId(), data.getEpc());
// 查询成功
response.getFields().put("搜索状态", "搜索成功");
if (data.getQueryResultsNumber() > 1){
response.getFields().put(ApiConstants.SELECT_STATUS, "系统错误");
response.getFields().put(ApiConstants.DESCRIPTION, "存在多个记录,请使用数据导出功能");
} else {
response.getFields().put(ApiConstants.SELECT_STATUS, "搜索成功");
}
response.getFields().put("TID", data.getTId());
response.getFields().put("EPC", data.getEpc());
response.getFields().put("标签序号", data.getTagSequence());
@ -327,15 +410,15 @@ public class KDocsServiceImpl implements IKDocsService {
} else {
log.info("未找到记录 - ID: {}", queryId);
// 未找到记录
response.getFields().put("搜索状态", "未找到记录");
response.getFields().put(ApiConstants.SELECT_STATUS, "未找到记录");
if (fields.containsKey("搜索日期起点") && fields.containsKey("搜索日期终点")) {
String message = String.format("已遍历整个数据库,生产日期从%s到%s查询到该TID。",
String message = String.format("已遍历整个数据库,生产日期从%s到%s找到匹配的记录。",
fields.get("搜索日期起点"), fields.get("搜索日期终点"));
response.getFields().put("情况说明", message);
response.getFields().put(ApiConstants.DESCRIPTION, message);
log.debug("未找到记录详情 - ID: {}, 日期范围: {} - {}",
queryId, fields.get("搜索日期起点"), fields.get("搜索日期终点"));
} else {
response.getFields().put("情况说明", "未找到匹配的记录");
response.getFields().put(ApiConstants.DESCRIPTION, "未找到匹配的记录");
}
}
@ -364,8 +447,8 @@ public class KDocsServiceImpl implements IKDocsService {
ApiContent response = new ApiContent();
response.setId(condition.getId());
Map<String, Object> responseFields = new HashMap<>();
responseFields.put("导出状态", "导出中");
responseFields.put("情况说明", "预计需要3分钟");
responseFields.put(ApiConstants.EXPORT_STATUS, "导出中");
responseFields.put(ApiConstants.DESCRIPTION, "预计需要3分钟");
response.setFields(responseFields);
result.add(response);
}
@ -432,19 +515,19 @@ public class KDocsServiceImpl implements IKDocsService {
Date futureDate = new Date(endTime + ApiConstants.PASS_TIME);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
log.info("数据库导出完成 - ID: {}, 记录数: {}, 耗时: {}分{}秒", condition.getId(), records.size(), minutes, seconds);
responseFields.put("导出状态", "导出成功");
responseFields.put("情况说明", "耗时: " + minutes + "分" + seconds + "秒");
responseFields.put(ApiConstants.EXPORT_STATUS, "导出成功");
responseFields.put(ApiConstants.DESCRIPTION, "耗时: " + minutes + "分" + seconds + "秒");
responseFields.put("文件路径", filePath);
responseFields.put("链接有效期", sdf.format(futureDate));
log.info("导出文件生成成功 - ID: {}, 路径: {}", condition.getId(), filePath);
} catch (Exception e) {
log.error("生成导出文件失败 - ID: {}", condition.getId(), e);
responseFields.put("导出状态", "导出失败");
responseFields.put("情况说明", "生成导出文件失败: " + e.getMessage());
responseFields.put(ApiConstants.EXPORT_STATUS, "导出失败");
responseFields.put(ApiConstants.DESCRIPTION, "生成导出文件失败: " + e.getMessage());
}
} else {
responseFields.put("导出状态", "未找到记录");
responseFields.put("情况说明", "未找到符合条件的记录");
responseFields.put(ApiConstants.EXPORT_STATUS, "未找到记录");
responseFields.put(ApiConstants.DESCRIPTION, "未找到符合条件的记录");
}
response.setFields(responseFields);
@ -453,8 +536,8 @@ public class KDocsServiceImpl implements IKDocsService {
log.error("处理导出任务失败 - ID: {}", condition.getId(), e);
response.setId(condition.getId());
Map<String, Object> responseFields = new HashMap<>();
responseFields.put("导出状态", "导出失败");
responseFields.put("情况说明", "处理导出任务失败: " + e.getMessage());
responseFields.put(ApiConstants.EXPORT_STATUS, "导出失败");
responseFields.put(ApiConstants.DESCRIPTION, "处理导出任务失败: " + e.getMessage());
response.setFields(responseFields);
}
@ -467,19 +550,7 @@ public class KDocsServiceImpl implements IKDocsService {
return result;
}
public static boolean startsWithCheck(String tid, String TIDCheck) {
if (tid == null || TIDCheck == null || tid.length() < TIDCheck.length()) {
return false;
}
char[] tidChars = tid.toCharArray();
char[] checkChars = TIDCheck.toCharArray();
for (int i = 0; i < checkChars.length; i++) {
if (tidChars[i] != checkChars[i]) {
return false;
}
}
return true;
}
}

@ -12,11 +12,9 @@ import hw.tagApi.system.service.ISysAttachInfoService;
import org.apache.poi.ss.usermodel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.*;
@ -244,4 +242,29 @@ public class TagExcelUtil {
throw new RuntimeException("保存文件信息失败: " + e.getMessage());
}
}
/**
* '*'
* @param originalField
* @param checkField '*'
* @return
*/
public static boolean startsWithCheck(String originalField, String checkField) {
if (originalField == null || checkField == null || originalField.length() < checkField.length()) {
return false;
}
char wildcard = '*';
char[] originalChars = originalField.toCharArray();
char[] checkChars = checkField.toCharArray();
// 逐字符比对跳过checkField中的通配符
for (int i = 0; i < checkChars.length; i++) {
if (checkChars[i] == wildcard) {
continue;// 通配符位置跳过校验
}
if (originalChars[i] != checkChars[i]) {
return false;// 非通配符位置字符不匹配
}
}
return true;
}
}

@ -103,6 +103,20 @@
where t_id = #{tId}
</select>
<select id="selectExistingTids" resultType="string">
SELECT t_id FROM hw_tag_record WHERE t_id IN
<foreach collection="list" item="tid" open="(" separator="," close=")">
#{tid}
</foreach>
</select>
<select id="selectExistingEpcs" resultType="string">
SELECT epc FROM hw_tag_record WHERE epc IN
<foreach collection="list" item="epc" open="(" separator="," close=")">
#{epc}
</foreach>
</select>
<insert id="insertHwTagRecord" parameterType="HwTagRecord">
insert into hw_tag_record
<trim prefix="(" suffix=")" suffixOverrides=",">

Loading…
Cancel
Save