| | |
| | | package cn.iocoder.yudao.module.wxcp.controller.admin.activity; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; |
| | | import cn.iocoder.yudao.module.wxcp.controller.admin.activity.vo.WxCpSignupPublicReqVO; |
| | | import cn.iocoder.yudao.module.wxcp.dal.dataobject.activity.WxCpActivityDO; |
| | | import cn.iocoder.yudao.module.wxcp.enums.activity.WxCpActivityStatusEnum; |
| | |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.net.URI; |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.List; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | |
| | |
| | | .append("body{margin:0;padding:16px;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;") |
| | | .append("background:#f5f6f8;color:#1f2329;font-size:15px;line-height:1.6}") |
| | | .append(".card{background:#fff;border-radius:12px;padding:18px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.06)}") |
| | | .append(".cover{display:block;width:100%;max-height:260px;object-fit:cover;border-radius:8px;margin-bottom:14px;background:#f2f3f5}") |
| | | .append("h1{font-size:19px;margin:0 0 14px;font-weight:600;word-break:break-all}") |
| | | .append(".meta{color:#646a73;font-size:14px}") |
| | | .append(".meta div{margin-bottom:6px;display:flex}") |
| | |
| | | .append("</style></head><body>"); |
| | | |
| | | // 活动信息 |
| | | sb.append("<div class=\"card\"><h1>").append(esc(activity.getTitle())).append("</h1>") |
| | | sb.append("<div class=\"card\">"); |
| | | // 封面图:来自附件中间表(record_type = wxcp_activity),业务表不存文件地址 |
| | | String coverUrl = resolveCoverUrl(activity.getId()); |
| | | if (StrUtil.isNotBlank(coverUrl)) { |
| | | sb.append("<img class=\"cover\" src=\"").append(esc(coverUrl)).append("\" alt=\"活动封面\">"); |
| | | } |
| | | sb.append("<h1>").append(esc(activity.getTitle())).append("</h1>") |
| | | .append("<div class=\"meta\">"); |
| | | String timeText = formatTimeRange(activity.getStartTime(), activity.getEndTime()); |
| | | if (StrUtil.isNotBlank(timeText)) { |
| | |
| | | } |
| | | sb.append("</div>"); |
| | | |
| | | // 报名表单 |
| | | // 报名表单:姓名、手机号均为选填,只填了手机号才校验格式 |
| | | sb.append("<div class=\"card\">") |
| | | .append("<label>姓名 <span style=\"color:#f53f3f\">*</span></label>") |
| | | .append("<input id=\"name\" type=\"text\" placeholder=\"请输入姓名\" maxlength=\"64\" autocomplete=\"name\">") |
| | | .append("<label>手机号 <span style=\"color:#f53f3f\">*</span></label>") |
| | | .append("<input id=\"mobile\" type=\"tel\" inputmode=\"numeric\" placeholder=\"请输入 11 位手机号\" maxlength=\"11\" autocomplete=\"tel\">") |
| | | .append("<label>姓名</label>") |
| | | .append("<input id=\"name\" type=\"text\" placeholder=\"请输入姓名(选填)\" maxlength=\"64\" autocomplete=\"name\">") |
| | | .append("<label>手机号</label>") |
| | | .append("<input id=\"mobile\" type=\"tel\" inputmode=\"numeric\" placeholder=\"请输入 11 位手机号(选填)\" maxlength=\"11\" autocomplete=\"tel\">") |
| | | .append("<label>参与说明</label>") |
| | | .append("<textarea id=\"content\" placeholder=\"可填写备注、需求或其他说明(选填)\" maxlength=\"1000\"></textarea>") |
| | | .append("<label>图片 / 附件</label>") |
| | |
| | | .append("var n=document.getElementById('name').value.trim();") |
| | | .append("var m=document.getElementById('mobile').value.trim();") |
| | | .append("var c=document.getElementById('content').value.trim();") |
| | | .append("if(!n){tip('请填写姓名');return}") |
| | | .append("if(!/^1[3-9]\\d{9}$/.test(m)){tip('手机号格式不正确');return}") |
| | | .append("if(m&&!/^1[3-9]\\d{9}$/.test(m)){tip('手机号格式不正确,请填 11 位手机号或留空');return}") |
| | | .append("btn.disabled=true;") |
| | | .append("var fs=fileInput.files;") |
| | | .append("var upload=Promise.resolve([]);") |
| | |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 取活动封面地址(取附件列表里的第一张) |
| | | */ |
| | | private String resolveCoverUrl(Long activityId) { |
| | | List<SystemStorageBlobRespVO> attachments = activityService.getActivityAttachments(activityId); |
| | | if (CollUtil.isEmpty(attachments)) { |
| | | return null; |
| | | } |
| | | SystemStorageBlobRespVO cover = attachments.get(0); |
| | | String raw = StrUtil.blankToDefault(cover.getPreviewURL(), cover.getUrl()); |
| | | if (StrUtil.isBlank(raw)) { |
| | | return null; |
| | | } |
| | | return toSameOriginUrl(raw); |
| | | } |
| | | |
| | | /** |
| | | * 把附件地址转成同源相对地址 |
| | | * |
| | | * 附件里存的是带签名的绝对地址,域名取自 yudao.storage.domain(通常是内网地址)。 |
| | | * 而手机扫码是通过公网域名打开本页面的,直连内网地址会加载不出图片。 |
| | | * 这里剥掉协议与域名只留 /admin-api/... 路径,交给同源代理转发, |
| | | * 内网、公网访问都能正常显示;因为只取路径不取域名,也不存在跳转到外部站点的风险。 |
| | | */ |
| | | private String toSameOriginUrl(String url) { |
| | | String trimmed = url.trim(); |
| | | if (!StrUtil.startWithAnyIgnoreCase(trimmed, "http://", "https://")) { |
| | | return trimmed; |
| | | } |
| | | try { |
| | | URI uri = URI.create(trimmed); |
| | | String path = uri.getRawPath(); |
| | | if (StrUtil.isBlank(path)) { |
| | | return trimmed; |
| | | } |
| | | String query = uri.getRawQuery(); |
| | | return StrUtil.isBlank(query) ? path : path + "?" + query; |
| | | } catch (IllegalArgumentException e) { |
| | | // 地址不合规就原样透出,不能因为一张封面图让整页打不开 |
| | | log.warn("活动封面地址解析失败,按原样输出:{}", trimmed); |
| | | return trimmed; |
| | | } |
| | | } |
| | | |
| | | private String renderMessagePage(String title, String desc) { |
| | | return "<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\">" |
| | | + "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" |