diff --git a/pom.xml b/pom.xml
index f8f25a2..0a29829 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,6 +29,11 @@
commons-lang
2.6
+
+ commons-codec
+ commons-codec
+ 1.10
+
org.eclipse.jetty
jetty-util
diff --git a/src/main/java/com/aliyun/api/gateway/demo/Request.java b/src/main/java/com/aliyun/api/gateway/demo/Request.java
index b89bfa4..25a44db 100644
--- a/src/main/java/com/aliyun/api/gateway/demo/Request.java
+++ b/src/main/java/com/aliyun/api/gateway/demo/Request.java
@@ -20,6 +20,7 @@
import com.aliyun.api.gateway.demo.enums.Method;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -195,4 +196,7 @@ public List getSignHeaderPrefixList() {
public void setSignHeaderPrefixList(List signHeaderPrefixList) {
this.signHeaderPrefixList = signHeaderPrefixList;
}
+ public void setSignHeaderPrefixList(String signHeaderPrefixList) {
+ this.signHeaderPrefixList = Arrays.asList(signHeaderPrefixList.split(","));
+ }
}
diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfig.java b/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfig.java
new file mode 100644
index 0000000..d42ee3e
--- /dev/null
+++ b/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfig.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.aliyun.api.gateway.demo.service;
+
+/**
+ * Jira 配置类
+ */
+public class JiraConfig {
+ /**
+ * Jira 服务器地址(例如:https://your-domain.atlassian.net)
+ */
+ private String jiraBaseUrl;
+
+ /**
+ * Jira 用户邮箱
+ */
+ private String username;
+
+ /**
+ * Jira API Token
+ */
+ private String apiToken;
+
+ /**
+ * 请求超时时间(毫秒)
+ */
+ private int timeout;
+
+ public JiraConfig() {
+ this.timeout = 30000; // 默认30秒
+ }
+
+ public JiraConfig(String jiraBaseUrl, String username, String apiToken) {
+ this.jiraBaseUrl = jiraBaseUrl;
+ this.username = username;
+ this.apiToken = apiToken;
+ this.timeout = 30000;
+ }
+
+ public JiraConfig(String jiraBaseUrl, String username, String apiToken, int timeout) {
+ this.jiraBaseUrl = jiraBaseUrl;
+ this.username = username;
+ this.apiToken = apiToken;
+ this.timeout = timeout;
+ }
+
+ public String getJiraBaseUrl() {
+ return jiraBaseUrl;
+ }
+
+ public void setJiraBaseUrl(String jiraBaseUrl) {
+ this.jiraBaseUrl = jiraBaseUrl;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getApiToken() {
+ return apiToken;
+ }
+
+ public void setApiToken(String apiToken) {
+ this.apiToken = apiToken;
+ }
+
+ public int getTimeout() {
+ return timeout;
+ }
+
+ public void setTimeout(int timeout) {
+ this.timeout = timeout;
+ }
+}
+
diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfigLoader.java b/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfigLoader.java
new file mode 100644
index 0000000..3e653f9
--- /dev/null
+++ b/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfigLoader.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.aliyun.api.gateway.demo.service;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * Jira 配置加载器
+ * 从配置文件或环境变量中加载 Jira 配置
+ */
+public class JiraConfigLoader {
+
+ private static final String DEFAULT_CONFIG_FILE = "jira-config.properties";
+
+ /**
+ * 从默认配置文件加载配置
+ * @return JiraConfig 对象
+ * @throws IOException
+ */
+ public static JiraConfig loadFromFile() throws IOException {
+ return loadFromFile(DEFAULT_CONFIG_FILE);
+ }
+
+ /**
+ * 从指定配置文件加载配置
+ * @param configFile 配置文件路径
+ * @return JiraConfig 对象
+ * @throws IOException
+ */
+ public static JiraConfig loadFromFile(String configFile) throws IOException {
+ Properties props = new Properties();
+
+ // 尝试从文件系统加载
+ try (InputStream input = new FileInputStream(configFile)) {
+ props.load(input);
+ } catch (IOException e) {
+ // 尝试从 classpath 加载
+ try (InputStream input = JiraConfigLoader.class.getClassLoader()
+ .getResourceAsStream(configFile)) {
+ if (input == null) {
+ throw new IOException("无法找到配置文件: " + configFile);
+ }
+ props.load(input);
+ }
+ }
+
+ return createConfigFromProperties(props);
+ }
+
+ /**
+ * 从环境变量加载配置
+ * @return JiraConfig 对象
+ */
+ public static JiraConfig loadFromEnv() {
+ String baseUrl = System.getenv("JIRA_BASE_URL");
+ String username = System.getenv("JIRA_USERNAME");
+ String apiToken = System.getenv("JIRA_API_TOKEN");
+ String timeoutStr = System.getenv("JIRA_TIMEOUT");
+
+ if (baseUrl == null || username == null || apiToken == null) {
+ throw new IllegalStateException(
+ "缺少必要的环境变量: JIRA_BASE_URL, JIRA_USERNAME, JIRA_API_TOKEN"
+ );
+ }
+
+ int timeout = 30000; // 默认30秒
+ if (timeoutStr != null && !timeoutStr.isEmpty()) {
+ try {
+ timeout = Integer.parseInt(timeoutStr);
+ } catch (NumberFormatException e) {
+ System.err.println("无效的超时时间配置,使用默认值: 30000ms");
+ }
+ }
+
+ return new JiraConfig(baseUrl, username, apiToken, timeout);
+ }
+
+ /**
+ * 从 Properties 对象创建 JiraConfig
+ * @param props Properties 对象
+ * @return JiraConfig 对象
+ */
+ private static JiraConfig createConfigFromProperties(Properties props) {
+ String baseUrl = props.getProperty("jira.base.url");
+ String username = props.getProperty("jira.username");
+ String apiToken = props.getProperty("jira.api.token");
+ String timeoutStr = props.getProperty("jira.timeout", "30000");
+
+ if (baseUrl == null || username == null || apiToken == null) {
+ throw new IllegalStateException(
+ "配置文件缺少必要的属性: jira.base.url, jira.username, jira.api.token"
+ );
+ }
+
+ int timeout = 30000;
+ try {
+ timeout = Integer.parseInt(timeoutStr);
+ } catch (NumberFormatException e) {
+ System.err.println("无效的超时时间配置,使用默认值: 30000ms");
+ }
+
+ return new JiraConfig(baseUrl, username, apiToken, timeout);
+ }
+
+ /**
+ * 优先从环境变量加载,如果失败则从配置文件加载
+ * @return JiraConfig 对象
+ * @throws IOException
+ */
+ public static JiraConfig load() throws IOException {
+ try {
+ return loadFromEnv();
+ } catch (IllegalStateException e) {
+ System.out.println("从环境变量加载失败,尝试从配置文件加载...");
+ return loadFromFile();
+ }
+ }
+}
+
diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/JiraService.java b/src/main/java/com/aliyun/api/gateway/demo/service/JiraService.java
new file mode 100644
index 0000000..f4e65f6
--- /dev/null
+++ b/src/main/java/com/aliyun/api/gateway/demo/service/JiraService.java
@@ -0,0 +1,323 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.aliyun.api.gateway.demo.service;
+
+import com.alibaba.fastjson.JSON;
+import com.aliyun.api.gateway.demo.service.dto.JiraIssue;
+import com.aliyun.api.gateway.demo.service.dto.JiraIssueCreateRequest;
+import com.aliyun.api.gateway.demo.service.dto.JiraIssueUpdateRequest;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.*;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.util.EntityUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Jira API 服务类
+ * 提供与 Jira REST API 交互的方法
+ */
+public class JiraService {
+
+ private final JiraConfig config;
+ private final String authHeader;
+
+ private static final String API_VERSION = "/rest/api/3";
+
+ /**
+ * 构造函数
+ * @param config Jira配置对象
+ */
+ public JiraService(JiraConfig config) {
+ this.config = config;
+ // 创建 Basic Auth 认证头
+ String auth = config.getUsername() + ":" + config.getApiToken();
+ byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.UTF_8));
+ this.authHeader = "Basic " + new String(encodedAuth);
+ }
+
+ /**
+ * 创建 Issue
+ * @param request 创建 Issue 的请求对象
+ * @return 创建的 Issue 信息
+ * @throws Exception
+ */
+ public JiraIssue createIssue(JiraIssueCreateRequest request) throws Exception {
+ String url = config.getJiraBaseUrl() + API_VERSION + "/issue";
+ String jsonBody = JSON.toJSONString(request);
+
+ String response = executePost(url, jsonBody);
+ return JSON.parseObject(response, JiraIssue.class);
+ }
+
+ /**
+ * 获取 Issue 详情
+ * @param issueIdOrKey Issue ID 或 Key(例如:PROJ-123)
+ * @return Issue 详情
+ * @throws Exception
+ */
+ public JiraIssue getIssue(String issueIdOrKey) throws Exception {
+ String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey;
+ String response = executeGet(url);
+ return JSON.parseObject(response, JiraIssue.class);
+ }
+
+ /**
+ * 更新 Issue
+ * @param issueIdOrKey Issue ID 或 Key
+ * @param request 更新请求对象
+ * @return 是否更新成功
+ * @throws Exception
+ */
+ public boolean updateIssue(String issueIdOrKey, JiraIssueUpdateRequest request) throws Exception {
+ String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey;
+ String jsonBody = JSON.toJSONString(request);
+
+ executePut(url, jsonBody);
+ return true;
+ }
+
+ /**
+ * 删除 Issue
+ * @param issueIdOrKey Issue ID 或 Key
+ * @return 是否删除成功
+ * @throws Exception
+ */
+ public boolean deleteIssue(String issueIdOrKey) throws Exception {
+ String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey;
+ executeDelete(url);
+ return true;
+ }
+
+ /**
+ * 搜索 Issues
+ * @param jql JQL 查询语句
+ * @param maxResults 最大返回结果数
+ * @return 搜索结果的 JSON 字符串
+ * @throws Exception
+ */
+ public String searchIssues(String jql, int maxResults) throws Exception {
+ String url = config.getJiraBaseUrl() + API_VERSION + "/search";
+
+ Map requestBody = new HashMap<>();
+ requestBody.put("jql", jql);
+ requestBody.put("maxResults", maxResults);
+
+ String jsonBody = JSON.toJSONString(requestBody);
+ return executePost(url, jsonBody);
+ }
+
+ /**
+ * 为 Issue 添加评论
+ * @param issueIdOrKey Issue ID 或 Key
+ * @param comment 评论内容
+ * @return 评论结果的 JSON 字符串
+ * @throws Exception
+ */
+ public String addComment(String issueIdOrKey, String comment) throws Exception {
+ String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey + "/comment";
+
+ Map requestBody = new HashMap<>();
+ Map body = new HashMap<>();
+ body.put("type", "doc");
+ body.put("version", 1);
+
+ Map content = new HashMap<>();
+ content.put("type", "paragraph");
+
+ Map text = new HashMap<>();
+ text.put("type", "text");
+ text.put("text", comment);
+
+ content.put("content", new Object[]{text});
+ body.put("content", new Object[]{content});
+
+ requestBody.put("body", body);
+
+ String jsonBody = JSON.toJSONString(requestBody);
+ return executePost(url, jsonBody);
+ }
+
+ /**
+ * 获取所有项目
+ * @return 项目列表的 JSON 字符串
+ * @throws Exception
+ */
+ public String getAllProjects() throws Exception {
+ String url = config.getJiraBaseUrl() + API_VERSION + "/project";
+ return executeGet(url);
+ }
+
+ /**
+ * 获取项目详情
+ * @param projectIdOrKey 项目 ID 或 Key
+ * @return 项目详情的 JSON 字符串
+ * @throws Exception
+ */
+ public String getProject(String projectIdOrKey) throws Exception {
+ String url = config.getJiraBaseUrl() + API_VERSION + "/project/" + projectIdOrKey;
+ return executeGet(url);
+ }
+
+ /**
+ * 转换 Issue 状态
+ * @param issueIdOrKey Issue ID 或 Key
+ * @param transitionId 转换 ID
+ * @return 是否转换成功
+ * @throws Exception
+ */
+ public boolean transitionIssue(String issueIdOrKey, String transitionId) throws Exception {
+ String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey + "/transitions";
+
+ Map requestBody = new HashMap<>();
+ Map transition = new HashMap<>();
+ transition.put("id", transitionId);
+ requestBody.put("transition", transition);
+
+ String jsonBody = JSON.toJSONString(requestBody);
+ executePost(url, jsonBody);
+ return true;
+ }
+
+ /**
+ * 执行 GET 请求
+ */
+ private String executeGet(String url) throws Exception {
+ HttpClient httpClient = new DefaultHttpClient();
+ HttpGet httpGet = new HttpGet(url);
+
+ // 设置请求头
+ httpGet.setHeader("Authorization", authHeader);
+ httpGet.setHeader("Content-Type", "application/json");
+ httpGet.setHeader("Accept", "application/json");
+
+ try {
+ HttpResponse response = httpClient.execute(httpGet);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+
+ if (statusCode >= 200 && statusCode < 300) {
+ return responseBody;
+ } else {
+ throw new Exception("Jira API Error: " + statusCode + " - " + responseBody);
+ }
+ } finally {
+ httpClient.getConnectionManager().shutdown();
+ }
+ }
+
+ /**
+ * 执行 POST 请求
+ */
+ private String executePost(String url, String jsonBody) throws Exception {
+ HttpClient httpClient = new DefaultHttpClient();
+ HttpPost httpPost = new HttpPost(url);
+
+ // 设置请求头
+ httpPost.setHeader("Authorization", authHeader);
+ httpPost.setHeader("Content-Type", "application/json");
+ httpPost.setHeader("Accept", "application/json");
+
+ // 设置请求体
+ if (jsonBody != null && !jsonBody.isEmpty()) {
+ StringEntity entity = new StringEntity(jsonBody, "UTF-8");
+ httpPost.setEntity(entity);
+ }
+
+ try {
+ HttpResponse response = httpClient.execute(httpPost);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+
+ if (statusCode >= 200 && statusCode < 300) {
+ return responseBody;
+ } else {
+ throw new Exception("Jira API Error: " + statusCode + " - " + responseBody);
+ }
+ } finally {
+ httpClient.getConnectionManager().shutdown();
+ }
+ }
+
+ /**
+ * 执行 PUT 请求
+ */
+ private String executePut(String url, String jsonBody) throws Exception {
+ HttpClient httpClient = new DefaultHttpClient();
+ HttpPut httpPut = new HttpPut(url);
+
+ // 设置请求头
+ httpPut.setHeader("Authorization", authHeader);
+ httpPut.setHeader("Content-Type", "application/json");
+ httpPut.setHeader("Accept", "application/json");
+
+ // 设置请求体
+ if (jsonBody != null && !jsonBody.isEmpty()) {
+ StringEntity entity = new StringEntity(jsonBody, "UTF-8");
+ httpPut.setEntity(entity);
+ }
+
+ try {
+ HttpResponse response = httpClient.execute(httpPut);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+
+ if (statusCode >= 200 && statusCode < 300) {
+ return responseBody;
+ } else {
+ throw new Exception("Jira API Error: " + statusCode + " - " + responseBody);
+ }
+ } finally {
+ httpClient.getConnectionManager().shutdown();
+ }
+ }
+
+ /**
+ * 执行 DELETE 请求
+ */
+ private String executeDelete(String url) throws Exception {
+ HttpClient httpClient = new DefaultHttpClient();
+ HttpDelete httpDelete = new HttpDelete(url);
+
+ // 设置请求头
+ httpDelete.setHeader("Authorization", authHeader);
+ httpDelete.setHeader("Content-Type", "application/json");
+ httpDelete.setHeader("Accept", "application/json");
+
+ try {
+ HttpResponse response = httpClient.execute(httpDelete);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+
+ if (statusCode >= 200 && statusCode < 300) {
+ return responseBody;
+ } else {
+ throw new Exception("Jira API Error: " + statusCode + " - " + responseBody);
+ }
+ } finally {
+ httpClient.getConnectionManager().shutdown();
+ }
+ }
+}
+
diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssue.java b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssue.java
new file mode 100644
index 0000000..204a985
--- /dev/null
+++ b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssue.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.aliyun.api.gateway.demo.service.dto;
+
+import java.util.Map;
+
+/**
+ * Jira Issue 响应对象
+ */
+public class JiraIssue {
+ /**
+ * Issue ID
+ */
+ private String id;
+
+ /**
+ * Issue Key (例如:PROJ-123)
+ */
+ private String key;
+
+ /**
+ * Issue 的 URL
+ */
+ private String self;
+
+ /**
+ * Issue 的字段信息
+ */
+ private Map fields;
+
+ public JiraIssue() {
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getSelf() {
+ return self;
+ }
+
+ public void setSelf(String self) {
+ this.self = self;
+ }
+
+ public Map getFields() {
+ return fields;
+ }
+
+ public void setFields(Map fields) {
+ this.fields = fields;
+ }
+
+ @Override
+ public String toString() {
+ return "JiraIssue{" +
+ "id='" + id + '\'' +
+ ", key='" + key + '\'' +
+ ", self='" + self + '\'' +
+ ", fields=" + fields +
+ '}';
+ }
+}
+
diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueCreateRequest.java b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueCreateRequest.java
new file mode 100644
index 0000000..a1e0c74
--- /dev/null
+++ b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueCreateRequest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.aliyun.api.gateway.demo.service.dto;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Jira Issue 创建请求对象
+ */
+public class JiraIssueCreateRequest {
+
+ private Map fields;
+
+ public JiraIssueCreateRequest() {
+ this.fields = new HashMap<>();
+ }
+
+ /**
+ * 设置项目
+ * @param projectKey 项目 Key
+ */
+ public void setProject(String projectKey) {
+ Map project = new HashMap<>();
+ project.put("key", projectKey);
+ fields.put("project", project);
+ }
+
+ /**
+ * 设置标题
+ * @param summary 标题
+ */
+ public void setSummary(String summary) {
+ fields.put("summary", summary);
+ }
+
+ /**
+ * 设置描述
+ * @param description 描述内容
+ */
+ public void setDescription(String description) {
+ // Jira Cloud 使用 Atlassian Document Format
+ Map descDoc = new HashMap<>();
+ descDoc.put("type", "doc");
+ descDoc.put("version", 1);
+
+ Map paragraph = new HashMap<>();
+ paragraph.put("type", "paragraph");
+
+ Map text = new HashMap<>();
+ text.put("type", "text");
+ text.put("text", description);
+
+ paragraph.put("content", new Object[]{text});
+ descDoc.put("content", new Object[]{paragraph});
+
+ fields.put("description", descDoc);
+ }
+
+ /**
+ * 设置 Issue 类型
+ * @param issueTypeName Issue 类型名称(例如:Task, Bug, Story)
+ */
+ public void setIssueType(String issueTypeName) {
+ Map issueType = new HashMap<>();
+ issueType.put("name", issueTypeName);
+ fields.put("issuetype", issueType);
+ }
+
+ /**
+ * 设置优先级
+ * @param priorityName 优先级名称(例如:High, Medium, Low)
+ */
+ public void setPriority(String priorityName) {
+ Map priority = new HashMap<>();
+ priority.put("name", priorityName);
+ fields.put("priority", priority);
+ }
+
+ /**
+ * 设置经办人
+ * @param accountId 用户账号 ID
+ */
+ public void setAssignee(String accountId) {
+ Map assignee = new HashMap<>();
+ assignee.put("accountId", accountId);
+ fields.put("assignee", assignee);
+ }
+
+ /**
+ * 设置标签
+ * @param labels 标签数组
+ */
+ public void setLabels(String[] labels) {
+ fields.put("labels", labels);
+ }
+
+ /**
+ * 添加自定义字段
+ * @param fieldName 字段名称
+ * @param value 字段值
+ */
+ public void addCustomField(String fieldName, Object value) {
+ fields.put(fieldName, value);
+ }
+
+ public Map getFields() {
+ return fields;
+ }
+
+ public void setFields(Map fields) {
+ this.fields = fields;
+ }
+}
+
diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueUpdateRequest.java b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueUpdateRequest.java
new file mode 100644
index 0000000..21816d9
--- /dev/null
+++ b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueUpdateRequest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.aliyun.api.gateway.demo.service.dto;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Jira Issue 更新请求对象
+ */
+public class JiraIssueUpdateRequest {
+
+ private Map fields;
+
+ public JiraIssueUpdateRequest() {
+ this.fields = new HashMap<>();
+ }
+
+ /**
+ * 更新标题
+ * @param summary 新标题
+ */
+ public void setSummary(String summary) {
+ fields.put("summary", summary);
+ }
+
+ /**
+ * 更新描述
+ * @param description 新描述
+ */
+ public void setDescription(String description) {
+ // Jira Cloud 使用 Atlassian Document Format
+ Map descDoc = new HashMap<>();
+ descDoc.put("type", "doc");
+ descDoc.put("version", 1);
+
+ Map paragraph = new HashMap<>();
+ paragraph.put("type", "paragraph");
+
+ Map text = new HashMap<>();
+ text.put("type", "text");
+ text.put("text", description);
+
+ paragraph.put("content", new Object[]{text});
+ descDoc.put("content", new Object[]{paragraph});
+
+ fields.put("description", descDoc);
+ }
+
+ /**
+ * 更新优先级
+ * @param priorityName 优先级名称
+ */
+ public void setPriority(String priorityName) {
+ Map priority = new HashMap<>();
+ priority.put("name", priorityName);
+ fields.put("priority", priority);
+ }
+
+ /**
+ * 更新经办人
+ * @param accountId 用户账号 ID
+ */
+ public void setAssignee(String accountId) {
+ Map assignee = new HashMap<>();
+ assignee.put("accountId", accountId);
+ fields.put("assignee", assignee);
+ }
+
+ /**
+ * 更新标签
+ * @param labels 标签数组
+ */
+ public void setLabels(String[] labels) {
+ fields.put("labels", labels);
+ }
+
+ /**
+ * 添加自定义字段
+ * @param fieldName 字段名称
+ * @param value 字段值
+ */
+ public void addCustomField(String fieldName, Object value) {
+ fields.put(fieldName, value);
+ }
+
+ public Map getFields() {
+ return fields;
+ }
+
+ public void setFields(Map fields) {
+ this.fields = fields;
+ }
+}
+
diff --git a/src/main/java/com/aliyun/api/gateway/demo/util/SignUtil.java b/src/main/java/com/aliyun/api/gateway/demo/util/SignUtil.java
index b2beb0e..a5f33cf 100644
--- a/src/main/java/com/aliyun/api/gateway/demo/util/SignUtil.java
+++ b/src/main/java/com/aliyun/api/gateway/demo/util/SignUtil.java
@@ -1,37 +1,79 @@
package com.aliyun.api.gateway.demo.util;
-import java.util.*;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
+
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
+
import com.aliyun.api.gateway.demo.constant.Constants;
import com.aliyun.api.gateway.demo.constant.HttpHeader;
import com.aliyun.api.gateway.demo.constant.SystemHeader;
+/**
+ * 签名工具类
+ * 用于生成阿里云API网关请求签名
+ */
public class SignUtil {
+ private SignUtil() {
+ // 工具类,禁止实例化
+ throw new IllegalStateException("Utility class");
+ }
+
+ /**
+ * 生成请求签名
+ *
+ * @param secret 密钥
+ * @param method HTTP方法
+ * @param path 请求路径
+ * @param headers 请求头
+ * @param querys 查询参数
+ * @param bodys 请求体参数
+ * @param signHeaderPrefixList 需要签名的请求头前缀列表
+ * @return Base64编码的签名字符串
+ * @throws RuntimeException 签名生成失败时抛出
+ */
public static String sign(String secret, String method, String path,
- Map headers,
- Map querys,
- Map bodys,
- List signHeaderPrefixList) {
+ Map headers,
+ Map querys,
+ Map bodys,
+ List signHeaderPrefixList) {
try {
Mac hmacSha256 = Mac.getInstance(Constants.HMAC_SHA256);
- hmacSha256.init(new SecretKeySpec(secret.getBytes(Constants.ENCODING), Constants.HMAC_SHA256));
+ byte[] keyBytes = secret.getBytes(Constants.ENCODING);
+ SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, Constants.HMAC_SHA256);
+ hmacSha256.init(secretKeySpec);
String stringToSign = buildStringToSign(method, path, headers, querys, bodys, signHeaderPrefixList);
- return Base64.encodeBase64String(hmacSha256.doFinal(stringToSign.getBytes(Constants.ENCODING)));
+ byte[] signBytes = hmacSha256.doFinal(stringToSign.getBytes(Constants.ENCODING));
+ return Base64.encodeBase64String(signBytes);
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new RuntimeException("Error while generating signature: " + e.getMessage(), e);
} catch (Exception e) {
- throw new RuntimeException("Error while generating signature", e);
+ throw new RuntimeException("Unexpected error during signature generation", e);
}
}
+ /**
+ * 构建待签名字符串
+ */
private static String buildStringToSign(String method, String path,
- Map headers,
- Map querys,
- Map bodys,
- List signHeaderPrefixList) {
+ Map headers,
+ Map querys,
+ Map bodys,
+ List signHeaderPrefixList) {
StringBuilder sb = new StringBuilder();
sb.append(method.toUpperCase()).append(Constants.LF);
@@ -44,40 +86,69 @@ private static String buildStringToSign(String method, String path,
.append(Constants.LF)
.append(getHeaderOrEmpty(headers, HttpHeader.HTTP_HEADER_DATE))
.append(Constants.LF);
+ } else {
+ // headers为null时,添加4个换行符
+ sb.append(Constants.LF).append(Constants.LF)
+ .append(Constants.LF).append(Constants.LF);
}
+
sb.append(buildHeaders(headers, signHeaderPrefixList))
.append(buildResource(path, querys, bodys));
return sb.toString();
}
+ /**
+ * 构建资源路径字符串(包含查询参数和body参数)
+ */
private static String buildResource(String path, Map querys, Map bodys) {
StringBuilder sb = new StringBuilder(path != null ? path : "");
Map sortMap = new TreeMap<>();
- if (querys != null) sortMap.putAll(querys);
- if (bodys != null) sortMap.putAll(bodys);
+ if (querys != null) {
+ sortMap.putAll(querys);
+ }
+ if (bodys != null) {
+ sortMap.putAll(bodys);
+ }
+
+ if (sortMap.isEmpty()) {
+ return sb.toString();
+ }
String paramString = sortMap.entrySet().stream()
.filter(entry -> !StringUtils.isBlank(entry.getKey()))
- .map(entry -> entry.getKey() + (StringUtils.isBlank(entry.getValue()) ? "" : Constants.SPE4 + entry.getValue()))
- .reduce((a, b) -> a + Constants.SPE3 + b)
- .orElse("");
+ .map(entry -> {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ return StringUtils.isBlank(value) ? key : key + Constants.SPE4 + value;
+ })
+ .collect(Collectors.joining(Constants.SPE3));
if (!paramString.isEmpty()) {
sb.append(Constants.SPE5).append(paramString);
}
+
return sb.toString();
}
+ /**
+ * 构建需要签名的请求头字符串
+ */
private static String buildHeaders(Map headers, List signHeaderPrefixList) {
if (headers == null || signHeaderPrefixList == null) {
return "";
}
+ // 过滤掉不需要的请求头前缀
List filteredPrefixes = new ArrayList<>(signHeaderPrefixList);
- filteredPrefixes.removeAll(Arrays.asList(SystemHeader.X_CA_SIGNATURE, HttpHeader.HTTP_HEADER_ACCEPT,
- HttpHeader.HTTP_HEADER_CONTENT_MD5, HttpHeader.HTTP_HEADER_CONTENT_TYPE, HttpHeader.HTTP_HEADER_DATE));
+ filteredPrefixes.removeAll(Arrays.asList(
+ SystemHeader.X_CA_SIGNATURE,
+ HttpHeader.HTTP_HEADER_ACCEPT,
+ HttpHeader.HTTP_HEADER_CONTENT_MD5,
+ HttpHeader.HTTP_HEADER_CONTENT_TYPE,
+ HttpHeader.HTTP_HEADER_DATE
+ ));
Collections.sort(filteredPrefixes);
Map sortedHeaders = new TreeMap<>(headers);
@@ -85,76 +156,42 @@ private static String buildHeaders(Map headers, List sig
StringBuilder signHeaders = new StringBuilder();
for (Map.Entry entry : sortedHeaders.entrySet()) {
- if (isHeaderToSign(entry.getKey(), filteredPrefixes)) {
- sb.append(entry.getKey()).append(Constants.SPE2).append(entry.getValue() == null ? "" : entry.getValue()).append(Constants.LF);
- if (signHeaders.length() > 0) signHeaders.append(Constants.SPE1);
- signHeaders.append(entry.getKey());
+ String headerName = entry.getKey();
+ if (isHeaderToSign(headerName, filteredPrefixes)) {
+ String headerValue = entry.getValue() != null ? entry.getValue() : "";
+ sb.append(headerName)
+ .append(Constants.SPE2)
+ .append(headerValue)
+ .append(Constants.LF);
+
+ if (signHeaders.length() > 0) {
+ signHeaders.append(Constants.SPE1);
+ }
+ signHeaders.append(headerName);
}
}
+
headers.put(SystemHeader.X_CA_SIGNATURE_HEADERS, signHeaders.toString());
return sb.toString();
}
+ /**
+ * 判断请求头是否需要参与签名
+ */
private static boolean isHeaderToSign(String headerName, List signHeaderPrefixList) {
- return !StringUtils.isBlank(headerName) &&
- (headerName.startsWith(Constants.CA_HEADER_TO_SIGN_PREFIX_SYSTEM) ||
- signHeaderPrefixList.stream().anyMatch(headerName::equalsIgnoreCase));
- }
-
- private static String getHeaderOrEmpty(Map headers, String key) {
- return headers.getOrDefault(key, "");
- }
-
- public static void ArrayIndexOutOfBoundsExample(String[] args) {
- String[] array = { "Apple", "Banana", "Cherry" };
- System.out.println(array[3]); // ArrayIndexOutOfBoundsException
- }
-
- public static void NullPointerExceptionExample(String[] args) {
- String str = null;
- System.out.println(str.length()); // NullPointerException
- }
-
- public static void InfiniteLoopExample(String[] args) {
- int count = 0;
- while (count >= 0) { // Infinite loop
- System.out.println("Looping...");
- count++;
+ if (StringUtils.isBlank(headerName)) {
+ return false;
}
- }
- public static void MemoryLeakExample(String[] args) {
- List list = new ArrayList<>();
- while (true) {
- list.add("A new object");
- }
+ // 系统请求头或匹配前缀列表的请求头需要签名
+ return headerName.startsWith(Constants.CA_HEADER_TO_SIGN_PREFIX_SYSTEM) ||
+ signHeaderPrefixList.stream().anyMatch(headerName::equalsIgnoreCase);
}
- public static void WrongThreadPoolUsageExample(String[] args) {
- // ❌ 错误用法:使用 Executors.newCachedThreadPool()
- // 该线程池会无限创建线程,在高并发场景下容易 OOM
- ExecutorService executor = Executors.newCachedThreadPool();
-
- // ❌ 提交过多任务,任务中还有阻塞操作
- for (int i = 0; i < 100000; i++) {
- final int taskId = i;
- executor.submit(() -> {
- try {
- // 模拟长时间阻塞
- Thread.sleep(10000);
- System.out.println("Task " + taskId + " finished.");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
+ /**
+ * 获取请求头的值,不存在时返回空字符串
+ */
+ private static String getHeaderOrEmpty(Map headers, String key) {
+ return headers.getOrDefault(key, "");
}
-
- // ❌ 忘记调用 shutdown() 或 shutdownNow()
- // 线程池将一直运行,导致进程无法正常退出
- // executor.shutdown();
-
- // ❌ 在主线程中直接退出可能导致部分任务丢失
- System.out.println("Main thread finished, but thread pool still running...");
-}
-
}
\ No newline at end of file