个人案例
- 从0到100:找搭子小程序开发笔记(一)
# 背景调查 “找搭子”小程序:能够解决人们在社交、休闲和约会方面的需求,提供方便快捷的方式来找到合适的伴侣或活动伙伴。许多人在社交场合中感到焦虑或不安,因此他们更倾向于使用在线平台来认识新的朋友或搭子。有些人可能生活在一个较小或有限的社交圈子中,很难扩展自己的社交网络。在这种情况下,“找搭子”小程序可以为他们提供一个更广泛的选择范围,认识来自不同背景和兴趣的人。 # 功能规划 - 1.搭子聚会发布与管理:用户可以创建和加入各种活动或事件,例如饭搭、旅搭、运动、户外、电影、音乐会等,并要求搭子一起参与。 - 2.报名:用户可以通过小程序报名参加搭子活动。 - 3.活动日程:提供活动的详细日程,方便安排时间。 - 4.会员管理:平台组织者可以使用小程序来管理会员资料。 - 5.活动反馈:活动的参与者可以通过小程序提供活动反馈和评价,发起人和平台可以借此改进未来的活动。 - 6.报名管理:活动发起人可以查看活动报名情况,包括已报名和审核的人数。 对于需要报名审核的活动进行报名名单审核。 - 7.签到管理:每次活动报名成功后,报名者获得一个签到二维码,出示给活动发起人,可以进行扫描签到。 # 概要设计 [图片][图片] # 数据库设计 ``` ActivityModel.DB_STRUCTURE = { _pid: 'string|true', ACTIVITY_ID: 'string|true', ACTIVITY_TYPE: 'int|true|default=0|comment=类型 0=后台发起,1=用户发起', ACTIVITY_USER_ID: 'string|false', ACTIVITY_TITLE: 'string|true|comment=标题', ACTIVITY_STATUS: 'int|true|default=1|comment=状态 0=未启用,1=使用中', ACTIVITY_CHECK_REASON: 'string|false|comment=审核理由', ACTIVITY_CATE_ID: 'string|true|default=0|comment=分类', ACTIVITY_CATE_NAME: 'string|false|comment=分类冗余', ACTIVITY_CANCEL_SET: 'int|true|default=1|comment=取消设置 0=不允,1=允许,2=仅截止前可取消', ACTIVITY_CHECK_SET: 'int|true|default=0|comment=审核 0=不需要审核,1=需要审核', ACTIVITY_IS_MENU: 'int|true|default=1|comment=是否公开展示名单', ACTIVITY_MAX_CNT: 'int|true|default=20|comment=人数上限 0=不限', ACTIVITY_START: 'int|false|comment=活动开始时间', ACTIVITY_END: 'int|false|comment=活动截止时间', ACTIVITY_STOP: 'int|true|default=0|comment=报名截止时间 0=永不过期', ACTIVITY_START_DAY: 'string|false', ACTIVITY_END_DAY: 'string|false', ACTIVITY_ORDER: 'int|true|default=9999', ACTIVITY_VOUCH: 'int|true|default=0', ACTIVITY_FORMS: 'array|true|default=[]', ACTIVITY_OBJ: 'object|true|default={}', ACTIVITY_JOIN_FORMS: 'array|true|default=[]', ACTIVITY_ADDRESS: 'string|false|comment=详细地址', ACTIVITY_ADDRESS_GEO: 'object|false|comment=详细地址坐标参数', ACTIVITY_QR: 'string|false', ACTIVITY_VIEW_CNT: 'int|true|default=0', ACTIVITY_JOIN_CNT: 'int|true|default=0', ACTIVITY_COMMENT_CNT: 'int|true|default=0', ACTIVITY_USER_LIST: 'array|true|default=[]|comment={name,id,pic}', ACTIVITY_ADD_TIME: 'int|true', ACTIVITY_EDIT_TIME: 'int|true', ACTIVITY_ADD_IP: 'string|false', ACTIVITY_EDIT_IP: 'string|false', }; ``` # 核心实现 ``` // 注册 async register(userId, { mobile, name, pic, forms, status }) { // 判断是否存在 let where = { USER_MINI_OPENID: userId } let cnt = await UserModel.count(where); if (cnt > 0) return await this.login(userId); where = { USER_MOBILE: mobile } cnt = await UserModel.count(where); if (cnt > 0) this.AppError('该手机已注册'); // 入库 let data = { USER_MINI_OPENID: userId, USER_MOBILE: mobile, USER_NAME: name, USER_PIC: pic, USER_OBJ: dataUtil.dbForms2Obj(forms), USER_FORMS: forms, USER_STATUS: Number(status), } await UserModel.insert(data); return await this.login(userId); } /** 获取手机号码 */ async getPhone(cloudID) { let cloud = cloudBase.getCloud(); let res = await cloud.getOpenData({ list: [cloudID], // 假设 event.openData.list 是一个 CloudID 字符串列表 }); if (res && res.list && res.list[0] && res.list[0].data) { let phone = res.list[0].data.phoneNumber; return phone; } else return ''; } /** 取得我的用户信息 */ async getMyDetail(userId) { let where = { USER_MINI_OPENID: userId } let fields = 'USER_PIC,USER_MOBILE,USER_NAME,USER_FORMS,USER_OBJ,USER_STATUS,USER_CHECK_REASON' return await UserModel.getOne(where, fields); } /** 修改用户资料 */ async editBase(userId, { mobile, name, pic, forms }) { let whereMobile = { USER_MOBILE: mobile, USER_MINI_OPENID: ['<>', userId] } let cnt = await UserModel.count(whereMobile); if (cnt > 0) this.AppError('该手机已注册'); let where = { USER_MINI_OPENID: userId } let user = await UserModel.getOne(where); if (!user) return; if (user.USER_PIC && user.USER_PIC != pic) { // 删除老头像文件 cloudUtil.deleteFiles(user.USER_PIC); // 更新活动和打卡里的头像 let ActivityModel = require('../model/activity_model.js'); let wherePic = { 'ACTIVITY_USER_LIST.USER_MINI_OPENID': userId, } let dataPic = { 'ACTIVITY_USER_LIST.$.USER_PIC': pic } ActivityModel.edit(wherePic, dataPic); } let data = { USER_MOBILE: mobile, USER_NAME: name, USER_PIC: pic, USER_OBJ: dataUtil.dbForms2Obj(forms), USER_FORMS: forms, }; if (user.USER_STATUS == UserModel.STATUS.UNCHECK) data.USER_STATUS = UserModel.STATUS.UNUSE; await UserModel.edit(where, data); } /** 登录 */ async login(userId) { let where = { 'USER_MINI_OPENID': userId }; let fields = 'USER_ID,USER_MINI_OPENID,USER_NAME,USER_PIC,USER_STATUS'; let user = await UserModel.getOne(where, fields); let token = {}; if (user) { // 正常用户 token.id = user.USER_MINI_OPENID; token.key = user.USER_ID; token.name = user.USER_NAME; token.pic = user.USER_PIC; token.status = user.USER_STATUS; // 异步更新最近更新时间 let dataUpdate = { USER_LOGIN_TIME: this._timestamp }; UserModel.edit(where, dataUpdate); UserModel.inc(where, 'USER_LOGIN_CNT', 1); } else token = null; return { token }; } ``` # UI设计 [图片] [图片] [图片] [图片] # 后台管理系统 [图片] [图片] [图片]
2024-06-11 - 从0到1:基于微信小程序的瑜伽馆预约平台的开发笔记
背景分析 随着国民健身意识越来越强,各式各样的健身模式不断出现。瑜伽也受到了大众的喜爱,瑜伽行业发展越来越快,作为馆主,你还在微信群里让你的会员使用接龙的方式进行约课吗?你还在用传统的Excel进行排课吗?如果有一款小程序会员点一下就能约课,会不会让你惊喜、意外、激动——没错,瑜伽预约小程序就是为了解决馆主会员约课的痛点应运而生。功能包括瑜伽馆动态,瑜伽常识,瑜伽老师预约,瑜伽课程预约等模块。 该系统基于MVC架构,采用基于微信小程序平台开发,校园用户操作轻松快捷:无需下载安装APP。 功能抽象 [图片] 数据库设计 [图片] 遇到的技术难点 小程序启动是用户体验中极为重要的一环,启动耗时过长会造成小程序用户流失 开发者代码注入(逻辑层)预约小程序启动时需要从代码包内读取小程序的配置和代码,并注入到 JS 引擎中。 在预约主包代码注入过程中,会触发小程序的 App.onLaunch 和首次 App.onShow 生命周期。 在预约开发者代码注入完成后,框架侧会根据预约用户访问的页面进行一些页面数据初始化工作,触发首页的 Page.onLoad, Page.onShow 事件。 对启动耗时的影响预约开发者代码的注入耗时直接影响小程序的启动耗时。 在主流的 JS 引擎中,代码注入耗时包括编译和执行等环节,代码量、同步接口调用和一些复杂的计算,都会影响注入耗时。 由于预约首页渲染需要使用逻辑层发送的数据,如果开发者代码注入耗时过长,也会延迟首页渲染开始的时间。 在部分平台预约上,微信客户端会使用 V8 引擎的 Code Caching 技术对代码编译结果进行缓存,降低二次注入时的编译耗时 开发者代码注入(渲染层)预约开发者的 wxss 和 wxml 会经过编译注入到渲染层,包含页面渲染需要的页面结构和样式信息。 渲染层的注入耗时主要和预约页面结构复杂度和使用的自定义组件数量有关。 渲染层和逻辑层的预约开发者代码注入是并行进行的。 对启动耗时的影响 由于预约首页渲染需要使用渲染层的页面结构和样式信息,如果开发者代码注入耗时过长,会延迟预约首页渲染开始的时间。 首页(初次)渲染在预约开发者代码注入完成后,结合逻辑层得到的数据和渲染层得到的页面结构和样式信息,预约小程序框架会进行小程序首页的渲染, 展示小程序首屏,并触发首页的 Page.onReady 事件。Page.onReady 事件触发标志小程序启动过程完成。 对启动耗时的影响预约首页渲染耗时主要受页面结构和参与渲染的数据量影响 小程序首屏渲染完成从开发者角度看,预约小程序首屏渲染完成的标志是首页 Page.onReady 事件触发。 从框架的角度来说,预约小程序的首屏的内容是基于小程序的初始数据,以及在渲染开始前到达的 setData 数据渲染的。 首屏渲染完成不表示小程序页面一定有完整内容,开发者触发的 setData(例如通过 wx.request 异步请求数据)不一定能参与到首屏渲染中。 由于框架和启动流程的差异,小程序定义的首屏渲染完成不等同于浏览器的 load 事件。 小程序启动阶段从预约用户点击访问小程序到小程序首屏渲染完成(首页 Page.onReady 事件触发)。 打开率/到达率预约小程序首屏渲染完成 PV 与 用户点击访问小程序 PV 的比值。流失率 = 1 - 打开率 小程序代码包优化代码包优化的核心手段是降低代码包大小,预约小程序代码包大小直接影响了下载耗时,影响用户启动预约小程序时的体验。 开发者可以采取以下手段优化预约小程序代码包大小 1 预约小程序分包加载 2 预约小程序代码重构和优化 3 控制代码包内图片等资源 4 及时清理没有使用到的预约小程序代码和资源 界面设计 [图片][图片][图片][图片][图片][图片] 后台界面设计 [图片][图片][图片][图片][图片][图片]
2022-03-16 - 基于微信小程序的大学校园社团平台的概要设计
UI设计背景和思路 随着社会的在新时代下的飞跃发展,尤其是高等教育事业的发展愈来愈可观,每年节节攀升的学生数量不断翻新, 作为大学生另一处学习发展中心的社团组织,也呈现出一种百花争艳的景象,而如何有效的对社团进行规范化管理, 就显得十分有必要。 好的校园社团管理系统不仅可以大大提高社团的办事效率,增强各社团之间的交流与沟通,还可以为社团节约大量的人力物力。 进行系统开发设计时,遵循界面友好、安全性较高、操作灵活、实用性较强的指标进行。该系统基于MVC架构,采用基于微信小程序平台开发, 校园用户操作轻松快捷:无需下载安装APP。 功能模块设计[图片] 数据库设计 [图片] 关键技术点微信小程序前端开发框架没有自带的验证功能,虽然有表单的数据控制属性,但是略显简陋,且不成体系,尤其是结合后端很别扭,因此小程序的表单验证时候一般有两种方法,一是自己裸写验证规则,但是需要比较扎实的正则表达式基础,一种是自己封装Validate插件进行表单验证,基于以上原因, 封装了前端数据校验规则一套: 小程序集中校验路由 function check(data, rules, that) { let returnData = {}; for (let k in rules) { let arr = rules[k].split('|'); let desc = ''; // 校友录小程序数据项说明 for (let i = 0; i < arr.length; i++) { if (arr[i].indexOf('name=') > -1) { desc = arr[i].replace('name=', ''); break; } } // 校验 let formName = arr[0]; let val = data[formName]; if (val === undefined) val = ''; if (!Array.isArray(val)) val = String(val).trim(); // 前后去校友录小程序空格 returnData[k] = val; for (let i = 1; i < arr.length; i++) { let result = ''; let rules = arr[i].split(':'); // 校友录小程序空不校验 if (rules[0] != 'required' && val == '') continue; switch (rules[0]) { case 'required': result = checkRequired(val, desc); break; case 'array': result = checkArray(val, desc); break; case 'date': result = checkDate(val, desc); break; case 'time': result = checkTime(val, desc); break; case 'datetime': result = checkDatimeTime(val, desc); break; case 'min': result = checkMin(val, Number(rules[1]), desc); break; case 'max': result = checkMax(val, Number(rules[1]), desc); break; case 'len': result = checkLen(val, Number(rules[1]), desc); break; case 'in': result = checkIn(val, rules[1], desc); break; case 'email': result = checkEmail(val, desc); break; case 'mobile': result = checkMobile(val, desc); break; case 'int': result = checkInt(val, desc); break; case 'id': result = checkId(val, desc); break; case 'letter': result = checkLetter(val, desc); break; case 'letter_num': result = checkLetterNum(val, desc); break; } if (result) { wx.showModal({ title: '温馨提示', content: result, showCancel: false, success(res) { // 校友录小程序自动聚焦 if (that) that.setData({ [formName + 'Focus']: true }); } }); return false; } else { if (that) { // 删除校友录小程序原有的自动聚焦 if (helper.isDefined(that.data[formName + 'Focus'])) { that.setData({ //TODO delete? [formName + 'Focus']: false }); } } } } } return returnData; } 小程序分项数据校验function checkRequired(value, desc) { if (value == '') return desc + '不能为空'; } /** * 校验校友录小程序字符长度 * @param {*} value * @param {*} min * @param {*} max */ function isCheckLen(value, min, max) { //TODO 数字怎么处理 if (!helper.isDefined(value)) return false; if (typeof (value) != 'string') return false; if (value.length < min || value.length > max) return false; return true; } /** * 校验校友录小程序数字大小 * @param {*} value * @param {*} min * @param {*} max */ function isCheckM(value, min, max) { if (!helper.isDefined(value)) return false; if (typeof (value) == 'string' && /^[0-9]+$/.test(value)) value = Number(value); if (typeof (value) != 'number') return false; if (value < min || value > max) return false; return true; } function checkMin(value, len, desc) { if (value.length < len) return desc + '不能小于' + len + '位'; }; function checkMax(value, len, desc) { if (value.length > len) return desc + '不能大于' + len + '位'; }; function checkLen(value, len, desc) { if (value.length != len) return desc + '必须为' + len + '位'; }; function checkMobile(value, desc) { if (value == '') return ''; if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) return desc + '格式不正确'; } function checkInt(value, desc) { if (value == '') return ''; if (!/^[0-9]+$/.test(value)) return desc + '必须为数字'; } function checkLetter(value, desc) { if (value == '') return; if (!/^[A-Za-z]+$/.test(value)) return desc + '必须为字母'; } function checkLetterNum(value, desc) { if (value == '') return; if (!/^\w+$/.test(value)) return desc + '必须为字母,数字和下划线'; } function checkId(value, desc, min = 1, max = 32) { if (value == '') return; if (value.length < min || value.length > max) return false; if (!/^\w+$/.test(value)) return desc + '必须为ID格式'; } function isCheckId(value, min = 1, max = 32) { if (!helper.isDefined(value)) return false; if (typeof (value) != 'string') return false; if (value.length < min || value.length > max) return false; if (!/^\w+$/.test(value)) return false; return true; } // 校友录小程序邮箱 function checkEmail(value, desc) { if (value == '') return; let hint = desc + '必须为邮箱格式'; let reg = /^[A-Za-z0-9+]+[A-Za-z0-9\.\_\-+]*@([A-Za-z0-9\-]+\.)+[A-Za-z0-9]+$/; if (!reg.test(value)) return hint; } // 校友录小程序短日期,形如 (2008-07-22) function checkDate(value, desc) { if (value == '') return; let hint = '请选择' + desc; if (value.length != 10) return hint; let r = value.match(/^(\d{1,4})(-|\/)(\d{1,2})\2(\d{1,2})$/); if (r == null) return hint; let d = new Date(r[1], r[3] - 1, r[4]); let chk = d.getFullYear() == r[1] && (d.getMonth() + 1) == r[3] && d.getDate() == r[4]; if (!chk) return hint; } // 校友录小程序短时间,形如 (13:04:06) function checkTime(value, desc) { if (value == '') return; let hint = desc + '必须为时间格式'; if (value.length != 8) return hint; let a = value.match(/^(\d{1,2})(:)?(\d{1,2})\2(\d{1,2})$/); if (a == null) return hint; if (a[1] > 24 || a[3] > 60 || a[4] > 60) return hint; } // 长时间,形如 (2008-07-22 13:04:06) function checkDatimeTime(value, desc) { if (value == '') return; let hint = desc + '必须为完整时间格式'; if (value.length != 19) return hint; var reg = /^(\d{1,4})(-|\/)(\d{1,2})\2(\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/; var r = value.match(reg); if (r == null) return hint; var d = new Date(r[1], r[3] - 1, r[4], r[5], r[6], r[7]); let chk = d.getFullYear() == r[1] && (d.getMonth() + 1) == r[3] && d.getDate() == r[4] && d.getHours() == r[5] && d.getMinutes() == r[6] && d.getSeconds() == r[7]; if (!chk) return hint; } function checkArray(value, desc) { if (value == '') return; if (!Array.isArray(value)) return desc + '填写错误'; } function checkIn(value, ref, desc) { if (value == '') return; let arr = ref.split(','); if (!arr.includes(value) && !arr.includes(value + '')) return desc + '填写错误'; } /** * 检查校友录小程序枚举类型 * @param {*} value * @param {*} ref 格式 1,2,3 */ function isCheckIn(value, ref) { if (!helper.isDefined(value)) return false; let arr = ref.split(','); if (!arr.includes(value) && !arr.includes(value + '')) return false; //字符,数字都支持 return true; } UI设计[图片][图片][图片][图片][图片][图片] 后台设计[图片][图片][图片][图片] [图片][图片]
2022-03-11 - 微信开发者工具 1.05.2111300 Stable 更新说明
下载地址Windows64、Windows 32、MacOS、MacOS(M1) 基于 1.05.2111051 RC 版 修复以下 Bug 1. `F` 修复 开启独立域调试时小程序的 canvas 画布可能被遮挡的问题 1. `F` 修复 模拟器终止时点击可视化表现异常 1. `F` 修复 启动sensor面板后,wx.getLocation返回字段类型和文档不对应的问题 1. `F` 修复 懒注入开启时,模拟器加载 babel helper 文件失败的问题 1. `F` 修复 非独立域环境下 AppData 面板不显示数据的问题 1. `F` 修复 小游戏模拟器分包加载时,提示找不 babel helper 文件的bug 1. `F` 修复 懒注入模式使用weui组件展示白屏 1. `F` 修复 懒注入wcc编译报错问题 1. `F` 修复 可视化开启时,当前页面不存在时也要有局部编译的提示 1、自动化测试录制回放 新增自动化测试入口,开发者可以将测试操作提前录制好,然后通过执行录制脚本来实现测试过程的回放。 [图片] 详情可见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/auto/record.html 2、无障碍访问模式支持 新增无障碍访问模式插件,方便开发者在小程序开发者工具就可以调试无障碍访问的工具。目前支持大部分通用的aria-语法。 [图片] 详情可见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/aria.html 3、小程序插件开发支持配置独立的插件appid小程序插件开发模式中,我们新增了独立的插件appid 配置,与开发过程中的宿主小程序appid区分开,方便大家模拟真实插件的使用场景。 [图片] 4、公众号调试jsapi授权 我们完善了公众号网页调试的一些jsapi的授权方式(比如getLocation),向真机对齐。 [图片] 同时可以在右上角菜单里清除授权数据方便调试: [图片] 5、HTTP V2 增加查询登录态接口本地更新新增了查询登陆状态接口,支持http v2 和 命令行 调用。 6、URL Link 和 Short Link 解析支持本次更新我们新增了对 URL Link 和 Short Link 解析与调试的支持。可以在编译模式弹窗中的「添加方式」里选择: [图片] 7、新增小程序页面返回时的询问弹窗新增支持配置自定义返回弹框,开发者通过以下接口可以配置是否开启弹框提示: ·wx.enableAlertBeforeUnload({ message: 'xxxx' }) wx.disableAlertBeforeUnload() 8、预览/上传时,代码包超出限制后可查看详细文件大小点击提示弹框中的 【查看文件列表】,即可查看对应超限分包的文件列表信息。 [图片]
2021-12-02