feat(B批收尾): B1门禁可信/B7-1菜单环防护/B2空值守卫/B7-2分页/B7-3字段清洗/B3空态/B4索引key/B5-3 comment三集合选A新建schema(漂移清零)/B6清理+admin配置+changelog
This commit is contained in:
@@ -1,441 +1,458 @@
|
||||
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
|
||||
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
|
||||
|
||||
// 导入 createConfig 模块
|
||||
const createConfig = require('uni-config-center');
|
||||
// 导入 buildTemplateData 模块
|
||||
const buildTemplateData = require('./build-template-data');
|
||||
// 导入 utils 模块中的 parserDynamicField 函数
|
||||
const { parserDynamicField } = require('./utils');
|
||||
// 导入 schema-name-adapter 模块
|
||||
const schemaNameAdapter = require('./schema-name-adapter');
|
||||
// 导入 preset-condition 模块中的 presetCondition 和 conditionConvert 函数
|
||||
const { presetCondition, conditionConvert } = require("./preset-condition");
|
||||
|
||||
// 导入 uni-sms-co 模块
|
||||
const uniSmsCo = uniCloud.importObject('uni-sms-co');
|
||||
// 导入 uniCloud.database 模块
|
||||
const db = uniCloud.database();
|
||||
// 使用 createConfig 函数创建 smsConfig 对象
|
||||
const smsConfig = createConfig({
|
||||
pluginId: 'uni-sms-co'
|
||||
}).config();
|
||||
// 定义 errCode 函数,返回错误码
|
||||
function errCode(code) {
|
||||
return 'uni-sms-co-' + code;
|
||||
}
|
||||
|
||||
// 定义 tableNames 对象
|
||||
const tableNames = {
|
||||
template: 'opendb-sms-template', // 模板表名为 'opendb-sms-template'
|
||||
task: 'opendb-sms-task', // 任务表名为 'opendb-sms-task'
|
||||
log: 'opendb-sms-log' // 日志表名为 'opendb-sms-log'
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
_before: async function () { // 通用预处理器
|
||||
if (!smsConfig.smsKey || smsConfig.smsKey.length <= 20 || !smsConfig.smsSecret || smsConfig.smsSecret.length <= 20) {
|
||||
throw new Error('请先配置smsKey和smsSecret')
|
||||
}
|
||||
|
||||
this.tableNames = tableNames
|
||||
|
||||
/**
|
||||
* 优化 schema 的命名规范,需要兼容 uni-admin@2.1.6 以下版本
|
||||
* 如果是在uni-admin@2.1.6版本以上创建的项目可以将其注释
|
||||
* */
|
||||
await schemaNameAdapter.call(this)
|
||||
},
|
||||
_after: function (error, result) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
// 如果错误是 Error 实例,则返回包含错误信息的对象
|
||||
return {
|
||||
errCode: 'error',
|
||||
errMsg: error.message
|
||||
};
|
||||
}
|
||||
if (error.errCode) {
|
||||
// 如果错误对象中包含 errCode 属性,则直接返回错误对象
|
||||
return error;
|
||||
}
|
||||
// 抛出其他类型的错误
|
||||
throw error;
|
||||
}
|
||||
// 返回结果
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建短信任务
|
||||
* @param {{receiver: *[], type: string}} to
|
||||
* @param {String} to.type=user to.all=true时用来区分发送类型
|
||||
* @param {Array} to.receiver 用户ID's / 用户标签ID's
|
||||
* @param {Object} to.condition 用户筛选条件
|
||||
* @param {String} templateId 短信模板ID
|
||||
* @param {Array} templateData 短信模板数据
|
||||
* @param {Object} options
|
||||
* @param {String} options.taskName 任务名称
|
||||
*/
|
||||
async createSmsTask(to, templateId, templateData, options = {}) {
|
||||
if (!templateId) {
|
||||
// 如果缺少 templateId,则返回错误信息
|
||||
return {
|
||||
errCode: errCode('template-id-required'),
|
||||
errMsg: '缺少templateId'
|
||||
};
|
||||
}
|
||||
|
||||
if (!to.condition && (!to.receiver || to.receiver.length <= 0)) {
|
||||
// 如果没有预设条件且没有接收者,则返回错误信息
|
||||
return {
|
||||
errCode: errCode('send-users-is-null'),
|
||||
errMsg: '请选择要发送的用户'
|
||||
};
|
||||
}
|
||||
|
||||
const clientInfo = this.getClientInfo();
|
||||
|
||||
// 查询短信模板
|
||||
const { data: templates } = await db.collection(this.tableNames.template).where({ _id: templateId }).get();
|
||||
if (templates.length <= 0) {
|
||||
// 如果短信模板不存在,则返回错误信息
|
||||
return {
|
||||
errCode: errCode('template-not-found'),
|
||||
errMsg: '短信模板不存在'
|
||||
};
|
||||
}
|
||||
const [template] = templates;
|
||||
|
||||
// 预设条件
|
||||
if (presetCondition[to.condition]) {
|
||||
to.condition = typeof presetCondition[to.condition] === "function" ? presetCondition[to.condition]() : presetCondition[to.condition];
|
||||
}
|
||||
|
||||
// 创建短信任务
|
||||
const task = await db.collection(this.tableNames.task).add({
|
||||
app_id: clientInfo.appId,
|
||||
name: options.taskName,
|
||||
template_id: templateId,
|
||||
template_content: template.content,
|
||||
vars: templateData,
|
||||
to,
|
||||
send_qty: 0,
|
||||
success_qty: 0,
|
||||
fail_qty: 0,
|
||||
create_date: Date.now()
|
||||
});
|
||||
|
||||
uniSmsCo.createUserSmsMessage(task.id);
|
||||
|
||||
// 返回任务创建成功的异步结果
|
||||
return new Promise(resolve => setTimeout(() => resolve({
|
||||
errCode: 0,
|
||||
errMsg: '任务创建成功',
|
||||
taskId: task.id
|
||||
}), 300));
|
||||
},
|
||||
|
||||
async createUserSmsMessage(taskId, execData = {}) {
|
||||
const parallel = 100
|
||||
let beforeId
|
||||
const { data: tasks } = await db.collection(this.tableNames.task).where({ _id: taskId }).get()
|
||||
|
||||
if (tasks.length <= 0) {
|
||||
return {
|
||||
errCode: errCode('task-id-not-found'),
|
||||
errMsg: '任务ID不存在'
|
||||
}
|
||||
}
|
||||
|
||||
const [task] = tasks
|
||||
let query = {
|
||||
mobile: db.command.exists(true)
|
||||
}
|
||||
|
||||
// 指定用户发送
|
||||
if (task.to.type === 'user' && task.to.receiver.length > 0 && !task.to.condition) {
|
||||
let index = 0
|
||||
if (execData.beforeId) {
|
||||
const i = task.to.receiver.findIndex(id => id === execData.beforeId)
|
||||
index = i !== -1 ? i + 1 : 0
|
||||
}
|
||||
|
||||
const receiver = task.to.receiver.slice(index, index + parallel)
|
||||
query._id = db.command.in(receiver)
|
||||
beforeId = receiver[receiver.length - 1]
|
||||
}
|
||||
|
||||
// 指定用户标签
|
||||
if (task.to.type === 'userTags') {
|
||||
query.tags = db.command.in(task.to.receiver)
|
||||
}
|
||||
|
||||
// 自定义条件
|
||||
if (task.to.condition) {
|
||||
const condition = conditionConvert(task.to.condition, db.command)
|
||||
|
||||
query = {
|
||||
...query,
|
||||
...condition
|
||||
}
|
||||
}
|
||||
|
||||
if ((task.to.condition || task.to.type === "userTags") && execData.beforeId) {
|
||||
query._id = db.command.gt(execData.beforeId)
|
||||
}
|
||||
|
||||
// 动态数据仅支持uni-id-users表字段
|
||||
const dynamicField = parserDynamicField(task.vars)
|
||||
const userFields = dynamicField['uni-id-users'] ? dynamicField['uni-id-users'].reduce((res, field) => {
|
||||
res[field] = true
|
||||
return res
|
||||
}, {}): {}
|
||||
|
||||
const { data: users } = await db.collection('uni-id-users')
|
||||
.where(query)
|
||||
.field({
|
||||
mobile: true,
|
||||
...userFields
|
||||
})
|
||||
.limit(parallel)
|
||||
.orderBy('_id', 'asc')
|
||||
.get()
|
||||
|
||||
if (users.length <= 0) {
|
||||
// 更新要发送的短信数量
|
||||
const count = await db.collection(this.tableNames.log).where({ task_id: taskId }).count()
|
||||
await db.collection(this.tableNames.task).where({ _id: taskId }).update({
|
||||
send_qty: count.total
|
||||
})
|
||||
|
||||
// 开始发送
|
||||
uniSmsCo.sendSms(taskId)
|
||||
|
||||
return new Promise(resolve => setTimeout(() => resolve({
|
||||
errCode: 0,
|
||||
errMsg: '创建完成'
|
||||
}), 500))
|
||||
}
|
||||
|
||||
if (!beforeId) {
|
||||
beforeId = users[users.length - 1]._id
|
||||
}
|
||||
|
||||
let docs = []
|
||||
for (const user of users) {
|
||||
const varData = await buildTemplateData(task.vars, user)
|
||||
docs.push({
|
||||
uid: user._id,
|
||||
task_id: taskId,
|
||||
mobile: user.mobile,
|
||||
var_data: varData,
|
||||
status: 0,
|
||||
create_date: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
await db.collection(this.tableNames.log).add(docs)
|
||||
|
||||
uniSmsCo.createUserSmsMessage(taskId, { beforeId })
|
||||
|
||||
return new Promise(resolve => setTimeout(() => resolve(), 500))
|
||||
},
|
||||
async sendSms(taskId) {
|
||||
const { data: tasks } = await db.collection(this.tableNames.task).where({ _id: taskId }).get();
|
||||
if (tasks.length <= 0) {
|
||||
// 如果找不到任务,则输出警告信息并返回
|
||||
console.warn(`task [${taskId}] not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const [task] = tasks;
|
||||
const isStaticTemplate = !task.vars.length;
|
||||
|
||||
let sendData = {
|
||||
appId: task.app_id,
|
||||
smsKey: smsConfig.smsKey,
|
||||
smsSecret: smsConfig.smsSecret,
|
||||
templateId: task.template_id,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const { data: records } = await db.collection(this.tableNames.log)
|
||||
.where({ task_id: taskId, status: 0 })
|
||||
.limit(isStaticTemplate ? 50 : 1)
|
||||
.field({ mobile: true, var_data: true })
|
||||
.get();
|
||||
|
||||
if (records.length <= 0) {
|
||||
// 如果没有要发送的记录,则返回发送完成的异步结果
|
||||
return {
|
||||
errCode: 0,
|
||||
errMsg: '发送完成'
|
||||
};
|
||||
}
|
||||
|
||||
if (isStaticTemplate) {
|
||||
sendData.phoneList = records.reduce((res, user) => {
|
||||
res.push(user.mobile);
|
||||
return res;
|
||||
}, []);
|
||||
} else {
|
||||
const [record] = records;
|
||||
sendData.phone = record.mobile;
|
||||
sendData.data = record.var_data;
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送短信
|
||||
await uniCloud.sendSms(sendData);
|
||||
// 修改发送状态为已发送
|
||||
await db.collection(this.tableNames.log).where({
|
||||
_id: db.command.in(records.map(record => record._id))
|
||||
}).update({
|
||||
status: 1,
|
||||
send_date: Date.now()
|
||||
});
|
||||
// 更新任务的短信成功数
|
||||
await db.collection(this.tableNames.task).where({ _id: taskId })
|
||||
.update({
|
||||
success_qty: db.command.inc(records.length)
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[sendSms Fail]', e);
|
||||
// 修改发送状态为发送失败
|
||||
await db.collection(this.tableNames.log).where({
|
||||
_id: db.command.in(records.map(record => record._id))
|
||||
}).update({
|
||||
status: 2,
|
||||
reason: e.errMsg || '未知原因',
|
||||
send_date: Date.now()
|
||||
});
|
||||
// 更新任务的短信失败数
|
||||
await db.collection(this.tableNames.task).where({ _id: taskId })
|
||||
.update({
|
||||
fail_qty: db.command.inc(records.length)
|
||||
});
|
||||
}
|
||||
|
||||
uniSmsCo.sendSms(taskId);
|
||||
|
||||
return new Promise(resolve => setTimeout(() => resolve(), 500));
|
||||
},
|
||||
async template() {
|
||||
const { data: templates = [] } = await db.collection(this.tableNames.template).get();
|
||||
// 获取所有短信模板
|
||||
return templates;
|
||||
},
|
||||
|
||||
async task(id) {
|
||||
const { data: tasks } = await db.collection(this.tableNames.task).where({ _id: id }).get();
|
||||
if (tasks.length <= 0) {
|
||||
// 如果找不到任务,则返回 null
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回第一个找到的任务
|
||||
return tasks[0];
|
||||
},
|
||||
|
||||
async updateTemplates(templates) {
|
||||
if (templates.length <= 0) {
|
||||
// 如果模板信息为空,则返回错误
|
||||
return {
|
||||
errCode: errCode('template-is-null'),
|
||||
errMsg: '缺少模板信息'
|
||||
};
|
||||
}
|
||||
|
||||
let group = [];
|
||||
for (const template of templates) {
|
||||
group.push(
|
||||
db.collection(this.tableNames.template).doc(String(template.templateId)).set({
|
||||
name: template.templateName,
|
||||
content: template.templateContent,
|
||||
type: template.templateType,
|
||||
sign: template.templateSign
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(group);
|
||||
|
||||
return {
|
||||
errCode: 0,
|
||||
errMsg: '更新成功'
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @param to
|
||||
* @param templateId
|
||||
* @param templateData
|
||||
* @param options {Object}
|
||||
* @param options.condition 群发条件
|
||||
* */
|
||||
async preview (to, templateId, templateData, options = {}) {
|
||||
const count = 1
|
||||
let query = {
|
||||
mobile: db.command.exists(true)
|
||||
}
|
||||
|
||||
// 指定用户发送
|
||||
if (to.type === 'user' && to.receiver.length > 0 && !to.condition) {
|
||||
// const receiver = to.receiver.slice(0, 10)
|
||||
query._id = db.command.in(to.receiver)
|
||||
}
|
||||
|
||||
// 指定用户标签
|
||||
if (to.type === 'userTags') {
|
||||
query.tags = db.command.in(to.receiver)
|
||||
}
|
||||
|
||||
// 自定义条件
|
||||
let condition = to.condition
|
||||
if (presetCondition[to.condition]) {
|
||||
condition = typeof presetCondition[to.condition] === "function" ? presetCondition[to.condition]() : presetCondition[to.condition]
|
||||
}
|
||||
|
||||
if (condition) {
|
||||
query = {
|
||||
...query,
|
||||
...conditionConvert(condition, db.command)
|
||||
}
|
||||
}
|
||||
|
||||
const {data: users} = await db.collection('uni-id-users').where(query).limit(count).get()
|
||||
const {total} = await db.collection('uni-id-users').where(query).count()
|
||||
|
||||
if (users.length <= 0) {
|
||||
return {
|
||||
errCode: errCode('users-is-null'),
|
||||
errMsg: '请添加要发送的用户'
|
||||
}
|
||||
}
|
||||
|
||||
const {data: templates} = await db.collection(this.tableNames.template).where({_id: templateId}).get()
|
||||
if (templates.length <= 0) {
|
||||
return {
|
||||
errCode: errCode('template-not-found'),
|
||||
errMsg: '模板不存在'
|
||||
}
|
||||
}
|
||||
const [template] = templates
|
||||
|
||||
let docs = []
|
||||
for (const user of users) {
|
||||
const varData = buildTemplateData(templateData, user)
|
||||
const content = template.content.replace(/\$\{(.*?)\}/g, ($1, param) => varData[param] || $1)
|
||||
docs.push(`【${template.sign}】${content}`)
|
||||
}
|
||||
|
||||
return {
|
||||
errCode: 0,
|
||||
errMsg: '',
|
||||
list: docs,
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
|
||||
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
|
||||
|
||||
const createConfig = require('uni-config-center');
|
||||
const buildTemplateData = require('./build-template-data');
|
||||
const { parserDynamicField } = require('./utils');
|
||||
const schemaNameAdapter = require('./schema-name-adapter');
|
||||
const { presetCondition, conditionConvert } = require('./preset-condition');
|
||||
|
||||
const uniSmsCo = uniCloud.importObject('uni-sms-co');
|
||||
const db = uniCloud.database();
|
||||
const smsConfig = createConfig({ pluginId: 'uni-sms-co' }).config();
|
||||
|
||||
function errCode(code) {
|
||||
return 'uni-sms-co-' + code;
|
||||
}
|
||||
|
||||
function errorResult(code, message, extra = {}) {
|
||||
return { errCode: errCode(code), errMsg: message, ...extra };
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function hasValue(value) {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
function isId(value) {
|
||||
return (typeof value === 'string' && value.trim().length > 0) ||
|
||||
(typeof value === 'number' && Number.isFinite(value));
|
||||
}
|
||||
|
||||
function validateTemplateData(templateData) {
|
||||
if (templateData === undefined || templateData === null) templateData = [];
|
||||
if (!Array.isArray(templateData)) {
|
||||
return { error: errorResult('template-data-invalid', 'templateData必须是数组') };
|
||||
}
|
||||
|
||||
const normalized = [];
|
||||
for (const item of templateData) {
|
||||
if (!isPlainObject(item) || typeof item.field !== 'string' || !item.field.trim()) {
|
||||
return { error: errorResult('template-data-invalid', '短信模板变量必须包含有效的field') };
|
||||
}
|
||||
// 0 和空字符串都是有意义的变量值;仅拒绝缺失/null,避免隐式转换造成数据丢失。
|
||||
if (!hasValue(item.value) ||
|
||||
!['string', 'number', 'boolean'].includes(typeof item.value) ||
|
||||
(typeof item.value === 'number' && !Number.isFinite(item.value))) {
|
||||
return { error: errorResult('template-data-invalid', `短信变量${item.field}的value无效`) };
|
||||
}
|
||||
normalized.push({ ...item, field: item.field.trim() });
|
||||
}
|
||||
return { data: normalized };
|
||||
}
|
||||
|
||||
function validateCondition(condition) {
|
||||
if (!isPlainObject(condition)) {
|
||||
return errorResult('condition-invalid', '发送条件必须是对象');
|
||||
}
|
||||
for (const key of Object.keys(condition)) {
|
||||
const field = condition[key];
|
||||
if (!isPlainObject(field) || typeof field.type !== 'string' || !hasValue(field.value)) {
|
||||
return errorResult('condition-invalid', `发送条件字段${key}格式无效`);
|
||||
}
|
||||
if (!['search', 'select', 'range', 'date', 'timestamp', 'lt'].includes(field.type)) {
|
||||
return errorResult('condition-invalid', `不支持的发送条件类型:${field.type}`);
|
||||
}
|
||||
if (['select', 'range', 'date', 'timestamp'].includes(field.type) &&
|
||||
(!Array.isArray(field.value) || field.value.length === 0)) {
|
||||
return errorResult('condition-invalid', `发送条件字段${key}的value不能为空`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeTarget(to, requireTarget = true) {
|
||||
if (!isPlainObject(to)) {
|
||||
return { error: errorResult('send-target-invalid', '发送目标to必须是对象') };
|
||||
}
|
||||
const type = to.type === undefined ? 'user' : to.type;
|
||||
if (type !== 'user' && type !== 'userTags') {
|
||||
return { error: errorResult('send-target-invalid', '发送目标type必须是user或userTags') };
|
||||
}
|
||||
if (to.receiver !== undefined && !Array.isArray(to.receiver)) {
|
||||
return { error: errorResult('send-target-invalid', '发送目标receiver必须是数组') };
|
||||
}
|
||||
const receiver = Array.isArray(to.receiver) ? to.receiver : [];
|
||||
if (receiver.some(item => !isId(item))) {
|
||||
return { error: errorResult('send-target-invalid', '发送目标receiver包含无效ID') };
|
||||
}
|
||||
|
||||
let condition;
|
||||
let hasCondition = false;
|
||||
if (typeof to.condition === 'string') {
|
||||
const conditionName = to.condition.trim();
|
||||
if (!conditionName || !Object.prototype.hasOwnProperty.call(presetCondition, conditionName)) {
|
||||
return { error: errorResult('condition-invalid', '预设发送条件无效') };
|
||||
}
|
||||
condition = typeof presetCondition[conditionName] === 'function' ?
|
||||
presetCondition[conditionName]() : presetCondition[conditionName];
|
||||
hasCondition = true;
|
||||
} else if (to.condition !== undefined && to.condition !== null) {
|
||||
if (!isPlainObject(to.condition)) {
|
||||
return { error: errorResult('condition-invalid', '发送条件必须是预设名称或对象') };
|
||||
}
|
||||
const conditionError = validateCondition(to.condition);
|
||||
if (conditionError) return { error: conditionError };
|
||||
condition = to.condition;
|
||||
hasCondition = Object.keys(condition).length > 0;
|
||||
}
|
||||
|
||||
if (type === 'userTags' && receiver.length === 0) {
|
||||
return { error: errorResult('send-users-is-null', '请选择要发送的用户标签') };
|
||||
}
|
||||
if (requireTarget && receiver.length === 0 && !hasCondition) {
|
||||
return { error: errorResult('send-users-is-null', '请选择要发送的用户') };
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
...to,
|
||||
type,
|
||||
receiver,
|
||||
...(hasCondition ? { condition } : {})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function validateTemplateId(templateId) {
|
||||
if (!isId(templateId)) return errorResult('template-id-required', '缺少有效的templateId');
|
||||
return null;
|
||||
}
|
||||
|
||||
const tableNames = {
|
||||
template: 'opendb-sms-template',
|
||||
task: 'opendb-sms-task',
|
||||
log: 'opendb-sms-log'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
_before: async function () {
|
||||
if (!smsConfig.smsKey || smsConfig.smsKey.length <= 20 ||
|
||||
!smsConfig.smsSecret || smsConfig.smsSecret.length <= 20) {
|
||||
throw new Error('请先配置smsKey和smsSecret');
|
||||
}
|
||||
|
||||
this.tableNames = tableNames;
|
||||
await schemaNameAdapter.call(this);
|
||||
},
|
||||
|
||||
_after: function (error, result) {
|
||||
if (!error) return result;
|
||||
console.error(error);
|
||||
if (error instanceof Error) return errorResult('error', error.message);
|
||||
if (error && error.errCode) {
|
||||
return { ...error, errMsg: error.errMsg || error.message || '请求处理失败' };
|
||||
}
|
||||
if (error && typeof error === 'object') {
|
||||
return errorResult('error', error.errMsg || error.message || '请求处理失败');
|
||||
}
|
||||
return errorResult('error', '请求处理失败');
|
||||
},
|
||||
|
||||
/** 创建短信任务 */
|
||||
async createSmsTask(to, templateId, templateData, options = {}) {
|
||||
const templateIdError = validateTemplateId(templateId);
|
||||
if (templateIdError) return templateIdError;
|
||||
|
||||
const target = normalizeTarget(to);
|
||||
if (target.error) return target.error;
|
||||
const vars = validateTemplateData(templateData);
|
||||
if (vars.error) return vars.error;
|
||||
if (options === null || options === undefined) options = {};
|
||||
if (!isPlainObject(options)) return errorResult('task-options-invalid', '任务options必须是对象');
|
||||
if (options.taskName !== undefined &&
|
||||
(typeof options.taskName !== 'string' || !options.taskName.trim())) {
|
||||
return errorResult('task-name-invalid', '任务名称不能为空');
|
||||
}
|
||||
|
||||
const normalizedTemplateId = String(templateId).trim();
|
||||
const { data: templates = [] } = await db.collection(this.tableNames.template)
|
||||
.where({ _id: normalizedTemplateId }).get();
|
||||
if (!Array.isArray(templates) || templates.length === 0) {
|
||||
return errorResult('template-not-found', '短信模板不存在');
|
||||
}
|
||||
const [template] = templates;
|
||||
if (!template || typeof template.content !== 'string' || typeof template.sign !== 'string') {
|
||||
return errorResult('template-invalid', '短信模板内容或签名无效');
|
||||
}
|
||||
|
||||
const clientInfo = this.getClientInfo && this.getClientInfo() || {};
|
||||
const task = await db.collection(this.tableNames.task).add({
|
||||
app_id: clientInfo.appId,
|
||||
name: options.taskName || '',
|
||||
template_id: normalizedTemplateId,
|
||||
template_content: template.content,
|
||||
vars: vars.data,
|
||||
to: target.data,
|
||||
send_qty: 0,
|
||||
success_qty: 0,
|
||||
fail_qty: 0,
|
||||
create_date: Date.now()
|
||||
});
|
||||
|
||||
uniSmsCo.createUserSmsMessage(task.id);
|
||||
return new Promise(resolve => setTimeout(() => resolve({
|
||||
errCode: 0,
|
||||
errMsg: '任务创建成功',
|
||||
taskId: task.id
|
||||
}), 300));
|
||||
},
|
||||
|
||||
async createUserSmsMessage(taskId, execData = {}) {
|
||||
if (!isId(taskId)) return errorResult('task-id-required', '缺少有效的任务ID');
|
||||
if (!isPlainObject(execData)) return errorResult('exec-data-invalid', '执行参数必须是对象');
|
||||
|
||||
const parallel = 100;
|
||||
let beforeId;
|
||||
const { data: tasks = [] } = await db.collection(this.tableNames.task)
|
||||
.where({ _id: String(taskId) }).get();
|
||||
if (!Array.isArray(tasks) || tasks.length === 0) {
|
||||
return errorResult('task-id-not-found', '任务ID不存在');
|
||||
}
|
||||
|
||||
const [task] = tasks;
|
||||
const storedTarget = normalizeTarget(task && task.to, true);
|
||||
if (storedTarget.error || !task || !isPlainObject(task.to)) {
|
||||
return errorResult('task-invalid', '短信任务的发送目标无效');
|
||||
}
|
||||
const taskTo = { ...storedTarget.data };
|
||||
const receiver = Array.isArray(taskTo.receiver) ? taskTo.receiver : [];
|
||||
const vars = task.vars === undefined || task.vars === null ? [] : task.vars;
|
||||
const varsResult = validateTemplateData(vars);
|
||||
if (varsResult.error) return errorResult('task-invalid', '短信任务的vars无效');
|
||||
|
||||
let query = { mobile: db.command.exists(true) };
|
||||
if (taskTo.type === 'user' && receiver.length > 0 && !taskTo.condition) {
|
||||
let index = 0;
|
||||
if (isId(execData.beforeId)) {
|
||||
const i = receiver.findIndex(id => String(id) === String(execData.beforeId));
|
||||
index = i !== -1 ? i + 1 : 0;
|
||||
}
|
||||
const currentReceiver = receiver.slice(index, index + parallel);
|
||||
// 批次耗尽时使用空集合查询,继续走统一的收尾逻辑,避免从头递归发送。
|
||||
query._id = db.command.in(currentReceiver);
|
||||
if (currentReceiver.length > 0) beforeId = currentReceiver[currentReceiver.length - 1];
|
||||
}
|
||||
if (taskTo.type === 'userTags') query.tags = db.command.in(receiver);
|
||||
if (taskTo.condition && Object.keys(taskTo.condition).length > 0) {
|
||||
query = { ...query, ...conditionConvert(taskTo.condition, db.command) };
|
||||
}
|
||||
if ((taskTo.condition && Object.keys(taskTo.condition).length > 0 || taskTo.type === 'userTags') &&
|
||||
isId(execData.beforeId)) {
|
||||
query._id = db.command.gt(execData.beforeId);
|
||||
}
|
||||
|
||||
const dynamicField = parserDynamicField(varsResult.data);
|
||||
const userFields = dynamicField['uni-id-users'] ? dynamicField['uni-id-users'].reduce((res, field) => {
|
||||
res[field] = true;
|
||||
return res;
|
||||
}, {}) : {};
|
||||
|
||||
const { data: users = [] } = await db.collection('uni-id-users')
|
||||
.where(query)
|
||||
.field({ mobile: true, ...userFields })
|
||||
.limit(parallel)
|
||||
.orderBy('_id', 'asc')
|
||||
.get();
|
||||
|
||||
if (!Array.isArray(users) || users.length === 0) {
|
||||
const count = await db.collection(this.tableNames.log).where({ task_id: taskId }).count();
|
||||
await db.collection(this.tableNames.task).where({ _id: taskId }).update({ send_qty: count.total });
|
||||
uniSmsCo.sendSms(taskId);
|
||||
return new Promise(resolve => setTimeout(() => resolve({
|
||||
errCode: 0,
|
||||
errMsg: '创建完成'
|
||||
}), 500));
|
||||
}
|
||||
|
||||
if (!beforeId) beforeId = users[users.length - 1]._id;
|
||||
const docs = [];
|
||||
for (const user of users) {
|
||||
const varData = buildTemplateData(varsResult.data, user || {});
|
||||
docs.push({
|
||||
uid: user._id,
|
||||
task_id: taskId,
|
||||
mobile: user.mobile,
|
||||
var_data: varData,
|
||||
status: 0,
|
||||
create_date: Date.now()
|
||||
});
|
||||
}
|
||||
await db.collection(this.tableNames.log).add(docs);
|
||||
uniSmsCo.createUserSmsMessage(taskId, { beforeId });
|
||||
return new Promise(resolve => setTimeout(() => resolve(), 500));
|
||||
},
|
||||
|
||||
async sendSms(taskId) {
|
||||
if (!isId(taskId)) return errorResult('task-id-required', '缺少有效的任务ID');
|
||||
const { data: tasks = [] } = await db.collection(this.tableNames.task)
|
||||
.where({ _id: String(taskId) }).get();
|
||||
if (!Array.isArray(tasks) || tasks.length === 0) {
|
||||
return errorResult('task-id-not-found', '任务ID不存在');
|
||||
}
|
||||
|
||||
const [task] = tasks;
|
||||
const vars = task.vars === undefined || task.vars === null ? [] : task.vars;
|
||||
const varsResult = validateTemplateData(vars);
|
||||
if (varsResult.error) return errorResult('task-invalid', '短信任务的vars无效');
|
||||
const isStaticTemplate = varsResult.data.length === 0;
|
||||
const sendData = {
|
||||
appId: task.app_id,
|
||||
smsKey: smsConfig.smsKey,
|
||||
smsSecret: smsConfig.smsSecret,
|
||||
templateId: task.template_id,
|
||||
data: {}
|
||||
};
|
||||
|
||||
const { data: records = [] } = await db.collection(this.tableNames.log)
|
||||
.where({ task_id: taskId, status: 0 })
|
||||
.limit(isStaticTemplate ? 50 : 1)
|
||||
.field({ mobile: true, var_data: true })
|
||||
.get();
|
||||
if (!Array.isArray(records) || records.length === 0) {
|
||||
return { errCode: 0, errMsg: '发送完成' };
|
||||
}
|
||||
|
||||
if (isStaticTemplate) {
|
||||
sendData.phoneList = records.map(record => record.mobile);
|
||||
} else {
|
||||
const [record] = records;
|
||||
sendData.phone = record.mobile;
|
||||
sendData.data = record.var_data || {};
|
||||
}
|
||||
|
||||
try {
|
||||
await uniCloud.sendSms(sendData);
|
||||
await db.collection(this.tableNames.log).where({
|
||||
_id: db.command.in(records.map(record => record._id))
|
||||
}).update({ status: 1, send_date: Date.now() });
|
||||
await db.collection(this.tableNames.task).where({ _id: taskId }).update({
|
||||
success_qty: db.command.inc(records.length)
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[sendSms Fail]', e);
|
||||
await db.collection(this.tableNames.log).where({
|
||||
_id: db.command.in(records.map(record => record._id))
|
||||
}).update({
|
||||
status: 2,
|
||||
reason: e && (e.errMsg || e.message) || '未知原因',
|
||||
send_date: Date.now()
|
||||
});
|
||||
await db.collection(this.tableNames.task).where({ _id: taskId }).update({
|
||||
fail_qty: db.command.inc(records.length)
|
||||
});
|
||||
return errorResult('send-failed', e && (e.errMsg || e.message) || '短信发送失败');
|
||||
}
|
||||
|
||||
uniSmsCo.sendSms(taskId);
|
||||
return new Promise(resolve => setTimeout(() => resolve(), 500));
|
||||
},
|
||||
|
||||
async template() {
|
||||
const { data: templates = [] } = await db.collection(this.tableNames.template).get();
|
||||
return Array.isArray(templates) ? templates : [];
|
||||
},
|
||||
|
||||
async task(id) {
|
||||
if (!isId(id)) return errorResult('task-id-required', '缺少有效的任务ID');
|
||||
const { data: tasks = [] } = await db.collection(this.tableNames.task)
|
||||
.where({ _id: String(id) }).get();
|
||||
if (!Array.isArray(tasks) || tasks.length === 0) {
|
||||
return errorResult('task-id-not-found', '任务ID不存在');
|
||||
}
|
||||
return tasks[0];
|
||||
},
|
||||
|
||||
async updateTemplates(templates) {
|
||||
if (!Array.isArray(templates) || templates.length === 0) {
|
||||
return errorResult('template-is-null', '缺少模板信息');
|
||||
}
|
||||
const normalized = [];
|
||||
for (const template of templates) {
|
||||
if (!isPlainObject(template) || !isId(template.templateId) ||
|
||||
typeof template.templateName !== 'string' || !template.templateName.trim() ||
|
||||
typeof template.templateContent !== 'string' || !template.templateContent.trim() ||
|
||||
typeof template.templateType !== 'string' || !template.templateType.trim() ||
|
||||
typeof template.templateSign !== 'string' || !template.templateSign.trim()) {
|
||||
return errorResult('template-invalid', '短信模板字段不完整或格式无效');
|
||||
}
|
||||
normalized.push({
|
||||
id: String(template.templateId).trim(),
|
||||
name: template.templateName.trim(),
|
||||
content: template.templateContent,
|
||||
type: template.templateType,
|
||||
sign: template.templateSign
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all(normalized.map(template => db.collection(this.tableNames.template)
|
||||
.doc(template.id).set({
|
||||
name: template.name,
|
||||
content: template.content,
|
||||
type: template.type,
|
||||
sign: template.sign
|
||||
})));
|
||||
return { errCode: 0, errMsg: '更新成功' };
|
||||
},
|
||||
|
||||
async preview(to, templateId, templateData, options = {}) {
|
||||
const templateIdError = validateTemplateId(templateId);
|
||||
if (templateIdError) return templateIdError;
|
||||
const target = normalizeTarget(to);
|
||||
if (target.error) return target.error;
|
||||
const vars = validateTemplateData(templateData);
|
||||
if (vars.error) return vars.error;
|
||||
|
||||
const normalizedTemplateId = String(templateId).trim();
|
||||
const { data: templates = [] } = await db.collection(this.tableNames.template)
|
||||
.where({ _id: normalizedTemplateId }).get();
|
||||
if (!Array.isArray(templates) || templates.length === 0) {
|
||||
return errorResult('template-not-found', '模板不存在');
|
||||
}
|
||||
const [template] = templates;
|
||||
if (!template || typeof template.content !== 'string' || typeof template.sign !== 'string') {
|
||||
return errorResult('template-invalid', '短信模板内容或签名无效');
|
||||
}
|
||||
|
||||
const count = 1;
|
||||
let query = { mobile: db.command.exists(true) };
|
||||
const normalizedTo = target.data;
|
||||
if (normalizedTo.type === 'user' && normalizedTo.receiver.length > 0 && !normalizedTo.condition) {
|
||||
query._id = db.command.in(normalizedTo.receiver);
|
||||
}
|
||||
if (normalizedTo.type === 'userTags') query.tags = db.command.in(normalizedTo.receiver);
|
||||
if (normalizedTo.condition && Object.keys(normalizedTo.condition).length > 0) {
|
||||
query = { ...query, ...conditionConvert(normalizedTo.condition, db.command) };
|
||||
}
|
||||
|
||||
const { data: users = [] } = await db.collection('uni-id-users').where(query).limit(count).get();
|
||||
const { total = 0 } = await db.collection('uni-id-users').where(query).count();
|
||||
if (!Array.isArray(users) || users.length === 0) {
|
||||
return errorResult('users-is-null', '请添加要发送的用户');
|
||||
}
|
||||
|
||||
const docs = users.map(user => {
|
||||
const varData = buildTemplateData(vars.data, user || {});
|
||||
const content = template.content.replace(/\$\{(.*?)\}/g, ($1, param) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(varData, param)) return $1;
|
||||
const value = varData[param];
|
||||
return value === undefined || value === null ? $1 : String(value);
|
||||
});
|
||||
return `【${template.sign}】${content}`;
|
||||
});
|
||||
|
||||
return { errCode: 0, errMsg: '', list: docs, total };
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user