二次开发

uniCloud

架构介绍

  1. 使用基于 koa 风格的 uniCloud 云函数路由库uni-cloud-routeropen in new window,集成封装众多实用的公共方法。
  2. 简单、快速、高效、借鉴了一些thinkphp的优点。

云函数目录结构:

├─ admin--------------------------------------后台
│  ├─ controller------------------------------控制器
│  ├─ service---------------------------------服务层
│  ├─ middleware------------------------------中间件
├─ client-------------------------------------前台
├─ common-------------------------------------公共
│  ├─ base------------------------------------基类
│  ├─ uni-config-center-----------------------配置中心
│  ├─ uni-cloud-router----------------------- uni koa路由库
│  ├─ uni-id----------------------------------uni-id用户体系
│  ├─ uni-captcha-----------------------------图形验证码
│  ├─ uni-sec-check---------------------------内容安全
│  └─ uni-stat--------------------------------uni统计
├─ crontab------------------------------------自定义定时任务
├─ uni-sec-check-cron-------------------------内容安全定时任务
├─ uni-stat-cron------------------------------uni统计定时任务
└─ uni-stat-receiver--------------------------uni统计定时任务

增删改查页面示例

/*引入基类*/
const Controller = require("../base");

/**
 * 类别管理
 */
module.exports = class Category extends Controller {
    /**
     * 列表
     */
    async index() {
        /*获取参数*/
        let keyword = this.getData('keyword'); //关键词 
        // this.getData('参数名','提示信息','数据类型','默认值')

        /*搜索*/
        let where = {};
        if (keyword) {
            where.name = {$regex: keyword}; //关键词模糊搜索
        }

        /*获取数据*/
        let result = await this.dbCollection('article_category').select({
            where,
            order: "sort asc"
        });

        /*返回数据*/
        this.success(result);
    }

    /**
     * 数据验证
     * 格式:{字段名:['提示信息','数据类型','默认值','是否必填(默认是)']}
     */
    validateData() {
        return this.validate({
            pid: ['上级类别不能为空'],
            name: ['类别名称不能为空'],
            sort: ['排序不能为空', 'int', 100],
            status: ['状态不能为空', 'int', this.enum.EnableOrDisable.enable]
        });
    }

    /**
     * 新增
     */
    async add() {
        /*获取数据并验证*/
        let data = this.validateData();

        /*添加类别*/
        await this.dbCollection('article_category').insert({
            ...data,
            create_time: Date.now(),
            update_time: Date.now()
        });

        /*返回数据*/
        return this.success('添加成功');
    }

    /**
     * 修改
     */
    async edit() {
        /*获取数据并验证*/
        let _id = this.getData('_id', 'id不能为空');
        let data = this.validateData();

        /*更新*/
        await this.dbCollection('article_category').update({
            _id,
            data: {
                ...data,
                update_time: Date.now()
            }
        });

        /*返回数据*/
        this.success('更新成功')
    }

    /**
     * 删除
     */
    async delete() {
        /*获取参数*/
        let _id = this.getData('_id', 'id不能为空');

        /*判断是否存在文章*/
        let count = await this.dbCollection('article').count({
            category_id: _id,
            is_delete: this.enum.YesOrNo.no
        });
        if (count > 0) {
            this.error('该类别下存在文章不能删除')
        }

        /*删除*/
        await this.dbCollection('article_category').delete(_id);

        /*返回数据*/
        this.success('删除成功')
    }
};

添加数据

使用 insert 方法添加数据,支持批量添加,返回_id或_id数组(批量添加返回_id数组)

/* 1.单条数据添加 */
let _id = await this.dbCollection('article').insert({
    title: 'life cms',
    content: 'Hello,life cms!',
    create_time: Date.now(),
    update_time: Date.now()
});

/* 2.批量数据添加 */
let _ids = await this.dbCollection('article').insert([
    {
        title: 'life cms 01',
        content: 'Hello,life cms!',
        create_time: Date.now(),
        update_time: Date.now()
    },
    {
        title: 'life cms 02',
        content: 'Hello,life cms!',
        create_time: Date.now(),
        update_time: Date.now()
    }
]);

更新数据

使用 update 方法更新数据,支持通过_id更新、通过where更新,返回受影响记录数

/* 1.通过_id更新 */
await this.dbCollection('article').update({
    _id: '64cb91c1337a9f4db7cddb69',
    data: {
        title: 'life cms',
        content: 'Hello,life cms!',
        update_time: Date.now()
    }
})

/* 2.通过where更新 */
await this.dbCollection('article').update({
    where: {title: 'life cms'},
    data: {
        title: 'life cms v2',
        content: 'Hello,life cms!',
        update_time: Date.now()
    }
})

删除数据

使用 delete 方法删除数据,支持通过_id删除、通过where删除、通过_id数组删除,返回受影响记录数

/* 1.通过_id删除 */
await this.dbCollection('article_category').delete(_id);

/* 2.通过where删除 */
let where = {title: 'life cms'}
await this.dbCollection('article_category').delete(where);

/* 3.通过_id数组删除 */
let _ids = ['64cb91c1337a9f4db7cddb69', '88cb91c1337a9f4db7cddb69']
await this.dbCollection('article_category').delete(_ids);

查询数据-单条数据

使用 find 方法查询单条数据,返回数据对象。

支持参数: field(查询字段)、unfield(屏蔽查询字段)、join(联表查询)、where(查询条件)、order(排序)

/*参数示例*/
let result = await this.dbCollection('article').find({
    field: '_id,title,create_time,content', //查询字段
    unfield: 'content', //屏蔽字段
    join: { //join:联表,超过一个表可用数组 join:[{……},{……}]
        field: 'name as category_name', //查询关联表的字段
        from: "article_category as ac", //关联表名与别名,as 后面为关联表别名
        on: "ac._id = category_id" //关联字段
    },
    where: {
        category_id: '64cb91c1337a9f4db7cddb69', //当前表字段
        'ac.name': '心灵' //关联表字段
    },
    order: 'update_time desc' //按更新时间倒序
});

/* 1.单表查询 */
let result = await this.dbCollection('article').find({
    where: {title: 'life cms'},
    order: "sort asc"
});

/* 2.多表查询(join) */
let result = await this.dbCollection('ad').find({
    join: [
        {
            field: 'name as location_name',
            from: "ad_location as al", //关联表1
            on: "al.code = location"
        },
        {
            field: 'name as category_name',
            from: "article_category as ac", //关联表2
            on: "ac._id = category_id"
        }
    ],
    where: {title: 'life cms'},
    order: "sort asc"
});

查询数据-多条数据

使用 select 方法查询列表数据,返回列表数组

支持参数:field(查询字段)、unfield(屏蔽查询字段)、join(联表查询)、where(查询条件)、order(排序)、group(分组)、limit(获取指定数量的数据)、skip(跳过指定数量的数据)、unwind(将数组字段拆分成多个文档)

/*参数示例*/
let result = await this.dbCollection('article').select({
    field: '_id,title,create_time,content', //查询字段
    unfield: 'content', //屏蔽字段
    join: { //join:联表,超过一个表可用数组 join:[{……},{……}]
        field: 'name as category_name', //查询关联表的字段
        from: "article_category as ac", //关联表名与别名,as 后面为关联表别名
        on: "ac._id = category_id" //关联字段
    },
    where: {
        category_id: '64cb91c1337a9f4db7cddb69', //当前表字段
        'ac.name': '心灵' //关联表字段
    },
    skip: 10,// 排除前10条数据
    limit: 100, //获取前100条数据
    order: 'update_time desc' //按更新时间倒序
});

/* 1.单表查询 */
let result = await this.dbCollection('article').select({
    where: {title: 'life cms'},
    order: "sort asc"
});

/* 2.多表查询(join) */
let result = await this.dbCollection('ad').select({
    join: [
        {
            field: 'name as location_name',
            from: "ad_location as al", //关联表1
            on: "al.code = location"
        },
        {
            field: 'name as category_name',
            from: "article_category as ac", //关联表2
            on: "ac._id = category_id"
        }
    ],
    where: {title: 'life cms'},
    order: "sort asc"
});

/* 3.分组查询 */
let result = await this.dbCollection('search_log').select({
    group: 'keyword,count(keyword) as count',
    order: 'count desc'
})

查询数据-单个字段值

使用 value 方法获取某个字段值。

支持参数: field(查询字段)、join(联表查询)、where(查询条件)、order(排序)

/*获取title值*/
let result = await this.dbCollection('article').value({
    field: 'title',
    where: {_id: '64cb91c1337a9f4db7cddb69'},
    order: "sort asc"
});

查询数据-聚合查询

  1. 使用 count 方法获取记录数。
  2. 使用 sum 方法获取总和。
  3. 使用 min 方法获取最小值。
  4. 使用 max 方法获取最大值。
  5. 使用 avg 方法获取平均值。

支持参数: field(查询字段)、join(联表查询)、where(查询条件)、order(排序)

/*1.获取记录数-count*/
let result = await this.dbCollection('article').count();

/*2.获取总和-sum*/
let result = await this.dbCollection('article').sum({
    field: 'read', //统计字段
    where: {_id: '64cb91c1337a9f4db7cddb69'} //条件
});

/*3.获取平均值-avg*/
let result = await this.dbCollection('article').avg({
    field: 'read', //统计字段
    where: {_id: '64cb91c1337a9f4db7cddb69'} //条件
});

/*4.获取最大值-max*/
let result = await this.dbCollection('article').max({
    field: 'read', //比较字段
    where: {_id: '64cb91c1337a9f4db7cddb69'} //条件
});

/*5.获取最小值-min*/
let result = await this.dbCollection('article').min({
    field: 'read', //比较字段
    where: {_id: '64cb91c1337a9f4db7cddb69'} //条件
});

查询数据-查询条件

支持 $ne(不等于)、$gt(大于)、$gte(大于等于)、$lt(小于)、 $regex(模糊搜索、正则搜索)、$in(in查询)、$not(not查询)、$or(或)、$and( 并) 等查询条件

/*查询条件示例*/
let result = await this.dbCollection('article').select({
    where: {
        title: 'life cms', // 等于
        title: {$ne: 'life cms'}, // 不等于
        read: {$gt: 1000}, // 大于
        read: {$gte: 1000}, // 大于等于
        read: {$lt: 1000}, // 小于
        read: {$lte: 1000}, // 小于等于
        title: {$regex: 'life'}, // 关键词模糊搜索,相当于mysql的  like '%life%'
        title: {$regex: '^life/'}, // 关键词模糊搜索,相当于mysql的  like '%life'
        title: {$regex: 'life$'}, // 关键词模糊搜索,相当于mysql的  like 'life%'
        cateroy_id: {$in: ['_id01', '_id02']}, // in 查询
        cateroy_id: {$not: {$in: ['_id01', '_id02']}}, // not 查询
        $or: [{title: 'life cms 01'}, {title: 'life cms 02'}], // or 查询
        $and: [{title: 'life cms 01'}, {read: {$gt: 1000}}] // and 查询
    }
});

修改表结构

使用alter方法修改表结构,支持添加字段、重名名字段、删除字段。

/*添加字段*/
await this.dbCollection('test').alter({
   command: 'add column', // 添加字段命令
   field: 'status', // 添加字段名称
   defaultVal: 0, // 默认值
})
 // 往test表中新增一个名为status的字段,默认值为0


/*重命名字段*/
await this.dbCollection('test').alter({
   command: 'rename', // 重命名字段命令
   oldField: 'status', // 旧字段名称
   newField: 'state', // 新字段名称
})
// 修改test表的status字段名称为state


/*删除字段*/
await this.dbCollection('test').alter({
   command: 'drop column', // 删除字段命令
   field: 'state', // 删除字段名称
})
// 删除test表中的state字段

数据表前缀

  1. 支持数据表前缀设置,简化数据表名输入。

如:数据表'o_article',操作时只需传入'article',例 this.dbCollection('article')。

  1. 配置:uni-config-center/sys/config.js的database节点。
/*数据表前缀*/
 database: {
     prefix: 'o_', //系统数据表前缀
     other_prefix: ['uni-', 'opendb-'] // 第三方数据表前缀
 }
 // 第三方数据表前缀不会启用简化前缀,调用例 this.dbCollection('uni-id')

请求参数

  1. 通过 this.getData() 方法获取请求参数。
  2. 格式:this.getData('参数名','提示信息','数据类型','默认值','是否必填,默认是')
  3. 示例:let keyword=this.getData('keyword'),参数名必填,其他参数非必填。
  4. 验证的数据类型支持:int、number、mobile、email、tel、url、password、length、dateRange等。
let keyword = this.getData('keyword'); //关键词,没填提示信息参数,可为空。
let username = this.getData('username', '用户名', 'length|2,32'); // 需大于2个字符小于32个字符
let create_time = this.getData('create_time', '日期', 'dateRange'); // 值格式:['2023-08-01','2023-08-30']
let mobile = this.getData('mobile', '手机号码', 'mobile'); // 手机号码不能为空,且需为需为手机号码格式
let pageSize = this.getData('pageSize', '分页记录数', 'int', 10);// 分页记录数不能为空,且需为整型格式

响应输出

  1. 通过 this.success() 方法中断程序执行,输出成功的提示信息或数据对象,格式:this.success(msg,data,code) ,msg如果是数据对象或布尔值、整型,会自动赋值给data。
  2. 通过 this.error() 方法中断程序执行,输出失败提示信息。格式:this.error(msg,code)。
  3. 通过云函数根目录下的config.js的Response可配置输出json格式的code、msg键名与默认值。
let count = await this.dbCollection('article').count({
    category_id: _id,
    is_delete: this.enum.YesOrNo.no
});
if (count > 0) {
    this.error('该类别下存在文章不能删除')
}
let result = await this.dbCollection('article_category').delete(_id);
this.success('删除成功')
// 或 this.success(result)

检举

  1. 检举在common/base/enum下定义,可在enum.js中定义,也可新建一个文件。
/**
  * 显示/隐藏检举
  */
 ShowOrHide: {
     show: 1,
     hide: 0,
     properties: {
         1: '显示',
         0: '隐藏'
     }
 },

 /**
  * 是/否检举
  */
 YesOrNo: {
     yes: 1,
     no: 0,
     properties: {
         1: '是',
         0: '否'
     }
 }
  1. 调用。
this.enum.YesOrNo.yes // 1
this.enum.YesOrNo.properties // {1:是,0:否}

缓存

  1. 设置缓存:this.common.cache('缓存名称','缓存值','缓存时间'),缓存时间单位为秒,不填时默认为0,表示永不过期。
  2. 读取缓存:this.common.cache('缓存名称')。
  3. 删除缓存:this.common.cache('缓存名称',null)。
/*设置缓存*/
let menu = [{id: 1, name: '首页'}]
this.common.cache('menu', menu, 7200) // 7200秒后过期
// 或 this.common.cache('menu', menu) // 永不过期

/*读取缓存*/
let cacheMenu = this.common.cache('menu')

/*删除缓存*/
this.common.cache('menu', null)

定时任务

  1. 云函数crontab,专门处理定时任务,支持多个定时任务处理,支持处理远程访问任务和本地任务。
  2. 定时任务添加入口,crontab云函数下的controller/index.js的 index 方法。
  3. 定时任务定义入口,crontab云函数下的controller/base.js的 run 方法。
  4. 定时格式格式:this.run('定时任务名称', '执行时间' , '任务类型'),任务类型值为request表示远程访问任务,local表示本地任务,默认值为local,本地任务即执行本地service目录(基于 koa 风格的 uniCloud 云函数路由库uni-cloud-router规范的service目录)下定义的任务。
/* 远程访问任务 */
await this.run('collect', 24 * 60 * 60 , 'request'); //文章采集,每24小时执行一次

/* 本地任务 */
await this.run('articleRefreshSort', 24 * 60 * 60); //更新文章排序,每24小时执行一次

前台 (uni-app)

架构介绍

  1. 使用this.$uniCloudRequest()访问云函数,自动判断是否需要登录。
  2. 集成封装众多实用的组件与公共方法,支持uViewopen in new window UI 组件。

目录结构:

├─ common-------------------------------------公共
│  ├─ css-------------------------------------css
│     ├─ base.scss----------------------------公共css
│     ├─ iconfont.css-------------------------字体图标css
│     ├─ iconfont.ttf-------------------------字体图标ttf
│  ├─ sdk-------------------------------------js sdk
│  ├─ base.js---------------------------------公共方法
│  ├─ util.js---------------------------------工具方法
│  ├─ validate.js-----------------------------验证方法
│  ├─ init.js---------------------------------初始化
├─ components---------------------------------组件
├─ store--------------------------------------状态管理
├─ static-------------------------------------静态资源
├─ res----------------------------------------App图标与启动图
├─ pages--------------------------------------页面
├─ uni_modules--------------------------------插件目录
├─ config.js----------------------------------配置文件
├─ pages.json---------------------------------页面路由

开发示例

<template>
	<view class="page">
		<view class="content">
			<view class="register">
				<view class="title">欢迎注册!</view>
				<view class="form">
					<view class="item">
						<iconfont type="mobile"></iconfont>
						<u-input type="number" v-model="form.mobile" placeholder="请输入手机号码" maxlength="11" border="none" clearable />
					</view>
					<view class="item">
						<iconfont type="pwd"></iconfont>
						<u-input type="password" v-model="form.password" placeholder="请输入密码" maxlength="32" border="none" clearable />
					</view>
					<view class="item">
						<iconfont type="user"></iconfont>
						<u-input type="text" v-model="form.nickname" placeholder="请输入昵称" maxlength="30" border="none" clearable />
					</view>
					<view class="item">
						<iconfont type="captcha"></iconfont>
						<u-input type="text" v-model="form.captcha" placeholder="请输入验证码" maxlength="6" border="none" clearable />
						<u-code ref="uCode" @change="updateSendSmsCodeTip" seconds="60" @start="hasSendSmsCode = true" @end="hasSendSmsCode = false"></u-code>
						<u-button class="btn-send" @click="sendSmsCode" :text="sendSmsCodeTip" :disabled="hasSendSmsCode"></u-button>
					</view>
					<view class="protocol">
						<u-checkbox-group @change="agreeChange"><u-checkbox :checked="agree" shape="circle" activeColor="#8cc7b5" label="同意"></u-checkbox></u-checkbox-group>
						<navigator url="/pages/common/protocol"><view class="name">《用户协议和隐私政策》</view></navigator>
					</view>
					<button class="button" @click="register">注册</button>
				</view>
				<view class="other"><navigator url="login/index" open-type="navigate" hover-class="none">已有账号,点此登录</navigator></view>
			</view>
		</view>
	</view>
</template>
<script>
import { mapMutations } from 'vuex';
export default {
	data() {
		return {
			form: {
				mobile: '',
				password: '',
				nickname: '',
				captcha: ''
			},
			agree: true,
			hasSendSmsCode: false,
			sendSmsCodeTip: ''
		};
	},
	onLoad() {},
	methods: {
		...mapMutations({
			login: 'user/login'
		}),

		/**
		 * 发送验证码
		 */
		sendSmsCode() {
			if (this.$refs.uCode.canGetCode) { //允许发送验证码
				this.$loading('发送中…');
				this.$uniCloudRequest({
					action: 'auth/sendSmsCode',
					data: {
						mobile: this.form.mobile,
						type: 'register'
					},
					success: res => {
						if (res.errCode === 0) {
							this.$success('发送成功');
							this.$refs.uCode.start(); //开始倒计时
						} else {
							this.$alert(res.errMsg);
						}
					}
				});
			}
		},

		/**
		 * 更新发送验证码提示
		 */
		updateSendSmsCodeTip(text) {
			this.sendSmsCodeTip = text;
		},

		/**
		 * 同意《用户协议与隐私政策》
		 */
		agreeChange() {
			this.agree = !this.agree;
		},

		/**
		 * 注册
		 */
		register() {
			if (!this.agree) {
				this.$alert('请同意《用户协议与隐私政策》');
				return;
			}
			this.$loading('注册中…');
			this.$uniCloudRequest({
				action: 'auth/register',
				data: {
					...this.form
				},
				success: res => {
					if (res.errCode === 0) {
						this.$success('注册成功');
						this.login(res.userInfo); //注册完自动登录
                        uni.switchTab({url: '/pages/user/index'});//跳转至用户中心
					} else {
						this.$alert(res.errMsg);
					}
				}
			});
		}
	}
};
</script>

后台 (AntDesign UI)

架构介绍

  1. 使用this.$uniCloudRequest()访问云函数,自动判断是否需要登录。
  2. 引入Ant Design of Vueopen in new window UI 框架,支持所有AntDesign UI方法和组件open in new window
  3. 集成封装众多公众方法与组件,支持动态路由配置(后台-菜单管理)。

特点

后台采用AntDesign UI + uniCloud,封装了AntDesign UI原生访问uniCloud的方法,支持本地uniCloud调试,非云函数url化方式,可以用AntDesign UI原生的开发方式开发后台,支持所有AntDesign UI方法和组件,支持动态路由配置(后台-菜单管理),无需在page.json上配置路由,权限配置与“后台-系统管理-菜单管理”关联,配置简单,可以控制到每个页面、按钮。

开发示例

<!-- 添加/编辑 -->
<template>
   <a-modal :width="750" :visible="show" :confirm-loading="loading" :title="form._id ? '修改广告位' : '添加广告位'" :body-style="{ paddingBottom: '8px' }" @update:visible="closeEdit" @ok="save">
      <a-form ref="form" :model="form" :rules="rules" :label-col="{ md: { span: 7 }, sm: { span: 24 } }" :wrapper-col="{ md: { span: 17 }, sm: { span: 24 } }">
         <a-row :gutter="16">
            <a-col :md="12" :sm="24" :xs="24">
               <a-form-item label="广告位名称" name="name">
                  <a-input allow-clear placeholder="请输入广告位名称" v-model:value="form.name" />
               </a-form-item>
               <a-form-item label="广告位宽度" name="width">
                  <a-input-number :min="0" class="base-fluid" placeholder="广告位宽度" v-model:value="form.width" />
               </a-form-item>
               <a-form-item label="排序号:" name="sort">
                  <a-input-number :min="0" class="base-fluid" placeholder="请输入排序号" v-model:value="form.sort" />
               </a-form-item>
            </a-col>
            <a-col :md="12" :sm="24" :xs="24">
               <a-form-item label="广告位标识" name="code">
                  <a-input allow-clear placeholder="请输入广告位标识" v-model:value="form.code" />
               </a-form-item>
               <a-form-item label="广告位高度" name="heigh">
                  <a-input allow-clear placeholder="广告位高度" v-model:value="form.heigh" />
               </a-form-item>
               <a-form-item label="备注" name="remark">
                  <a-textarea allow-clear auto-size placeholder="广告位备注" v-model:value="form.remark" />
               </a-form-item>
            </a-col>
         </a-row>
      </a-form>
   </a-modal>
</template>

<script>
export default {
   name: 'LocationEdit',
   props: {
      show: Boolean, //是否展示
      data: Object //数据源
   },
   data() {
      return {
         form: {},
         rules: {
            name: [{ required: true, type: 'string', message: '请输入广告位名称', trigger: 'blur' }],
            code: [{ required: true, type: 'string', message: '请输入广告位标识', trigger: 'blur' }],
            sort: [{ required: true, type: 'number', message: '请输入排序号', trigger: 'blur' }]
         },
         loading: false
      };
   },
   watch: {
      /*初始化表单数据*/
      data: {
         handler(value) {
            this.form = value;
            if (this.$refs.form) {
               this.$refs.form.clearValidate();
            }
         },
         immediate: true
      }
   },
   methods: {
      /**
       * 保存
       */
      save() {
         this.$refs.form.validate().then(() => { //表单验证
            this.loading = true;
            this.$uniCloudRequest({ //调用云函数
               action: 'ad/location/' + (this.form._id ? 'edit' : 'add'),
               data: this.form,
               success: res => {
                  this.loading = false;
                  if (res.errCode === 0) {
                     this.$message.success(res.errMsg);
                     this.closeEdit(); //关闭弹窗
                     this.$emit('reload'); //刷新数据
                  } else {
                     this.$message.error(res.errMsg);
                  }
               }
            });
         });
      },
      /**
       * 关闭弹窗
       */
      closeEdit() {
         this.$emit('update:show', false);
      }
   }
};
</script>

动态路由

在后台“系统管理-菜单管理”中管理动态路由。 image

提示

路由地址与组件路径一致时,路由地址对应的组件路径可不填。

Last Updated:
Contributors: bcxz