2d1510060f55ac012cb862363203313744c0432f..b207fe04226e921e2b834a8162ef38947de431b4
2026-09-28 刘帅
注释优化
b207fe 对比 | 目录
2026-09-28 刘帅
html格式化
feda65 对比 | 目录
2026-09-28 刘帅
html格式化
cd4ca8 对比 | 目录
2026-09-28 刘帅
脱敏接口调整及返回数据调整
2914ee 对比 | 目录
14个文件已修改
434 ■■■■■ 已修改文件
cloud-server-account/src/main/java/cn/mb/cloud/common/sentinel/fallback/DefaultFeignFallback.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-account/src/main/java/com/dsh/privacy/SensitiveDataMaskingAdvice.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-competition/src/main/java/com/dsh/competition/enums/CompetitionStatusEnum.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-gateway/src/main/java/cn/mb/cloud/gateway/filter/SensitiveDataMaskingGlobalFilter.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/course/feignClient/activity/model/UserCouponPayment.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/course/mapper/AppGuideBannerMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/course/mapper/AppGuideConfigMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/course/mapper/QrLinkMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/modular/system/controller/system/SensitiveDataController.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/webapp/WEB-INF/view/system/accountAuditLog/account_audit_log.html 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/webapp/WEB-INF/view/system/appGuide/appGuide.html 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-other/src/main/java/com/dsh/other/controller/SiteController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-other/src/main/java/com/dsh/other/controller/TVipController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-other/src/main/resources/static/app-guide.html 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-account/src/main/java/cn/mb/cloud/common/sentinel/fallback/DefaultFeignFallback.java
@@ -18,12 +18,11 @@
import java.util.Set;
/**
 * Account-side replacement for the common Sentinel Feign fallback.
 * 账户服务对公共 Sentinel Feign 降级处理的替代实现。
 *
 * <p>The dependency implementation casts {@link Method#getGenericReturnType()}
 * directly to {@link Class}. That fails for generic Feign methods such as
 * {@code List<IntegralCommodity>}; resolve the raw type first so a proper
 * empty fallback value can be returned.</p>
 * <p>依赖中的实现会将 {@link Method#getGenericReturnType()} 直接转换为
 * {@link Class}。对于 {@code List<IntegralCommodity>} 等泛型 Feign 方法会转换失败;
 * 此处先解析原始类型,以便返回正确的空降级值。</p>
 */
public class DefaultFeignFallback<T> implements MethodInterceptor {
    private static final Logger log = LoggerFactory.getLogger(DefaultFeignFallback.class);
cloud-server-account/src/main/java/com/dsh/privacy/SensitiveDataMaskingAdvice.java
@@ -7,6 +7,9 @@
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@@ -15,13 +18,17 @@
import java.util.Map;
/**
 * Response boundary protection for personal phone and identity-card data.
 * It is deliberately based on field names so it also covers ad-hoc Maps
 * produced by reporting APIs.
 * 手机号和身份证号等个人敏感数据的响应边界保护。
 * 基于字段名进行识别,因此也可覆盖报表接口临时生成的 Map 数据。
 */
@ControllerAdvice
public class SensitiveDataMaskingAdvice implements ResponseBodyAdvice<Object> {
    private static final String INTERNAL_SENSITIVE_ACCESS = "X-Internal-Sensitive-Access";
    /** 客户端接收原始数据、仅在展示时脱敏的 APP 接口。 */
    private static final Set<String> APP_UNMASKED_PATHS = new HashSet<String>(Arrays.asList(
            "/base/exploreWP/storeDetail",
            "/base/exploreWP/customerList"
    ));
    private final HttpServletRequest request;
@@ -31,9 +38,12 @@
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // The only unmasked account response is the management-only endpoint.
        return !(request.getRequestURI().startsWith("/internal/sensitive/")
                && "true".equalsIgnoreCase(request.getHeader(INTERNAL_SENSITIVE_ACCESS)));
        String path = request.getRequestURI();
        // 内部管理调用必须携带敏感数据访问请求头。明确列出的两个 APP 接口返回原始数据,
        // 由客户端在展示时脱敏;其他账户服务响应均进行脱敏。
        return !(APP_UNMASKED_PATHS.contains(path)
                || (path.startsWith("/internal/sensitive/")
                && "true".equalsIgnoreCase(request.getHeader(INTERNAL_SENSITIVE_ACCESS))));
    }
    @Override
cloud-server-competition/src/main/java/com/dsh/competition/enums/CompetitionStatusEnum.java
@@ -5,11 +5,19 @@
 */
public enum CompetitionStatusEnum {
    /** 未开始。 */
    NOT_STARTED(1),
    /** 进行中。 */
    IN_PROGRESS(2),
    /** 已结束。 */
    ENDED(3),
    /** 已取消。 */
    CANCELED(4);
    /** 状态编码。 */
    private final Integer code;
    CompetitionStatusEnum(Integer code) {
cloud-server-gateway/src/main/java/cn/mb/cloud/gateway/filter/SensitiveDataMaskingGlobalFilter.java
@@ -15,20 +15,39 @@
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Final API boundary for APP traffic, including responses returned by every service. */
/**
 * APP 流量的最终接口边界,覆盖各服务返回的响应数据。
 */
@Component
public class SensitiveDataMaskingGlobalFilter implements GlobalFilter, Ordered {
    private static final Pattern FIELD = Pattern.compile("(?i)(\\\"(?:[a-z]*phone|[a-z]*mobile|telephone|tel)\\\"\\s*:\\s*\\\")([^\\\"]*)(\\\")");
    private static final Pattern IDCARD = Pattern.compile("(?i)(\\\"(?:[a-z]*idcard|[a-z]*identitycard|identitynumber|idnumber)\\\"\\s*:\\s*\\\")([^\\\"]*)(\\\")");
    /**
     * APP 需要获取原始数据后自行进行展示脱敏。
     * 请保持此列表精确,其他路由仍由网关边界进行脱敏保护。
     */
    private static final Set<String> APP_UNMASKED_PATHS = new HashSet<String>(Arrays.asList(
            "/other/api/site/queryMySiteById",
            "/account/base/exploreWP/storeDetail",
            "/other/base/store/queryStoreFreeBenefit",
            "/other/base/site/querySiteInfo",
            "/activity/huimin/getStudentListByCarId",
            "/account/base/exploreWP/customerList"
    ));
    @Override public int getOrder() { return -2; }
    @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // This route itself enforces the admin check and writes an audit record.
        if (exchange.getRequest().getURI().getPath().startsWith("/system/sensitive/")) return chain.filter(exchange);
        String path = exchange.getRequest().getURI().getPath();
        // /system/sensitive 会自行完成管理员校验和审计。以下 APP 路径有意返回原始字段,
        // 由客户端在展示时脱敏;其他所有响应仍由网关脱敏。
        if (path.startsWith("/system/sensitive/") || APP_UNMASKED_PATHS.contains(path)) return chain.filter(exchange);
        ServerHttpResponseDecorator decorated = new ServerHttpResponseDecorator(exchange.getResponse()) {
            @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                MediaType type = getHeaders().getContentType();
cloud-server-management/src/main/java/com/dsh/course/feignClient/activity/model/UserCouponPayment.java
@@ -8,12 +8,26 @@
/** 优惠券现金购买成功订单的统计传输对象。 */
@Data
public class UserCouponPayment {
    /** 主键。 */
    private Long id;
    /** 优惠券 ID。 */
    private Integer couponId;
    /** 用户 ID。 */
    private Integer userId;
    /** 支付状态(1:待支付;2:已支付)。 */
    private Integer status;
    /** 支付方式(1:积分兑换;2:积分加现金)。 */
    private Integer payType;
    /** 现金支付金额。 */
    private Double cash;
    /** 支付时间。 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date paymentTime;
}
cloud-server-management/src/main/java/com/dsh/course/mapper/AppGuideBannerMapper.java
@@ -1,4 +1,12 @@
package com.dsh.course.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dsh.course.entity.AppGuideBanner;
public interface AppGuideBannerMapper extends BaseMapper<AppGuideBanner> {}
/**
 * APP 下载引导宣传图数据访问层。
 *
 * <p>继承 {@link BaseMapper},提供 {@link AppGuideBanner} 的基础增删改查能力。</p>
 */
public interface AppGuideBannerMapper extends BaseMapper<AppGuideBanner> {
}
cloud-server-management/src/main/java/com/dsh/course/mapper/AppGuideConfigMapper.java
@@ -1,4 +1,12 @@
package com.dsh.course.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dsh.course.entity.AppGuideConfig;
public interface AppGuideConfigMapper extends BaseMapper<AppGuideConfig> {}
/**
 * APP 下载引导配置数据访问层。
 *
 * <p>继承 {@link BaseMapper},提供 {@link AppGuideConfig} 的基础增删改查能力。</p>
 */
public interface AppGuideConfigMapper extends BaseMapper<AppGuideConfig> {
}
cloud-server-management/src/main/java/com/dsh/course/mapper/QrLinkMapper.java
@@ -1,4 +1,12 @@
package com.dsh.course.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dsh.course.entity.QrLink;
public interface QrLinkMapper extends BaseMapper<QrLink> {}
/**
 * 二维码短链接数据访问层。
 *
 * <p>继承 {@link BaseMapper},提供 {@link QrLink} 的基础增删改查能力。</p>
 */
public interface QrLinkMapper extends BaseMapper<QrLink> {
}
cloud-server-management/src/main/java/com/dsh/guns/modular/system/controller/system/SensitiveDataController.java
@@ -28,7 +28,11 @@
import java.util.Map;
import java.util.UUID;
/** Deliberately narrow, auditable unmasking capability for the admin account. */
/**
 * 超级管理员敏感信息查询接口。
 *
 * <p>仅提供范围明确、可审计的敏感信息明文查询能力。</p>
 */
@RestController
@RequestMapping("/system/sensitive")
public class SensitiveDataController {
@@ -40,6 +44,12 @@
    @Resource private RefereeClient refereeClient;
    @Resource private TOperatorService operatorService;
    /**
     * 查询 APP 用户的敏感信息。
     *
     * @param id APP 用户 ID
     * @return 用户基本信息及敏感字段;用户不存在时返回 {@code null}
     */
    @GetMapping("/app-user/{id}")
    public Map<String, Object> appUser(@PathVariable Integer id) {
        assertAdmin();
@@ -48,6 +58,12 @@
        return result;
    }
    /**
     * 查询后台员工的敏感信息。
     *
     * @param id 员工用户 ID
     * @return 员工基本信息及敏感字段;员工不存在时返回 {@code null}
     */
    @GetMapping("/employee/{id}")
    public Map<String, Object> employee(@PathVariable Integer id) {
        assertAdmin();
@@ -64,6 +80,12 @@
        return result;
    }
    /**
     * 查询学员的敏感信息。
     *
     * @param id 学员 ID
     * @return 学员基本信息及敏感字段;学员不存在时返回 {@code null}
     */
    @GetMapping("/student/{id}")
    public Map<String, Object> student(@PathVariable Integer id) {
        assertAdmin();
@@ -73,10 +95,13 @@
    }
    /**
     * 预约管理查看关联预约人的敏感信息。
     * 根据预约记录查询关联预约人的敏感信息。
     *
     * <p>仅通过预约记录定位用户,避免前端以任意用户 ID 查询;该接口保留在
     * /system/sensitive 路径下,以使用全局的脱敏豁免和网关放行策略。</p>
     *
     * @param bookingId 预约记录 ID
     * @return 预约关联用户的基本信息及敏感字段;预约或用户不存在时返回 {@code null}
     */
    @GetMapping("/booking/{bookingId}")
    public Map<String, Object> booking(@PathVariable Integer bookingId) {
@@ -92,6 +117,12 @@
        return result;
    }
    /**
     * 查询教练的敏感信息。
     *
     * @param id 教练 ID
     * @return 教练基本信息及敏感字段;教练不存在时返回 {@code null}
     */
    @GetMapping("/coach/{id}")
    public Map<String, Object> coach(@PathVariable Integer id) {
        assertAdmin();
@@ -112,6 +143,12 @@
        return result;
    }
    /**
     * 查询裁判的敏感信息。
     *
     * @param id 裁判 ID
     * @return 裁判基本信息及敏感字段;裁判不存在时返回 {@code null}
     */
    @GetMapping("/referee/{id}")
    public Map<String, Object> referee(@PathVariable Integer id) {
        assertAdmin();
@@ -136,6 +173,12 @@
        return result;
    }
    /**
     * 查询运营商关联员工的敏感信息。
     *
     * @param id 运营商 ID
     * @return 关联员工的基本信息及敏感字段;运营商或员工不存在时返回 {@code null}
     */
    @GetMapping("/operator/{id}")
    public Map<String, Object> operator(@PathVariable Integer id) {
        assertAdmin();
cloud-server-management/src/main/webapp/WEB-INF/view/system/accountAuditLog/account_audit_log.html
@@ -1,32 +1,41 @@
@layout("/common/_container.html"){
<div class="row">
    <div class="col-sm-12">
        <div class="ibox float-e-margins">
            <div class="ibox-title"><h5>操作日志</h5></div>
            <div class="ibox-content">
                <div class="row row-lg"><div class="col-sm-12">
                    <div class="row" style="margin-bottom: 15px;">
                        <div class="col-sm-3">
                            <#NameCon id="userAccount" name="账号" placeholder="请输入账号" />
                        </div>
                        <div class="col-sm-3">
                            <#SelectCon id="operationType" name="操作类型">
                                <option value="">请选择操作类型</option>
                                <option value="LOGIN">登录</option><option value="CREATE">新增</option>
                                <option value="UPDATE">修改</option><option value="DELETE">删除</option><option value="READ">查询</option>
                                <option value="VIEW_SENSITIVE">查看敏感信息</option><option value="STATUS_CHANGE">状态变更</option><option value="FEIGN_CALL">服务调用</option>
                            </#SelectCon>
                        </div>
                        <div class="col-sm-3">
                            <#button name="搜索" icon="fa-search" clickFun="AccountAuditLog.search()"/>
                            <#button name="重置" icon="fa-trash" clickFun="AccountAuditLog.resetSearch()" space="true"/>
                        </div>
                    </div>
                    <table id="AccountAuditLogTable" data-mobile-responsive="true" data-click-to-select="false"></table>
                </div></div>
  <div class="col-sm-12">
    <div class="ibox float-e-margins">
      <div class="ibox-title">
        <h5>操作日志</h5>
      </div>
      <div class="ibox-content">
        <div class="row row-lg">
          <div class="col-sm-12">
            <div class="row" style="margin-bottom: 15px;">
              <div class="col-sm-3">
                <#NameCon id="userAccount" name="账号" placeholder="请输入账号" />
              </div>
              <div class="col-sm-3">
                <#SelectCon id="operationType" name="操作类型">
                  <option value="">请选择操作类型</option>
                  <option value="LOGIN">登录</option>
                  <option value="CREATE">新增</option>
                  <option value="UPDATE">修改</option>
                  <option value="DELETE">删除</option>
                  <option value="READ">查询</option>
                  <option value="VIEW_SENSITIVE">查看敏感信息</option>
                  <option value="STATUS_CHANGE">状态变更</option>
                  <option value="FEIGN_CALL">服务调用</option>
                </#SelectCon>
              </div>
              <div class="col-sm-3">
                <#button name="搜索" icon="fa-search" clickFun="AccountAuditLog.search()" />
                <#button name="重置" icon="fa-trash" clickFun="AccountAuditLog.resetSearch()" space="true" />
              </div>
            </div>
            <table id="AccountAuditLogTable" data-mobile-responsive="true" data-click-to-select="false"></table>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
<script src="${ctxPath}/modular/system/accountAuditLog/account_audit_log.js"></script>
@}
@}
cloud-server-management/src/main/webapp/WEB-INF/view/system/appGuide/appGuide.html
@@ -1,11 +1,85 @@
@layout("/common/_container.html"){
<style>.app-guide-section{margin-bottom:25px}.qr{width:90px;height:90px}.table td{vertical-align:middle}</style>
<div class="row"><div class="col-sm-12"><div class="ibox float-e-margins"><div class="ibox-title"><h5>APP 下载引导与场地订场二维码</h5></div><div class="ibox-content">
  <div class="app-guide-section"><h4>APP 下载链接</h4><form id="configForm" class="form-inline">
    <input type="hidden" name="id" id="configId"><p><input class="form-control" name="apkUrl" style="width:350px" placeholder="Android 下载链接"> <input class="form-control" name="iosStoreUrl" style="width:350px" placeholder="iOS 下载链接"></p>
    <button type="button" class="btn btn-primary" onclick="saveConfig()">保存下载配置</button></form></div>
  <hr><div class="app-guide-section"><h4>宣传图</h4><form class="form-inline"><input id="bannerFile" type="file" accept="image/*" class="form-control"><input id="bannerUrl" type="hidden"><button type="button" class="btn btn-default" onclick="uploadBanner()">上传图片</button><input id="bannerTitle" class="form-control" placeholder="标题(可选)"><input id="bannerSort" class="form-control" style="width:100px" placeholder="排序"><button type="button" class="btn btn-primary" onclick="addBanner()">添加</button></form><p id="uploadTip" class="text-muted"></p><table class="table table-bordered"><thead><tr><th>预览</th><th>标题</th><th>排序</th><th>状态</th><th>操作</th></tr></thead><tbody id="bannerRows"></tbody></table></div>
  <hr><div class="app-guide-section"><h4>场地订场二维码</h4><p class="text-muted">从现有场地管理数据中选择,每个场地仅允许一个启用二维码。</p><form class="form-inline"><select id="siteSelect" class="form-control" style="min-width:360px"><option value="">请选择场地</option></select><button type="button" class="btn btn-primary" onclick="createQr()">生成二维码</button></form><table class="table table-bordered"><thead><tr><th>名称</th><th>App页面</th><th>门店/场地</th><th>二维码</th><th>引导链接</th><th>状态</th><th>操作</th></tr></thead><tbody id="linkRows"></tbody></table></div>
</div></div></div></div>
<style>
  .app-guide-section {
    margin-bottom: 25px
  }
  .qr {
    width: 90px;
    height: 90px
  }
  .table td {
    vertical-align: middle
  }
</style>
<div class="row">
  <div class="col-sm-12">
    <div class="ibox float-e-margins">
      <div class="ibox-title">
        <h5>APP 下载引导与场地订场二维码</h5>
      </div>
      <div class="ibox-content">
        <div class="app-guide-section">
          <h4>APP 下载链接</h4>
          <form id="configForm" class="form-inline">
            <input type="hidden" name="id" id="configId">
            <p>
              <input class="form-control" name="apkUrl" style="width:350px" placeholder="Android 下载链接">
              <input class="form-control" name="iosStoreUrl" style="width:350px" placeholder="iOS 下载链接">
            </p>
            <button type="button" class="btn btn-primary" onclick="saveConfig()">保存下载配置</button>
          </form>
        </div>
        <hr>
        <div class="app-guide-section">
          <h4>宣传图</h4>
          <form class="form-inline"><input id="bannerFile" type="file" accept="image/*" class="form-control">
            <input id="bannerUrl" type="hidden">
            <button type="button" class="btn btn-default" onclick="uploadBanner()">上传图片</button>
            <input id="bannerTitle" class="form-control" placeholder="标题(可选)">
            <input id="bannerSort" class="form-control" style="width:100px" placeholder="排序">
            <button type="button" class="btn btn-primary" onclick="addBanner()">添加</button>
          </form>
          <p id="uploadTip" class="text-muted"></p>
          <table class="table table-bordered">
            <thead>
              <tr>
                <th>预览</th>
                <th>标题</th>
                <th>排序</th>
                <th>状态</th>
                <th>操作</th>
              </tr>
            </thead>
            <tbody id="bannerRows"></tbody>
          </table>
        </div>
        <hr>
        <div class="app-guide-section">
          <h4>场地订场二维码</h4>
          <p class="text-muted">从现有场地管理数据中选择,每个场地仅允许一个启用二维码。</p>
          <form class="form-inline"><select id="siteSelect" class="form-control" style="min-width:360px">
              <option value="">请选择场地</option>
            </select><button type="button" class="btn btn-primary" onclick="createQr()">生成二维码</button></form>
          <table class="table table-bordered">
            <thead>
              <tr>
                <th>名称</th>
                <th>App页面</th>
                <th>门店/场地</th>
                <th>二维码</th>
                <th>引导链接</th>
                <th>状态</th>
                <th>操作</th>
              </tr>
            </thead>
            <tbody id="linkRows"></tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
</div>
<script src="${ctxPath}/modular/system/appGuide/appGuide.js"></script>
@}
@}
cloud-server-other/src/main/java/com/dsh/other/controller/SiteController.java
@@ -677,9 +677,18 @@
    })
    public ResultUtil<Map<String, Object>> queryMySiteById(Integer id) {
        try {
            Integer uid = tokenUtil.getUserIdFormRedis();
            if (uid == null) {
                return ResultUtil.tokenErr();
            }
            HashMap<String, Object> map = new HashMap<>();
            System.err.println("预约id"+id);
            SiteBooking siteBooking = siteBookingService.getById(id);
            // This endpoint now returns the original contact data to the APP,
            // so an authenticated user may only read their own booking.
            if (siteBooking == null || !uid.equals(siteBooking.getAppUserId())) {
                return ResultUtil.error("预约记录不存在");
            }
            Integer targetAudience = siteBooking.getTargetAudience();
            System.err.println("预约"+siteBooking);
            Integer storeId = siteBooking.getStoreId();
cloud-server-other/src/main/java/com/dsh/other/controller/TVipController.java
@@ -60,6 +60,10 @@
        for (Vip vip : list) {
            List<CouponVipResp> couponVipRespList = new ArrayList<>();
            List<TicketVipResp> ticketVipRespList = new ArrayList<>();
            // The APP contract requires arrays even when this membership card
            // has no coupon or ticket benefits.
            vip.setCouponList(couponVipRespList);
            vip.setTicketList(ticketVipRespList);
            String couponJson = vip.getCouponJson();
            JSONArray couponJsonArray = JSONArray.parseArray(couponJson);
            if(couponJsonArray==null){
cloud-server-other/src/main/resources/static/app-guide.html
@@ -1,6 +1,111 @@
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><title>玩湃生活</title>
<style>body{margin:0;background:#f7f8fa;color:#172033;font:16px -apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif}.wrap{max-width:540px;margin:auto;padding:24px 18px 42px}.banner{width:100%;border-radius:14px;margin:0 0 12px;display:block}.empty{height:180px;background:linear-gradient(135deg,#2468f2,#74a3ff);border-radius:14px}.name{font-size:25px;font-weight:700;margin:24px 0 8px}.desc{color:#667085;margin:0 0 28px}.open{width:100%;border:0;border-radius:24px;padding:14px;background:#2468f2;color:white;font-size:17px;font-weight:600}.tip{font-size:13px;color:#98a2b3;text-align:center;margin-top:14px}</style></head>
<body><main class="wrap"><div id="banners" class="empty"></div><h1 id="appName" class="name">玩湃生活</h1><p class="desc">下载 APP,畅享便捷订场服务</p><button class="open" onclick="openApp()">打开 APP</button><p id="tip" class="tip"></p></main>
<script>var cfg={},code=new URLSearchParams(location.search).get('code')||'',el=id=>document.getElementById(id);fetch(new URL('public/app-guide/config'+(code?'?code='+encodeURIComponent(code):''),location.href)).then(r=>r.json()).then(r=>{if(r.code!==200)throw Error(r.msg);cfg=r.data;document.title=cfg.appName;el('appName').textContent=cfg.appName;var bs=cfg.banners||[];if(bs.length){el('banners').className='';el('banners').innerHTML=bs.map(x=>'<img class="banner" src="'+x.imageUrl+'" alt="'+(x.title||'宣传图')+'">').join('')}}).catch(e=>el('tip').textContent=e.message);
function marketUrl(){var u=navigator.userAgent.toLowerCase(),m=cfg.androidMarketUrls||{},brand=/huawei|honor/.test(u)?'huawei':/xiaomi|redmi/.test(u)?'xiaomi':/oppo/.test(u)?'oppo':/vivo/.test(u)?'vivo':/samsung/.test(u)?'samsung':'';return (brand&&m[brand])||cfg.androidDefaultUrl||cfg.apkUrl}function openApp(){var t=cfg.target||{},q=Object.keys(t).filter(k=>t[k]!=null&&k!=='type'&&k!=='route').map(k=>k+'='+encodeURIComponent(t[k])).join('&');var scheme=(cfg.appScheme||'playpai')+'://'+(t.route||'home').replace(/^\//,'')+(q?'?'+q:'');location.href=scheme;setTimeout(function(){var u=navigator.userAgent.toLowerCase(),url=/iphone|ipad|ipod/.test(u)?cfg.iosStoreUrl:marketUrl();if(url)location.href=url;else el('tip').textContent='请在应用商店搜索下载 '+(cfg.appName||'玩湃生活')},1600)}</script></body></html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
  <title>玩湃生活</title>
  <style>
    body {
      margin: 0;
      background: #f7f8fa;
      color: #172033;
      font: 16px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif
    }
    .wrap {
      max-width: 540px;
      margin: auto;
      padding: 24px 18px 42px
    }
    .banner {
      width: 100%;
      border-radius: 14px;
      margin: 0 0 12px;
      display: block
    }
    .empty {
      height: 180px;
      background: linear-gradient(135deg, #2468f2, #74a3ff);
      border-radius: 14px
    }
    .name {
      font-size: 25px;
      font-weight: 700;
      margin: 24px 0 8px
    }
    .desc {
      color: #667085;
      margin: 0 0 28px
    }
    .open {
      width: 100%;
      border: 0;
      border-radius: 24px;
      padding: 14px;
      background: #2468f2;
      color: white;
      font-size: 17px;
      font-weight: 600
    }
    .tip {
      font-size: 13px;
      color: #98a2b3;
      text-align: center;
      margin-top: 14px
    }
  </style>
</head>
<body>
  <main class="wrap">
    <div id="banners" class="empty"></div>
    <h1 id="appName" class="name">玩湃生活</h1>
    <p class="desc">下载 APP,畅享便捷订场服务</p><button class="open" onclick="openApp()">打开 APP</button>
    <p id="tip" class="tip"></p>
  </main>
  <script>
    var cfg = {},
      code = new URLSearchParams(location.search).get('code') || '',
      el = id => document.getElementById(id);
    fetch(new URL('public/app-guide/config' + (code ? '?code=' + encodeURIComponent(code) : ''), location.href)).then(r => r.json()).then(r => {
      if (r.code !== 200) throw Error(r.msg);
      cfg = r.data;
      document.title = cfg.appName;
      el('appName').textContent = cfg.appName;
      var bs = cfg.banners || [];
      if (bs.length) {
        el('banners').className = '';
        el('banners').innerHTML = bs.map(x => '<img class="banner" src="' + x.imageUrl + '" alt="' + (x.title || '宣传图') + '">').join('')
      }
    }).catch(e => el('tip').textContent = e.message);
    function marketUrl() {
      var u = navigator.userAgent.toLowerCase(),
        m = cfg.androidMarketUrls || {},
        brand = /huawei|honor/.test(u) ? 'huawei' : /xiaomi|redmi/.test(u) ? 'xiaomi' : /oppo/.test(u) ? 'oppo' : /vivo/.test(u) ? 'vivo' : /samsung/.test(u) ? 'samsung' : '';
      return (brand && m[brand]) || cfg.androidDefaultUrl || cfg.apkUrl
    }
    function openApp() {
      var t = cfg.target || {},
        q = Object.keys(t).filter(k => t[k] != null && k !== 'type' && k !== 'route').map(k => k + '=' + encodeURIComponent(t[k])).join('&');
      var scheme = (cfg.appScheme || 'playpai') + '://' + (t.route || 'home').replace(/^\//, '') + (q ? '?' + q : '');
      location.href = scheme;
      setTimeout(function() {
        var u = navigator.userAgent.toLowerCase(),
          url = /iphone|ipad|ipod/.test(u) ? cfg.iosStoreUrl : marketUrl();
        if (url) location.href = url;
        else el('tip').textContent = '请在应用商店搜索下载 ' + (cfg.appName || '玩湃生活')
      }, 1600)
    }
  </script>
</body>
</html>