package com.talentcard.server.business.agreement; import java.time.LocalDate; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.talentcard.server.business.DatabaseRows; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.safety.Safelist; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class MerchantAgreementService { private final JdbcTemplate jdbc; public MerchantAgreementService(JdbcTemplate jdbc) { this.jdbc = jdbc; } @Transactional(readOnly = true) public Map detail(String type) { if (!List.of("user", "privacy").contains(type)) throw new BusinessException(ErrorCode.BAD_REQUEST, "Unsupported agreement type"); List> rows = DatabaseRows.normalize(jdbc.queryForList(""" select agreement_type, title, version, effective_date, valid_until, content_html, status from merchant_agreement where agreement_type = ? limit 1 """, type)); if (rows.isEmpty()) return unavailable(type, "Agreement is unavailable"); Map row = rows.get(0); Map result = new LinkedHashMap<>(); result.put("type", row.get("agreementType")); result.put("title", row.get("title")); result.put("version", row.get("version")); result.put("effectiveDate", row.get("effectiveDate")); LocalDate validUntil = row.get("validUntil") instanceof LocalDate date ? date : null; if (((Number) row.getOrDefault("status", 0)).intValue() != 1 || (validUntil != null && validUntil.isBefore(LocalDate.now()))) { result.put("state", "UNAVAILABLE"); result.put("message", "Agreement is unavailable"); result.put("contentHtml", ""); return result; } result.put("state", "ACTIVE"); result.put("message", ""); result.put("contentHtml", sanitize(String.valueOf(row.get("contentHtml")))); return result; } private Map unavailable(String type, String message) { Map result = new LinkedHashMap<>(); result.put("type", type); result.put("title", "Agreement"); result.put("version", ""); result.put("effectiveDate", ""); result.put("state", "UNAVAILABLE"); result.put("message", message); result.put("contentHtml", ""); return result; } private String sanitize(String html) { // 图片 src 不走 Jsoup 的协议白名单:/uploads/xxx.png 这种相对地址会因为"不是 http/https" // 被整条删掉,商户端就拿不到图片。这里手动挡掉危险协议。 Safelist safelist = new Safelist() .addTags("a", "abbr", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "del", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "li", "ol", "p", "pre", "q", "small", "span", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", "video", "source") .addAttributes(":all", "style", "class") .addAttributes("a", "href", "title") .addAttributes("img", "src", "alt", "title", "width", "height") .addAttributes("video", "src", "controls", "poster", "width", "height") .addAttributes("source", "src", "type") .addProtocols("a", "href", "http", "https"); Document source = Jsoup.parseBodyFragment(html == null ? "" : html); source.outputSettings().prettyPrint(false); // 小程序 rich-text 不认 //,换成它支持的 /,后台设的样式才能生效。 source.select("s, strike").forEach(node -> node.tagName("del")); source.select("u").forEach(node -> node.tagName("ins")); source.select("img[src]").forEach(image -> { String src = image.attr("src").trim().toLowerCase(java.util.Locale.ROOT); if (src.startsWith("javascript:") || src.startsWith("vbscript:")) image.removeAttr("src"); }); Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); Document document = Jsoup.parseBodyFragment(Jsoup.clean(source.body().html(), "", safelist, outputSettings)); document.outputSettings().prettyPrint(false); document.select("img").forEach(image -> image.attr("style", appendResponsiveStyle(image.attr("style"), "max-width:100%;height:auto;"))); document.select("video").forEach(video -> video.attr("style", appendResponsiveStyle(video.attr("style"), "max-width:100%;height:auto;"))); return document.body().html(); } private String appendResponsiveStyle(String style, String responsiveStyle) { if (style == null || style.isBlank()) return responsiveStyle; return style.endsWith(";") ? style + responsiveStyle : style + ";" + responsiveStyle; } }