-
2021-09-27 17:15:02
自己的模块,常常要对接上下游,如何在上下游对接的过程中,保证正确性呢?
一般和上下游对接的有两种方式:
一:接口对接
在接口对接中,需要注意下面的点。
1,和上下游约定接口的功能是什么,要准确描述该接口要做哪些事情,什么情况下返回成功,什 么情况下返回失败。
在接口暴露的地方做好这些注释,
2,定义好接口中的参数的数据结构,确定参数中哪些是必传的,哪些是不必传的,他们的传值范围是什么?
在接口impl层面的第一件事情,先打印请求参数日志(可以写个aop按照类名+方法名打印日志)
紧接着第二件事就是按照约定的数据结构进行参数校验,对错误的参数请求做好拦截,防止他们捣乱系统,破坏数据。
第三,对于非必传参数,接口应该如何处理,也需要明确,这一点一般反应在该接口在不同的场景中使用。
3,明确返回值和错误信息:如果处理成功返回success。如果失败,不能仅仅返回失败就完事了,必须在返回信息明确的告知上下游是什么原因导致这次请求处理失败,是参数校验不通过,缺省哪些值?还是要操作的数据,在数据库中不存在?等等。
4,对接口进行单元测试:根据步骤1的注释,必须按照这些预期功能进行测试,如果预期提供的功能,在单元测试中都成功通过,那么说明该接口提供的功能是可靠的,可以放心的和上下游对接了。
一般来说,接口对接做好上面的四件事就足够了,但是还远远不够 !接口设计的时候还必须考虑以下事情:
a,请求是否要支持幂等,也就是说相同的请求来了(可能是多线程并发请求,也可能是重复请求),该接口如何定义自己的功能,这一般反应在增删改等接口。
比如,有个接口支持插入用户信息,那么相同的用户是成功插入,还是应该提示该用户已存在呢?假设要求手机号唯一,那么大部分的做法是,先检查手机号是否存在,然后写入数据库,这种处理方式是不能解决问题的,因为,当第一个线程检查还未写入,第二个线程来检查会发现记录不存在,最终导致产生两条手机号重复的数据,,因此在定义这样的接口时,要么上层加分布式锁,要么在关系型数据库中,对该表加上对应的唯一索引。
另外如果是让数据库来保证业务唯一性,那么还要分析,重复的请求,在代码执行中,是否节外生枝。
b,是否有分布式事务?比如该接口是一个扣减库存的接口,那么在和上下游对接的过程中,需要注意如何处理分布式事务。
c,如果在接口impl中,要调用上下游接口,那么一定要打印好入参和全部返回信息的日志,日志信息要便于理解,方便排查问题。
综上:一般来说,在利用接口实现上下游对接中,那么就差不多了。
二:消息对接
利用mq等中间件消息
消息对接和接口对接大部分都是相同的。
不同之处在于,消息处理完毕不会立刻返回上下游,此次消息的处理结果是什么。因此当前系统,必须对处理失败的消息,做好响应。
1,首先处理消息有个小技巧,接消息的类,不要进行进行任何业务逻辑处理,仅仅打印消息日志就好了。应该有单独的一个类的方法,来实现消息的处理。这样做的好处,在于把消息也看成一次普通的接口请求。所以设计消息的处理,大部分可以参照接口的设计。
2,对重要的消息,做好记录,后续可查,中间件上的消息一般保存7天,如果某天被告知前7天的某条数据处理有问题,如果没有记录,就不得而知了。
3,消息处理做好幂等,这里比较重点。
4,处理失败的消息,记录好失败的原因,根据业务判断是否需要进行重试。
如果做到以上,消息对接不会有太大的问题,但是还需要考虑以下事情:
a,消息是否需要延迟处理
b,消息是否需要按照顺序来处理
综上:一般来说,在利用消息实现上下游对接中,那么就差不多了。
当一个功能中,接口较多时,还要注意在和上下游对接中,需要梳理不同接口和消息
的请求时机,否则就会导致数据混乱。
重中之重是,一定要做好单元测试!!!
最后,记录点非技术的问题:
1,在和上下游对接中,要和同事搞好关系,在生产环境遇到问题不要甩锅,要及时帮忙排查产生 问题的原因和提供技术解决方案。
2,以自己负责的业务为中心,梳理好所有相关的功能,这样当出现问题时,才能第一时间察觉到是哪个环节出现问题了。
更多相关内容 -
系统对接方案.docx
2020-01-11 11:07:14一、系统对接方案,内容包括: 1.一次性对接方案 2.后期日常维护方案 3.系统稳定性保障方案 4.时间安排计划 5.相关人员安排 -
医院信息化建设的重要性及意义.doc
2022-07-18 14:48:52医院信息化建设的重要性及意义.doc -
大数据分析对电力营销的重要性.doc
2022-07-15 09:05:13大数据分析对电力营销的重要性.doc -
论广播电视网引入PTN的重要性
2020-10-23 02:51:54结合目前国家大力推进三网融合这个大背景,分析了在广播电视网中引入基于T-MPLS/MPLS-TP技术PTN的必要性和可行性。 -
液压回撤设备在综采工作面的应用研究
2020-05-24 13:33:43新型设备的应用,改变了原有回撤工艺速度慢、环节多、效率低、事故多、岗位多、安全性和适用性能差等特点,降低了劳动强度,提高了工作效率,回撤时间由50 d减少到30 d,大大提高了回撤速度,降低了人工成本及巷道维护费用... -
浅谈数据对接
2020-03-16 15:47:19那怎么才能快速做好数据对接,而且重复劳动尽量少地完成,返工也要尽量少。我觉得要分几个步骤去进行。切记不能一头热栽在文档上,死命地对,很可能你的接口后期要崩死,考虑平台方的修改和己方数据的缺失或者并不...浅谈数据对接
一般来讲,数据对接就是将己方收集到的客户数据转化为平台方的规范要求的数据格式上传的一种方式。各个平台对于数据的要求格式各不相同。那怎么才能快速做好数据对接,而且重复劳动尽量少地完成,返工也要尽量少。我觉得要分几个步骤去进行。切记不能一头热栽在文档上,死命地对,很可能你的接口后期要崩死,考虑平台方的修改和己方数据的缺失或者并不完全符合的情况。 1.粗略看下对接文档和客户需求(这个必须看,不看后期等着改死吧)。特别对日期敏感,数据对接很可能为了验收 ,有一段日期里的数据特别重要。 2.了解收集到的客户数据,并将字段和了解到的对接文档字段一一对应。对于编码一定要重视,一般数据上传失败会在这里栽跟头。 3.等了解了己方数据库字段和文档的对应关系后,别急着下手。还有一步是一定要做的。查看数据库的垃圾数据,想必大家都知道,但凡数据大了之后,总有垃圾数据会在上传的时候影响上传的数据的准确性。当然会有人建立日志去记录,上传数据的成功率。但是去搜索日志需要耗费精力,还要去确定问题的根源,会浪费大量时间。与其去后期处理,不如预先做好预防,提前将垃圾数据给筛掉,保证数据上传的精确。 4.前面工作做完才能安心码代码编写接口将数据上传。
下面举个例子 (后台框架mybatis+mvc)
对接方式 :导出txt (昨天的数据) 再上传平台
后台sql<select id="getMeatTzInHz" resultType="net.***.**.platform.meat.entity.MeatTzInHz"> SELECT instock.slaughterCode, instock.instockDate, SUM(instock.checkInstockNum), AVG(instock.price) AS price, SUM(instock.quantityAndWeight) AS quantityAndWeight, SUM(instock.transportDeathNum) AS transportDeathNum, produceCode FROM ( SELECT COALESCE(trace_mainbody_employee.mainbody_record_info :: JSON ->> 'meatCommerceCode', '') AS slaughterCode, --主体编码 COALESCE(trace_mainbody_employee.mainbody_name, '') AS slaughterName, --主体名称 TO_CHAR(trace_instock.instock_date,'yyyyMMdd') AS instockDate, --进场日期 COALESCE(substr(trace_mainbody_provider.mainbody_record_info :: JSON ->> 'commerceCode',1,9), '') AS ownerCode, --货主编码 COALESCE(trace_mainbody_provider.mainbody_name, '') AS ownerName, --货主名称 COALESCE(trace_batch.inspection_info :: JSON ->> 'CitypfTzPigCertCode', '') AS checkCertCode, --生猪产地检疫证号,--json格式要得到CitypfTzPigCertCode CAST(COALESCE(trace_batch.batch_other_info :: JSON ->> 'detectionQuantity', '0') AS int) AS checkInstockNum, --检疫证进场数量,--json格式要得到CitypfTzQuarantineCount COALESCE(trace_batch.batch_price, 0) AS price, --批次单价 COALESCE(trace_batch.batch_quantity, 0) AS quantityAndWeight FROM t_trace_instock trace_instock --入库基础 INNER JOIN t_trace_instock_detail trace_instock_detail ON ( trace_instock.ID = trace_instock_detail.instock_id AND trace_instock_detail.del_flg = '0' ) INNER JOIN t_trace_mainbody trace_mainbody_employee --企业表 ON ( trace_mainbody_employee.trace_mainbody_code = trace_instock.trace_mainbody_code AND trace_mainbody_employee.mainbody_type in ('101') AND trace_mainbody_employee.del_flg = '0' AND trace_mainbody_employee.mainbody_record_info :: JSON ->> 'meatCommerceCode' != '' <if test="division != null and division != ''"> AND split_part(trace_mainbody_employee.mainbody_record_info :: JSON ->> 'divisionName',',',2) like ('%' || #{division,jdbcType = VARCHAR} || '%') </if> <if test="mainBodyCode != null and mainBodyCode != ''"> AND main.mainbody_code = #{mainBodyCode,jdbcType = VARCHAR} </if> ) LEFT JOIN t_trace_mainbody trace_mainbody_provider --供应商表 ON ( trace_mainbody_provider.mainbody_code = trace_instock.supplier_code AND trace_mainbody_provider.mainbody_type = '102' AND trace_mainbody_provider.del_flg = '0' ) INNER JOIN t_trace_batch trace_batch --批次表 ON ( trace_batch.batch_code = trace_instock_detail.batch_code AND trace_batch.del_flg = '0' ) WHERE trace_mainbody_employee.mainbody_record_info :: JSON ->> 'nodeType' = '101' AND trace_instock.del_flg = '0' <![CDATA[ AND CAST(trace_instock.sys_reg_tmsp AS DATE) >= CAST(#{startTagSysRegTmsp,jdbcType = VARCHAR} AS DATE) AND CAST(trace_instock.sys_reg_tmsp AS DATE) <= CAST(#{endTagSysRegTmsp,jdbcType = VARCHAR} AS DATE) ]]> ) instock GROUP BY instock.slaughterCode, instock.instockDate, produceCode </select>
实体
public class MeatTzInHz implements Serializable { /** * 屠宰厂编码 */ protected String slaughterCode; /** * 进厂日期 */ protected String instockDate; /** * 检疫证进场数量 */ protected int checkInstockNum; /** * 采购价 */ protected double price; /** * 实际数量和重量 */ protected double quantityAndWeight; /** * 途亡数 */ protected int transportDeathNum; /** * 产地编码 */ protected String produceCode; @Override public String toString() { return FileEnum.DIVIDE_COMMA.getSymbol() + slaughterCode + FileEnum.DIVIDE_COMMA.getSymbol() + instockDate + FileEnum.DIVIDE_COMMA.getSymbol() + checkInstockNum + FileEnum.DIVIDE_COMMA.getSymbol() + price + FileEnum.DIVIDE_COMMA.getSymbol() + quantityAndWeight + FileEnum.DIVIDE_COMMA.getSymbol() + transportDeathNum + FileEnum.DIVIDE_COMMA.getSymbol() + produceCode + FileEnum.ENTER_LINE_BREAK.getSymbol(); }
mapper
List<MeatTzOutHz> getMeatTzOutHz(DataCondition dataCondition);
service
public List<MeatTzInHz> getMeatTzInHz(DataCondition dataCondition) { List<MeatTzInHz> meatTzInHzList = meatMapper.getMeatTzInHz(dataCondition); //验证数据非空 if(CollectionUtils.isEmpty(meatTzInHzList)) { return new ArrayList<>(); } return meatTzInHzList; }
导出service(方法名自写,只剖了方法体) ExportService
List<MeatTzOutHz> meatTzOutHzList = meatService.getMeatTzOutHz(dataCondition); meatTzOutHzList.stream() .filter(meatTzOutHz -> StringUtils.isNotBlank(meatTzOutHz.getSlaughterCode())) .forEach(meatTzOutHz -> { String fileName = txtFileUtil.getFileName(FileEnum.POSTFIX_MEAT_TZ_OUT_HZ_TXT.getSymbol(), meatTzOutHz.getSlaughterCode(), meatTzOutHz.getOutstockDate()); try { txtFileUtil.writeFileToDateDirectory(meatTzOutHz.toString(), fileName); } catch (IOException e) { e.printStackTrace(); } });
定时任务
大同小异@Autowired private ExportService exportService; /** * 秒 分 时 日 月 年 */ @Scheduled(cron = "0 30 4,11 * * ? ") private void grouppurchasing() { List<MainBody> mainBodyList = nodeService.getMainBodyByNodeType(2); if(CollectionUtils.isNotEmpty(mainBodyList)){ logger.info("生成数据文件开始!"); mainBodyList.forEach(mainBody -> { logger.info("生成企业:{} 数据文件开始!", mainBody.getMainbodyName()); DataCondition dataCondition = new DataCondition(); dataCondition.setDivision("上海"); dataCondition.setStartTagSysRegTmsp(DateUtil.getYesterdayByDefultFormat()); dataCondition.setEndTagSysRegTmsp(DateUtil.getNowByDefultFormat()); //dataCondition.setNodeTypeList(Arrays.asList(PlatformConstant.NODE_TYPE_110)); dataCondition.setMainBodyCode(mainBody.getMainbodyCode()); baseNodeInfo(dataCondition ,true); exportService.exportGpToTxt(dataCondition); logger.info("生成企业:{} 数据文件结束!", mainBody.getMainbodyName()); }); logger.info("生成数据文件结束!"); } }
工具类 fileEnum txtFileUtil
public enum FileEnum { POSTFIX_MEAT_TZ_OUT_TXT("{0}_{1}_T2.TXT"),; private String symbol; FileEnum(String symbol) { this.symbol = symbol; } public String getSymbol() { return symbol; } }
public String getFileName(String pattern, Object... arguments) { if (arguments == null || arguments.length == 0) { return pattern; } return MessageFormat.format(pattern, arguments); } /** * 写文件 * * @param line * @param fileName * @throws IOException */ public void writeFileToDateDirectory(String line, String fileName) throws IOException { String dateDir = DateUtil.getYesterdayByDefultFormat() + "/"; String dirFile = this.fileOutputDirectory + dateDir + fileName; byte[] buff = line.getBytes(Charset.forName("GBK")); FileOutputStream out = fileMap.get(dirFile); if (out == null) { File directoryFile = new File(this.fileOutputDirectory + dateDir); File file = new File(dirFile); if (!directoryFile.exists()) { directoryFile.mkdirs(); } // 文件不存在,创建 if (!file.exists()) { file.createNewFile(); } // append 参数为false,覆盖原来的文件 out = new FileOutputStream(file, false); fileMap.put(dirFile, out); } out.write(buff); }
DateUtil
private static Date add(Date date, int calendarField, int amount) { if (date == null) { throw new IllegalArgumentException("The date must not be null"); } else { Calendar c = Calendar.getInstance(); c.setTime(date); c.add(calendarField, amount); return c.getTime(); } } /** * 增加天 * * @param date * @param amount * @return */ public static Date addDays(Date date, int amount) { return add(date, 5, amount); } /** * 获取昨天 * @return */ public static Date getYesterday() { return addDays(getNow(), -1); } /** * 把日期类型格式化成字符串 * * @param date * @param format * @return */ public static String convert2String(Date date, String format) { SimpleDateFormat formater = new SimpleDateFormat(format); try { return formater.format(date); } catch (Exception e) { return null; } } /** * 获取昨天的日期并转化为yyyyMMdd * @return */ public static String getYesterdayByDefultFormat() { return convert2String(getYesterday(), DATE_FORMAT_YYYYMMDD); }
到这里,应该能生成txt文件了。还有一步上传。
平台方要求 生成zip 再用webservice的方式上传到平台
先po功能代码 ,再给工具类/** * webservice 上传zip文件(一天一次) * 6:30 12:30 */ @Scheduled(cron = "0 30 6,12 * * ? ") private void uploadZipByWebservice() { //打包 String zipPath = "/sh"; String zipName = DateUtil.getYesterdayByDefultFormat(); ZipUtils.createZipFile(zipPath,zipName); //地址 String fileName = "sh" + zipName + ".zip"; Identity identity = new Identity(); ReqEntity<Identity> reqEntity = new ReqEntity<>(); identity.setFileName(fileName); identity.setFileCreateTime(new SimpleDateFormat("yyyy-MM-dd").format(new Date())); File file = new File("/data/"+ fileName); identity.setFileSize(String.valueOf(file.length())); reqEntity.setIdentity(identity); reqUtil.sendXml(reqEntity); }
看着很简洁 ,包含了三个工具类,和 两个实体类
实体类@Data //只检测属性不检测方法了 @JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.ANY, getterVisibility=JsonAutoDetect.Visibility.NONE) public class Identity implements Serializable { private String UserName; private String Password; private String FileName; private String FileSize; private String FileCreateTime; }
父类
@Data //只检测属性不检测方法了 @JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.ANY, getterVisibility=JsonAutoDetect.Visibility.NONE) public class ReqEntity<Identity> implements Serializable { private String pfileType; private String pbuffer; private String usercode; private String userPass; private Identity identity; private String pErrmasg; }
其实就是定义一些字段,啥也没写。=。=
工具类的话
生成压缩文件 zipUtil/** * 创建压缩文件 */ public static void createZipFile(String saveFilePath ,String zipName) { FileOutputStream fileOutputStream = null; try { File zipFile = new File(saveFilePath + zipName.replaceAll("(.*?)(/*)$","$1") + ".zip"); fileOutputStream = new FileOutputStream(zipFile); ZipUtils.toZip(saveFilePath + "/"+zipName+"/", fileOutputStream, false); } catch (Exception e) { e.printStackTrace(); } } /** * 压缩成ZIP 方法1 * * @param srcDir 压缩文件夹路径 * @param out 压缩文件输出流 * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构; * false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败) * @throws RuntimeException 压缩失败会抛出运行时异常 */ public static void toZip(String srcDir, OutputStream out, boolean KeepDirStructure) throws RuntimeException { long start = System.currentTimeMillis(); ZipOutputStream zos = null; try { zos = new ZipOutputStream(out); File sourceFile = new File(srcDir); compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure); long end = System.currentTimeMillis(); logger.info("压缩完成,耗时:" + (end - start) + " ms"); } catch (Exception e) { throw new RuntimeException("zip error from ZipUtils", e); } finally { if (zos != null) { try { zos.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 递归压缩方法 * * @param sourceFile 源文件 * @param zos zip输出流 * @param name 压缩后的名称 * @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构; * false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败) * @throws Exception */ private static void compress(File sourceFile, ZipOutputStream zos, String name, boolean KeepDirStructure) throws Exception { byte[] buf = new byte[BUFFER_SIZE]; if (sourceFile.isFile()) { // 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字 zos.putNextEntry(new ZipEntry(name)); // copy文件到zip输出流中 int len; FileInputStream in = new FileInputStream(sourceFile); while ((len = in.read(buf)) != -1) { zos.write(buf, 0, len); } // Complete the entry zos.closeEntry(); in.close(); } else { File[] listFiles = sourceFile.listFiles(); if (listFiles == null || listFiles.length == 0) { // 需要保留原来的文件结构时,需要对空文件夹进行处理 if (KeepDirStructure) { // 空文件夹的处理 zos.putNextEntry(new ZipEntry(name + "/")); // 没有文件,不需要文件的copy zos.closeEntry(); } } else { for (File file : listFiles) { // 判断是否需要保留原来的文件结构 if (KeepDirStructure) { // 注意:file.getName()前面需要带上父文件夹的名字加一斜杠, // 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了 compress(file, zos, name + "/" + file.getName(), KeepDirStructure); } else { compress(file, zos, file.getName(), KeepDirStructure); } } } } } /** * 根据文件路径读取文件转出 byte[] * * @param filePath 文件路径 * @return 字节数组 * @throws IOException */ public static byte[] getFileToByteArray(String filePath) throws IOException { File file = new File(filePath); long fileSize = file.length(); if (fileSize > Integer.MAX_VALUE) { return null; } FileInputStream fileInputStream = new FileInputStream(file); byte[] buffer = new byte[(int) fileSize]; int offset = 0; int numRead = 0; while (offset < buffer.length && (numRead = fileInputStream.read(buffer, offset, buffer.length - offset)) >= 0) { offset += numRead; } // 确保所有数据均被读取 if (offset != buffer.length) { logger.error("Could not completely read file : {}" , file.getName()); throw new IOException("Could not completely read file " + file.getName()); } fileInputStream.close(); return buffer; }
还有上传的一个工具类 reqUtil
@Value("${webservice.url}") private String webUrl; /** * xml 上报方式 * @param reqEntity * @return */ public String sendXml( ReqEntity<Identity> reqEntity) { URL url = null; String fileName = reqEntity.getIdentity().getFileName(); String fileSize = reqEntity.getIdentity().getFileSize(); String createTime = reqEntity.getIdentity().getFileCreateTime(); String fileStr = ""; String responsMess = ""; try { url = new URL(webUrl); reqUtilLogger.info("requestUrl:" + url); HttpURLConnection con = (HttpURLConnection) url.openConnection(); byte[] zipOutput = ZipUtils.getFileToByteArray("/data/"+ fileName); fileStr = new sun.misc.BASE64Encoder().encode(zipOutput); //拼接好xml StringBuffer sb = new StringBuffer(); sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" + " <soap:Header>\n" + " <Identify xmlns=\"FjUpdate1\">\n" + " <UserName>"+userName+"</UserName>\n" + " <Password>"+password+"</Password>\n" + " <FileName>"+fileName+"</FileName>\n" + " <FileSize>"+fileSize+"</FileSize>\n" + " <FileCreateTime>"+createTime+"</FileCreateTime>\n" + " </Identify>\n" + " </soap:Header>\n" + " <soap:Body>\n" + " <UpFile xmlns=\"FjUpdate1\">\n" + " <pFileType>" + fileName.split("\\.")[0] + "</pFileType>\n" + " <pBuffer>"+fileStr+"</pBuffer>\n" + " <usercode>"+userName+"</usercode>\n" + " <userpass>"+password+"</userpass>\n" + " </UpFile>\n" + " </soap:Body>\n" + "</soap:Envelope>\n"); String xmlStr = sb.toString(); System.out.println(xmlStr); //设置好header信息 con.setRequestMethod("POST"); con.setRequestProperty("content-type", "text/xml; charset=utf-8"); con.setRequestProperty("Content-Length", String.valueOf(xmlStr.getBytes().length)); // con.setRequestProperty("soapActionString", soapActionString); //post请求需要设置 con.setDoOutput(true); con.setDoInput(true); //对请求body 往里写xml 设置请求参数 OutputStream ops = con.getOutputStream(); ops.write(xmlStr.getBytes()); ops.flush(); ops.close(); //设置响应回来的信息 InputStream ips = con.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int length = 0; while( (length = ips.read(buf)) != -1){ baos.write(buf, 0, length); baos.flush(); } byte[] responsData = baos.toByteArray(); baos.close(); //处理写响应信息 responsMess = new String(responsData,"utf-8"); reqUtilLogger.info("响应报文:"+ responsMess); reqUtilLogger.info(String.valueOf(con.getResponseCode())); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return responsMess; }
这里@value的参数就不po了,大家懂的自然懂,怎么配置。
userName和password自行定义即可。这样,就大功告成了。(^-^)V。
很多人会问,用这么多类,你的 maven需要导入很多依赖包
基础的mybatis,springframework等的我就不po了,大家都知道的。然后就是正常的io依赖也没啥特别的。自行解决就好。 -
已经离职了一段时间,前老板突然间给你说,你以前的工作出了差错让你回去对接怎么办?
2021-06-07 11:14:36本来离职时我想做好交接工作再走,但前前老板硬是不用,可结果还是出了问题,不过我还是答应了前前老板,回去帮他处理工作,后来前前老板被我的一番操作,惊呆了,连连对我表示夸赞。 我是一个美工,在一家小型的...离职2个星期后,前老板突然打电话,说以前的工作出了差错,问我损失该怎么办。本来离职时我想做好交接工作再走,但前前老板硬是不用,可结果还是出了问题,不过我还是答应了前前老板,回去帮他处理工作,后来前前老板被我的一番操作,惊呆了,连连对我表示夸赞。
我是一个美工,在一家小型的广告公司上班儿,公司算上前老板只有4个人,另外两个人负责制作,偶尔工作比较多的时候,我就会在网上找一些工作室来帮我修图,不过这也是前老板同意的。
后来因为我跟前老板申请加薪,一直没有得到回应,再加上公司还经常加班,所以我就跟前老板提出了离职。
因为我们公司规模比较小,所以也没有正规的离职流程,我告诉前老板自己要离职后,前老板马上就答应了,不过前老板对我说:“你得再帮我顶替几天,等我找到人手了,你马上就可以走。”
当时我答应了前老板的要求,也想着站好最后一班岗,等新人来了交接好工作以后,自己也省的麻烦,没想到在我休假的那天早上接到了前老板的电话,前老板说:“公司招到人了,你不用来了,从今天开始就算你离职了。”
我在迷迷糊糊的睡意中,感到了一丝慌张,我对前老板说:“那我明天回去交接一下工作吧。”
可前老板却说:“不用交接了,你的工作我都清楚,没事儿,你不用来公司了。”
挂断电话后,我还担心前老板会不会把工资给我结清,不过没出五分钟前老板就把我的工资转了过来,当时我还给前老板发了一条信息:“我还是回去把工作交接一下吧,这让新员工也能更容易进入工作状态。”可前老板却一直没有给我回信息。
后来我也想这样也好,反正我已经把话说到位了,工资我也拿到手了,不用我回去交接工作,还省的我浪费时间了。
结果我离职两个星期后,前老板突然打电话说我之前的工作出了差错,问我损失怎么办。
我心想:“交接工作的事情我不止提了两次,前老板硬是不用我交接,现在出了问题,他有一副怪罪我的口语气,真是让我生气。”
虽然有点生气,但我还是稳定了一下情绪,然后把具体的情况向前老板问清楚后,我对前老板说:“您放心,先别着急,这事儿我回去处理。”
等我回到公司后发现,前老板找到新员工,我还有过一面之缘,他是前老板的侄子,我觉得一定是当初网上找的工作是把图片发过来的时候,他没有审图,不过事情已经这样了,不能再怪他了,解决问题才是最重要的。
我先是联系了一下网上负责设计和制作的工作室,把出现的问题和他们对接了一下,然后我又对他们说:“这个订单出的问题责任在我,我们前老板特别生气,还要让我承担损失,您看您那边能不能帮帮忙,重新帮我做一份儿,咱们都合作这么长时间了,以后我再有订单多找你们做,下个月给你们的订单数量一定翻倍。”
最后经过多番沟通,对方终于同意帮我重新制作一份儿了,这次危机也就解除了,前老板看到我的处理方式都惊呆了,他没有想到还有这种解决办法,对我连连夸赞,还说我走是他损失了人才,而且前老板认为这家工作室的态度很好,也说以后会多和他们合作。
多亏我把这次事件完美的处理好了,直到现在,我和前老板还有联系,他经常会给我一些兼职的工作,帮助他修图,这也算是我一份非常不错的副业收入。
你的情况跟我很相似,已经离职了一段时间,前前老板突然说,你以前的工作出了差错,让你回去对接,我建议你可以按照以下4个重点来处理:
首先,答应前老板回去对接工作
先答应前前老板帮他处理工作,问清楚之前工作的差错到底出在哪里,提前想好解决办法,这样不仅能够体现你的职业素养,而且能让前前老板控制好情绪,以稳定的心态跟你进行沟通,你也会因为负责任的态度,跟前老板取得更进一步的友好关系。
哪怕你现在还有另外一份工作,这件事情又被你现在的领导知道了,你现在的领导也不会怪你,同样会认为你做出了正确的选择,因为大部分公司的管理者都会认为你是怎么对待以前公司的,就会怎么对待现在的公司,所以你的表现至关重要。
其次,一定不要答应承担损失
前前老板在你离职时,不让你跟新员工交接工作,其实是为了节省人力成本,因为只要你多在公司待一天,前老板就要多付一天的工资。
事已至此,既然你已经离职,而且多次强调过交接工作,又被前前老板拒绝了,那肯定是没有责任的,所以一定不能承担经济损失。
除此之外,你还要保留好聊天记录,如果以后遇到了不必要的麻烦,你和前老板之间的聊天记录将会是一份重要的证据。
寻找最好的解决方案,降低所有涉事者的损失,达到共赢的局面
遇到事情的时候,控制好自己的负面情绪是最重要的,其实凡事不只有两个选项,去想想更多的可能性,也许你能找到一个让所有人都可以共赢的方案。
我当初遇到事情的时候就是想我不能承担经济损失,前老板也不想承担经济损失,那么可以让网上的工作室来承担,自己把责任揽下来,然后让工作室帮忙,最后再答应他,以后多给他们一些订单,这样就得到了一个所有人都满意的结局。
我建议你也可以试试寻找有没有像我一样的解决方法,让所有人都能达到共赢。
事情处理好后,继续保持和前前老板的沟通,也许未来还有意外惊喜
现在是副业刚需,像我们做修图工作的,其实特别好找兼职的副业工作,并且身在职场,人脉对你来讲非常重要。
当所有的事情已经处理完之后,我建议你可以继续与前老板保持沟通,因为这样,如果前老板在未来有可以赚钱的工作,也会提早想到你。
而且身在职场,经过多年的历练之后,大家的工作能力几乎都会相差无疑,所以最重要的还是比拼人脉资源,你的前老板,一样也是你重要的人脉资源。
写在最后:
已经离职了一段时间,前前老板突然说,你以前的工作出了差错,让你回去对接,不论发生什么样的情况,最重要的就是控制好自己的负面情绪,不要和前老板产生矛盾,然后寻找到一个可以共赢的解决办法,最终让所有人都能在这次事件中共同获益。
在工作中不要害怕出现问题,只有不断的面对问题,不断的解决问题,才能让你的工作能力更进一步,这对你来说是一次非常不错的,提升自己的机会。
-
前后端对接规范初版
2021-07-30 16:43:51我认为且希望的工作流程。 1、收到客户新需求 2、产品经理初筛功能并完成原型以及功能点文档(原型需注明每个交互,功能点文档利于管理) 3、项目组开会,产品讲解业务逻辑和功能点,技术人员确定需求可行性 4、...写在前面
我认为且希望的工作流程。
1、收到客户新需求
2、产品经理初筛功能并完成原型以及功能点文档(原型需注明每个交互,功能点文档利于管理)
3、项目组开会,产品讲解业务逻辑和功能点,技术人员确定需求可行性
4、分配技术人员,准备开发
5、前后端对接接口返回结构,排查可能出现的问题,满足前后端代码友好,前端性能不受到影响
6、书写接口文档,注明接口中返回字段所对应的内容
7、根据接口文档拿到mock的demo数据,前端先开始开发
8、真实接口完成,前端接入真实接口
9、不断的测试、迭代,中途出现的问题放在版本管理工具中便于查看和确认
10、完成新功能迭代。
谈谈接口
随着前后端的分离,后端工程师不需要编写页面,不需要写JS,只需要提供接口即可,可是就是仅仅这一个接口,对于很多后端开发工程师而言,在实际开发,同前端对接的过程中,依然问题重重
很多后端同学说我只负责写接口,其他我一概不管,这样造成的后果就是
1、接口结构无序、杂乱无章
2、接口和实际业务场景不相匹配、不可用
3、频繁的同前端沟通,简单的事情复杂化,前后端都很恼火
4、事情没做好
后端在编写接口前,首先是对业务的理解,在对业务未理解透彻之前,编码都是无意义的,作为后端来说,需要锻炼自己对整个系统全局考虑的能力,接口之间并非是毫无关联的,我们在写第一个接口之间,其他接口之间的业务逻辑也许考虑到,这在后端团队合作开发不同功能的情况下显得尤为重要.
后端在开发接口时,我觉得主要从以下几个方面需要注意:
接口url 定义
接口类型、参数
全局错误码定义
接口json格式
接口文档编写
接口url定义
对于后端开发人员来说,接口前端入参,最终组合查询数据库资源,经过一系列相关业务场景下的计算,响应给前端json数据,每一层url的path定义需要清晰明了,这和后端在使用AOP定义事务管理同理,后端service需要满足一定的命名规范,这样方便统一管理,而且有这层规范后,后续的前后端对接会轻松很多
为了在许多API和长时间内提供一致的开发人员体验,API使用的所有名称应为:
-
简单
-
直觉
-
一致
这包括接口,资源,集合,方法和消息的名称。
由于许多开发人员不是英文母语人士,因此这些命名约定的目标之一是确保大多数开发人员能够轻松了解API。 它通过鼓励在命名方法和资源时使用简单,一致和小的词汇表来实现。
-
API中使用的名称应该是正确的美国英语。例如,许可证(而不是许可证),颜色(而不是颜色)。
-
可以简单地使用常用的简短形式或长字的缩写。例如,API优于应用程序编程接口。
-
尽可能使用直观,熟悉的术语。例如,当描述删除(和销毁)资源时,删除是优先于擦除。
-
对同一概念使用相同的名称或术语,包括跨API共享的概念。
-
避免名称重载。为不同的概念使用不同的名称。
-
仔细考虑使用可能与常用编程语言中的关键字冲突的名称。可以使用这些名称,但在API审查期间可能会触发额外的审查。谨慎和谨慎地使用它们。
接口类型、参数
关于接口的请求类型,目前比较常用的:
GET
、POST
、PUT
、DELETE
、PATCH
GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE):从服务器删除资源。
后端可根据不同的业务场景定义不同的接口类型
在定义接口参数之时,目前我们常用的几种提交方式
表单提交,application/x-www-form-urlencoded
表单提交主要针对
key-value
的提交形式如下Java片段:
1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping
(
"/queryAll"
)
public
RestfulMessage queryAll(RuleCheckLogs ruleCheckLogs,
@RequestParam
(value =
"current_page"
,defaultValue =
"1"
)Integer current_page
,
@RequestParam
(value =
"page_size"
,defaultValue =
"10"
)Integer page_size
,
@RequestParam
(value =
"tableName"
,required =
false
) String tableName){
RestfulMessage restfulMessage=
new
RestfulMessage();
try
{
assertArgumentNotEmpty(ruleCheckLogs.getProjectId(),
"质检方案id不能为空"
); restfulMessage.setData(qcRuleCheckLogsService.queryRuleLogsByPage(ruleCheckLogs,tableName,current_page,page_size));
}
catch
(Exception e){
restfulMessage=wrapperException(e);
}
return
restfulMessage;
}
文件流提交
json提交,application/json
json提交方式在SpringMVC或Spring Boot中主要有两种,一种是以
@RequestBody
注解接收方式,另外一种是以HttpEntity<String> requestEntity
字节接收Java代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping
(
"/mergeModelEntitys"
)
public
RestfulMessage mergeModelEntitys(HttpEntity<String> requestEntity){
RestfulMessage restfulMessage=
new
RestfulMessage();
try
{
JsonObject paramsJson = paramJson(requestEntity);
assertJsonNotEmpty(paramsJson,
"请求参数不能为空"
);
//more...
}
catch
(Exception e){
restfulMessage=wrapperException(e);
}
return
restfulMessage;
}
全局错误码定义
错误码的定义同HTTP请求状态码一样,对接者能通过系统定义的错误码,快速了解接口返回错误信息,方便排查错误原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"code"
:
"8200"
,
"message"
:
"Success"
,
"data"
: {
"total_page"
: 1,
"current_page"
: 1,
"page_size"
: 10,
"count"
: 5,
"data"
: [
{
"id"
:
"a29ab07f1d374c22a72e884d4e822b29"
,
//......
}
//....
]
}
}
接口json格式
后端响应json给前端需要注意以下几点:
1、json格式需固定
例如如下图形
如上图所示,横向是时间,纵向是value值
我们给出的json结构应该如此:
1
2
3
4
5
6
7
8
9
10
11
[
{
"date"
:
"2018-01"
,
"value"
:100
},
{
"date"
:
"2018-02"
,
"value"
:200
}
//more...
]
在工作中,我们经常碰见这样的数据格式:
1
2
3
4
5
6
7
8
9
[
"2018-01"
:{
value:100
},
"2018-02"
:{
value:200
}
//more...
]
这里所说的json格式固定主要针对此种情况,后端给到前端的接口格式必须是固定的,所有动态数据值都需相应的key与之对应
2、所有返回接口数据需直接可用,越简单越好
后端提供给前端的接口数据,最终交给前端的工作,只需要让前端渲染数据即可,越简单越好,不应掺杂过多的业务逻辑让前端处理,所有复杂的业务逻辑,能合并规避掉的都需后端处理掉。
接口文档编写
接口文档编写是前后端对接重要依据,后端写明接口文档,前端根据接口文档对接。
接口文档需注明字段对应页面内容。不写接口文档的坏处:
第一:前后端开发没有标准,没有依据。
第二:容易扯皮,没法追踪,职责不清。
第三:开发效率低。等等。
文档形势目前主要分几种:
1、依赖swagger框架,自动生成接口文档(swagger只能生成基于key-value详细参数方式,针对json格式,无法说明具体请求内容)
2、手动编写说明文档,推荐markdown编写
在这里多说几句,渲染页面是前端去做,复杂的逻辑极大影响页面的加载速度和浏览器性能,我举出几个现在发现的问题。
1、需要顺序显示的内容返回值为中文key:value 的对象。这个结构没有索引没有顺序,且中文key这种低级的不符合前后端规范的错误希望能够规避。这种数据前端处理很复杂影响性能,代码不必要的逻辑增加,代码冗余。
像这种有多个内容,且有顺序的数组应返回结构如下。
StepDate: [
{
name:'立项',
date:'2016-07-05'
},
{
name:'开题',
date:'2019-10-28'
},
{
name:'中期',
date:'2019-10-29'
},{
name:'结题申请',
date:'2019-10-31'
},
{
name:'结题',
date:null
}]
贴上目前前端处理的代码,这些不必要的逻辑代码都可以省略的。
2、如果返回值是数组或字符串,转成该结构再返回,避免直接返回字符串等前端处理。
对象数组直接返回了字符串,还有其他的也一样。
前端现在需自己转成需要的格式,多此一举,这点简单东西后端应该处理好再返回。
3、如果是有逻辑需要增加或者展示的、需要遍历的数据,返回数组。
举例来说,关键词需要区分为每个,后端返回字符串后,前端只能自己进行字符串的拆分,转换为数组结构,重新遍历并渲染,增加了不必要的逻辑,影响性能。
现在前端逻辑部分增加了返回值的重构
在渲染部分还增加了重新渲染的逗号,等同于多做了很多无用功。
4、接口中无数据内容的字段,统一返回null(有定义的字段必须返回,不可undefined)
现在的返回值什么状态都有,有空数组,有null,有没返回的,前端做处理很冗余和麻烦。
暂时只发现这些问题,并不是很复杂的数据处理,但是很影响前端性能,本就该是后端处理的,后端稍微用心点就可以规避这些问题,提出了问题可以商讨和解决问题而不是一味地反驳,问题的解决需要充分的沟通。所以综上所述,在每次新接口开发前先对接一下前后端双方都合适的数据结构,磨合成默契的前提后可以不再每次进行对接,从而共同优化项目。
接口对接
万事俱备,只欠东风,虽然上面我们准备了所有我们该准备的,接口定义完美无缺,接口文档也已说明,但在对接时任然可能出现问题,此时我想我们还需注意的细节
1、后端接口需自行进行Junit单元测试
Spring目前集成Junit框架可方便进行单元测试,包括对业务bean的方法测试,以及针对api的mock测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@RunWith
(SpringRunner.
class
)
@SpringBootTest
public
class
QcWebApplicationTests {
@Autowired
private
WebApplicationContext context;
private
MockMvc mvc;
@Autowired
QcFieldService qcFieldService;
@Before
public
void
setUp()
throws
Exception {
//初始化mock对象
mvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
public
void
queryByDsId(){
try
{
//针对mock-接口Controller层测试
mvc.perform(MockMvcRequestBuilders.post(
"/qc/entity/queryByDsId"
)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.param(
"dsId"
,
"7d4c101498c742368ef7232f492b95bc"
)
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
catch
(Exception e) {
e.printStackTrace();
}
}
@Test
public
void
testUpdateField(){
QcField qcField=
new
QcField();
qcField.setId(
"513ee55f5dc2498cb69b14b558bc73e6"
);
qcField.setShortName(
"密码"
);
//业务bean-service方法测试
qcFieldService.updateBatchFields(Lists.newArrayList(qcField));
}
2、使用工具测试,推荐PostMan
作为接口调试神器,Postman大名想必大家都已知道
作为后端来说,我们需要学会查看chrome推荐给我们的审查元素的功能,可参看Chrome开发工具介绍
chrome提供了一个可以copy当前接口的url功能,最终生成curl命令行
最终通过
Copy as cURL(bash)
功能可生成curl命令1
curl
'http://demo.com/qc/ds/getAll'
-H
'Origin: http://demo.com'
-H
'Accept-Encoding: gzip, deflate'
-H
'Accept-Language: zh-CN,zh;q=0.9'
-H
'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36'
-H
'Content-Type: application/x-www-form-urlencoded'
-H
'Accept: application/json, text/plain, */*'
-H
'Referer: http://demo.com/index.html'
-H
'Connection: keep-alive'
--data
'current_page=1&page_size=6&'
--compressed
以上命令可以在Linux等各终端直接执行
curl命令是一个利用URL规则在命令行下工作的文件传输工具。它支持文件的上传和下载,所以是综合传输工具,但按传统,习惯称curl为下载工具。作为一款强力工具,curl支持包括HTTP、HTTPS、ftp等众多协议,还支持POST、cookies、认证、从指定偏移处下载部分文件、用户代理字符串、限速、文件大小、进度条等特征。做网页处理流程和数据检索自动化,curl可以祝一臂之力。
postman提供导入curl命令行
3、前后端需心平气和沟通,勿推卸责任,前后端开发人员水平不尽相同,作为同事,需要的是团结合作,努力将事情做好,而非相互推卸责任。
最后希望项目能够越做越好,公司蒸蒸日上。
-
-
省级基础电信企业网络与信息安全工作考核要点与评分标准.docx
2022-01-03 19:51:23企业网络与信息安全工作考核要点与评分标准 -
前后端对接的思考及总结
2018-06-10 13:10:00前后端对接的思考及总结 说在前面的话 随着前端NodeJs技术的火爆,现在的前端已经非以前传统意义上的前端了,各种前端框架(Vue、React、Angular......)井喷式发展,配合NodeJs服务端渲染引擎,目前前端能完成的工作不... -
学校复学做好疫情防控的工作方案整理3篇.pdf
2021-12-11 23:22:23学校复学做好疫情防控的工作方案整理3篇.pdf -
酒店网络维护工作总结.doc
2022-06-22 16:25:43酒店网络维护工作总结.doc -
浅谈如何做好煤炭企业档案管理工作
2020-06-14 20:47:53企业档案是企业管理的基础,而煤炭企业作为特殊的行业,其活动覆盖广,安全性要求高,每一项活动都会产生价值很高的档案资料,故因努力做好煤炭企业的档案管理工作。本文将从加大档案工作宣传力度、提高档案人员业务素质,... -
酒店网络维护工作总结(精选多篇).doc
2022-06-22 16:24:52酒店网络维护工作总结(精选多篇).doc -
【2019年中总结】五种途径对接天猫精灵音响控制您的智能设备,打破传统产业,让语音AI控制无处不在!
2019-07-29 19:47:22综上所述,对于一些中小企业公司来说,可以免去搭建硬服务器这块硬伤,因为做好一个服务器,考虑到均衡负载、高并发 的稳定性,考虑到大数据统计,以及全球部署,维护起来都是很大的一笔费用。 因此,接入阿里的... -
国家发布工业互联网专项工作组2019年工作计划.doc
2019-06-25 23:51:54工业互联网专项工作组2019年工作计划:一、加强统筹推进;二、提升基础设施能力;三、构建标识解析体系;四、建设工业互联网平台;五、突破核心技术标准;六、培育新模式新业态;七、发展产业生态;八、增强安全保障... -
综采工作面过大转角回采技术
2020-06-24 03:57:01通过对工作面大转角现场的分析,提出了工作面调斜、巷道扩修、设备管理等一系列过转角针对性的安全技术措施,实现了综采工作面在不停产的情况下安全快速过30°转角。实践证明,此技术的应用有效缓解了生产接替,且解决了... -
如何高效对接第三方支付
2020-05-24 17:41:24目前我们已经服务30个国家和地区,不同国家往往需要对接不同的第三方支付公司,所以最近两年,研发组对接了大量的第三方支付公司,积累了一定的经验。 本文主要分享如何对接第三方支付,以及在生产上实际遇到的一些... -
企业为何要进行知识管理?企业知识管理的重要性!附带工具推荐!
2020-07-27 14:29:16技术服务团队在对接客户支持时,需要电脑对资料进行查找,然后通过微信的方式远程语音或视频对接。工作效率低下。 出现以上的问题都是因为团队或企业,在日常的工作中缺乏了对知识(资料、文档、经验)进行统一有效... -
10kV电力电缆预防性试验作业指导书
2020-11-26 09:02:07根据试验性质,确定试验项目,组织作业人员学习作业指导书,使全体作业人员熟悉作业内容、作业标准、安全注意事项 -
【渝粤教育】广东开放大学 社会工作综合能力 形成性考核 (27)
2021-12-07 10:09:12题目:社会工作不同于其他理论性社会科学学科的重要之点是( )。 题目:关于社会工作价值观操作原则的说法,正确的有( )。 题目:作为一名专业社会工作者,必须遵守的原则不包括( )。 题目:下来社会工作价值观的... -
另一个小程序 返回的支付结果如何得到_如何高效对接第三方支付
2020-10-18 15:58:39本文主要分享如何对接第三方支付,以及在生产上实际遇到的一些问题,避免大家重复踩坑。一、五个接口先简单阐述一下,对接第三方支付时,需要对接如下5个核心接口1.发起支付该接口主要用于从第三方获取token,当用户... -
工作总结及 工作思路 输电运维班 .docx
2022-06-12 15:49:24工作总结及 工作思路 输电运维班 .docx工作总结及 工作思路 输电运维班 .docx工作总结及 工作思路 输电运维班 .docx工作总结及 工作思路 输电运维班 .docx工作总结及 工作思路 输电运维班 .docx工作总结及 工作思路 ... -
集团公司信息化工作和2020年工作打算汇报材料 (2).pdf
2022-06-21 13:27:31集团公司信息化工作和2020年工作打算汇报材料 (2).pdf集团公司信息化工作和2020年工作打算汇报材料 (2).pdf集团公司信息化工作和2020年工作打算汇报材料 (2).pdf集团公司信息化工作和2020年工作打算汇报材料 (2).pdf... -
集团公司信息化工作和2020年工作打算汇报材料 (2).docx
2022-06-21 12:26:49集团公司信息化工作和2020年工作打算汇报材料 (2).docx集团公司信息化工作和2020年工作打算汇报材料 (2).docx集团公司信息化工作和2020年工作打算汇报材料 (2).docx集团公司信息化工作和2020年工作打算汇报材料 (2).... -
集团公司信息化工作和2020年工作打算汇报材料.pdf
2022-06-21 12:43:59集团公司信息化工作和2020年工作打算汇报材料.pdf集团公司信息化工作和2020年工作打算汇报材料.pdf集团公司信息化工作和2020年工作打算汇报材料.pdf集团公司信息化工作和2020年工作打算汇报材料.pdf集团公司信息化... -
集团公司信息化工作和2020年工作打算汇报材料.docx
2022-06-21 12:27:26集团公司信息化工作和2020年工作打算汇报材料.docx集团公司信息化工作和2020年工作打算汇报材料.docx集团公司信息化工作和2020年工作打算汇报材料.docx集团公司信息化工作和2020年工作打算汇报材料.docx集团公司信息... -
为什么说测试是衔接岗位?从三个方面说明测试衔接的重要性
2021-12-07 17:06:52测试岗位越往上走,越能发掘沟通的重要性。 如果想做好一个测试管理人才,除了跟进项目上的事情,还要学会跟不同的人员沟通。这样才能推动问题有效的快速解决。我们从三个方面说明测试衔接的必要性。 第一部分...