| | |
| | | } |
| | | |
| | | /** |
| | | * Internal-only data source for the management sensitive-data query. The |
| | | * caller is authenticated and authorised by the management service; its |
| | | * Feign interceptor adds the header that permits the response to remain |
| | | * unmasked for that single request. |
| | | * 仅供管理端敏感信息查询使用的内部数据接口。 |
| | | * 调用方由管理服务完成认证和授权;其 Feign 拦截器会添加请求头, |
| | | * 以允许本次响应不进行脱敏。 |
| | | */ |
| | | @PostMapping("/internal/sensitive/app-user/{id}") |
| | | public Map<String, Object> sensitiveAppUser(@PathVariable("id") Integer id, |
| | |
| | | } |
| | | |
| | | /** |
| | | * Internal data source used when an APP booking submits its displayed, |
| | | * masked phone number. Limit the response to the single field needed for |
| | | * that booking; callers must use the internal sensitive-data header. |
| | | * APP 订场提交页面展示的脱敏手机号时使用的内部数据接口。 |
| | | * 仅返回该预约所需的手机号字段;调用方必须携带内部敏感数据访问请求头。 |
| | | */ |
| | | @PostMapping("/internal/sensitive/site-booking/app-user/{id}/phone") |
| | | public Map<String, Object> siteBookingPhone(@PathVariable("id") Integer id, |
| | |
| | | public void updateAppUser(@RequestBody TAppUser appUser) { |
| | | try { |
| | | TAppUser appUser1 = appUserService.getById(appUser.getId()); |
| | | // A management edit form may contain the displayed masked value. |
| | | // Never persist that display value over the original identifier. |
| | | // 管理端编辑表单可能携带页面展示的脱敏值。 |
| | | // 不得使用该展示值覆盖原始证件或联系方式。 |
| | | if (appUser.getPhone() != null && appUser.getPhone().contains("*")) appUser.setPhone(appUser1.getPhone()); |
| | | if (appUser.getIdCard() != null && appUser.getIdCard().contains("*")) appUser.setIdCard(appUser1.getIdCard()); |
| | | appUserService.updateById(appUser); |
| | |
| | | |
| | | private void mask(Object value, IdentityHashMap<Object, Boolean> visited) { |
| | | if (value == null || value instanceof String || value.getClass().isEnum() || visited.put(value, Boolean.TRUE) != null) return; |
| | | // Path 的实现类(如 sun.nio.fs.UnixPath)本身是 Iterable;遍历其子路径会不断创建新对象, |
| | | // IdentityHashMap 无法识别该循环,因而会导致 StackOverflowError。 |
| | | // java.nio.file.Path 的实现类(如 sun.nio.fs.UnixPath)实现了 Iterable 接口; |
| | | // 遍历其子路径会不断创建新对象,IdentityHashMap 无法识别该循环,因而会导致 StackOverflowError。 |
| | | if (value instanceof java.nio.file.Path || isJdkType(value.getClass())) return; |
| | | if (value instanceof Map) { |
| | | for (Object entryObject : ((Map<?, ?>) value).entrySet()) { |
| | |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** |
| | | * Allows a single response field to bypass sensitive-data masking. |
| | | * Use only for endpoints that are authorised to return the original value. |
| | | * 允许单个响应字段跳过敏感数据脱敏。 |
| | | * 仅限已获授权返回原始数据的接口使用。 |
| | | */ |
| | | @Target(ElementType.FIELD) |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * Safe replacement for the common Sentinel Feign fallback. |
| | | * 公共 Sentinel Feign 降级处理的安全替代实现。 |
| | | * |
| | | * <p>The dependency version assumes every generic return type is a Class. That |
| | | * is false for declarations such as {@code Map<String, Object>}, whose runtime |
| | | * type is {@link ParameterizedType}; the failed cast used to hide the original |
| | | * downstream exception behind an AssertionError.</p> |
| | | * <p>依赖版本假定所有泛型返回类型均为 {@link Class}。对于 |
| | | * {@code Map<String, Object>} 等声明并不成立,其运行时类型为 |
| | | * {@link ParameterizedType};此前转换失败会以 AssertionError 掩盖原始下游异常。</p> |
| | | */ |
| | | public class DefaultFeignFallback<T> implements MethodInterceptor { |
| | | private static final Logger log = LoggerFactory.getLogger(DefaultFeignFallback.class); |
| | |
| | | Class<?> returnType = rawType(declaredReturnType); |
| | | Object data = defaultValue(returnType); |
| | | |
| | | // Only methods that explicitly declare ResponseData may receive a |
| | | // ResponseData fallback. Returning it from Map/List methods causes a |
| | | // second ClassCastException in the Feign proxy. |
| | | // 仅显式声明返回 ResponseData 的方法可以返回 ResponseData 降级结果。 |
| | | // 在 Map/List 方法中返回该对象会导致 Feign 代理再次抛出 ClassCastException。 |
| | | if (ResponseData.class.equals(returnType)) { |
| | | Type dataType = firstTypeArgument(declaredReturnType); |
| | | return ResponseData.builder() |
| | |
| | | @FeignClient("mb-cloud-account") |
| | | public interface AppUserClient { |
| | | |
| | | /** Only invoked by the super-administrator sensitive data query. */ |
| | | /** 仅供超级管理员敏感信息查询接口调用。 */ |
| | | @PostMapping("/internal/sensitive/app-user/{id}") |
| | | Map<String, Object> sensitiveAppUser(@PathVariable("id") Integer id); |
| | | |
| | | /** Only invoked by the super-administrator sensitive student-data query. */ |
| | | /** 仅供超级管理员学员敏感信息查询接口调用。 */ |
| | | @PostMapping("/internal/sensitive/student/{id}") |
| | | Map<String, Object> sensitiveStudent(@PathVariable("id") Integer id); |
| | | |
| | |
| | | @Autowired |
| | | private ITSiteService siteService; |
| | | |
| | | /** |
| | | * 打开 APP 下载引导管理页面。 |
| | | * |
| | | * @return APP 下载引导管理页面路径 |
| | | */ |
| | | @GetMapping("") |
| | | public String index() { |
| | | return "/system/appGuide/appGuide.html"; |
| | | } |
| | | |
| | | /** |
| | | * 查询可用于生成订场二维码的场地列表。 |
| | | * |
| | | * @return 按 ID 倒序排列的场地列表 |
| | | */ |
| | | @ResponseBody |
| | | @GetMapping("/sites") |
| | | public List<TSite> queryAvailableSites() { |
| | | return siteService.list(new QueryWrapper<TSite>().orderByDesc("id")); |
| | | } |
| | | |
| | | /** |
| | | * 查询 APP 下载引导页面所需的配置、宣传图和订场二维码数据。 |
| | | * |
| | | * @return 下载引导页面的聚合数据 |
| | | */ |
| | | @ResponseBody |
| | | @GetMapping("/data") |
| | | public Map<String, Object> data() { |
| | |
| | | return r; |
| | | } |
| | | |
| | | /** |
| | | * 保存 APP 下载引导配置。 |
| | | * |
| | | * @param config 下载引导配置 |
| | | * @return 保存结果 |
| | | */ |
| | | @ResponseBody |
| | | @PostMapping("/config") |
| | | public Map<String, Object> saveConfig(AppGuideConfig config) { |
| | |
| | | return ok(); |
| | | } |
| | | |
| | | /** |
| | | * 保存 APP 下载引导宣传图。 |
| | | * |
| | | * @param banner 宣传图配置 |
| | | * @return 保存结果 |
| | | */ |
| | | @ResponseBody |
| | | @PostMapping("/banner") |
| | | public Map<String, Object> saveBanner(AppGuideBanner banner) { |
| | |
| | | return ok(); |
| | | } |
| | | |
| | | /** |
| | | * 删除 APP 下载引导宣传图。 |
| | | * |
| | | * @param id 宣传图 ID |
| | | * @return 删除结果 |
| | | */ |
| | | @ResponseBody |
| | | @PostMapping("/banner/delete") |
| | | public Map<String, Object> deleteBanner(Long id) { |
| | |
| | | return ok(); |
| | | } |
| | | |
| | | /** |
| | | * 为指定场地创建订场二维码。 |
| | | * |
| | | * @param storeId 门店 ID |
| | | * @param siteId 场地 ID |
| | | * @param name 二维码名称 |
| | | * @param channel 投放渠道 |
| | | * @param pageName 引导页面名称 |
| | | * @return 创建结果及二维码引导链接 |
| | | */ |
| | | @ResponseBody |
| | | @PostMapping("/siteQr") |
| | | public Map<String, Object> createSiteQr(Integer storeId, Integer siteId, String name, String channel, String pageName) { |
| | |
| | | return r; |
| | | } |
| | | |
| | | /** |
| | | * 修改场地订场二维码的启用状态。 |
| | | * |
| | | * @param id 二维码 ID |
| | | * @param enabled 启用状态(0:停用;1:启用) |
| | | * @return 修改结果 |
| | | */ |
| | | @ResponseBody |
| | | @PostMapping("/siteQr/status") |
| | | public Map<String, Object> changeStatus(Long id, Integer enabled) { |
| | |
| | | return ok(); |
| | | } |
| | | |
| | | /** 逻辑删除场地订场二维码。 */ |
| | | /** |
| | | * 逻辑删除场地订场二维码。 |
| | | * |
| | | * @param id 二维码 ID |
| | | * @return 删除结果 |
| | | */ |
| | | @ResponseBody |
| | | @PostMapping("/siteQr/delete") |
| | | public Map<String, Object> deleteSiteQr(Long id) { |
| | |
| | | return ok(); |
| | | } |
| | | |
| | | /** |
| | | * 生成并输出场地订场二维码图片。 |
| | | * |
| | | * @param id 二维码 ID |
| | | * @param response HTTP 响应 |
| | | * @throws IOException 图片输出失败时抛出 |
| | | */ |
| | | @GetMapping("/siteQr/{id}.png") |
| | | public void qrImage(@PathVariable Long id, HttpServletResponse response) throws IOException { |
| | | QrLink link = qrLinkMapper.selectById(id); |
| | |
| | | return wb; |
| | | } |
| | | |
| | | /** Export files are an external disclosure channel and always remain masked. */ |
| | | /** |
| | | * 导出文件属于对外数据出口,敏感字段必须始终保持脱敏。 |
| | | * |
| | | * @param columnName 列名,用于识别手机号和身份证号等敏感字段 |
| | | * @param value 原始单元格值 |
| | | * @return 脱敏后的单元格值 |
| | | */ |
| | | private static String maskSensitiveValue(String columnName, String value) { |
| | | if (value == null) return null; |
| | | String column = columnName == null ? "" : columnName.toLowerCase(); |
| | |
| | | import java.util.Iterator; |
| | | import java.util.Map; |
| | | |
| | | /** Ensures every normal management JSON response uses the same privacy rules. */ |
| | | /** 确保所有常规管理端 JSON 响应均遵循统一的隐私保护规则。 */ |
| | | @ControllerAdvice |
| | | public class SensitiveDataMaskingAdvice implements ResponseBodyAdvice<Object> { |
| | | private final HttpServletRequest request; |
| | | public SensitiveDataMaskingAdvice(HttpServletRequest request) { this.request = request; } |
| | | |
| | | @Override public boolean supports(MethodParameter type, Class<? extends HttpMessageConverter<?>> converter) { |
| | | // Only the explicit, admin-only and audited endpoint returns the source value. |
| | | // 仅明确限定、仅管理员可访问且已审计的接口返回原始数据。 |
| | | return !request.getRequestURI().startsWith("/system/sensitive/"); |
| | | } |
| | | @Override public Object beforeBodyWrite(Object body, MethodParameter type, MediaType mediaType, |
| | |
| | | } |
| | | private void mask(Object value, IdentityHashMap<Object, Boolean> seen) { |
| | | if (value == null || value instanceof String || value.getClass().isEnum() || seen.put(value, Boolean.TRUE) != null) return; |
| | | // Reactive streams and their internal queues are runtime infrastructure, not |
| | | // response DTOs. In particular, Reactor's MpscLinkedQueue implements |
| | | // Iterable but deliberately throws UnsupportedOperationException from |
| | | // iterator(). Do not introspect it (or any other reactive infrastructure). |
| | | // 响应式流及其内部队列属于运行时基础设施,并非响应 DTO。特别是 Reactor 的 |
| | | // MpscLinkedQueue 虽实现了 Iterable,却会在 iterator() 中主动抛出 |
| | | // UnsupportedOperationException;不得反射遍历它或其他响应式基础设施。 |
| | | if (isReactiveInfrastructure(value.getClass())) return; |
| | | // UnixPath implements Iterable. Its iterator creates a new Path for every |
| | | // segment, so identity-based cycle detection cannot stop the recursion. |
| | | // UnixPath 实现了 Iterable,其迭代器会为每个路径段创建新的 Path 对象, |
| | | // 因此基于对象标识的循环检测无法终止递归。 |
| | | if (value instanceof Path) return; |
| | | if (value instanceof Map) { |
| | | for (Object item : ((Map<?, ?>) value).entrySet()) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Some third-party containers expose Iterable only for API compatibility and |
| | | * intentionally do not support iteration. A response privacy filter must never |
| | | * turn such an implementation detail into a 500 response. |
| | | * 部分第三方容器仅为 API 兼容而实现 Iterable,并不支持实际迭代。 |
| | | * 响应隐私过滤器不得因这类实现细节导致接口返回 500 错误。 |
| | | */ |
| | | private void maskIterable(Iterable<?> values, IdentityHashMap<Object, Boolean> seen) { |
| | | try { |
| | | Iterator<?> iterator = values.iterator(); |
| | | while (iterator.hasNext()) mask(iterator.next(), seen); |
| | | } catch (UnsupportedOperationException ignored) { |
| | | // There is no safely accessible element to mask. |
| | | // 没有可安全访问并进行脱敏的元素。 |
| | | } |
| | | } |
| | | |
| | |
| | | (function(){ |
| | | function esc(v){return $('<div>').text(v==null?'':v).html()} function val(n,v){$('#configForm [name='+n+']').val(v==null?'':v)} |
| | | window.reloadAppGuide=function(){ $.get(Feng.ctxPath+'/appGuide/data',function(d){var c=d.config||{};['id','apkUrl','iosStoreUrl'].forEach(function(k){val(k,c[k])});$('#bannerRows').html((d.banners||[]).map(function(x){return '<tr><td><img class="qr" src="'+esc(x.imageUrl)+'"></td><td>'+esc(x.title)+'</td><td>'+esc(x.sort)+'</td><td>'+((x.enabled===1)?'启用':'停用')+'</td><td><button class="btn btn-danger btn-xs" onclick="deleteBanner(\''+x.id+'\')">删除</button></td></tr>'}).join(''));$('#linkRows').html((d.links||[]).map(function(x){var id=String(x.id),q=Feng.ctxPath+'/appGuide/siteQr/'+encodeURIComponent(id)+'.png';return '<tr><td>'+esc(x.name)+'</td><td>'+esc(x.pageName)+'</td><td>'+x.storeId+' / '+x.siteId+'</td><td><a href="'+q+'" target="_blank"><img class="qr" src="'+q+'"></a></td><td><code>'+esc(x.guideUrl)+'</code></td><td>'+((x.enabled===1)?'启用':'停用')+'</td><td><button class="btn btn-warning btn-xs" onclick="changeQr(\''+id+'\','+(x.enabled===1?0:1)+')">'+(x.enabled===1?'停用':'启用')+'</button> <button class="btn btn-danger btn-xs" onclick="deleteQr(\''+id+'\')">删除</button></td></tr>'}).join(''))})}; |
| | | window.saveConfig=function(){$.post(Feng.ctxPath+'/appGuide/config',$('#configForm').serialize(),function(){Feng.success('保存成功');reloadAppGuide()})}; |
| | | window.uploadBanner=function(){var f=$('#bannerFile')[0].files[0];if(!f){Feng.info('请选择图片');return}var d=new FormData();d.append('file',f);$.ajax({url:Feng.ctxPath+'/mgr/uploadFile',data:d,type:'POST',contentType:false,processData:false,success:function(url){$('#bannerUrl').val(url);$('#uploadTip').text('上传成功')}})}; |
| | | window.addBanner=function(){if(!$('#bannerUrl').val()){Feng.info('请先上传图片');return}$.post(Feng.ctxPath+'/appGuide/banner',{imageUrl:$('#bannerUrl').val(),title:$('#bannerTitle').val(),sort:$('#bannerSort').val()||0,enabled:1},function(){reloadAppGuide();$('#bannerUrl,#bannerTitle,#bannerSort,#bannerFile').val('')})}; |
| | | window.deleteBanner=function(id){$.post(Feng.ctxPath+'/appGuide/banner/delete',{id:id},reloadAppGuide)}; |
| | | window.createQr=function(){var o=$('#siteSelect option:selected'),pageName='com.sinata.wepark.ui.home.PlaceDetailActivity';if(!o.val()){Feng.info('请选择场地');return}$.post(Feng.ctxPath+'/appGuide/siteQr',{storeId:o.data('store'),siteId:o.val(),name:o.text(),pageName:pageName},function(r){if(!r.success){Feng.error(r.message);return}Feng.success('已生成');reloadAppGuide()})}; |
| | | window.changeQr=function(id,enabled){$.post(Feng.ctxPath+'/appGuide/siteQr/status',{id:id,enabled:enabled},reloadAppGuide)};$.get(Feng.ctxPath+'/appGuide/sites',function(s){$('#siteSelect').append((s||[]).map(function(x){return '<option value="'+x.id+'" data-store="'+x.storeId+'">['+x.storeId+'] '+esc(x.name)+'</option>'}).join(''))});$(reloadAppGuide); |
| | | window.deleteQr=function(id){if(!confirm('确认删除该场地订场二维码?'))return;$.post(Feng.ctxPath+'/appGuide/siteQr/delete',{id:id},reloadAppGuide)}; |
| | | })(); |
| | | (function() { |
| | | function esc(v) { |
| | | return $('<div>').text(v == null ? '' : v).html() |
| | | } |
| | | |
| | | function val(n, v) { |
| | | $('#configForm [name=' + n + ']').val(v == null ? '' : v) |
| | | } |
| | | window.reloadAppGuide = function() { |
| | | $.get(Feng.ctxPath + '/appGuide/data', function(d) { |
| | | var c = d.config || {}; |
| | | ['id', 'apkUrl', 'iosStoreUrl'].forEach(function(k) { |
| | | val(k, c[k]) |
| | | }); |
| | | $('#bannerRows').html((d.banners || []).map(function(x) { |
| | | return '<tr><td><img class="qr" src="' + esc(x.imageUrl) + '"></td><td>' + esc(x.title) + '</td><td>' + esc(x.sort) + '</td><td>' + ((x.enabled === 1) ? '启用' : '停用') + '</td><td><button class="btn btn-danger btn-xs" onclick="deleteBanner(\'' + x.id + '\')">删除</button></td></tr>' |
| | | }).join('')); |
| | | $('#linkRows').html((d.links || []).map(function(x) { |
| | | var id = String(x.id), |
| | | q = Feng.ctxPath + '/appGuide/siteQr/' + encodeURIComponent(id) + '.png'; |
| | | return '<tr><td>' + esc(x.name) + '</td><td>' + esc(x.pageName) + '</td><td>' + x.storeId + ' / ' + x.siteId + '</td><td><a href="' + q + '" target="_blank"><img class="qr" src="' + q + '"></a></td><td><code>' + esc(x.guideUrl) + '</code></td><td>' + ((x.enabled === 1) ? '启用' : '停用') + '</td><td><button class="btn btn-warning btn-xs" onclick="changeQr(\'' + id + '\',' + (x.enabled === 1 ? 0 : 1) + ')">' + (x.enabled === 1 ? '停用' : '启用') + '</button> <button class="btn btn-danger btn-xs" onclick="deleteQr(\'' + id + '\')">删除</button></td></tr>' |
| | | }).join('')) |
| | | }) |
| | | }; |
| | | window.saveConfig = function() { |
| | | $.post(Feng.ctxPath + '/appGuide/config', $('#configForm').serialize(), function() { |
| | | Feng.success('保存成功'); |
| | | reloadAppGuide() |
| | | }) |
| | | }; |
| | | window.uploadBanner = function() { |
| | | var f = $('#bannerFile')[0].files[0]; |
| | | if (!f) { |
| | | Feng.info('请选择图片'); |
| | | return |
| | | } |
| | | var d = new FormData(); |
| | | d.append('file', f); |
| | | $.ajax({ |
| | | url: Feng.ctxPath + '/mgr/uploadFile', |
| | | data: d, |
| | | type: 'POST', |
| | | contentType: false, |
| | | processData: false, |
| | | success: function(url) { |
| | | $('#bannerUrl').val(url); |
| | | $('#uploadTip').text('上传成功') |
| | | } |
| | | }) |
| | | }; |
| | | window.addBanner = function() { |
| | | if (!$('#bannerUrl').val()) { |
| | | Feng.info('请先上传图片'); |
| | | return |
| | | } |
| | | $.post(Feng.ctxPath + '/appGuide/banner', { |
| | | imageUrl: $('#bannerUrl').val(), |
| | | title: $('#bannerTitle').val(), |
| | | sort: $('#bannerSort').val() || 0, |
| | | enabled: 1 |
| | | }, function() { |
| | | reloadAppGuide(); |
| | | $('#bannerUrl,#bannerTitle,#bannerSort,#bannerFile').val('') |
| | | }) |
| | | }; |
| | | window.deleteBanner = function(id) { |
| | | $.post(Feng.ctxPath + '/appGuide/banner/delete', { |
| | | id: id |
| | | }, reloadAppGuide) |
| | | }; |
| | | window.createQr = function() { |
| | | var o = $('#siteSelect option:selected'), |
| | | pageName = 'com.sinata.wepark.ui.home.PlaceDetailActivity'; |
| | | if (!o.val()) { |
| | | Feng.info('请选择场地'); |
| | | return |
| | | } |
| | | $.post(Feng.ctxPath + '/appGuide/siteQr', { |
| | | storeId: o.data('store'), |
| | | siteId: o.val(), |
| | | name: o.text(), |
| | | pageName: pageName |
| | | }, function(r) { |
| | | if (!r.success) { |
| | | Feng.error(r.message); |
| | | return |
| | | } |
| | | Feng.success('已生成'); |
| | | reloadAppGuide() |
| | | }) |
| | | }; |
| | | window.changeQr = function(id, enabled) { |
| | | $.post(Feng.ctxPath + '/appGuide/siteQr/status', { |
| | | id: id, |
| | | enabled: enabled |
| | | }, reloadAppGuide) |
| | | }; |
| | | $.get(Feng.ctxPath + '/appGuide/sites', function(s) { |
| | | $('#siteSelect').append((s || []).map(function(x) { |
| | | return '<option value="' + x.id + '" data-store="' + x.storeId + '">[' + x.storeId + '] ' + esc(x.name) + '</option>' |
| | | }).join('')) |
| | | }); |
| | | $(reloadAppGuide); |
| | | window.deleteQr = function(id) { |
| | | if (!confirm('确认删除该场地订场二维码?')) return; |
| | | $.post(Feng.ctxPath + '/appGuide/siteQr/delete', { |
| | | id: id |
| | | }, reloadAppGuide) |
| | | }; |
| | | })(); |
| | |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.*; |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** 对外下载引导页与 App 扫码解析接口。 */ |
| | | @RestController |
| | | @Api(tags = "APP下载引导与场地二维码") |
| | | public class AppGuideController { |
| | | @Autowired private AppGuideConfigMapper appGuideConfigMapper; |
| | | @Autowired private AppGuideBannerMapper appGuideBannerMapper; |
| | | @Autowired private QrLinkMapper qrLinkMapper; |
| | | @Autowired private TokenUtil tokenUtil; |
| | | @Autowired |
| | | private AppGuideConfigMapper appGuideConfigMapper; |
| | | |
| | | @Autowired |
| | | private AppGuideBannerMapper appGuideBannerMapper; |
| | | |
| | | @Autowired |
| | | private QrLinkMapper qrLinkMapper; |
| | | |
| | | @Autowired |
| | | private TokenUtil tokenUtil; |
| | | |
| | | // @GetMapping("/q/{shortCode}") |
| | | // public void guidePage(@PathVariable String shortCode, HttpServletRequest request, |
| | |
| | | @ApiOperation("获取下载引导页配置(无需登录)") |
| | | public ResultUtil<Map<String, Object>> guideConfig(String code) { |
| | | AppGuideConfig config = appGuideConfigMapper.selectOne(new QueryWrapper<AppGuideConfig>() |
| | | .eq("enabled", EnableStatusEnum.ENABLED.getCode()).orderByDesc("updateTime").last("limit 1")); |
| | | if (config == null) return ResultUtil.error("下载引导页尚未配置"); |
| | | .eq("enabled", EnableStatusEnum.ENABLED.getCode()) |
| | | .orderByDesc("updateTime") |
| | | .last("limit 1")); |
| | | if (config == null) { |
| | | return ResultUtil.error("下载引导页尚未配置"); |
| | | } |
| | | QrLink link = validLink(code); |
| | | if (code != null && !code.isEmpty() && link == null) return ResultUtil.error("二维码无效或已失效"); |
| | | if (code != null && !code.isEmpty() && link == null) { |
| | | return ResultUtil.error("二维码无效或已失效"); |
| | | } |
| | | Map<String, Object> data = new HashMap<>(); |
| | | data.put("appName", StringUtils.isNotBlank(config.getAppName()) ? config.getAppName() : "玩湃生活"); |
| | | data.put("androidPackage", StringUtils.isNotBlank(config.getAndroidPackage()) ? config.getAndroidPackage() : "com.sinata.wepark"); |
| | |
| | | data.put("androidDeepLink", config.getAndroidDeepLink() + (StringUtils.isNotBlank(code) ? "?route=" + code : "")); |
| | | data.put("iosDeepLink", "weparklife://app" + (StringUtils.isNotBlank(code) ? "?code=" + code : "")); |
| | | data.put("banners", appGuideBannerMapper.selectList(new QueryWrapper<AppGuideBanner>() |
| | | .eq("enabled", EnableStatusEnum.ENABLED.getCode()).orderByAsc("sort").orderByDesc("id"))); |
| | | .eq("enabled", EnableStatusEnum.ENABLED.getCode()) |
| | | .orderByAsc("sort") |
| | | .orderByDesc("id"))); |
| | | // if (link != null) data.put("target", target(link)); |
| | | return ResultUtil.success(data); |
| | | } |
| | |
| | | @ApiOperation("App扫码解析二维码") |
| | | public ResultUtil<Map<String, Object>> resolve(String code) { |
| | | QrLink link = validLink(code); |
| | | if (link == null) return ResultUtil.error("二维码无效或已失效"); |
| | | if (link == null) { |
| | | return ResultUtil.error("二维码无效或已失效"); |
| | | } |
| | | return ResultUtil.success(target(link)); |
| | | } |
| | | |
| | | private QrLink validLink(String code) { |
| | | if (code == null || !code.matches("[A-Za-z0-9]{6,32}")) return null; |
| | | QrLink link = qrLinkMapper.selectOne(new QueryWrapper<QrLink>().eq("shortCode", code).last("limit 1")); |
| | | if (code == null || !code.matches("[A-Za-z0-9]{6,32}")) { |
| | | return null; |
| | | } |
| | | QrLink link = qrLinkMapper.selectOne(new QueryWrapper<QrLink>() |
| | | .eq("shortCode", code) |
| | | .last("limit 1")); |
| | | if (link == null || !Integer.valueOf(1).equals(link.getEnabled()) |
| | | || (link.getExpiresAt() != null && link.getExpiresAt().before(new Date()))) return null; |
| | | || (link.getExpiresAt() != null && link.getExpiresAt().before(new Date()))) { |
| | | return null; |
| | | } |
| | | return link; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | private Object parseJson(String text) { |
| | | if (text == null || text.trim().isEmpty()) return Collections.emptyMap(); |
| | | try { return JSON.parseObject(text); } catch (Exception ignored) { return Collections.emptyMap(); } |
| | | if (text == null || text.trim().isEmpty()) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | try { |
| | | return JSON.parseObject(text); |
| | | } catch (Exception ignored) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.dsh.other.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableLogic; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | |
| | | import java.util.Date; |
| | | |
| | | /** APP 下载引导宣传图实体,对应表 t_app_guide_banner。 */ |
| | | @Getter |
| | | @Setter |
| | | @TableName("t_app_guide_banner") |
| | | @ApiModel(description = "APP 下载引导宣传图") |
| | | public class AppGuideBanner { |
| | | @TableId(type = IdType.ID_WORKER) |
| | | @ApiModelProperty("主键 ID") |
| | | private Long id; |
| | | @TableField("imageUrl") private String imageUrl; |
| | | |
| | | @TableField("imageUrl") |
| | | @ApiModelProperty("宣传图地址") |
| | | private String imageUrl; |
| | | |
| | | @ApiModelProperty("宣传图标题") |
| | | private String title; |
| | | |
| | | @ApiModelProperty("排序值") |
| | | private Integer sort; |
| | | |
| | | @ApiModelProperty("启用状态:0-停用,1-启用") |
| | | private Integer enabled; |
| | | |
| | | /** 逻辑删除标识:0-正常,1-删除。 */ |
| | | @TableLogic |
| | | @TableField("isDeleted") |
| | | @ApiModelProperty("逻辑删除标识:0-正常,1-删除") |
| | | private Integer isDeleted; |
| | | /** 创建时间。 */ @TableField("createTime") private Date createTime; |
| | | |
| | | /** 创建时间。 */ |
| | | @TableField("createTime") |
| | | @ApiModelProperty("创建时间") |
| | | private Date createTime; |
| | | } |
| | |
| | | package com.dsh.other.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableLogic; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | |
| | |
| | | @Getter |
| | | @Setter |
| | | @TableName("t_app_guide_config") |
| | | @ApiModel(description = "APP 下载引导配置") |
| | | public class AppGuideConfig { |
| | | /** 主键ID(雪花算法生成)。 */ @TableId(type = IdType.ID_WORKER) |
| | | /** 主键ID(雪花算法生成)。 */ |
| | | @TableId(type = IdType.ID_WORKER) |
| | | @ApiModelProperty("主键 ID") |
| | | private Long id; |
| | | @TableField("appName") private String appName; |
| | | @TableField("appScheme") private String appScheme; |
| | | @TableField("androidPackage") private String androidPackage; |
| | | @TableField("iosPackage") private String iosPackage; |
| | | @TableField("iosStoreUrl") private String iosStoreUrl; |
| | | @TableField("androidDefaultUrl") private String androidDefaultUrl; |
| | | @TableField("androidMarketUrls") private String androidMarketUrls; |
| | | @TableField("apkUrl") private String apkUrl; |
| | | @TableField("androidDeepLink") private String androidDeepLink; |
| | | @TableField("iosDeepLink") private String iosDeepLink; |
| | | @TableField("guideDomain") private String guideDomain; |
| | | |
| | | @TableField("appName") |
| | | @ApiModelProperty("APP 名称") |
| | | private String appName; |
| | | |
| | | @TableField("appScheme") |
| | | @ApiModelProperty("APP 自定义 Scheme") |
| | | private String appScheme; |
| | | |
| | | @TableField("androidPackage") |
| | | @ApiModelProperty("Android 应用包名") |
| | | private String androidPackage; |
| | | |
| | | @TableField("iosPackage") |
| | | @ApiModelProperty("iOS 应用包名") |
| | | private String iosPackage; |
| | | |
| | | @TableField("iosStoreUrl") |
| | | @ApiModelProperty("iOS App Store 地址") |
| | | private String iosStoreUrl; |
| | | |
| | | @TableField("androidDefaultUrl") |
| | | @ApiModelProperty("Android 默认应用市场地址") |
| | | private String androidDefaultUrl; |
| | | |
| | | @TableField("androidMarketUrls") |
| | | @ApiModelProperty("Android 品牌应用市场地址 JSON") |
| | | private String androidMarketUrls; |
| | | |
| | | @TableField("apkUrl") |
| | | @ApiModelProperty("APK 下载地址") |
| | | private String apkUrl; |
| | | |
| | | @TableField("androidDeepLink") |
| | | @ApiModelProperty("Android 深度链接") |
| | | private String androidDeepLink; |
| | | |
| | | @TableField("iosDeepLink") |
| | | @ApiModelProperty("iOS 深度链接") |
| | | private String iosDeepLink; |
| | | |
| | | @TableField("guideDomain") |
| | | @ApiModelProperty("二维码引导域名") |
| | | private String guideDomain; |
| | | |
| | | @ApiModelProperty("启用状态:0-停用,1-启用") |
| | | private Integer enabled; |
| | | |
| | | /** 逻辑删除标识:0-正常,1-删除。 */ |
| | | @TableLogic |
| | | @TableField("isDeleted") |
| | | @ApiModelProperty("逻辑删除标识:0-正常,1-删除") |
| | | private Integer isDeleted; |
| | | @TableField("updateTime") private Date updateTime; |
| | | |
| | | @TableField("updateTime") |
| | | @ApiModelProperty("更新时间") |
| | | private Date updateTime; |
| | | } |
| | |
| | | package com.dsh.other.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableLogic; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | |
| | | import java.util.Date; |
| | | |
| | | /** 二维码短链接实体,对应表 t_qr_link。 */ |
| | | @Getter |
| | | @Setter |
| | | @TableName("t_qr_link") |
| | | @ApiModel(description = "二维码短链接") |
| | | public class QrLink { |
| | | @TableId(type = IdType.ID_WORKER) |
| | | @ApiModelProperty("主键 ID") |
| | | private Long id; |
| | | @TableField("shortCode") private String shortCode; |
| | | |
| | | @TableField("shortCode") |
| | | @ApiModelProperty("二维码短码") |
| | | private String shortCode; |
| | | |
| | | @ApiModelProperty("二维码类型") |
| | | private String type; |
| | | @TableField("storeId") private Integer storeId; |
| | | @TableField("siteId") private Integer siteId; |
| | | @TableField("bookingId") private Integer bookingId; |
| | | |
| | | @TableField("storeId") |
| | | @ApiModelProperty("关联门店 ID") |
| | | private Integer storeId; |
| | | |
| | | @TableField("siteId") |
| | | @ApiModelProperty("关联场地 ID") |
| | | private Integer siteId; |
| | | |
| | | @TableField("bookingId") |
| | | @ApiModelProperty("关联预约订单 ID") |
| | | private Integer bookingId; |
| | | |
| | | @ApiModelProperty("二维码名称") |
| | | private String name; |
| | | |
| | | @ApiModelProperty("投放渠道") |
| | | private String channel; |
| | | @TableField("pageName") private String pageName; |
| | | @TableField("expiresAt") private Date expiresAt; |
| | | |
| | | @TableField("pageName") |
| | | @ApiModelProperty("跳转页面名称") |
| | | private String pageName; |
| | | |
| | | @TableField("expiresAt") |
| | | @ApiModelProperty("失效时间") |
| | | private Date expiresAt; |
| | | |
| | | @ApiModelProperty("启用状态:0-停用,1-启用") |
| | | private Integer enabled; |
| | | |
| | | /** 逻辑删除标识:0-正常,1-删除。 */ |
| | | @TableLogic |
| | | @TableField("isDeleted") |
| | | @ApiModelProperty("逻辑删除标识:0-正常,1-删除") |
| | | private Integer isDeleted; |
| | | /** 创建时间。 */ @TableField("createTime") private Date createTime; |
| | | @TableField("updateTime") private Date updateTime; |
| | | |
| | | /** 创建时间。 */ |
| | | @TableField("createTime") |
| | | @ApiModelProperty("创建时间") |
| | | private Date createTime; |
| | | |
| | | @TableField("updateTime") |
| | | @ApiModelProperty("更新时间") |
| | | private Date updateTime; |
| | | } |
| | |
| | | @PostMapping("/base/appUser/updateAppUser") |
| | | void updateAppUser(AppUser appUser); |
| | | |
| | | /** Gets only the original phone for the authenticated user's site booking. */ |
| | | /** 获取已认证用户订场所需的原始手机号。 */ |
| | | @PostMapping("/internal/sensitive/site-booking/app-user/{id}/phone") |
| | | Map<String, Object> siteBookingPhone(@PathVariable("id") Integer appUserId, |
| | | @RequestHeader("X-Internal-Sensitive-Access") String sensitiveAccess); |
| | |
| | | package com.dsh.other.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.dsh.other.entity.AppGuideBanner; |
| | | public interface AppGuideBannerMapper extends BaseMapper<AppGuideBanner> {} |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | /** |
| | | * APP 下载引导宣传图数据访问层。 |
| | | * |
| | | * <p>继承 {@link BaseMapper},提供 {@link AppGuideBanner} 的基础增删改查能力。</p> |
| | | */ |
| | | @Mapper |
| | | public interface AppGuideBannerMapper extends BaseMapper<AppGuideBanner> { |
| | | } |
| | |
| | | package com.dsh.other.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.dsh.other.entity.AppGuideConfig; |
| | | public interface AppGuideConfigMapper extends BaseMapper<AppGuideConfig> {} |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | /** |
| | | * APP 下载引导配置数据访问层。 |
| | | * |
| | | * <p>继承 {@link BaseMapper},提供 {@link AppGuideConfig} 的基础增删改查能力。</p> |
| | | */ |
| | | @Mapper |
| | | public interface AppGuideConfigMapper extends BaseMapper<AppGuideConfig> { |
| | | } |
| | |
| | | package com.dsh.other.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.dsh.other.entity.QrLink; |
| | | public interface QrLinkMapper extends BaseMapper<QrLink> {} |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | /** |
| | | * 二维码短链接数据访问层。 |
| | | * |
| | | * <p>继承 {@link BaseMapper},提供 {@link QrLink} 的基础增删改查能力。</p> |
| | | */ |
| | | @Mapper |
| | | public interface QrLinkMapper extends BaseMapper<QrLink> { |
| | | } |
| | |
| | | import com.dsh.other.entity.SysAccountAuditLog; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | /** |
| | | * 账户敏感信息访问审计日志数据访问层。 |
| | | * |
| | | * <p>继承 {@link BaseMapper},提供 {@link SysAccountAuditLog} 的基础增删改查能力。</p> |
| | | */ |
| | | @Mapper |
| | | public interface SysAccountAuditLogMapper extends BaseMapper<SysAccountAuditLog> { |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.dsh.other.entity.SysAccountAuditLog; |
| | | |
| | | /** |
| | | * 账户敏感信息访问审计日志服务。 |
| | | * |
| | | * <p>继承 {@link IService},提供审计日志的基础服务能力。</p> |
| | | */ |
| | | public interface SysAccountAuditLogService extends IService<SysAccountAuditLog> { |
| | | |
| | | /** |
| | | * 追加一条审计日志。 |
| | | * |
| | | * @param auditLog 待写入的审计日志 |
| | | */ |
| | | void append(SysAccountAuditLog auditLog); |
| | | } |