JIRA插件用户专属数据存储与REST API访问最优方案咨询
我之前做过类似的Jira插件开发需求,给你梳理下完全符合要求的最优实现方案——不用标签包裹输入字段,纯靠AJS调用REST API来实现用户专属数据的存储和展示:
第一步:后端REST API与数据持久化实现
首先得搭建后端的存储和查询接口,这里推荐用Jira官方支持的Active Objects做数据持久化,不用自己写SQL,还能和Jira的插件生命周期完美兼容。
1. 定义数据实体类
创建一个AO实体类,用来映射存储用户-工单的专属数据:
import net.java.ao.Entity; import net.java.ao.Preload; import net.java.ao.schema.StringLength; import net.java.ao.schema.Unique; @Preload public interface UserIssueData extends Entity { // 当前用户的key @Unique(on = {"USER_KEY", "ISSUE_KEY"}) String getUserKey(); void setUserKey(String userKey); // 对应的工单key String getIssueKey(); void setIssueKey(String issueKey); // 要存储的用户信息(示例字段,可按需扩展) @StringLength(StringLength.UNLIMITED) String getUserInfo1(); void setUserInfo1(String userInfo1); String getUserInfo2(); void setUserInfo2(String userInfo2); }
2. 开发REST端点
创建REST资源类,提供保存和查询数据的接口,同时做权限校验,确保只有当前用户能访问自己的数据:
import com.atlassian.jira.component.ComponentAccessor; import com.atlassian.jira.user.ApplicationUser; import net.java.ao.DBParam; import net.java.ao.Query; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.HashMap; import java.util.Map; @Path("/user-issue-data") public class UserIssueDataResource { private final UserIssueDataDao userIssueDataDao; // 构造函数注入AO的DAO public UserIssueDataResource(UserIssueDataDao userIssueDataDao) { this.userIssueDataDao = userIssueDataDao; } // 保存用户数据 @POST @Path("/save") @Consumes(MediaType.APPLICATION_JSON) public Response saveUserData(Map<String, String> payload) { ApplicationUser currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser(); if (currentUser == null) { return Response.status(Response.Status.UNAUTHORIZED).build(); } String issueKey = payload.get("issueKey"); String userInfo1 = payload.get("userInfo1"); String userInfo2 = payload.get("userInfo2"); // 先查询是否已有该用户-工单的数据,有则更新,无则创建 UserIssueData existingData = userIssueDataDao.findUnique( Query.select() .where("USER_KEY = ? AND ISSUE_KEY = ?", currentUser.getKey(), issueKey) ); if (existingData != null) { existingData.setUserInfo1(userInfo1); existingData.setUserInfo2(userInfo2); existingData.save(); } else { userIssueDataDao.create( UserIssueData.class, new DBParam("USER_KEY", currentUser.getKey()), new DBParam("ISSUE_KEY", issueKey), new DBParam("USER_INFO1", userInfo1), new DBParam("USER_INFO2", userInfo2) ); } return Response.ok().build(); } // 查询当前用户对应工单的数据 @GET @Path("/get/{issueKey}") @Produces(MediaType.APPLICATION_JSON) public Response getUserData(@PathParam("issueKey") String issueKey) { ApplicationUser currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser(); if (currentUser == null) { return Response.status(Response.Status.UNAUTHORIZED).build(); } UserIssueData data = userIssueDataDao.findUnique( Query.select() .where("USER_KEY = ? AND ISSUE_KEY = ?", currentUser.getKey(), issueKey) ); Map<String, String> result = new HashMap<>(); if (data != null) { result.put("userInfo1", data.getUserInfo1()); result.put("userInfo2", data.getUserInfo2()); } return Response.ok(result).build(); } }
3. 在插件配置中注册REST和AO
在atlassian-plugin.xml里添加以下配置:
<!-- 注册AO实体 --> <ao key="user-issue-data-ao" class="com.your.plugin.package.UserIssueData"/> <!-- 注册REST资源 --> <rest-resource key="user-issue-data-rest" path="/user-issue-data" class="com.your.plugin.package.UserIssueDataResource"> <description>REST endpoints for user-specific issue data</description> </rest-resource>
第二步:前端右侧面板与AJS交互实现
接下来要在工单查看页面右侧添加自定义面板,用AJS动态渲染输入字段并调用REST API。
1. 注册Web Panel
在atlassian-plugin.xml里定义右侧面板的位置:
<web-panel key="user-issue-data-panel" location="viewissue.right.context" weight="100"> <description>User-specific data panel on issue view page</description> <label>我的专属数据</label> <resource name="view" type="download" location="/js/user-issue-panel.js"/> </web-panel>
2. 编写AJS前端代码
创建/js/user-issue-panel.js,用AJS动态创建输入字段、绑定保存事件,并在页面加载时查询已有数据:
AJS.toInit(function() { // 获取当前工单的key(从页面DOM中提取) const issueKey = AJS.$('#key-val').text().trim(); const panelContainer = AJS.$('<div id="user-issue-data-container" class="aui-panel-content"></div>'); // 动态创建输入字段和按钮 const input1 = AJS.$('<input type="text" id="user-info-1" placeholder="输入信息1" class="aui-field-input">'); const input2 = AJS.$('<input type="text" id="user-info-2" placeholder="输入信息2" class="aui-field-input">'); const saveBtn = AJS.$('<button id="save-user-data" class="aui-button aui-button-primary">保存</button>'); const displayArea = AJS.$('<div id="saved-data-display" class="aui-message aui-message-success" style="display:none;margin-top:10px;"></div>'); // 组装面板内容 panelContainer.append( AJS.$('<div class="aui-field"><label>我的专属信息</label></div>'), input1, AJS.$('<br><br>'), input2, AJS.$('<br><br>'), saveBtn, displayArea ); // 替换默认面板内容 AJS.$('#user-issue-data-panel .aui-panel-content').replaceWith(panelContainer); // 加载已保存的数据 function loadSavedData() { AJS.request('/rest/your-plugin-key/1.0/user-issue-data/get/' + issueKey, { method: 'GET', contentType: 'application/json', success: function(response) { const data = JSON.parse(response.responseText); if (data.userInfo1) input1.val(data.userInfo1); if (data.userInfo2) input2.val(data.userInfo2); updateDisplayArea(data); }, error: function() { AJS.messages.error('#user-issue-data-container', {body: '加载数据失败,请稍后重试'}); } }); } // 更新展示区域 function updateDisplayArea(data) { if (data.userInfo1 || data.userInfo2) { displayArea.html(`已保存信息:<br>信息1:${data.userInfo1 || '未填写'}<br>信息2:${data.userInfo2 || '未填写'}`).show(); } else { displayArea.hide(); } } // 绑定保存按钮事件 saveBtn.click(function() { const payload = { issueKey: issueKey, userInfo1: input1.val().trim(), userInfo2: input2.val().trim() }; AJS.request('/rest/your-plugin-key/1.0/user-issue-data/save', { method: 'POST', contentType: 'application/json', data: JSON.stringify(payload), success: function() { AJS.messages.success('#user-issue-data-container', {body: '数据保存成功'}); updateDisplayArea(payload); }, error: function() { AJS.messages.error('#user-issue-data-container', {body: '保存失败,请稍后重试'}); } }); }); // 页面初始化时加载数据 loadSavedData(); });
关键注意事项
- 替换代码中的
com.your.plugin.package和your-plugin-key为你自己的插件包路径和插件key; - 权限控制:后端已经校验了当前登录用户,确保用户只能访问自己的数据;
- 错误处理:前端添加了AJS的消息提示,提升用户体验;
- 数据唯一性:AO实体里用
@Unique注解确保每个用户-工单组合只有一条数据,避免重复存储。
按照这个流程实现,完全不需要用标签包裹输入字段,全程通过AJS动态渲染和调用REST API完成,是Jira插件开发中这类需求的标准最优方案。
内容的提问来源于stack exchange,提问作者Imamudin Naseem




