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<String, Object> detail(String type) {
|
if (!List.of("user", "privacy").contains(type)) throw new BusinessException(ErrorCode.BAD_REQUEST, "Unsupported agreement type");
|
List<Map<String, Object>> 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<String, Object> row = rows.get(0);
|
Map<String, Object> 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<String, Object> unavailable(String type, String message) {
|
Map<String, Object> 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 不认 <s>/<strike>/<u>,换成它支持的 <del>/<ins>,后台设的样式才能生效。
|
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;
|
}
|
}
|