最近开发了一套web敬老院专属服务平台

笔者最近喜欢用“笔者”二字称呼自己,说来也搞笑,大三以来到如今大四实习等待转正学习编程之前,笔者本人其实是文学社的成员,创作诗词与短篇小说。没想到喜欢上了编程。闲话少叙,各位见谅。

平台介绍

面向什么行业,能做什么,能为用户带来什么?

面向当今社会奋斗狂潮(其实就是绝大部分人离开农村,去往城市打工,老人留守过多)下的留守老人入住敬老院的服务,可以使老人的子女能看到敬老院的实况,老人的健康情况,老人的居住实况,敬老院的医养计划情况,护工护理工作情况。家人放心,老人舒心,可以为企业分析流水,信息管理,订单回溯,实时通过平台与老人家人联系,整合信息,修改策略,敬老院也需要互联网赋能,敬老院也需要互联网+。

平台架构:

后端架构springBoot框架,采用redis与ThreadLocal共存信息权限校验,采用线程池定时任务触发,采用智能excel表格导入与导出,采用自定义请求头信息校验身份。前端采用vue2与elementUI设计模板,结合router,axios,site等常用vue技术。

架构优势:

代码极其简洁,可读性极高,耦合度低,高度抽离公用工具,拥有独特的上下文设计,线程隔离级别用户信息保护。

平台局部页面预览

客户服务中心局部预览

 

 

 

 

 

 

 

后台管理中心局部预览

 

 

 

 

 

 

 

 

 平台架构关键技术实现干货

笔者是java全干,编写这套系统的初衷是为了一个需求,由于非笔者自己公司的工作,笔者采用的技术点广泛,代码简洁优雅,缩减业务代码的重复度,减少查库是架构最大的特性,当然也包括线程隔离级用户信息保护。

ThreadLocal的使用

本地线程也可以当做局部线程,这个变量拿到了一个引用,真实的数据存储在堆中,每一个线程拿到的都是新的,内容也都是需要重新注入的,这也就保证了线程间的数据不会互相暴露,但是每一次每一个线程运用到它的时候,都会拿到一个新的引用,那么引用也是会占据jvm内存空间的,所以引用的删除也是业务执行之后必不可少的一部分(且需要注意的是如果在请求结束后视图渲染完处理的话,如果执行过程抛出异常,又不处理响应,接口超时,引用就不会remove掉)

通过它的这些特性,我们就可以实现redis与它的存储用户信息,权限等关键公用字段,当然redis不安全我们可以仅仅把不是很重要的信息存进redis比如账户,姓名等。

笔者在平台中对ThreadLocal使用的实现代码

@Slf4j
public class AppContext {
    /*transient 保护该上下文安全 限定内存存储 不允许被传输到磁盘或者被进行网络传输*/
    static transient ThreadLocal<Map<String, String>> contextMap = new ThreadLocal<>();
    /*项目单体没有微服务架构概念 app_code作为微服务标识 所以可以给定值*/
    public final static String APP_CODE = "live";
    /*当前用户类型 employee管理员账号 与 elderSon用户账号*/
    public final static String USER_TYPE = "user_type";
    /*当前用管理员用户权限等级 admin是普通管理员权限为 1 */
    public final static String USER_ROLE_ADMIN = "user_role_admin";
    /*当前管理员用户权限等级 superAdmin是超级管理员权限为 2以上*/
    public final static String USER_ROLE_SUPER_ADMIN = "user_role_super_admin";
    /*当前用户id 对应数据库主键id值*/
    public final static String USER_ID = "user_id";
    /*当前用户姓名 对应数据库name字段值 不为空*/
    public final static String USER_NAME = "user_name";
    /*当前用户电话 对应数据库phone字段值 可为空*/
    public final static String USER_PHONE = "user_phone";
    /*当前管理员用户权限等级 对应数据库auth_level字段值 用于判断是否为管理员 以及管理员类型*/
    public final static String USER_AUTH_LEVEL = "user_auth_level";
    /*当前管理员用户职位 对应数据库unit字段值*/
    public final static String USER_UNIT = "user_unit";
    /*钉钉登录微应用信息*/
    public final static String APP_LOGIN_DING_ID = "app_login_ding_id";
    public final static String APP_LOGIN_DING_SECRET = "app_login_ding_secret";
    public final static String APP_LOGIN_RETURN_URL = "app_login_return_url";
    /*融云聊天平台应用信息*/
    public final static String CLOUD_CHAT_KEY = "cloud_chat_key";
    public final static String CLOUD_CHAT_SECRET = "cloud_chat_secret";
    public final static String CLOUD_NET_URI = "cloud_net_uri";
    public final static String CLOUD_NET_URI_RESERVE = "cloud_net_uri_reserve";

    public static Map<String, String> getContextMap() {
        return contextMap.get();
    }

    public static Map<String, String> getNewContextMap() {
        Map<String, String> map = contextMap.get();
        return map == null ? new HashMap<>() : new HashMap<>(map);
    }

    public static void setContextMap(Map<String, String> context) {
        if (context == null) {
            contextMap.remove();
        } else {
            contextMap.set(context);
        }
    }

    public static String get(String key) {
        Map<String, String> map = getContextMap();
        return map == null ? null : map.get(key);
    }

    public static String put(String key, String value) {
        Map<String, String> map = getContextMap();
        if (map == null) {
            map = new HashMap<>();
            setContextMap(map);
        }
        return map.put(key, value);
    }

    public static void setDingVerifyInfo(DingAppInfo dingAppInfo) {
        if (dingAppInfo.checkKeyWordIsNotNull(dingAppInfo)) {
            put(APP_LOGIN_DING_ID, dingAppInfo.getApp_id());
            put(APP_LOGIN_DING_SECRET, dingAppInfo.getApp_secret());
            put(APP_LOGIN_RETURN_URL, dingAppInfo.getApp_return_url());
        }
    }

    public static void setCloudChatInfo(CloudChatAppInfo cloudChatAppInfo) {
        if (cloudChatAppInfo.checkKeyWordIsNotNull(cloudChatAppInfo)) {
            put(CLOUD_CHAT_KEY, cloudChatAppInfo.getCloud_key());
            put(CLOUD_CHAT_SECRET, cloudChatAppInfo.getCloud_secret());
            put(CLOUD_NET_URI, cloudChatAppInfo.getCloud_net_uri());
            put(CLOUD_NET_URI_RESERVE, cloudChatAppInfo.getCloud_net_uri_reserve());
        }
    }

    public static void setContextMapData(Context context) {
        put(USER_TYPE, context.getUser_type());
        put(USER_ID, context.getUser_id());
        if (StringUtils.hasText(context.getUser_phone())) {
            put(USER_PHONE, context.getUser_phone());
        }
        put(USER_NAME, context.getUser_name());
        if (Objects.equals(AppContext.get(USER_TYPE), "employee")) {
            put(USER_AUTH_LEVEL, context.getUser_auth_level());
            if (Integer.parseInt(AppContext.getUserAuthLevel()) >= 2) {
                put(USER_ROLE_SUPER_ADMIN, "true");
                log.info(context.getUser_name() + "认证管理员成功");
            } else {
                log.info(context.getUser_name() + "认证职工成功");
                put(USER_ROLE_ADMIN, "true");
            }
            put(USER_UNIT, context.getUser_unit());
        }
    }

    public static String getUserId() {
        return get(USER_ID);
    }

    public static String getUserPhone() {
        return get(USER_PHONE);
    }

    public static String getUserName() {
        return get(USER_NAME);
    }

    public static String getUserAuthLevel() {
        return get(USER_AUTH_LEVEL);
    }

    public static String getUserUnit() {
        return get(USER_UNIT);
    }

    public static String getAppLoginDingId() {
        return get(APP_LOGIN_DING_ID);
    }

    public static String getAppLoginDingSecret() {
        return get(APP_LOGIN_DING_SECRET);
    }

    public static String getAppLoginReturnUrl() {
        return get(APP_LOGIN_RETURN_URL);
    }

    public static String getCloudChatKey() {
        return get(CLOUD_CHAT_KEY);
    }

    public static String getCloudChatSecret() {
        return get(CLOUD_CHAT_SECRET);
    }

    public static String getCloudNetUri() {
        return get(CLOUD_NET_URI);
    }

    public static String getCloudNetUriReserve() {
        return get(CLOUD_NET_URI_RESERVE);
    }

    public static String getAppCode() {
        return APP_CODE;
    }

    public static String getUserType() {
        return get(USER_TYPE);
    }

    public static boolean getUserRoleAdmin() {
        return Objects.equals(get(USER_ROLE_ADMIN), "true");
    }

    public static boolean getUserRoleSuperAdmin() {
        return Objects.equals(get(USER_ROLE_SUPER_ADMIN), "true");
    }

    public static boolean isAdmin() {
        return getUserRoleAdmin() || getUserRoleSuperAdmin();
    }

    public static boolean isSuperAdmin() {
        return getUserRoleSuperAdmin();
    }

    public static boolean checkLoginContext() {
        return StringUtils.hasText(AppContext.getAppLoginDingId())
                && StringUtils.hasText(AppContext.getCloudChatKey());
    }

    public static boolean checkContextUserInfo() {
        return StringUtils.hasText(AppContext.getUserType())
                && StringUtils.hasText(AppContext.getUserId())
                && StringUtils.hasText(AppContext.getUserName());
    }

    public static void fillLoginContext() {
        DingAppInfo appInfo = SpringContextHolder.getBean(DingAppInfoService.class).findAppInfo(APP_CODE);
        setDingVerifyInfo(appInfo);
        CloudChatAppInfo cloudChatAppInfo = SpringContextHolder.getBean(CloudChatAppInfoService.class).findAppInfo(APP_CODE);
        setCloudChatInfo(cloudChatAppInfo);
    }

    public static void clear() {
        contextMap.remove();
    }
}

笔者不再为大家分析代码中的某些实现逻辑(例如怎么在静态方法里还调用了组件的查库方法,如果您看过笔者的前几篇文章,您就会发现笔者就是实际问题实际分析得来的总结干货)

平台中能使用到ThreadLocal的业务场景

首先是请求的拦截器中使用,拦截器前端控制器方法(也就是开发人员口中的api)执行前需要获取令牌信息与类型信息,校验通过后填充本线程用户数据:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
                return true;
            }
            String token = request.getHeader("token");
            String user_type = request.getHeader("user_type");
            if (StringUtils.hasText(token) && StringUtils.hasText(user_type)) {
                Context context = new Context();
                if (Objects.equals(user_type, "elder_son")) {
                    ElderSon elderSon = elderSonService.getElderSonByElderSonId(token);
                    context.setContextByElderSon(elderSon);
                    return true;
                } else if (Objects.equals(user_type, "employee")) {
                    Employee employee = employeeService.getEmployeeById(token);
                    context.setContextByEmployee(employee);
                    return true;
                }
            } else if (StringUtils.hasText(user_type)) {
                return false;
            }
            return false;
        } catch (Exception e) {
            AppContext.clear();
            return false;
        } finally {
            System.gc();
        }
    }

从上述代码中我们不难看到对于预检行为的放行,这是因为笔者简单的自定义了请求头,并没有对cookie做处理,自定义的请求头除了我们跨域问题需要处理,还需要处理的就是预检行为。

之前笔者说过remove这次引用极其重要

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        AppContext.clear();
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

具体效果就是对于某些业务方法需要校验权限才能允许执行,但是这里对于员工的权限校验一般的开发人员会直接查库,查redis,但是本地线程的速度很快且可以为redis分担压力(这里举例需要校验权限的查询全部老人信息方法)

public List<Elder> findElderList() {
        if (!AppContext.isAdmin()) {
            return null;
        }
        List<Elder> list = list();
        return Objects.nonNull(list) ? list.stream().sorted(Comparator.comparing(Elder::getElder_age)).collect(Collectors.toList()) : null;
    }

无需查看,无需查redis,只需获取调用静态方法获取当前线程数据即可

定时任务工具

实现定时任务的方案有很多,目前笔者自己常用的有:

  • xxl-job

  • springboot 的 @Scheduled

  • Quartz 框架

但是这不代表就这些:

  • 使用java线程实现

  • 使用java的TimerTask

  • redis键值过期监听回调

注意最后三种不适合真实业务场景,时间误差大,四五笔者就不解释了,六的问题属于机制问题,惰性删除机制,不明白的读者可以去查阅百度。

笔者给朋友写的,涉及定时任务的业务对于准确性要求不高,故而笔者这里采取全局线程池放上线程去实现定时,也就是第四种(注意笔者进行了初次封装与实验,但不保证高并发会稳定)。

笔者自封装TaskExe定时任务下发工具实现代码

public class TaskExe {

    private static final TimerHashMap cache = TimerCacheFactory.getCache();

    private static ThreadPoolTaskExecutor executor = null;

    private static final ThreadPoolTaskExecutor USER_THREAD_POOL_TASK_EXECUTOR = getUserThreadPoolTaskExecutor();

    public TaskExe() {
    }

    public static void setTimer(String serverKey, TaskInfo taskInfo) {
        boolean sign = cache.containsKey(serverKey);
        if (!sign) {
            try {
                USER_THREAD_POOL_TASK_EXECUTOR.submit(() -> {
                    TaskInfo put = cache.put(serverKey, taskInfo);
                    if (put == null) {
                        try {
                            Thread.sleep((long)(taskInfo.getTaskTime() * 1000));
                            if (taskInfo.getType() == TaskType.TASK_DELETE_CACHE) {
                                TaskInfo remove = cache.remove(serverKey);
                                if (remove == null) {
                                    throw new Exception("计时任务删除失败");
                                }
                            } else if (taskInfo.getType() == TaskType.TASK_INTER_CACHE) {
                                boolean result = cache.endTaskInCache(serverKey, taskInfo);
                                if (!result) {
                                    throw new Exception("计时任务无法正常结束");
                                }
                            }
                        } catch (Exception var4) {
                            var4.printStackTrace();
                        }
                    }

                });
            } catch (Exception var4) {
                var4.printStackTrace();
            }

        }
    }

    public static boolean taskIsEnd(String serverKey) {
        boolean result = cache.containsKey(serverKey);
        if (result) {
            return cache.get(serverKey).getTaskTime() == 0;
        } else {
            return false;
        }
    }

    public static synchronized ThreadPoolTaskExecutor getUserThreadPoolTaskExecutor() {
        if (executor == null) {
            Class var0 = TaskExe.class;
            synchronized(TaskExe.class) {
                if (executor == null) {
                    executor = new ThreadPoolTaskExecutor();
                    int core = Runtime.getRuntime().availableProcessors();
                    executor.setCorePoolSize(core);
                    executor.setMaxPoolSize(core * 2 + 1);
                    executor.setKeepAliveSeconds(3);
                    executor.setQueueCapacity(40);
                    executor.setThreadNamePrefix("userService-thread-execute");
                    executor.setRejectedExecutionHandler(new CallerRunsPolicy());
                }
            }
        }

        return executor;
    }
}

阅读代码能力好的读者这里也发现了原理很简单,准确性也很难保证,数据一致性无法保证(所有会受到宕机影响的数据一致性操作,都不是真正的数据一致性操作)。

这里有用到TimerHashMap 这种数据结构,也是笔者封装的

public class TimerHashMap implements Map<String,TaskInfo> {
    private final static Map<String, TaskInfo> local = new HashMap<>(1000);

    public boolean endTaskInCache(String key,TaskInfo taskInfo){
        synchronized (local) {
            if (local.containsKey(key)) {
                boolean remove = local.remove(key, taskInfo);
                if (remove) {
                    taskInfo.setTaskTime(0);
                    TaskInfo put = local.put(key, taskInfo);
                    return put == null;
                }
                return false;
            }
            return false;
        }
    }


    @Override
    public int size() {
        return local.size();
    }

    @Override
    public boolean isEmpty() {
        return local.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return local.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return local.containsValue(value);
    }

    @Override
    public TaskInfo get(Object key) {
        synchronized (local) {
            return local.get(key);
        }
    }

    @Nullable
    @Override
    public TaskInfo put(String key, TaskInfo value) {
        synchronized (local){
            return local.put(key, value);
        }
    }

    @Override
    public TaskInfo remove(Object key) {
        synchronized (local){
            return local.remove(key);
        }
    }

    @Override
    public void putAll(@NotNull Map<? extends String, ? extends TaskInfo> m) {
        synchronized (local){
            local.putAll(m);
        }
    }

    @Override
    public void clear() {
        synchronized (local){
            local.clear();
        }
    }

    @NotNull
    @Override
    public Set<String> keySet() {
        synchronized (local){
            return local.keySet();
        }
    }

    @NotNull
    @Override
    public Collection<TaskInfo> values() {
        synchronized (local){
            return local.values();
        }
    }

    @NotNull
    @Override
    public Set<Entry<String, TaskInfo>> entrySet() {
        synchronized (local){
            return local.entrySet();
        }
    }
}

这段代码里笔者为了保证原子操作,防止互相影响,对更新与删除加入了同步机制

另外也能发现一种对象TaskInfo与一种枚举TaskType

@Data
public class TaskInfo {
    private String TaskServerName;
    private Integer TaskTime;
    private TaskType Type = TaskType.TASK_DELETE_CACHE;

    public TaskInfo(String serverName, Integer taskTime, TaskType type){
        this.TaskServerName = serverName;
        this.TaskTime = taskTime;
        this.Type = type;
    }
    public TaskInfo(){
    }
}
public enum TaskType {
    TASK_DELETE_CACHE,
    TASK_INTER_CACHE,
    TASK_SUPER_CACHE;

    private TaskType() {
    }
}

以及TimerCacheFactory实现的TimerHashMap的全局唯一性

public class TimerCacheFactory {
    private static volatile TimerHashMap cache = null;

    public TimerCacheFactory() {
    }

    public static TimerHashMap getCache() {
        if (cache == null) {
            Class var0 = TimerCacheFactory.class;
            synchronized(TimerCacheFactory.class) {
                if (cache == null) {
                    cache = new TimerHashMap();
                }
            }
        }

        return cache;
    }

    public static void releaseCache() {
        if (cache != null) {
            cache.clear();
        }

    }
}

笔者最近下班回家也就做了这些,谨以此文,缅怀我少睡的那些夜晚,少做的那些好梦。不管怎么说,将所思所想付诸于实践之上能获得更多的成长,心性或是技术,阅历或是认知。

照影舒月,春花渐明,四月人愁思,也不过三两春风。

可叹光阴偷渡,可叹终日忙碌。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ForestSpringH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值