dothetrick

dothetrick 查看完整档案

上海编辑  |  填写毕业院校某互联网公司  |  高级开发 编辑 github.com/dothetrick 编辑
编辑

摸爬滚打中的程序员
公众号:技术Fun

个人动态

dothetrick 发布了文章 · 2月23日

用uni-app和springboot做出的高效记忆小程序,技术点总结

临时起意

老早前就听说过一些高效记忆的方法,其中听的最多的就是艾宾浩斯记忆法和费曼学习法。

恰好赶上过年放假,就在想除了吃吃吃之外,还能干点什么。

本来想学习理财的知识,一看概念还真不少,什么市盈率,市净率,ROI,XXX。

怎么学的牢固点不容易忘呢?一搜高效学习的方法,这俩货又出来了,那干脆把他们结合起来做个小程序好了。

产品概念

作为一个和产品经理斗争多年的老后端,终于有一天要考虑产品怎么呈现了,有种苍天饶过谁的感觉。。

记得之前吐槽产品最多的话就是:产品逻辑这么复杂,我都理解不了,让用户怎么用。

本着这个原则,做出来的东西就是要简单,简单,再简单。看一眼就知道怎么用。

所以这个程序本质上就三个点:

  • 内容输入
  • 艾宾浩斯曲线复习。共有8个阶段,根据笔记创建时间判断是否需要复习。
  • 费曼学习法-讲出来

introduce

开发框架

最终选择的开发组合是uni-app + springboot。

后端服务就是用自己最熟悉的springboot,同时使用一个非常好用的微信开发包:weixin-java-miniapp

在小程序端的框架选择上是费了点时间。由于之前有过一些vue的基础,就想着最好还是用vue来做,那么小程序的vue框架就有个mp-vue。

但是这时的想法是,万一之后用户多了,是不是可以搞出ios和android的app呢(梦想是要有的,万一实现了呢)。起码PC页面还是要做一个的。

基于这些考虑,就需要一个基于vue开发的多端代码生成框架,最终选择了uni-app。

uni-app

uni-app官方提供了一个编辑器HBuilderX,但是对于新学一个编辑器感觉没有必要,使用vscode开发也是可以的。

这里通过@vue/cli来创建环境,搭建只需要两条命令。

  • 创建代码库:vue create -p dcloudio/uni-preset-vue my-project
  • 生成微信小程序代码:npm run dev:mp-weixin

之后使用微信小程序官方开发工具,打开uni-app生成的代码目录就可以了,dev命令是实时监听修改的,开发体验还不错,上图。

work-screen

登录

通过uni-app的统一登录接口,可以获取到微信小程序中的openid等信息,这里发送到后端,用来创建系统用户。

后端创建或登录成功后返回一个token,将token缓存到手机中,在之后的每次请求中发送给后端。

login() {
    var that = this;
    uni.login({
        provider: 'weixin',
        success: function (res) {
            // 获取用户信息
            uni.getUserInfo({
                provider: 'weixin',
                success: function (infoRes) {
                    that.$request({
                        header: {
                            'X-WX-Code': res.code,
                            'X-WX-Encrypted-Data': infoRes.encryptedData,
                            'X-WX-IV': infoRes.iv,
                        },
                        url: "/user/wx/login",
                    }).then(res => {
                        uni.setStorageSync("mkey", res.data.mkey)
                        //保存用户信息
                        uni.setStorageSync('userInfo', res.data.mainUser);
                        that.$goto('/pages/home/home')
                    })
                }
            });
        }
    });
}

封装request函数实现加载中效果

小程序中请求api会有等待的时间,要在每次请求时加一个等待请求的页面提示,通过封装底层的request,统一加等待效果。

export default (params) => {
    //判断用户登录状态
    var mkey = uni.getStorageSync('mkey')
    //加载中页面
    uni.showLoading({
        title: '加载中',
        mask: true
    })
    var loginHeader = {
        'X-MKEY': mkey
    };

    var header = params.header == undefined ? loginHeader : params.header
    return new Promise((resolve, reject) => {
        uni.request({
            header: header,
            success: ({ data, statusCode, header }) => {
                resolve(data)
            },
            fail: (error) => {
                reject(error)
            },
            complete: () => {
                //关闭加载中
                uni.hideLoading()
            },
            ...params,
            url
        })
    })
}

文件上传与存储

编辑器中可以通过启用相机或相册,添加图片,需要将图片保存起来。

这里使用腾讯云的COS保存图片信息,需要在后端生成cos的带有鉴权信息的上传连接,返回给小程序。

然后通过链接直接上传图片到cos,不再通过后端,节省服务器资源。

上传成功后,会返回图片的url,这里要注意url是在返回的header里面。

useCamera() {
    var that = this;
    uni.chooseImage({
        count: 1, //最多可选图片张数
        success: async function (res) {
            //图片路径
            var filePath = res.tempFilePaths[0];
            //获取腾讯云COS预签名url
            var urlData = await that.request({
                url: "/resource/cosurl"
            });
            that.preUrl = urlData.data.preUrl;
            //获取图片信息
            var fileInfo = await uni.getImageInfo({
                src: filePath
            })
            //上传图片
            uni.uploadFile({
                url: that.preUrl,
                filePath: filePath,
                name: "file",
                formData: {
                    "key": urlData.data.key + "." + fileInfo[1].type
                },
                success: (res) => {
                    that.imgUrl = res.header.Location
                }
            });
        }
    });
}

没域名没证书怎么提供后端服务?

微信小程序中,要求调用后端接口只能使用https,这意味着要购买并备案一个域名,并且要搞到证书才可以提供后端服务。

不过现在,腾讯提供了一个云开发的服务,可以在小程序中使用。

云开发提供了完整的云函数和云数据库的支持,完全可以满足后端服务的需求。但是看了下文档后,发现代码编写和云数据库操作与我之前常用的springboot+mysql+redis开发体系差距较大,学习成本比较高。

但是云函数中调用外部接口却没有https的限制,这就是说完全可以把云函数当成一个网关来使用,通过云函数来调用后端服务。并且云开发现在提供免费额度。

yunfunc

云开发的使用步骤

  • 注册小程序开发账号后,在开发工具上点击云开发。

yunfunc-1

  • 之后会选择使用哪种方式,直接选择预付费,会提供免费使用的额度。
  • 开通后,在开发工具中,创建时选择云开发,也可以在小程序代码同级目录下,新建一个云函数目录。

yunfunc-2

yunfunc-3

  • 之后在云函数目录中,新建一个云函数:httpApi,写好代码后,直接右键点击就可以上传云函数。

yunfunc-4

  • 云函数只实现一个通用的http请求转发功能,将请求代理到实际的服务器上。代码实现如下:
// 云函数入口文件
const cloud = require('wx-server-sdk')
// 这里使用了request-promise
var rp = require('request-promise');
cloud.init()
// 实际服务的ip地址,可以直接用ip端口
var host = "http://xxx.xxx.xxx.xx:8990";
// 云函数入口函数
exports.main = async (event, context) => {
  var options = {
    url: host + event.url,
    method: event.method,
    json: true,
    headers: event.header
  }
  // 判断请求类型
  if (event.method != 'post') {
    options.qs = event.data
  } else {
    options.body = event.data
  }
  var res = await rp(options)
    .then(function (res) {
      return res
    })
    .catch(function (err) {
      return err
    });

  return res;
}

小程序代码的改造

小程序中,之前的https请求,需要替换为云函数请求。之前已经把请求封装到了request方法中,直接对request方法内部改造就可以。

  • 首先在main.js中声明要使用云函数,加入下面的代码。
wx.cloud.init({
    // 此处请填入环境 ID, 环境 ID 可打开云控制台查看
    env: '你的环境id',
    traceUser: true,
})
  • 修改request为使用云函数
export default (params) => {
    //判断用户登录状态
    var mkey = uni.getStorageSync('mkey')
    //加载中页面
    uni.showLoading({
        title: '加载中',
        mask: true
    })
    var loginHeader = {
        'X-MKEY': mkey
    };

    var header = params.header == undefined ? loginHeader : params.header
    return new Promise((resolve, reject) => {
        //使用云函数
        wx.cloud.callFunction({
            name: 'httpApi',
            data: {
                url: params.url,
                header: header,
                data: params.data,
                method: params.method
            },
            success: function (res) {
                uni.hideLoading();
                var data = res.result
                resolve(data)
            },
            fail: (err) => {
                console.log("err", err)
                uni.hideLoading();
            }
        })
    })
}

这样就可以把之前对后端服务的请求,通过云函数代理后,发送给服务器,直接省了域名。通过这篇文章希望可以帮助想要开发小程序的同学少走些弯路。

微信中搜索:记忆笔记,就可以找到这个小程序,欢迎试用。

查看原文

赞 0 收藏 0 评论 0

dothetrick 回答了问题 · 2月19日

PHP openssl_encrypt的错误原因是什么?

传的加密向量值有问题,长度是2的次方

随便打16个字符传进去

关注 3 回答 2

dothetrick 回答了问题 · 1月29日

2021年了,大家用什么方式开发小程序?

在现有基础上看吧,

熟悉VUE就uniapp,
熟悉RN就taro

这样效率最高

关注 3 回答 3

dothetrick 回答了问题 · 1月27日

发送http请求出现异常,怎样设置重试? 例如出现异常后 间隔30秒 3分钟 3小时 重试三次.

消息队列不太好实现这种逻辑,一般都是用来解耦两个系统的。

用redis实现更简单点,可以使用redis的set或list数据结构。比如对应1分钟后,10分钟后,一小时后创建三个list。

用一个job轮训重试,成功了就删除,失败了就放到下一个list中。

关注 5 回答 4

dothetrick 发布了文章 · 1月26日

Mybatis-plus使用TableNameHandler分表详解(附完整示例源码)

为什么要分表

Mysql是当前互联网系统中使用非常广泛的关系数据库,具有ACID的特性。

但是mysql的单表性能会受到表中数据量的限制,主要原因是B+树索引过大导致查询时索引无法全部加载到内存。读取磁盘的次数变多,而磁盘的每次读取对性能都有很大的影响。

这时一个简单可行的方案就是分表(当然土豪也可以堆硬件),将一张数据量庞大的表的数据,拆分到多个表中,这同时也减少了B+树索引的大小,减少磁盘读取次数,提高性能。

两种基础分表逻辑

说完了为什么要分表,下面聊聊业务开发中常见的两种基础的分表逻辑。

按日期分表

这种方式通常会在表名的最后加上年月日,主要适用于按日期划分的统计数据或操作记录。在线实时展示的只有最近表中的数据,其他数据用于离线统计等。

按id取模分表

这种方式需要一个id生成器,例如snowflake id或分布式id服务。它保证了相同id的数据都在一张表中,主要适用于保存用户基础信息,系统中的资源信息,购买记录等。当然这种分表方式扩展性较差,后期数据持续增多后需要按id大小分库再分表处理。

下面看下这两种分表逻辑在mybatis-plus中的实现。

Mybatis-plus中的分表实现

说到java的分表中间件,可能有人会想到sharding-jdbc,作为使用很广泛的一个分表中间件,功能也比较完善,但是使用它需要引入额外的jar包和增加学习成本。

实际上mybatis-plus本身就提供了一个分表的解决方案,配置使用都很简单,适合快速开发系统。

动态表名处理器

没错,mybatis-plus提供了动态表名处理器接口TableNameHandler,只需要在系统中实现该接口,并作为插件加载到mybatis-plus中就可以使用,下面来看下详细的步骤。

3.4版本之前的动态表名接口是ITableNameHandler,需要和分页插件配合使用。

3.4版本新增了TableNameHandler,在方法参数上取消了MetaObject。这里用最新的版本为例,使用方式差别不大。

假设我们的系统中有两种分表方式,按日期分表和按id取模分表。通过四个步骤来看下具体的使用示例。

1.创建日期表名处理器

先来看下日期处理的表名处理器,实现TableNameHandler接口后,在dynamicTableName方法中实现动态生成表名的逻辑,方法的返回值就是查询时要使用的表名。

/**
 * 按天分表解析
 */
public class DaysTableNameParser implements TableNameHandler {

    @Override
    public String dynamicTableName(String sql, String tableName) {
        String dateDay = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        return tableName + "_" + dateDay;
    }
}
2.创建id取模表名处理器

再来看下按id取模表名处理器的实现,这个处理器相对日期处理就要复杂一些,主要原因为需要动态传入用于分表的id值

在之前的版本中可以在方法中通过解析MetaObject中带有的sql查询信息,获取分表使用的值。但是这种方式比较复杂,对于不同的QueryMapper分析的方式不同,比较容易出错。新版本中的方法取消了MetaObject参数,需要使用其他方式传入。

需要注意的是,表名处理器是作为mybatis-plus的插件,在项目启动时实例化的。这意味着,在运行过程中只有一个对象,多线程处理过程中,一个线程对参数的修改,会影响到其他线程。为了解决这个问题,可以使用ThreadLocal来定义参数。

由于现在的框架中大部分会使用线程池,例如springboot web项目中的tomcat。所以在每次使用后,需要手动清除本次数据,防止线程复用时的影响。

具体实现如下:

/**
 * 按id取模分表处理器
 */
public class IdModTableNameParser implements TableNameHandler {
    private Integer mod;

    //使用ThreadLocal防止多线程相互影响
    private static ThreadLocal<Integer> id = new ThreadLocal<Integer>();

    public static void setId(Integer idValue) {
        id.set(idValue);
    }

    IdModTableNameParser(Integer modValue) {
        mod = modValue;
    }

    @Override
    public String dynamicTableName(String sql, String tableName) {
        Integer idValue = id.get();
        if (idValue == null) {
            throw new RuntimeException("请设置id值");
        } else {
            String suffix = String.valueOf(idValue % mod);
            //这里清除ThreadLocal的值,防止线程复用出现问题
            id.set(null);
            return tableName + "_" + suffix;
        }
    }
}
3.加载表名处理器

表名处理器实际是mybatis-plus的插件,需要在初始化时创建实例并加载。因为系统中存在两种分表类型,在初始化时可以指定每张表使用的表名处理器。具体实现如下:

@Configuration
@MapperScan(basePackages = "com.yourcom.proname.repository.mapper.mainDb*", sqlSessionFactoryRef = "mainSqlSessionFactory")
public class MainDb {
    @Bean(name = "mainDataSource")
    @ConfigurationProperties(prefix = "dbconfig.maindb")
    public DataSource druidDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "mainTransactionManager")
    public DataSourceTransactionManager masterTransactionManager(@Qualifier(value = "mainDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "mainSqlSessionFactory")
    @ConfigurationPropertiesBinding()
    public SqlSessionFactory sqlSessionFactory(@Qualifier(value = "mainDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
          //加载插件
        factoryBean.setPlugins(mybatisPlusInterceptor());
        return factoryBean.getObject();
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
        HashMap<String, TableNameHandler> map = new HashMap<String, TableNameHandler>();

        //这里为不同的表设置对应表名处理器
        map.put("user_daily_record", new DaysTableNameParser());
        map.put("user_consume_flow", new IdModTableNameParser(10));

        dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
        return interceptor;
    }
}
4.在controller中使用

下面通过controller中的三个接口,展示下使用方式:

@RestController
public class TableTestController {
    @Resource
    IUserDailyRecordService userDailyRecordService;

    @Resource
    IUserConsumeFlowService userConsumeFlowService;

    @GetMapping("user/record/today")
    public CommonResVo<UserDailyRecord> getRecordToday(Integer userId) throws Exception {
        //这里在查询时,会根据系统当前时间,自动生成当天的表名
        UserDailyRecord userDailyRecord = userDailyRecordService.getOne(new LambdaQueryWrapper<UserDailyRecord>().eq(UserDailyRecord::getUserId, userId));
        return CommonResVo.success(userDailyRecord);
    }

    @GetMapping("user/consume/flow")
    public CommonResVo<List<UserConsumeFlow>> getConsumeFlow(Integer userId) throws Exception {
        //设置用于分表的id值
        IdModTableNameParser.setId(userId);
        List<UserConsumeFlow> userConsumeFlowList = userConsumeFlowService.list(new LambdaQueryWrapper<UserConsumeFlow>().eq(UserConsumeFlow::getUserId, userId));
        return CommonResVo.success(userConsumeFlowList);
    }

    /**
     * 新增数据
     */
    @PostMapping("user/consume/flow")
    public CommonResVo<Boolean> addConsumeFlow(@RequestBody UserConsumeFlow userConsumeFlow) throws Exception {
        Integer userId = userConsumeFlow.getUserId();
        //设置用于分表的id值
        IdModTableNameParser.setId(userId);
        userConsumeFlowService.save(userConsumeFlow);
        return CommonResVo.success(true);
    }
}

这篇对mybatis-plus动态表名处理器的介绍,通过实现TableNameHandler接口,可以按实际情况灵活定义表名的生成规则,希望对大家有帮助。

项目完整示例地址:https://gitee.com/dothetrick/...

以上内容属个人学习总结,如有不当之处,欢迎在评论中指正

查看原文

赞 0 收藏 0 评论 0

dothetrick 回答了问题 · 1月23日

解决线程是先创建好,还是有任务的时候再创建?

先创建监听好点,提交后在创建会导致第一次的处理时间过长,可能还会有并发问题。

这种情况最好还是用线程池,并设定最大数量可以防止线程过多。线程池中的线程数量也是可以动态控制的。

关注 3 回答 2

dothetrick 关注了问题 · 1月20日

SpringBoot 怎么实现数据源的动态添加修改

问题描述

项目需求:要求把数据库的连接信息写入系统参数表里边,后面可以在某个页面上进行编辑修改,内部代码变量(数据源2的引用)也可以生效,之前只有过手动配置@Bean的经验,像这样页面上修改后能重新加载数据库连接资源对象没有经验,有经验的朋友可以提供一下思路,感谢!!!

关注 2 回答 1

dothetrick 回答了问题 · 1月19日

解决我想在springboot 启动前读取数据库中的配置到内存,并且在拦截器里面获取启动时读取的配置

如果只是想在拦截器里面使用配置信息,不用在spring boot启动前读数据库。

自定义的拦截器也是在IOC里面的BEAN,可以启动后正常使用数据库。

这个问题看起来是想在启动时做一些初始化拦截器的操作。
可以在拦截器里用@PostConstruct注解写一个函数,在函数里直接读数据库初始化拦截器。
这个过程会在web项目启动前完成的

关注 3 回答 2

dothetrick 关注了问题 · 1月13日

mybatis-plus怎么添加自定义查询条件

项目中所有用到QueryWrapper全局添加自定义查询条件的sql,
每次都要写个查询条件queryWrapper.eq来查询数据权限,希望全局能调用,求大佬帮助!

关注 2 回答 1

dothetrick 回答了问题 · 1月8日

解决springboot redis 如何存放对象?

如果是使用spring boot的redis template的话。可以把序列化读写方式都设为string。

之后读写对象的时候,都先把对象转换为json字符串去读写。

这样虽然多了一步处理并且不太优雅,但是确实实用,可以避免很多底层的坑。代码迁移性也很好

关注 5 回答 4

认证与成就

  • 获得 14 次点赞
  • 获得 41 枚徽章 获得 3 枚金徽章, 获得 13 枚银徽章, 获得 25 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2015-10-28
个人主页被 1.9k 人浏览