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>