likes
comments
collection
share

一个活动限制这个限制那个!有了它!你在XX就是最亮的仔啦!!!

作者站长头像
站长
· 阅读数 39

上次写的简单的活动条件限制案例、后续我把它升级成了一个模块的形式、方便统一管理活动的限制条件、主要还是存储活动条件的值、具体逻辑还得自己编写。

数据库的设计

活动限制表【activity_limit】

字段类型描述
idbigint活动限制id
namevarbinary活动名称、一般用于描述
keyvarbinary活动key、可以对应活动表名
limit_template_idbigint限制模板id

限制枚举表【limit_enum】

字段类型描述
idbigint枚举id
limit_template_idbigint模板id
describevarbinary限制描述
valuevarbinary限制值
create_timedatetime创建时间

限制模板表【limit_template】

字段类型描述
idbigint模板id
namevarchar模板名称
keyvarbinary模板key
typeint模板类型、1:单选、2:下拉单选、3、多选多选(或者)、4、多选(并且、或者)
create_timedatetime创建时间

存储限制表【limit_value】

字段类型描述
idbigint限制值id
activity_idbigint活动id或其他id
activity_limit_idbigint限制活动的id
valuevarchar限制值
condition_valueint限制多选(0或者、1并且)
create_timedatetime创建时间

以上是一些表的设计、但是要注意的是limit_value表通过活动Id去查询数据的话查询问题、就是如我有两个不同的活动保存限制的时候肯定会碰上两边活动Id值一模一样的情况、这个时候通过Id查就会把其他活动的限制也查出来、所以要关联其他表进行查询。

编写模块

编写模块主要能学到的就是一个Spring Boot中的自动装配的编写逻辑思想、看过一些Spring Boot自动装配的都应该知道这个META-INF/spring.factories文件、其中文件内容就包含自动配置bean的配置信息。

简单理解spring.factories就是为了扫描第三方类文件注入成Bean的一种方式、避免了我们在父级中使用@ComponentScan("xx.xxx")去标子级类的一个全路径。

举个例子、如别人写了个第三方工具、我自己项目中使用他这个工具我还要@ComponentScan("xx.xxx")指定去手动扫描他的路径下标记了@Component注解的类到容器中这不很麻烦。

我们只需要在编写的第三方项目中resources下去创建一个META-INF文件夹下的spring.factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.limit.config.CustomAutoConfiguration,\
  com.limit.config.CustomAfterAutoConfiguration

可以理解为配置到这里面的类也是一个配置类就和普通类使用@Configuration注解当前类和其中@Bean标记的对象方法都会注入到是Spring容器中。

而这些自动配置的类中会配合一些条件注解来控制Bean的注入、上次我写的条件注解的演示案例可以去看看。

/**
 * 自动配置后置配类、生成redis_mysql类型时操作数据库方法、下方@ConditionalOnProperty()就是条件注解之一
 * 表明了Properties中的limit.type = redis_mysql 才会将这个配置类进行配置到容器中。
 * @Author 突突突突突
 * @blog https://juejin.cn/user/844892408381735
 * @Date 2023/3/5 13:32
 */
@ConditionalOnProperty(prefix = "limit", name = "type", havingValue = "redis_mysql")
public class CustomAfterAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer<Object> fastJsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        template.setEnableTransactionSupport(true);
        return template;
    }
}

我们自定义的第三方工具可能会使用Redis、而默认的RedisTemplate的序列化器存储数据到Redis中会产生乱码、而我们在第三方工具项目中也去定义一个这个RedisTemplate对象前提是Bean容器中没有redisTemplate名称的对象存在、才能将RedisTemplate注入到容器中使用、而我们自己的项目中也去创建一个RedisTemplate对象的话、我们第三方工具中定义的RedisTemplate对象将不会进行注入到容器中。

从上描述可以看来是有新的配置、程序就用新的配置、没有新的配置、程序就用最底层定义好的配置(默认配置)。

代码编写

抽象出最顶层的待实现方法、不同的限制去(实现/继承)重写对应限制的逻辑部分。

/**
 * 活动限制的抽象
 *
 * @param <LimitType> 限制数据的类型
 * @Author 突突突突突
 * @blog https://juejin.cn/user/844892408381735
 * @Date 2023/3/5 14:25
 */
public interface LimitAdapter<LimitType> {
    /**
     * 限制的key、用于获取限制的值
     *
     * @return 限制的唯一名称
     */
    String limitKye();

    /**
     * 限制的名称、用于描述当前限制类型
     *
     * @return 限制名称
     */
    String limitName();

    /**
     * 是否进行要进行逻辑处理。
     *
     * @param activityKey 活动唯一值
     * @param activityId  活动id
     * @return true 无限制跳过操作、false 有限制(执行对应的操作如 execut(x,x))
     */
    Boolean isMust(String activityKey, Long activityId);

    /**
     * 得到限制的值
     *
     * @param activityKey 活动唯一值
     * @param activityId  活动id
     * @return 限制的value值
     */
    LimitType getLimitValue(String activityKey, Long activityId);

    /**
     * 执行处理【用户是否符合限制】、自定义限制规则。
     *
     * @param activityKey 活动唯一值
     * @param activityId  活动id
     * @return true 符合、false 不符合。
     */
    Boolean execute(String activityKey, Long activityId);
}

由于限制要么就是单选要么就是多选、这边会再抽象出两个类来表示、分别是SingleLimit/MultiLimit

/**
 * 单选限制
 *
 * @Author https://juejin.cn/user/844892408381735
 * @Date 2023/3/5 18:48
 */
@Slf4j
public abstract class SingleLimit implements LimitAdapter<LimitValueBO> {

    @Resource
    private OperateLimitValue operateLimitValue;

    @Override
    public Boolean isMust(String activityKey, Long id) {
        LimitValueBO limitValueBO = getLimitValue(activityKey, id);
        if (log.isDebugEnabled()) {
            log.debug("执行isMust方法、传递参数为[{},{}]、操作[{}]、类型[{}]、限制值[{}]", activityKey, id, limitName(), limitKye(), limitValueBO);
        }
        return ObjectUtils.isEmpty(limitValueBO) || ObjectUtils.isEmpty(limitValueBO.getValue());
    }


    @Override
    public LimitValueBO getLimitValue(String activityKey, Long id) {
        return operateLimitValue.getLimitValueByLimitTemplateKey(activityKey, limitKye(), id);
    }

}
/**
 * 多选选限制、代码和单选限制目前一样。
 * @Author https://juejin.cn/user/844892408381735
 * @Date 2023/3/5 18:48
 */
@Slf4j
public abstract class MultiLimit implements LimitAdapter<LimitValueBO> {
    ...
}

上方代码都是第三方限制工具包提供的默认的待实现方法、具体实现逻辑还得自己去实现、下方代码就是当前项目需要对自己的限制的实现。

编写好对应限制的实现类之后、我们来编写测试的Controller

@Slf4j
@Controller
public class FeeBasedActivitiesController {
    private String activityKey = "fee_based_activities";
    
    // 存储所有限制的bean对象
    @Resource
    private List<LimitAdapter<?>> limitAdapter;
    /**
    *  模拟参与限制
    * @param activityId 活动编号
    */
    @RequestMapping(value = "/participate/{activityId}", method = RequestMethod.POST)
    public String participateActivityPost(Model model,@PathVariable Long activityId) {
        Activity activity = activityList.get(activityId);
        if (null == activity) {
            model.addAttribute("msg", "没有该活动。");
            return "error";
        }
        // 模拟用户信息
        Account account = new Account();
        account.setId(666L);
        account.setName("小明");
        account.setIsVip(1);
        AccountContext.setAccount(account);
        // 遍历所有限制
        for (LimitAdapter<?> adapter : limitAdapter) {
            // 是否跳过限制
            if (adapter.isMust(activityKey, activityId)) {
                continue;
            }
            if (!adapter.execute(activityKey, activityId)) {
                model.addAttribute("msg", String.format("不满足[%s]的限制",adapter.limitName()));
                return "error";
            }
        }
        return "redirect:/participate/"+activityId;
    }
}

以上代码主要是抽象一些概念、利用Spring容器化将限制注入到容器中提供使用、上方代码有一个关键点要注意的就是存储在数据库中的限制的数据、而不是用户的数据。

说到数据库中存储的数据、项目没有很大访问量的情况下直接查数据库这种是没啥问题的、但是一般情况下都会在加上一层Redis缓存、加了缓存后又会出现其他概率性的问题比如“如何保证缓存一致性”这个问题可以去看网上写的解决方案没有最优只有减少概率性。(代码中操作缓存的没有贴代码在文章中)

前端限制渲染展示

为了保证限制模块的可用性、可以拆分成HTML片段、需要的时候就引用片段。 前后不分离项目来做一个演示。

定义公共的模块

<tr th:each="limit : ${limitTemplate}">
    <td th:if="${limit.type eq 1}">
        <div th:if="${null == limitTemplateValue}">
            <input type="radio" th:value="${l.value}" th:text="${l.describe}"
                   th:name="'['+${limit.key}+']'+'.limitValue'"
                   th:checked="${i.first}" th:each="l,i : ${limit.limitEnumBOS}">
        </div>
        <div th:if="${null != limitTemplateValue}">
            <span th:each="j : ${limitTemplateValue}" th:if="${limit.key == j.limitTemplateKey}">
                <input type="hidden" th:name="'['+${limit.key}+']'+'.id'" th:value="${j.id}">
                <input type="radio" th:value="${l.value}" th:text="${l.describe}"
                       th:name="'['+${limit.key}+']'+'.limitValue'"
                       th:checked="${null != j.value && l.value == j.value}"
                       th:each="l : ${limit.limitEnumBOS}">
            </span>
        </div>
    </td>
    ...... 其他类型的基本都是重复操作、现在中取其中类型为1的
</tr>
<div th:replace="slot/limitTemplate::limitTemplate"></div>

然后使用thymeleaf中对应的插入公共模块标签标记到对应的页面中、然后Controller中可以这样写。

@RequestMapping("/add")
public String add(Model model) {
    List<LimitTemplateBO> limitTemplateBOS = operateLimitTemplate.getLimitTemplateByActivityLimitKey(activityKey);
    model.addAttribute("limitTemplate", limitTemplateBOS);
    return "fee_based_activities/add";
}

将数据查询出来塞入Model中渲染到页面上、如果是前后分离的话就异步查询数据渲染到前端。

还有一个要注意的点就是修改页是需要把保存的数据也查出来并渲染的。

@RequestMapping("/update/{id}")
public String update(Model model, @PathVariable Long id) {
    Activity activity = activityList.get(id);
    if (null == activityList.get(id)) {
        model.addAttribute("msg", "没有该活动。");
        return "error";
    }
    // 模板限制
    List<LimitTemplateBO> limitTemplateBOS = operateLimitTemplate.getLimitTemplateByActivityLimitKey(activityKey);
    model.addAttribute("limitTemplate", limitTemplateBOS);
    // 模板限制值
    List<LimitValueBO> limitValueBOS = operateLimitValue.getLimitValue(activityKey, id);
    model.addAttribute("limitTemplateValue", limitValueBOS);

    model.addAttribute("activity", activity);
    return "fee_based_activities/update";
}

最后就到了保存数据到数据库的操作了。

@Autowired
private OperateLimitValue operateLimitValue;

@RequestMapping(value = "/add", method = RequestMethod.POST)
public String addPost(HttpServletRequest request,Model model) {
    try {
        operateLimitValue.saveLimitValue(request, activityKey, activity.getId());
    } catch (Exception e) {
        log.error("添加异常",e);
        model.addAttribute("msg", e.getMessage());
        return "error";
    }
    return "redirect:/index";
}

saveLimitValue主要保存就是这个方法里面、将request传递进去就能拿到请求中的提交参数值、做一些逻辑判断处理并校验传递的值是否和枚举值一致。

举个例子:

String val = request.getParameter("[" + key + "].limitValue")

其中我的key就是活动限制的key、这样的话就能找到表单中我限制条件对应的值、saveLimitValue方法中代码有点多就不贴出来了。

小结

写这个模块的原因就是因为某些场景下需要限制某些用户是否能参与、有了限制模块就很好管理限制、不需要在往活动表中加字段表明限制的值、避免后续大量活动都单独往自己的表中表明限制。

因为上面代码限制类全是自动注入到一个集合中的、所以搞不了某个限制与某个限制要一起为成立、或某一个限制成立就行、这里没有限制与限制之间的绑定关系、其实这个还得看需求场景、设计肯定是能设计出来的。

要代码的可以找我!!!、发现问题或有新创意的可以滴滴我!!!

转载自:https://juejin.cn/post/7208200530307366949
评论
请登录