package cn.iocoder.yudao.module.wxcp.controller.admin.activity; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; 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 cn.iocoder.yudao.module.wxcp.service.activity.WxCpActivityService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** * 企业微信活动 - 报名落地页与报名接口(免登录) * * 说明:这两个接口刻意放在 controller.admin 包下,从而带上 /admin-api 前缀。 * 原因是前端 Vite 的开发代理只转发 /admin-api,若放到 /app-api, * 扫码后请求会打到前端静态服务返回 404,报名页打不开。 * * @author 超级管理员 */ @Tag(name = "用户 App - 企业微信活动报名") @RestController @RequestMapping("/wxcp/activity") @Validated @Slf4j public class WxCpActivityPublicController { private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); @Resource private WxCpActivityService activityService; @GetMapping(value = "/signup-page", produces = MediaType.TEXT_HTML_VALUE) @PermitAll @Operation(summary = "活动报名页(HTML)", description = "二维码指向此页面,企业微信成员扫码后打开") @Parameter(name = "id", description = "活动编号", required = true) public String signupPage(@RequestParam("id") Long id, @RequestParam(value = "wxUserId", required = false) String wxUserId) { WxCpActivityDO activity = activityService.getActivity(id); if (activity == null) { return renderMessagePage("活动不存在", "活动可能已被删除,请联系活动发起人确认。"); } if (!isSignupOpen(activity)) { return renderMessagePage("报名已结束", "该活动当前不可报名(可能未发布、已结束或不在报名时间内)。"); } return renderSignupPage(activity, wxUserId); } @PostMapping("/signup") @PermitAll @Operation(summary = "提交活动报名", description = "免登录;有企业微信身份时按成员账号去重,否则按手机号去重") public CommonResult signup(@Valid @RequestBody WxCpSignupPublicReqVO reqVO) { return success(activityService.createSignup(reqVO)); } /** * 报名是否开放:活动需为已发布,且处于报名时间窗口内 */ private boolean isSignupOpen(WxCpActivityDO activity) { if (!WxCpActivityStatusEnum.PUBLISHED.getStatus().equals(activity.getStatus())) { return false; } LocalDateTime now = LocalDateTime.now(); if (activity.getSignupStartTime() != null && now.isBefore(activity.getSignupStartTime())) { return false; } return activity.getSignupEndTime() == null || !now.isAfter(activity.getSignupEndTime()); } private String renderSignupPage(WxCpActivityDO activity, String wxUserId) { StringBuilder sb = new StringBuilder(2048); sb.append("") .append("") .append("").append(esc(activity.getTitle())).append("") .append(""); // 活动信息 sb.append("

").append(esc(activity.getTitle())).append("

") .append("
"); String timeText = formatTimeRange(activity.getStartTime(), activity.getEndTime()); if (StrUtil.isNotBlank(timeText)) { sb.append("
活动时间").append(esc(timeText)).append("
"); } if (StrUtil.isNotBlank(activity.getLocation())) { sb.append("
活动地点").append(esc(activity.getLocation())).append("
"); } String signupText = formatTimeRange(activity.getSignupStartTime(), activity.getSignupEndTime()); if (StrUtil.isNotBlank(signupText)) { sb.append("
报名时间").append(esc(signupText)).append("
"); } sb.append("
已报名").append(activity.getSignupCount() != null ? activity.getSignupCount() : 0); if (activity.getSignupLimit() != null && activity.getSignupLimit() > 0) { sb.append(" / ").append(activity.getSignupLimit()).append(" 人"); } else { sb.append(" 人(不限名额)"); } sb.append("
"); if (StrUtil.isNotBlank(activity.getContent())) { sb.append("
").append(esc(activity.getContent())).append("
"); } sb.append("
"); // 报名表单 sb.append("
") .append("") .append("") .append("") .append("") .append("") .append("") .append("") .append("
") .append("") .append("") .append("
") .append("") .append("
提交后如需修改,请联系活动发起人
") .append("
"); // 提交逻辑:先把文件传到 system 的公共上传接口拿到 blobId,再带上 blobId 提交报名 sb.append(""); return sb.toString(); } private String renderMessagePage(String title, String desc) { return "" + "" + "" + esc(title) + "

" + esc(title) + "

" + esc(desc) + "

"; } private String formatTimeRange(LocalDateTime start, LocalDateTime end) { if (start == null && end == null) { return ""; } if (start == null) { return "截止 " + end.format(TIME_FMT); } if (end == null) { return start.format(TIME_FMT) + " 起"; } return start.format(TIME_FMT) + " ~ " + end.format(TIME_FMT); } /** * HTML 转义,避免活动标题/内容里的特殊字符破坏页面或造成 XSS */ private String esc(String raw) { if (raw == null) { return ""; } return raw.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } /** * JS 字符串字面量转义(在 esc 基础上再处理换行与反斜杠) */ private String jsStr(String raw) { return raw.replace("\\", "\\\\") .replace("\n", "\\n") .replace("\r", "\\r"); } }