需求来源:因为禅道免费版不包含批量导入任务功能,如果要使用的话,需要购买禅道官方的插件。(就是不想花钱,嘿嘿),于是花了一天时间研究如何自己二开。
首先呢,禅道是PHP开发的,本人是.net忠实粉丝,对PHP完全看不懂,也没玩过。
先给大家看看效果:
上图,这是我的“任务”导入模板。
上图,是操作界面。
这是一键导入之后的效果。
这里啰嗦两句:为什么我要用nginx反向代理去实现
1、一开始我使用jsonp解决跨域问题,发现jsonp只能get不能post提交,导致我导入大量数据的时候,浏览器链接超长报错。
2、后面继续google,发现用cors可以解决跨域问题并支持post提交,但无奈php里 我是写的jq,加入vue 可能会有更多问题(这里我也没尝试)ajax提交一直提示
,非常无奈,放弃cors跨域方式,转用 nginx反向代理最终解决 禅道 跨域问题!
好了,啰嗦这么久,下面进入开发主题。
第一步:首先,我们来到禅道二次开发官网
https://devel.easycorp.cn/book/extension-new/intro-52.html(地址贴给大家)
找到扩展机制开发,然后来到视图层的扩展,开始我们先要扩展一个view(否则按钮出不来),我们选用钩子扩展,因为钩子扩展后续禅道更新,不会影响它源代码
1 2 <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js"></script> 3 <script> 4 $(function() 5 { 6 //$('#mainHeader').css('background', 'red'); 7 //alert("顶顶顶顶顶"); 8 //console.log("?????"); 9 $('#exportActionMenu').append('<input onchange="daorudata(this)" id="daoru_ipt" type="file" accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" value="导出模板" class="" style="display:none;"/>') 10 $('#importActionMenu').append('<li><a href="#" onclick="daoru()" class="import">导入excel</a></li>'); 11 $('#exportActionMenu').append('<li><a href="http://192.168.0.175:8065/%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xls" class="import">导出模板</a></li>'); 12 }) 13 14 function daoru(){ 15 $("#daoru_ipt").click(); 16 } 17 18 function daorudata(e) 19 { 20 //alert("选择了文件"); 21 console.log($("#daoru_ipt")); 22 var file = $("#daoru_ipt")[0].files[0]; 23 var reader = new FileReader(); 24 reader.onload = function (e) { 25 var data = e.target.result; 26 var workbook = XLSX.read(data, { type: 'binary' }); 27 // 假设您的.xls文件只有一个sheet 28 var sheetName = workbook.SheetNames[0]; 29 var worksheet = workbook.Sheets[sheetName]; 30 // 将数据转换为JSON格式 31 var jsonData = XLSX.utils.sheet_to_json(worksheet); 32 33 try{ 34 // 遍历jsonData,格式化日期字段 35 jsonData.forEach(function (row) { 36 // 假设日期字段名称为"date" 37 // console.log(row); 38 if (row.截止日期) { 39 var excelDateValue = row.截止日期; 40 // 将日期序列转换为JavaScript Date对象 41 var dateObject = new Date((excelDateValue - (25567 + 1)) * 86400 * 1000); 42 // 格式化日期为可读的字符串,比如"YYYY-MM-DD" 43 var formattedDate = dateObject.toISOString().split('T')[0]; 44 row.截止日期 = formattedDate; 45 //console.log(formattedDate); 46 } 47 if (row.预计开始) { 48 var excelDateValue = row.预计开始; 49 // 将日期序列转换为JavaScript Date对象 50 var dateObject = new Date((excelDateValue - (25567 + 1)) * 86400 * 1000); 51 // 格式化日期为可读的字符串,比如"YYYY-MM-DD" 52 var formattedDate = dateObject.toISOString().split('T')[0]; 53 row.预计开始 = formattedDate; 54 //console.log(formattedDate); 55 56 } 57 }); 58 }catch(err){ 59 alert("日期格式不正确,请检查"); 60 return; 61 } 62 console.log(jsonData); 63 var str= JSON.stringify(jsonData); 64 console.log(str); 65 66 //jsonp请求接口方式 67 // $.ajax({ 68 // url: "http://192.168.60.123:5000/api/zentao/ImportTask", 69 // //url: "http://192.168.0.175:5001/api/zentao/ImportTask", 70 // type: "POST", 71 // data:{"data":str}, 72 // dataType: "jsonp", //指定服务器返回的数据类型 73 // jsonp: "theFunction", //指定参数名称 74 // jsonpCallback: "showData", //指定回调函数名称 75 // success: function (data) { 76 // //console.log("返回"); 77 // //console.log(data); 78 // //var result = JSON.stringify(data); //json对象转成字符串 79 // //$("#text").val(result); 80 // }, 81 // error:function(res){ 82 // console.log("导入报错"); 83 // console.log(res); 84 // } 85 // }); 86 87 $.ajax({ 88 url: "/api/zentao/ImportTask", 89 dataType: 'json', 90 type: 'post', 91 data:{ str : str}, 92 success:function(response){ 93 console.log("返回结果"); 94 console.log(response); 95 if(response.code=="1"){ 96 alert("导入成功"); 97 window.location.reload(); 98 }else{ 99 100 alert(response.msg); 101 } 102 } 103 }); 104 }; 105 reader.readAsBinaryString(file); 106 $("#daoru_ipt").val(null); 107 } 108 109 function showData(data){ 110 var res=JSON.parse(data); 111 console.log("返回结果"); 112 console.log(res); 113 if(res.tag=="1"){ 114 alert("导入成功"); 115 window.location.reload(); 116 }else 117 { 118 alert(res.msg); 119 } 120 } 121 // function daochu(){ 122 // //alert("导出"); 123 // // 列标题 124 // let str = '<tr>'; 125 // str+='<td>所属执行</td>'; 126 // str+='<td>所属模块</td>'; 127 // str+='<td>指派给</td>'; 128 // str+='<td>任务模式</td>'; 129 // str+='<td>任务名称</td>'; 130 // str+='<td>任务描述</td>'; 131 // str+='<td>任务类型</td>'; 132 // str+='<td>优先级</td>'; 133 // str+='<td>最初预计</td>'; 134 // str+='<td>预计开始</td>'; 135 // str+='<td>截止日期</td></tr>'; 136 // // 循环遍历,每行加入tr标签,每个单元格加td标签 137 // for(let i = 0 ; i < 10 ; i++ ){ 138 // str+='<tr>'; 139 // str+='<td>项目确认阶段</td>'; 140 // str+='<td></td>'; 141 // str+='<td></td>'; 142 // str+='<td></td>'; 143 // str+='<td></td>'; 144 // str+='<td></td>'; 145 // str+='<td></td>'; 146 // str+='<td></td>'; 147 // str+='<td></td>'; 148 // str+='<td>2023-07-01</td>'; 149 // str+='<td>2023-07-05</td> '; 150 // str+='</tr>'; 151 // } 152 // var excelFile = "<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:x='urn:schemas-microsoft-com:office:excel' xmlns='http://www.w3.org/TR/REC-html40'>"; 153 // excelFile += "<head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>任务</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head>"; 154 // excelFile += "<body><table width='10%' border='1'>"; 155 // excelFile += str; 156 // excelFile += "</table></body>"; 157 // excelFile += "</html>"; 158 // var link = "data:application/vnd.ms-excel;base64," + base64(excelFile); 159 // var a = document.createElement("a"); 160 // a.download = "导入模板.xls"; 161 // a.href = link; 162 // a.click(); 163 // } 164 165 // function base64(content) { 166 // return window.btoa(unescape(encodeURIComponent(content))); 167 // } 168 </script> 169
提示一下:钩子脚本的命名规则为方法名. 扩展名.html.hook.php,所以我这个文件命名为:task.qhl.html.hook.php (task是方法名),qhl 是我随意命名的 ,文件需要放在:extension\custom\execution\ext\view 中,execution是文件名,下面一句话会告诉你怎么找到文件名。
方法名和文件名可以这样找到:比如 http://192.168.x.x/zentao/execution-task-22.html# 中的execution是文件名,task是方法名, 22是参数,其他模块也是一样的。
最后,我们刷新一下页面,就出来导入按钮啦!
第二步:好了,页面扩展完了,我们用.net 写api
1 using Api.Core.IServices.Material; 2 using Api.Core.Model.ViewModel; 3 using Api.Core.Model; 4 using AutoMapper; 5 using Microsoft.AspNetCore.Mvc; 6 using System.Threading.Tasks; 7 using MySqlX.XDevAPI.Common; 8 using System; 9 using Senparc.NeuChar.NeuralSystems; 10 using Microsoft.AspNetCore.Http; 11 using System.Collections.Generic; 12 using System.Linq; 13 using Api.Core.Model.User; 14 using Polly; 15 using System.IO; 16 17 namespace Api.Core.Controllers 18 { 19 /// <summary> 20 /// 禅道 21 /// </summary> 22 [Route("api/[controller]")] 23 [ApiController] 24 public class zentaoController : BaseController 25 { 26 private readonly IMaterialService _materialService; 27 28 private readonly IMapper _mapper; 29 30 public zentaoController(IMaterialService materialService, 31 IMapper mapper 32 ) 33 { 34 _materialService = materialService; 35 _mapper = mapper; 36 } 37 38 /// <summary> 39 /// 禅道导入任务 40 /// </summary> 41 /// <param name="data"></param> 42 /// <returns></returns> 43 [HttpPost("ImportTask")] 44 public Task<BaseResponse<bool>> ImportTask([FromForm]string str) 45 { 46 47 //IFormCollection queryParameters = HttpContext.Request.Form; 48 //string qstr = queryParameters["str"]; 49 //string qid = queryParameters["ID"]; 50 51 List<importTaskData> list= Newtonsoft.Json.JsonConvert.DeserializeObject<List<importTaskData>>(str); 52 //string callback = Request.Query["theFunction"]; 53 //校验一遍数据 54 int i = 0; 55 string[] rwlslst=new string[9] { "设计","开发","需求","测试","研究","讨论","界面","事务","其他"}; 56 foreach (var item in list) 57 { 58 i++; 59 if (item.所属执行.Split('/').Length != 2) 60 { 61 //string result = callback + "('{\"tag\":\"0\",\"msg\":\"第" + i + "行所属执行格式不正确\"}')"; 62 //Response.WriteAsync(result); 63 return Task.FromResult(Result(false, Model.StatusCode.Fail, "第" + i + "行所属执行格式不正确")); 64 } 65 if (!int.TryParse(item.优先级, out int yxj)) 66 { 67 //string result = callback + "('{\"tag\":\"0\",\"msg\":\"第" + i + "行优先级必须为数字\"}')"; 68 //Response.WriteAsync(result); 69 //return; 70 return Task.FromResult(Result(false, Model.StatusCode.Fail, "第" + i + "行优先级必须为数字")); 71 } 72 if (!float.TryParse(item.最初预计, out float zcyj)) 73 { 74 //string result = callback + "('{\"tag\":\"0\",\"msg\":\"第" + i + "行最初预计必须为数字\"}')"; 75 //Response.WriteAsync(result); 76 //return; 77 return Task.FromResult(Result(false, Model.StatusCode.Fail, "第" + i + "行最初预计必须为数字")); 78 } 79 if (!DateTime.TryParse(item.预计开始, out DateTime yjks)) 80 { 81 //string result = callback + "('{\"tag\":\"0\",\"msg\":\"第" + i + "行预计开始必须为日期格式\"}')"; 82 //Response.WriteAsync(result); 83 //return; 84 return Task.FromResult(Result(false, Model.StatusCode.Fail, "第" + i + "行预计开始必须为日期格式")); 85 } 86 if (!DateTime.TryParse(item.截止日期, out DateTime jzrq)) 87 { 88 //string result = callback + "('{\"tag\":\"0\",\"msg\":\"第" + i + "行截止日期必须为日期格式\"}')"; 89 //Response.WriteAsync(result); 90 //return; 91 return Task.FromResult(Result(false, Model.StatusCode.Fail, "第" + i + "行截止日期必须为日期格式")); 92 } 93 if (string.IsNullOrWhiteSpace(item.指派给)) 94 { 95 //string result = callback + "('{\"tag\":\"0\",\"msg\":\"第" + i + "行指派给不能为空\"}')"; 96 //Response.WriteAsync(result); 97 //return; 98 return Task.FromResult(Result(false, Model.StatusCode.Fail, "第" + i + "行指派给不能为空")); 99 } 100 if (string.IsNullOrWhiteSpace(item.任务名称)) 101 { 102 //string result = callback + "('{\"tag\":\"0\",\"msg\":\"第" + i + "行任务名称不能为空\"}')"; 103 //Response.WriteAsync(result); 104 //return; 105 return Task.FromResult(Result(false, Model.StatusCode.Fail, "第" + i + "行任务名称不能为空")); 106 } 107 if (string.IsNullOrWhiteSpace(item.任务类型)) 108 { 109 //string result = callback + "('{\"tag\":\"0\",\"msg\":\"第" + i + "行任务类型不能为空\"}')"; 110 //Response.WriteAsync(result); 111 //return; 112 return Task.FromResult(Result(false, Model.StatusCode.Fail, "第" + i + "行任务类型不能为空")); 113 } 114 if (!rwlslst.Contains(item.任务类型)) 115 { 116 //string result = callback + "('{\"tag\":\"0\",\"msg\":\"第" + i + "行任务类型只能是:设计、开发、需求、测试、研究、讨论、界面、事务、其他\"}')"; 117 //Response.WriteAsync(result); 118 //return; 119 return Task.FromResult(Result(false, Model.StatusCode.Fail, "第" + i + "行任务类型只能是:设计、开发、需求、测试、研究、讨论、界面、事务、其他")); 120 } 121 } 122 var fsl=_materialService.SaveTask(list); 123 if (fsl.Result.errcode==0) 124 { 125 //string result = callback + "('{\"tag\":\"1\",\"msg\":\"导入成功\"}')"; 126 //Response.WriteAsync(result); 127 return Task.FromResult(Result(true, Model.StatusCode.Success, "导入成功")); 128 } 129 else 130 { 131 //string result = callback + "('{\"tag\":\"0\",\"msg\":\""+ fsl.Result.errmsg + "\"}')"; 132 //Response.WriteAsync(result); 133 return Task.FromResult(Result(false, Model.StatusCode.Fail, fsl.Result.errmsg)); 134 } 135 136 137 } 138 } 139 }
以上代码是.net core 3.1的webapi ,具体业务逻辑,就自己写实现方法了。
第三步、我们开始配置nginx反向代理
安装nginx 忽略,自己去百度如何下载安装。下面是我nginx配置反向代理的代码,大概意思很简单:
监听80端口,正常先跳转到 第二个location配置的地址,也就是我的禅道地址:82端口。
如果请求路径是 ~/api/zentao/就跳转到第二个location ,我的api接口5001端口的地址去实现导入业务逻辑。
server { listen 80; server_name localhost; location ^~/api/zentao/ { proxy_pass http://192.168.x.x:5001/api/zentao/; } location /{ proxy_pass http://192.168.x.x:82/; } }
好了,以上大家就能看到最终导入效果了!记住重要的一点,修改了nginx配置,记得重启nginx服务。
第四步、解决禅道对nginx反向代理兼容问题
最后还有一个小bug,你会发现,禅道所有的创建任务,创建项目之类的保存按钮失效了。。无语!!!
又找了半天资料和官方资料,原来是禅道 增加了 CSRF 防御代码 与 nginx 的配置不兼容,导致了这个问题,暂时还没有继续深入研究如何配置 nginx 可以达到兼容。
不过在官方的问答区看到了最新版本已经增加了一个 CSRF 的开关: https://www.zentao.net/ask/38485.html
通过在 路径:app/zendao/config/my.php 用户配置文件中,增加一条 $config->framework->filterCSRF = false; 暂时关闭 CSRF 即可解决这个问题。
好了,今天就写到这里了。记录一下我踩的所有坑,避免大家走弯路。
标签:return,string,jq,nginx,result,str,var,console,net From: https://www.cnblogs.com/hzbug/p/17589371.html