package com.kingdee.eas.custom.perfweb.utils; import com.kingdee.eas.custom.integration.base.config.AppConfig; import com.kingdee.eas.custom.integration.base.config.SpringContextHolder; import com.kingdee.eas.custom.integration.base.service.AppConfigService; import com.kingdee.eas.custom.integration.base.service.StateValidator; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.springframework.web.util.UriComponentsBuilder; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; /** *类名称: UrlParamUtil *功能描述: 飞书待办跳转地址工具类 *负责拼接并编码 pcurl / appurl 参数,集中管理域名、入口路径和各业务页面路径 *创建日期: 2026-06-12 *作 者: 青梧 *版 本: 1.0 */ public class UrlParamUtil { private static final Logger logger = Logger.getLogger(UrlParamUtil.class); /** integration 飞书单点入口(相对 SHR 域名) */ private static final String FEISHU_AUTH_PATH = "/qy/auth/feishu"; //===== 基础域名(切换环境时改 BASE_HOST / SHR_HOST 等当前生效变量)===== /** OA 跳转网关前缀 - 正式环境(oAToSHR) */ public static String BASE_HOST_PROD = "http://ehr.shichuangyi.cn:8080/shr/api/oAToSHR?redirect="; /** OA 跳转网关前缀 - 测试环境(oAToSHR) */ public static String BASE_HOST_TEST = "http://10.0.3.24:8080/shr/api/oAToSHR?redirect="; /** 当前生效的 OA 待办网关 */ public static String BASE_HOST = BASE_HOST_PROD; /** SHR 系统地址 - 正式环境 */ public static String SHR_HOST_PROD = "http://ehr.shichuangyi.cn:8080"; /** SHR 系统地址 - 测试环境 */ public static String SHR_HOST_TEST = "http://10.0.3.24:8080"; /** 当前生效的 SHR 地址(dynamic.do 等业务页) */ public static String SHR_HOST = SHR_HOST_PROD; //正式环境 public static String PRODURL = "https://freport.shichuangyi.cn/oa-ofs/prod-api/approve/rest/ofs/ReceiveRequestInfoByJson"; //测试环境 public static String TESTURL = "https://mtest.shichuangyi.cn/prod-approval-api/approve/rest/ofs/ReceiveRequestInfoByJson"; //绩效工作台 - 正式环境 public static String JXGZT_PC_PROD = "http://ehr.shichuangyi.cn:8080/shr/dynamic.do?inFrame=true&fromHeader=true&uipk=com.kingdee.shr.perfweb.app.perfportal.core&serviceId=fUjSqQK1RouZaEJUI2jH5vI9KRA%3D"; public static String JXGZT_BOS_PROD = "http://ehr.shichuangyi.cn:8080/shr/dynamic.do?inFrame=true&fromHeader=true&uipk=com.kingdee.shr.perfweb.app.perfportal.core&serviceId=fUjSqQK1RouZaEJUI2jH5vI9KRA%3D"; //绩效工作台 - 测试环境 public static String JXGZT_PC_TEST = "http://10.0.3.24:8080/shr/dynamic.do?inFrame=true&fromHeader=true&uipk=com.kingdee.shr.perfweb.app.perfportal.core&serviceId=fUjSqQK1RouZaEJUI2jH5vI9KRA%3D"; public static String JXGZT_BOS_TEST = "http://10.0.3.24:8080/shr/dynamic.do?inFrame=true&fromHeader=true&uipk=com.kingdee.shr.perfweb.app.perfportal.core&serviceId=fUjSqQK1RouZaEJUI2jH5vI9KRA%3D"; public static String JXGZT_PC = JXGZT_PC_PROD; public static String JXGZT_BOS = JXGZT_BOS_PROD; //默认组织ID(考核结果汇总列表 hrOrgId) public static String DEFAULT_HR_ORG_ID = "00000000-0000-0000-0000-000000000000CCE7AED4"; /** *考核结果汇总列表页 query 模板。 *动态参数使用 {@code {period}}、{@code {perfPlanId}}、{@code {hrOrgId}} 占位,拼接时需对值做 URL 编码。 */ private static final String EVA_RESULT_MANAGER_LIST_QUERY = "uipk=com.kingdee.shr.perfweb.app.base.evalplan.evaResultManager.list" + "&serviceId=nQafn+gKQ+CngP3xudsk5PI9KRA=" + "&isOrg=0" + "&period={period}" + "&perfPlanId={perfPlanId}" + "&hrOrgId={hrOrgId}"; /** * 绩效考核结果报表列表页 query 模板(飞书待办跳转)。 * periodId 为对应待办的考核周期 ID。 */ private static final String CENTER_PERF_STAT_REPORT_LIST_QUERY = "uipk=com.kingdee.eas.custom.perfweb.handler.CenterPerfStatReport.list" + "&serviceId=eF6u2TdvSbScepkXUeRU1vI9KRA%3D" + "&inFrame=true" + "&periodId={periodId}"; private static final String DEFAULT_CHARSET = "UTF-8"; /** *对字符串进行 URL 编码(UTF-8),用于 query 参数值。 *例如:{@code urlEncode("a=b")} 返回 {@code "a%3Db"} * *@param value 待编码字符串,null 或空串返回空串 *@return 编码后的字符串 */ public static String urlEncode(String value) { if (value == null || value.isEmpty()) { return ""; } try { return URLEncoder.encode(value, DEFAULT_CHARSET); } catch (UnsupportedEncodingException e) { throw new RuntimeException("URL encode failed: " + value, e); } } /** *替换模板中的 {@code {key}} 占位符。 * *@param template 含占位符的模板,如 {@code "...&period={period}&perfPlanId={perfPlanId}"} *@param params key 对应占位符名称(不含花括号),value 为替换值(调用方自行决定是否已编码) *@return 替换后的字符串 */ public static String fillUrlTemplate(String template, Map params) { if (template == null) { return null; } if (params == null || params.isEmpty()) { return template; } String result = template; for (Map.Entry entry : params.entrySet()) { String replacement = entry.getValue() != null ? entry.getValue() : ""; result = result.replace("{" + entry.getKey() + "}", replacement); } return result; } /** *替换模板占位符,并对每个参数值自动做 URL 编码(传入 BOS 原始 ID 即可)。 */ public static String fillUrlTemplateEncoded(String template, Map rawParams) { Map encodedParams = new HashMap<>(); if (rawParams != null) { for (Map.Entry entry : rawParams.entrySet()) { encodedParams.put(entry.getKey(), urlEncode(entry.getValue())); } } return fillUrlTemplate(template, encodedParams); } /** *构建考核结果汇总列表页地址(dynamic.do)。 * *@param periodId 考核周期 ID(summaryInfo.getPeriod().getId()) *@param perfPlanId 考核方案 ID(summaryInfo.getEvaPlan().getId()) *@param hrOrgId 组织 ID,为空时使用 {@link #DEFAULT_HR_ORG_ID} *@return 完整 SHR 页面 URL,未套 OA redirect */ public static String buildEvaResultManagerListUrl(String periodId, String perfPlanId, String hrOrgId) { Map params = new HashMap<>(); params.put("period", periodId); params.put("perfPlanId", perfPlanId); params.put("hrOrgId", resolveHrOrgId(hrOrgId)); String query = fillUrlTemplateEncoded(EVA_RESULT_MANAGER_LIST_QUERY, params); return SHR_HOST + "/shr/dynamic.do?" + query; } /** *构建考核结果汇总列表页地址,hrOrgId 使用 {@link #DEFAULT_HR_ORG_ID}。 */ public static String buildEvaResultManagerListUrl(String periodId, String perfPlanId) { return buildEvaResultManagerListUrl(periodId, perfPlanId, DEFAULT_HR_ORG_ID); } /** * 构建考核结果汇总列表页的 OA 待办跳转地址(oAToSHR,非 Bot 消息)。 */ public static String buildEvaResultManagerListOaUrl(String periodId, String perfPlanId) { return buildOaRedirectUrl(buildEvaResultManagerListUrl(periodId, perfPlanId)); } /** * 构建考核结果汇总列表页的 OA 待办跳转地址(oAToSHR,非 Bot 消息)。 */ public static String buildEvaResultManagerListOaUrl(String periodId, String perfPlanId, String hrOrgId) { return buildOaRedirectUrl(buildEvaResultManagerListUrl(periodId, perfPlanId, hrOrgId)); } /** * 构建绩效考核结果报表列表页地址(dynamic.do)。 * * @param periodId 考核周期 ID */ public static String buildCenterPerfStatReportListUrl(String periodId) { Map params = new HashMap<>(); params.put("periodId", periodId != null ? periodId : ""); String query = fillUrlTemplateEncoded(CENTER_PERF_STAT_REPORT_LIST_QUERY, params); return SHR_HOST + "/shr/dynamic.do?" + query; } /** * 构建绩效考核结果报表列表页的 OA 待办跳转地址(oAToSHR,非 Bot 消息)。 */ public static String buildCenterPerfStatReportListOaUrl(String periodId) { return buildOaRedirectUrl(buildCenterPerfStatReportListUrl(periodId)); } /** * 绩效工作台 Bot 消息跳转地址(integration 单点 + 目标页)。 * 用于:启动考核、单人/批量评分、考核结果公示。 */ public static String buildPerfPortalFeishuMessageUrl() { return buildFeishuMessageUrl(JXGZT_PC); } /** * @deprecated 待办请用 {@link #buildOaRedirectUrl(String)};Bot 消息请用 {@link #buildPerfPortalFeishuMessageUrl()} */ public static String buildPerfPortalOaUrl() { return buildPerfPortalFeishuMessageUrl(); } /** * Bot 消息跳转地址:integration {@code /qy/auth/feishu?state=Base64(redirect|appType)}。 * 仅用于启动考核、单人/批量评分、考核结果公示;OA 待办请用 {@link #buildOaRedirectUrl(String)}。 * * @param targetUrl 登录成功后打开的 HR 业务页完整 URL(未编码,写入 state) */ public static String buildFeishuMessageUrl(String targetUrl) { if (StringUtils.isBlank(targetUrl)) { return ""; } try { StateValidator stateValidator = SpringContextHolder.getBean(StateValidator.class); AppConfigService appConfigService = SpringContextHolder.getBean(AppConfigService.class); AppConfig appConfig = appConfigService.getPrimaryApp("feishu"); if (appConfig == null || StringUtils.isBlank(appConfig.getAppType())) { throw new IllegalStateException("oauth-config 未配置 feishu 主应用 appType"); } String state = stateValidator.generate(targetUrl, appConfig.getAppType()); String url = UriComponentsBuilder.fromUriString(appConfigService.getShrServerName() + FEISHU_AUTH_PATH) .queryParam("state", state) .build(true) .toUriString(); logger.error("integration 单点地址:" +url); return url; } catch (Exception e) { logger.error("integration 单点地址构建失败,回退 oAToSHR,targetUrl=" + targetUrl, e); return buildOaRedirectUrl(targetUrl); } } /** * OA 待办跳转地址:{@code BASE_HOST + urlEncode(targetUrl)}(oAToSHR)。 */ public static String buildOaRedirectUrl(String targetUrl) { if (StringUtils.isBlank(targetUrl)) { return ""; } return BASE_HOST + urlEncode(targetUrl); } private static String resolveHrOrgId(String hrOrgId) { if (hrOrgId == null || hrOrgId.trim().isEmpty()) { return DEFAULT_HR_ORG_ID; } return hrOrgId.trim(); } }