• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

APP首页设计

武飞扬头像
一只醉了的猫
帮助1

学新通

这是我们APP原来的首页,整体的样式还是比较朴素的,特别在搞活动的时候,运营只能修改Banner、导航和商品推荐区的头图,来营造氛围,颇为令人诟病

为此,我们参考京东和淘宝的APP,将APP首页进行了重新设计,将其分成为这几个部分

  • 背景
  • 顶部搜索栏
  • Banner区
  • 资源位
  • 导航区
  • 营销活动区
  • 商品推荐区
  • 底部Tab按钮区

资源位可以横向切分成任意块,每一块拥有单独的点击事件。我们计划让运营可以在后台对APP首页的这八个部分进行前景图或背景图的配置,以此满足营造氛围感的需求。

这是我们预期的样子

学新通

确实好看很多吧!

为了支持这种设计,我们需要一个数据结构来存储配置

@Data
@EqualsAndHashCode(callSuper = true)
public class UiConfigurationDTO implements Serializable {

    private static final long serialVersionUID = 1L;
    /**
     * 配置名称
     */
    @ApiModelProperty("配置名称")
    private String name;

    /**
     * 平台
     * @see PlatformEnum
     */
    @ApiModelProperty("平台")
    private Integer platform;

    /**
     * 屏幕类型
     * @see ScreenTypeEnum
     */
    @ApiModelProperty("屏幕类型")
    private Integer screenType;

    /**
     * 配置类型
     */
    @EnumField(ConfigurationTypeEnum.class)
    @ApiModelProperty("配置类型")
    private Integer type;

    /**
     * 是否启用
     */
    @ApiModelProperty("是否启用")
    private Boolean useable;

    /**
     * 生效时间段 - 开始
     */
    @ApiModelProperty("生效时间段 - 开始")
    private Long startTime;

    /**
     * 生效时间段 - 结束
     */
    @ApiModelProperty("生效时间段 - 结束")
    private Long endTime;

    /**
     * 内部模块
     */
    @ApiModelProperty("内部模块")
    private List<UiModuleDTO> modules;
}
学新通
@Data
@NoArgsConstructor
public class UiModuleDTO implements Serializable, Redirect {

    private static final long serialVersionUID = 1L;

    /**
     * 模块ID
     */
    @ApiModelProperty("模块ID")
    private Long id;

    /**
     * 模块名称
     */
    @ApiModelProperty("模块名称")
    private String name;

    /**
     * 前景图ID
     */
    @ApiModelProperty("前景图ID")
    private Long foreground;

    /**
     * 前景图URL
     */
    @ApiModelProperty("前景图URL")
    @JSONField(serialize = false)
    private String foregroundUrl;

    /**
     * 背景图ID
     */
    @ApiModelProperty("背景图ID")
    private Long background;

    /**
     * 背景图URL
     */
    @ApiModelProperty("背景图URL")
    @JSONField(serialize = false)
    private String backgroundUrl;

    @ApiModelProperty("是否可见")
    private Boolean visible;

    /**
     * 跳转类型
     */
    @EnumField(YanXuanEnum.RedirectTypeEnum.class)
    @ApiModelProperty("跳转类型")
    private Integer redirectType;

    /**
     * 跳转目标
     */
    @ApiModelProperty("跳转目标")
    private String redirectTarget;

    /**
     * UI模块
     */
    @ApiModelProperty("内部模块")
    private List<UiModuleDTO> modules;

    public UiModuleDTO(Long id, String name) {
        this.id = id;
        this.name = name;
        this.foreground = 0L;
        this.background = 0L;
        this.visible = Boolean.FALSE;
        this.redirectType = 0;
        this.redirectTarget = "";
        this.modules = new ArrayList<>();
    }
}
学新通

考虑到IOS和Android之间UI规划和屏幕样式可能会有差别,我们在配置上标记了适用的系统和屏幕类型,而真正存储配置内容的UiModuleDTO类则采用了自由嵌套的结构。

很明显,UI配置是一个读多写少的场景,我们希望它可以提供尽可能好的并发读性能,所以我们打算将其存储在Redis中。另外考虑到运营会同时创建多个UI配置,有定期生效的需求,我们设计了这么几个Key

/**
 * [id]指代具体的id值
 * [uiConfiguration]指代uiConfiguration实例对象
 * [platform]指标适用平台
 * [screenType]指代适用屏幕样式
 * [startTime]指代uiConfiguration实例对象
 * [endTime]指代uiConfiguration实例对象
 */

// key1:用String存储ID对应的UI配置实例对象
SET uiConfiguration:id:[id] [uiConfiguration]

// key2:用zset存储ID对应的开始生效时间
ZADD uiConfiguration:[platform]:[screenType]:startTime [id] [startTime]

// key3:用zset存储ID对应的结束生效时间
ZADD uiConfiguration:[platform]:[screenType]:endTime [id] [endTime]
学新通

APP请求UI配置内容时,会携带platform和screenType参数,后端根据这两个参数可以构造出key2和key3。然后使用ZRANGEBYSCORE命令在key2中找出结束生效时间小于当前时间的失效ID集合,如果失效ID集合不为空,则把这些ID在key1、key2和key3中对应的值删除。

再使用ZRANGEBYSCORE命令在key3中找出开始生效时间小于当前时间的ID集合,若ID集合为空,说明没有可用的配置,反之则取开始时间最早的一个ID,然后在key1中找出具体的配置内容返回给APP

这里面有一个zset的小细节,当score值相同时,zset会根据value进行排序,这里使用的是字典序。我们预期的是当开始生效时间相同时,应该取ID小的那条记录。但如果ID是99、100之类的数字时,你会发现实际取到的是100,因为根据字典序的规则100要比99小。为了规避这个问题,我们在实际开发中,将ID处理成了统一长度的19位字符串(在数字ID前面填充0)

有同学可能会觉得每次查询的时候都去删除缓存很不合理,但大多数时候查询出的失效ID集合都是空哈,所以实际去删除缓存的次数不会多。

代码参考

@Slf4j
@Component
public class UiConfigurationDubboServiceImpl implements UiConfigurationDubboService {

    private final static List<UiModuleDTO> EMPTY_MODULES;

    private final UiConfigurationCache cache;

    static {
        EMPTY_MODULES = new ArrayList<>();
        EMPTY_MODULES.add(new UiModuleDTO(1L, "搜索栏顶部"));
        EMPTY_MODULES.add(new UiModuleDTO(2L, "首页底图"));
        EMPTY_MODULES.add(new UiModuleDTO(3L, "资源位"));
        EMPTY_MODULES.add(new UiModuleDTO(4L, "首页导航区"));
        EMPTY_MODULES.add(new UiModuleDTO(5L, "固定活动区1"));
        EMPTY_MODULES.add(new UiModuleDTO(6L, "固定活动区2"));
    }


	/*
     * 此处省略N行代码
     */

    /**
     * UI配置缓存类
     */
    private static class UiConfigurationCache {
        private final static String START_TIME_SET_KEY = "uiConfiguration:startTime:";
        private final static String END_TIME_SET_KEY = "uiConfiguration:endTime:";
        private final static String CONFIGURATION_KEY_PREFIX = "uiConfiguration:id:";

        private final RedisTemplate<String, Object> redisTemplate;

        public UiConfigurationCache(RedisTemplate<String, Object> redisTemplate) {
            this.redisTemplate = redisTemplate;
        }

        private void addCache(UiConfigurationDTO config) {
            String prefix = getPrefix(config);
            Long id = config.getId();
            String strId = String.format("9d", id);
            if (ConfigurationTypeEnum.TEMPORARY.getCode().equals(config.getType())) {
                redisTemplate.opsForZSet().add(START_TIME_SET_KEY   prefix, strId, config.getStartTime());
                redisTemplate.opsForZSet().add(END_TIME_SET_KEY   prefix, strId, config.getEndTime());
                redisTemplate.opsForValue().set(CONFIGURATION_KEY_PREFIX   prefix   ":"   id, config);
            } else {
                redisTemplate.opsForValue().set(CONFIGURATION_KEY_PREFIX   prefix   ":default", config);
            }
        }

        private String getPrefix(UiConfigurationDTO config) {
            String prefix = "";
            if (Objects.equals(config.getPlatform(),PlatformEnum.ANDROID.getCode())){
                prefix = prefix   config.getPlatform();
            } else {
                prefix = prefix   config.getPlatform()  ":"  config.getScreenType();
            }
            return prefix;
        }

        private void delCache(UiConfigurationDTO config) {
            String prefix = getPrefix(config);
            Long id = config.getId();
            String strId = String.format("9d", id);
            if (ConfigurationTypeEnum.TEMPORARY.getCode().equals(config.getType())) {
                redisTemplate.opsForZSet().remove(START_TIME_SET_KEY  prefix, strId);
                redisTemplate.opsForZSet().remove(END_TIME_SET_KEY   prefix, strId);
                redisTemplate.delete(CONFIGURATION_KEY_PREFIX   prefix   ":"   id);
            } else {
                redisTemplate.delete(CONFIGURATION_KEY_PREFIX   prefix   ":default");
            }
        }

        private UiConfigurationDTO getConfiguration(Integer platform) {
            long nowTime = System.currentTimeMillis();

            // 获取已失效的配置
            Set<Object> disableConfigurationIds = redisTemplate.opsForZSet().rangeByScore(END_TIME_SET_KEY   platform, 0, nowTime - 1);
            if (CollectionUtil.isNotEmpty(disableConfigurationIds)) {
                // 移除失效的UI配置缓存
                disableConfigurationIds.forEach(id -> {
                    String strId = String.format("9d", id);
                    redisTemplate.opsForZSet().remove(START_TIME_SET_KEY   platform, strId);
                    redisTemplate.opsForZSet().remove(END_TIME_SET_KEY   platform, strId);
                    redisTemplate.delete(CONFIGURATION_KEY_PREFIX   platform   ":"   id);
                });
            }

            Set<Object> configIds = redisTemplate.opsForZSet().rangeByScore(START_TIME_SET_KEY   platform, 0, nowTime);
            if (CollectionUtil.isEmpty(configIds)) {
                return (UiConfigurationDTO) redisTemplate.opsForValue().get(CONFIGURATION_KEY_PREFIX   platform   ":default");
            } else {
                Optional<Object> optional = configIds.stream().findFirst();
                String strId = (String) optional.orElseThrow(() -> new RuntimeException("未知异常"));
                long id = Long.parseLong(strId);
                return (UiConfigurationDTO) redisTemplate.opsForValue().get(CONFIGURATION_KEY_PREFIX   platform   ":"   id);
            }
        }

        private UiConfigurationDTO getConfiguration(Integer platform, Integer screenType) {
            long nowTime = System.currentTimeMillis();
            UiConfigurationDTO config = new UiConfigurationDTO();
            config.setPlatform(platform);
            config.setScreenType(screenType);
            String prefix = getPrefix(config);
            // 获取已失效的配置
            Set<Object> disableConfigurationIds = redisTemplate.opsForZSet().rangeByScore(END_TIME_SET_KEY   prefix, 0, nowTime - 1);
            if (CollectionUtil.isNotEmpty(disableConfigurationIds)) {
                // 移除失效的UI配置缓存
                disableConfigurationIds.forEach(strId -> {
                    long id = Long.parseLong((String) strId);
                    redisTemplate.opsForZSet().remove(START_TIME_SET_KEY   prefix, strId);
                    redisTemplate.opsForZSet().remove(END_TIME_SET_KEY   prefix, strId);
                    redisTemplate.delete(CONFIGURATION_KEY_PREFIX   prefix   ":"   id);
                });
            }

            Set<Object> configIds = redisTemplate.opsForZSet().rangeByScore(START_TIME_SET_KEY   prefix, 0, nowTime);
            if (CollectionUtil.isEmpty(configIds)) {
                return (UiConfigurationDTO) redisTemplate.opsForValue().get(CONFIGURATION_KEY_PREFIX   prefix   ":default");
            } else {
                Optional<Object> optional = configIds.stream().findFirst();
                String strId = (String) optional.orElseThrow(() -> new RuntimeException("未知异常"));
                long id = Long.parseLong(strId);
                return (UiConfigurationDTO) redisTemplate.opsForValue().get(CONFIGURATION_KEY_PREFIX   prefix   ":"   id);
            }
        }
    }
}
学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhiabeic
系列文章
更多 icon
同类精品
更多 icon
继续加载