|
|
|
|
@ -1,8 +1,12 @@
|
|
|
|
|
package com.ruoyi.web.controller.login;
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.date.DateUtil;
|
|
|
|
|
import com.google.code.kaptcha.Producer;
|
|
|
|
|
import com.ruoyi.RestResponse;
|
|
|
|
|
import com.ruoyi.basic.domain.AppAgreement;
|
|
|
|
|
import com.ruoyi.basic.domain.YjStore;
|
|
|
|
|
import com.ruoyi.basic.mapper.YjStoreMapper;
|
|
|
|
|
import com.ruoyi.basic.service.AppAgreementService;
|
|
|
|
|
import com.ruoyi.common.config.RuoYiConfig;
|
|
|
|
|
import com.ruoyi.common.constant.CacheConstants;
|
|
|
|
|
import com.ruoyi.common.constant.Constants;
|
|
|
|
|
@ -11,16 +15,27 @@ import com.ruoyi.common.core.redis.RedisCache;
|
|
|
|
|
import com.ruoyi.common.utils.JpushClientUtil;
|
|
|
|
|
import com.ruoyi.common.utils.sign.Base64;
|
|
|
|
|
import com.ruoyi.common.utils.uuid.IdUtils;
|
|
|
|
|
import com.ruoyi.framework.sms.SmsService;
|
|
|
|
|
import com.ruoyi.system.service.ISysConfigService;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
|
|
import org.springframework.http.MediaType;
|
|
|
|
|
import org.springframework.http.ResponseEntity;
|
|
|
|
|
import org.springframework.security.authentication.AuthenticationManager;
|
|
|
|
|
import org.springframework.util.FastByteArrayOutputStream;
|
|
|
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
|
|
import javax.imageio.ImageIO;
|
|
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
|
|
import javax.validation.constraints.Pattern;
|
|
|
|
|
import java.awt.image.BufferedImage;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.io.PrintWriter;
|
|
|
|
|
import java.time.Duration;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
|
|
@ -30,7 +45,7 @@ import java.util.concurrent.TimeUnit;
|
|
|
|
|
* @author ruoyi
|
|
|
|
|
*/
|
|
|
|
|
@RestController
|
|
|
|
|
public class CaptchaController
|
|
|
|
|
public class CaptchaController
|
|
|
|
|
{
|
|
|
|
|
@Resource(name = "captchaProducer")
|
|
|
|
|
private Producer captchaProducer;
|
|
|
|
|
@ -44,6 +59,59 @@ public class CaptchaController
|
|
|
|
|
@Autowired
|
|
|
|
|
private ISysConfigService configService;
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
private SmsService smsService;
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
private RedisTemplate<String, String> redisTemplate; // 注入 StringRedisTemplate
|
|
|
|
|
|
|
|
|
|
private static final String SMS_CODE_PREFIX = "sms_code:";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 发送短信验证码
|
|
|
|
|
*/
|
|
|
|
|
@PostMapping("/sms/sendCode")
|
|
|
|
|
public AjaxResult sendSmsCode(@RequestBody Map<String,String> map ) {
|
|
|
|
|
String phone=map.get("phone");
|
|
|
|
|
// 1. 验证手机号格式(可交由前端或参数校验)
|
|
|
|
|
if (!phone.matches("^1[3-9]\\d{9}$")) {
|
|
|
|
|
return AjaxResult.error("手机号格式不正确");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 限流检查:60秒内同一手机号只能发送一次(原子操作)
|
|
|
|
|
String limitKey = CacheConstants.SMS_SEND_LIMIT_KEY + phone;
|
|
|
|
|
// 尝试设置key,如果已存在则返回false,说明60秒内已发送过
|
|
|
|
|
Boolean absent = redisTemplate.opsForValue()
|
|
|
|
|
.setIfAbsent(limitKey, "1", Duration.ofSeconds(60));
|
|
|
|
|
if (Boolean.FALSE.equals(absent)) {
|
|
|
|
|
// 获取剩余时间(可选,用于提示)
|
|
|
|
|
Long expire = redisTemplate.getExpire(limitKey, TimeUnit.SECONDS);
|
|
|
|
|
String msg = expire != null && expire > 0
|
|
|
|
|
? "操作过于频繁,请" + expire + "秒后再试"
|
|
|
|
|
: "操作过于频繁,请稍后再试";
|
|
|
|
|
return AjaxResult.error(msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 生成6位随机验证码
|
|
|
|
|
String code = String.format("%06d", new Random().nextInt(999999));
|
|
|
|
|
// 2. 存入Redis,有效期1分钟
|
|
|
|
|
String codeKey = CacheConstants.SMS_CODE_KEY + phone;
|
|
|
|
|
redisCache.setCacheObject(codeKey, code, 1, TimeUnit.MINUTES);
|
|
|
|
|
// redisTemplate.opsForValue().set(codeKey, code, Duration.ofMinutes(1));
|
|
|
|
|
// 3. 发送短信
|
|
|
|
|
try {
|
|
|
|
|
smsService.sendSmsCode(phone, code);
|
|
|
|
|
return AjaxResult.success("验证码发送成功");
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
// 发送失败:删除验证码(避免无效验证码被使用)
|
|
|
|
|
redisTemplate.delete(codeKey);
|
|
|
|
|
// 删除限流key,允许用户立即重试
|
|
|
|
|
redisTemplate.delete(limitKey);
|
|
|
|
|
return AjaxResult.error(e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成验证码
|
|
|
|
|
@ -97,7 +165,333 @@ public class CaptchaController
|
|
|
|
|
ajax.put("img", Base64.encode(os.toByteArray()));
|
|
|
|
|
return ajax;
|
|
|
|
|
}
|
|
|
|
|
@Autowired
|
|
|
|
|
private AppAgreementService appAgreementService;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取协议
|
|
|
|
|
* @param type 1-用户服务协议,2-隐私政策
|
|
|
|
|
* @return 协议内容
|
|
|
|
|
*/
|
|
|
|
|
@GetMapping(value ="/api/termsOfService", produces = MediaType.TEXT_HTML_VALUE)
|
|
|
|
|
public ResponseEntity<String> agreementOfService(HttpServletResponse response) throws IOException {
|
|
|
|
|
AppAgreement agreement = appAgreementService.getLatestByType(1);
|
|
|
|
|
String html = buildHtmlPage(agreement, agreement.getTitle());
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
|
|
|
|
|
return new ResponseEntity<>(html, headers, HttpStatus.OK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*隐私政策
|
|
|
|
|
* @param response
|
|
|
|
|
* @return
|
|
|
|
|
* @throws IOException
|
|
|
|
|
*/
|
|
|
|
|
@GetMapping(value ="/api/privacyPolicy", produces = MediaType.TEXT_HTML_VALUE)
|
|
|
|
|
public ResponseEntity<String> privacyPolicy(HttpServletResponse response) throws IOException {
|
|
|
|
|
AppAgreement agreement = appAgreementService.getLatestByType(2);
|
|
|
|
|
String html = buildHtmlPage(agreement, agreement.getTitle());
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
|
|
|
|
|
return new ResponseEntity<>(html, headers, HttpStatus.OK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 技术支持
|
|
|
|
|
* @param response
|
|
|
|
|
* @return
|
|
|
|
|
* @throws IOException
|
|
|
|
|
*/
|
|
|
|
|
@GetMapping(value ="/api/technicalSupport", produces = MediaType.TEXT_HTML_VALUE)
|
|
|
|
|
public ResponseEntity<String> technicalSupport(HttpServletResponse response) throws IOException {
|
|
|
|
|
AppAgreement agreement = appAgreementService.getLatestByType(3);
|
|
|
|
|
String html = buildHtmlPage(agreement, agreement.getTitle());
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
|
|
|
|
|
return new ResponseEntity<>(html, headers, HttpStatus.OK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 技术支持
|
|
|
|
|
* @param response
|
|
|
|
|
* @return
|
|
|
|
|
* @throws IOException
|
|
|
|
|
*/
|
|
|
|
|
@GetMapping(value ="/api/marketing", produces = MediaType.TEXT_HTML_VALUE)
|
|
|
|
|
public ResponseEntity<String> marketing(HttpServletResponse response) throws IOException {
|
|
|
|
|
AppAgreement agreement = appAgreementService.getLatestByType(5);
|
|
|
|
|
String html = buildHtmlPage(agreement, agreement.getTitle());
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
|
|
|
|
|
return new ResponseEntity<>(html, headers, HttpStatus.OK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 公司信息
|
|
|
|
|
* @param response
|
|
|
|
|
* @return
|
|
|
|
|
* @throws IOException
|
|
|
|
|
*/
|
|
|
|
|
@GetMapping(value ="/api/companyInformation", produces = MediaType.TEXT_HTML_VALUE)
|
|
|
|
|
public ResponseEntity<String> companyInformation(HttpServletResponse response) throws IOException {
|
|
|
|
|
AppAgreement agreement = appAgreementService.getLatestByType(4);
|
|
|
|
|
String html = buildHtmlPage(agreement, agreement.getTitle());
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
headers.setContentType(MediaType.valueOf("text/html; charset=UTF-8"));
|
|
|
|
|
return new ResponseEntity<>(html, headers, HttpStatus.OK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建完整 HTML 页面
|
|
|
|
|
*/
|
|
|
|
|
private String buildHtmlPage(AppAgreement agreement, String title) {
|
|
|
|
|
String content = agreement != null ? agreement.getContent() : "<p>协议内容暂未发布,请联系客服。</p>";
|
|
|
|
|
String updateDate = agreement != null ? DateUtil.format(agreement.getEffectiveDate(), "yyyy-MM-dd") : "";
|
|
|
|
|
Integer type = agreement != null ? agreement.getType() : 0;
|
|
|
|
|
|
|
|
|
|
// 判断是否为官网页面(type=5),使用更宽的容器
|
|
|
|
|
boolean isCompanyPage = type == 4;
|
|
|
|
|
|
|
|
|
|
return "<!DOCTYPE html>\n" +
|
|
|
|
|
"<html lang=\"zh-CN\">\n" +
|
|
|
|
|
"<head>\n" +
|
|
|
|
|
" <meta charset=\"UTF-8\">\n" +
|
|
|
|
|
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes\">\n" +
|
|
|
|
|
" <title>" + escapeHtml(title) + "</title>\n" +
|
|
|
|
|
" <style>\n" +
|
|
|
|
|
" * { margin: 0; padding: 0; box-sizing: border-box; }\n" +
|
|
|
|
|
" html, body {\n" +
|
|
|
|
|
" width: 100%;\n" +
|
|
|
|
|
" overflow-x: hidden;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" body {\n" +
|
|
|
|
|
" font-family: -apple-system, BlinkMacSystemFont, \"PingFang SC\", \"Helvetica Neue\", Arial, sans-serif;\n" +
|
|
|
|
|
" background-color: #f5f7fa;\n" +
|
|
|
|
|
" padding: 20px 12px 40px;\n" +
|
|
|
|
|
" color: #333;\n" +
|
|
|
|
|
" line-height: 1.8;\n" +
|
|
|
|
|
" font-size: 16px;\n" +
|
|
|
|
|
" max-width: 100%;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box {\n" +
|
|
|
|
|
" max-width: " + (isCompanyPage ? "1200px" : "820px") + ";\n" +
|
|
|
|
|
" width: 100%;\n" +
|
|
|
|
|
" margin: 0 auto;\n" +
|
|
|
|
|
" background: #ffffff;\n" +
|
|
|
|
|
" border-radius: 12px;\n" +
|
|
|
|
|
" padding: " + (isCompanyPage ? "16px 12px" : "28px 22px") + ";\n" +
|
|
|
|
|
" box-shadow: 0 2px 12px rgba(0,0,0,0.06);\n" +
|
|
|
|
|
" overflow: hidden;\n" +
|
|
|
|
|
" word-wrap: break-word;\n" +
|
|
|
|
|
" word-break: break-word;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" /* 如果 content 内部有独立的 html 结构(如公司官网),则重置内部样式 */\n" +
|
|
|
|
|
" .agreement-box .page-wrapper {\n" +
|
|
|
|
|
" width: 100%;\n" +
|
|
|
|
|
" max-width: 100%;\n" +
|
|
|
|
|
" overflow: hidden;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box h1 {\n" +
|
|
|
|
|
" font-size: 22px;\n" +
|
|
|
|
|
" text-align: center;\n" +
|
|
|
|
|
" margin-bottom: 12px;\n" +
|
|
|
|
|
" font-weight: 600;\n" +
|
|
|
|
|
" color: #1a1a1a;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .update-date {\n" +
|
|
|
|
|
" text-align: center;\n" +
|
|
|
|
|
" color: #999;\n" +
|
|
|
|
|
" font-size: 14px;\n" +
|
|
|
|
|
" margin-bottom: 20px;\n" +
|
|
|
|
|
" border-bottom: 1px solid #eee;\n" +
|
|
|
|
|
" padding-bottom: 12px;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box h2 {\n" +
|
|
|
|
|
" font-size: 20px;\n" +
|
|
|
|
|
" margin-top: 24px;\n" +
|
|
|
|
|
" margin-bottom: 12px;\n" +
|
|
|
|
|
" font-weight: 600;\n" +
|
|
|
|
|
" color: #1a3a5c;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box h3 {\n" +
|
|
|
|
|
" font-size: 18px;\n" +
|
|
|
|
|
" margin-top: 20px;\n" +
|
|
|
|
|
" margin-bottom: 10px;\n" +
|
|
|
|
|
" font-weight: 600;\n" +
|
|
|
|
|
" color: #1a3a5c;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box h4 {\n" +
|
|
|
|
|
" font-size: 16px;\n" +
|
|
|
|
|
" margin-top: 16px;\n" +
|
|
|
|
|
" margin-bottom: 8px;\n" +
|
|
|
|
|
" font-weight: 600;\n" +
|
|
|
|
|
" color: #2a6a9c;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box p {\n" +
|
|
|
|
|
" margin-bottom: 10px;\n" +
|
|
|
|
|
" text-align: justify;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box ul, .agreement-box ol {\n" +
|
|
|
|
|
" padding-left: 22px;\n" +
|
|
|
|
|
" margin-bottom: 12px;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box ul li, .agreement-box ol li {\n" +
|
|
|
|
|
" margin-bottom: 6px;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box strong {\n" +
|
|
|
|
|
" color: #1a1a1a;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box a {\n" +
|
|
|
|
|
" color: #2b7cff;\n" +
|
|
|
|
|
" text-decoration: none;\n" +
|
|
|
|
|
" word-break: break-all;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box a:hover {\n" +
|
|
|
|
|
" text-decoration: underline;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" /* 图片自适应 */\n" +
|
|
|
|
|
" .agreement-box img {\n" +
|
|
|
|
|
" max-width: 100%;\n" +
|
|
|
|
|
" height: auto;\n" +
|
|
|
|
|
" display: block;\n" +
|
|
|
|
|
" margin: 10px 0;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" /* 表格自适应 */\n" +
|
|
|
|
|
" .agreement-box table {\n" +
|
|
|
|
|
" width: 100%;\n" +
|
|
|
|
|
" border-collapse: collapse;\n" +
|
|
|
|
|
" margin: 12px 0;\n" +
|
|
|
|
|
" font-size: 14px;\n" +
|
|
|
|
|
" display: block;\n" +
|
|
|
|
|
" overflow-x: auto;\n" +
|
|
|
|
|
" -webkit-overflow-scrolling: touch;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box table thead,\n" +
|
|
|
|
|
" .agreement-box table tbody,\n" +
|
|
|
|
|
" .agreement-box table tr {\n" +
|
|
|
|
|
" display: table;\n" +
|
|
|
|
|
" width: 100%;\n" +
|
|
|
|
|
" table-layout: fixed;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box table th,\n" +
|
|
|
|
|
" .agreement-box table td {\n" +
|
|
|
|
|
" padding: 8px 10px;\n" +
|
|
|
|
|
" border: 1px solid #ddd;\n" +
|
|
|
|
|
" text-align: left;\n" +
|
|
|
|
|
" word-break: break-word;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box table th {\n" +
|
|
|
|
|
" background: #f5f7fa;\n" +
|
|
|
|
|
" font-weight: 600;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" /* 卡片和网格容器自适应(用于公司官网) */\n" +
|
|
|
|
|
" .agreement-box .grid-2,\n" +
|
|
|
|
|
" .agreement-box .grid-3 {\n" +
|
|
|
|
|
" display: grid;\n" +
|
|
|
|
|
" gap: 16px;\n" +
|
|
|
|
|
" width: 100%;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .grid-2 { grid-template-columns: 1fr 1fr; }\n" +
|
|
|
|
|
" .agreement-box .grid-3 { grid-template-columns: 1fr 1fr 1fr; }\n" +
|
|
|
|
|
" .agreement-box .banner {\n" +
|
|
|
|
|
" width: 100%;\n" +
|
|
|
|
|
" overflow: hidden;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .partner-grid {\n" +
|
|
|
|
|
" display: grid;\n" +
|
|
|
|
|
" grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));\n" +
|
|
|
|
|
" gap: 12px;\n" +
|
|
|
|
|
" width: 100%;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .stat-item {\n" +
|
|
|
|
|
" background: #f8fafc;\n" +
|
|
|
|
|
" border-radius: 8px;\n" +
|
|
|
|
|
" padding: 14px 12px;\n" +
|
|
|
|
|
" text-align: center;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .stat-item .num {\n" +
|
|
|
|
|
" font-size: 24px;\n" +
|
|
|
|
|
" font-weight: 700;\n" +
|
|
|
|
|
" color: #1a3a5c;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .stat-item .label {\n" +
|
|
|
|
|
" font-size: 13px;\n" +
|
|
|
|
|
" color: #888;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .badge {\n" +
|
|
|
|
|
" display: inline-block;\n" +
|
|
|
|
|
" background: #e8f0fe;\n" +
|
|
|
|
|
" color: #1a3a5c;\n" +
|
|
|
|
|
" border-radius: 16px;\n" +
|
|
|
|
|
" padding: 2px 14px;\n" +
|
|
|
|
|
" font-size: 13px;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .badge-blue {\n" +
|
|
|
|
|
" background: #1a3a5c;\n" +
|
|
|
|
|
" color: #fff;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .tag {\n" +
|
|
|
|
|
" display: inline-block;\n" +
|
|
|
|
|
" background: rgba(26, 58, 92, 0.12);\n" +
|
|
|
|
|
" border-radius: 20px;\n" +
|
|
|
|
|
" padding: 2px 16px;\n" +
|
|
|
|
|
" font-size: 13px;\n" +
|
|
|
|
|
" margin: 2px 4px;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" .agreement-box .footer {\n" +
|
|
|
|
|
" margin-top: 24px;\n" +
|
|
|
|
|
" border-top: 1px solid #eee;\n" +
|
|
|
|
|
" padding-top: 16px;\n" +
|
|
|
|
|
" font-size: 14px;\n" +
|
|
|
|
|
" color: #999;\n" +
|
|
|
|
|
" text-align: center;\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" /* 移动端适配 */\n" +
|
|
|
|
|
" @media (max-width: 768px) {\n" +
|
|
|
|
|
" body { padding: 12px 8px 30px; font-size: 15px; }\n" +
|
|
|
|
|
" .agreement-box { padding: 16px 12px !important; }\n" +
|
|
|
|
|
" .agreement-box .grid-2,\n" +
|
|
|
|
|
" .agreement-box .grid-3 { grid-template-columns: 1fr !important; }\n" +
|
|
|
|
|
" .agreement-box h1 { font-size: 20px; }\n" +
|
|
|
|
|
" .agreement-box h2 { font-size: 18px; }\n" +
|
|
|
|
|
" .agreement-box h3 { font-size: 16px; }\n" +
|
|
|
|
|
" .agreement-box .partner-grid { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); }\n" +
|
|
|
|
|
" .agreement-box table { font-size: 12px; }\n" +
|
|
|
|
|
" .agreement-box table th,\n" +
|
|
|
|
|
" .agreement-box table td { padding: 5px 6px; }\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" @media (max-width: 480px) {\n" +
|
|
|
|
|
" body { padding: 8px 4px 20px; font-size: 14px; }\n" +
|
|
|
|
|
" .agreement-box { padding: 12px 8px !important; border-radius: 8px; }\n" +
|
|
|
|
|
" .agreement-box .partner-grid { grid-template-columns: 1fr 1fr; }\n" +
|
|
|
|
|
" }\n" +
|
|
|
|
|
" </style>\n" +
|
|
|
|
|
"</head>\n" +
|
|
|
|
|
"<body>\n" +
|
|
|
|
|
" <div class=\"agreement-box\">\n" +
|
|
|
|
|
(isCompanyPage ? "" : "<h1>" + escapeHtml(title) + "</h1>\n") +
|
|
|
|
|
(agreement != null && !isCompanyPage ? "<div class=\"update-date\">更新日期:" + updateDate + " 生效日期:" + updateDate + "</div>" : "") +
|
|
|
|
|
" <div class=\"" + (isCompanyPage ? "page-wrapper" : "") + "\">\n" +
|
|
|
|
|
content + "\n" +
|
|
|
|
|
" </div>\n" +
|
|
|
|
|
(!isCompanyPage ? "<div class=\"footer\">" + (agreement != null ? agreement.getVersion() : "") + "</div>" : "") +
|
|
|
|
|
" </div>\n" +
|
|
|
|
|
"</body>\n" +
|
|
|
|
|
"</html>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 简单的 HTML 转义,防止 XSS
|
|
|
|
|
*/
|
|
|
|
|
private String escapeHtml(String text) {
|
|
|
|
|
if (text == null) return "";
|
|
|
|
|
return text.replace("&", "&")
|
|
|
|
|
.replace("<", "<")
|
|
|
|
|
.replace(">", ">")
|
|
|
|
|
.replace("\"", """)
|
|
|
|
|
.replace("'", "'");
|
|
|
|
|
}
|
|
|
|
|
public void testBindAlias() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|