.net+jq+nginx反向代理实现禅道批量导入功能

发布时间 2023-07-29 10:28:31作者: 涵容之旅

需求来源:因为禅道免费版不包含批量导入任务功能,如果要使用的话,需要购买禅道官方的插件。(就是不想花钱,嘿嘿),于是花了一天时间研究如何自己二开。

首先呢,禅道是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 即可解决这个问题。

   

   好了,今天就写到这里了。记录一下我踩的所有坑,避免大家走弯路。