create-pixle-koa-template 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-app.js +76 -0
- package/package.json +17 -0
- package/template/.env +20 -0
- package/template/app.js +49 -0
- package/template/config/db.js +29 -0
- package/template/config/email.js +30 -0
- package/template/config/index.js +36 -0
- package/template/config/redis.js +19 -0
- package/template/controllers/AuthController.js +71 -0
- package/template/controllers/DownloadController.js +18 -0
- package/template/controllers/UploadController.js +60 -0
- package/template/controllers/UserController.js +90 -0
- package/template/middleware/auth.js +91 -0
- package/template/middleware/errorHandler.js +17 -0
- package/template/middleware/logger.js +41 -0
- package/template/middleware/notFound.js +84 -0
- package/template/middleware/upload.js +165 -0
- package/template/models/Auth.js +11 -0
- package/template/models/BaseDAO.js +449 -0
- package/template/models/User.js +10 -0
- package/template/package-lock.json +3427 -0
- package/template/package.json +34 -0
- package/template/public/404.html +160 -0
- package/template/routes/auth.js +21 -0
- package/template/routes/download.js +9 -0
- package/template/routes/index.js +105 -0
- package/template/routes/upload.js +28 -0
- package/template/routes/user.js +22 -0
- package/template/services/AuthService.js +190 -0
- package/template/services/CodeRedisService.js +94 -0
- package/template/services/DownloadService.js +54 -0
- package/template/services/EmailService.js +245 -0
- package/template/services/JwtTokenService.js +50 -0
- package/template/services/PasswordService.js +133 -0
- package/template/services/TokenRedisService.js +29 -0
- package/template/services/UserService.js +128 -0
- package/template/utils/crypto.js +9 -0
- package/template/utils/passwordValidator.js +81 -0
- package/template/utils/prototype/day.js +237 -0
- package/template/utils/prototype/deepClone.js +32 -0
- package/template/utils/prototype/index.js +61 -0
- package/template/utils/response.js +26 -0
- package/template//344/275/277/347/224/250/346/225/231/347/250/213.md +881 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 密码校验工具类
|
|
3
|
+
*/
|
|
4
|
+
class PasswordValidator {
|
|
5
|
+
/**
|
|
6
|
+
* 密码强度验证
|
|
7
|
+
*/
|
|
8
|
+
static validate(password) {
|
|
9
|
+
const rules = {
|
|
10
|
+
minLength: 8,
|
|
11
|
+
maxLength: 128,
|
|
12
|
+
requireUppercase: false,//必须大写
|
|
13
|
+
requireLowercase: false,//必须小写
|
|
14
|
+
requireNumbers: false,//必须有数字
|
|
15
|
+
requireSpecialChars: false,//必须有特殊字符
|
|
16
|
+
weakPassword: ['12345678','87654321', 'password', 'admin123', 'qwertyui'],//禁用弱密码,根据实际扩展
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const errors = [];
|
|
20
|
+
|
|
21
|
+
// 长度检查
|
|
22
|
+
if (password.length < rules.minLength) {
|
|
23
|
+
errors.push(`密码长度至少 ${rules.minLength} 位`);
|
|
24
|
+
}
|
|
25
|
+
if (password.length > rules.maxLength) {
|
|
26
|
+
errors.push(`密码长度不能超过 ${rules.maxLength} 位`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 大写字母检查
|
|
30
|
+
if (rules.requireUppercase && !/[A-Z]/.test(password)) {
|
|
31
|
+
errors.push('密码必须包含至少一个大写字母');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 小写字母检查
|
|
35
|
+
if (rules.requireLowercase && !/[a-z]/.test(password)) {
|
|
36
|
+
errors.push('密码必须包含至少一个小写字母');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 数字检查
|
|
40
|
+
if (rules.requireNumbers && !/\d/.test(password)) {
|
|
41
|
+
errors.push('密码必须包含至少一个数字');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 特殊字符检查
|
|
45
|
+
if (rules.requireSpecialChars && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
|
|
46
|
+
errors.push('密码必须包含至少一个特殊字符');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 常见弱密码检查
|
|
50
|
+
if (rules.weakPassword.includes(password.toLowerCase())) {
|
|
51
|
+
errors.push('密码过于简单,请使用更复杂的密码');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
isValid: errors.length === 0,
|
|
56
|
+
errors
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 密码相似度检查(防止与用户名、邮箱等相似)
|
|
62
|
+
*/
|
|
63
|
+
static checkSimilarity(password, userInfo) {
|
|
64
|
+
const similarities = [];
|
|
65
|
+
|
|
66
|
+
if (userInfo.username && password.toLowerCase().includes(userInfo.username.toLowerCase())) {
|
|
67
|
+
similarities.push('密码不能包含用户名');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (userInfo.email) {
|
|
71
|
+
const emailLocalPart = userInfo.email.split('@')[0];
|
|
72
|
+
if (password.toLowerCase().includes(emailLocalPart.toLowerCase())) {
|
|
73
|
+
similarities.push('密码不能包含邮箱地址');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return similarities;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = PasswordValidator;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date日期处理
|
|
3
|
+
*
|
|
4
|
+
*/
|
|
5
|
+
const day = () => {
|
|
6
|
+
//Date原型增加格式化日期格式,调用new Date().format('yyyy-MM-dd')
|
|
7
|
+
Object.defineProperty(Date.prototype, 'format', {
|
|
8
|
+
value(type) {
|
|
9
|
+
let _timeStamp = this instanceof Date ? this.getTime() : new Date().getTime();
|
|
10
|
+
function formatTimeStamp(type, timeStamp = _timeStamp) {
|
|
11
|
+
if (typeof timeStamp === 'number' && !isNaN(timeStamp)) {
|
|
12
|
+
let res = '';
|
|
13
|
+
let date = new Date(timeStamp)
|
|
14
|
+
let year = date.getFullYear()
|
|
15
|
+
let month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
16
|
+
let day = date.getDate().toString().padStart(2, '0')
|
|
17
|
+
let hour = date.getHours().toString().padStart(2, '0')
|
|
18
|
+
let minus = date.getMinutes().toString().padStart(2, '0')
|
|
19
|
+
let second = date.getSeconds().toString().padStart(2, '0')
|
|
20
|
+
switch (type) {
|
|
21
|
+
case 'yyyy':
|
|
22
|
+
res = `${year}`
|
|
23
|
+
break;
|
|
24
|
+
case 'MM':
|
|
25
|
+
res = `${month}`
|
|
26
|
+
break;
|
|
27
|
+
case 'dd':
|
|
28
|
+
res = `${day}`
|
|
29
|
+
break;
|
|
30
|
+
case 'HH':
|
|
31
|
+
res = `${hour}`
|
|
32
|
+
break;
|
|
33
|
+
case 'mm':
|
|
34
|
+
res = `${minus}`
|
|
35
|
+
break;
|
|
36
|
+
case 'ss':
|
|
37
|
+
res = `${second}`
|
|
38
|
+
break;
|
|
39
|
+
case 'yyyy-MM':
|
|
40
|
+
res = `${year}-${month}`
|
|
41
|
+
break;
|
|
42
|
+
case 'yyyy-MM-dd':
|
|
43
|
+
res = `${year}-${month}-${day}`
|
|
44
|
+
break;
|
|
45
|
+
case 'yyyy-MM-dd HH':
|
|
46
|
+
res = `${year}-${month}-${day} ${hour}`
|
|
47
|
+
break;
|
|
48
|
+
case 'yyyy-MM-dd HH:mm':
|
|
49
|
+
res = `${year}-${month}-${day} ${hour}:${minus}`
|
|
50
|
+
break;
|
|
51
|
+
case 'yyyy-MM-dd HH:mm:ss':
|
|
52
|
+
res = `${year}-${month}-${day} ${hour}:${minus}:${second}`
|
|
53
|
+
break;
|
|
54
|
+
case 'yyyy.MM.dd':
|
|
55
|
+
res = `${year}.${month}.${day}`
|
|
56
|
+
break;
|
|
57
|
+
case 'yyyy.MM.dd HH:mm':
|
|
58
|
+
res = `${year}.${month}.${day} ${hour}:${minus}`
|
|
59
|
+
break;
|
|
60
|
+
case 'yyyy.MM.dd HH:mm:ss':
|
|
61
|
+
res = `${year}.${month}.${day} ${hour}:${minus}:${second}`
|
|
62
|
+
break;
|
|
63
|
+
case 'HH:mm:ss':
|
|
64
|
+
res = `${hour}:${minus}:${second}`
|
|
65
|
+
break;
|
|
66
|
+
case 'HH:mm':
|
|
67
|
+
res = `${hour}:${minus}`
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
res = `${year}.${month}.${day} ${hour}:${minus}:${second}`
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
return res
|
|
75
|
+
}
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
return formatTimeStamp.call(this, type)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
//获取某年某月共多少天
|
|
84
|
+
//new Date().getMonthDays()
|
|
85
|
+
Object.defineProperty(Date.prototype, 'getMonthDays', {
|
|
86
|
+
value() {
|
|
87
|
+
let _timeStamp = this instanceof Date ? this.getTime() : new Date().getTime();
|
|
88
|
+
let date = new Date(_timeStamp).format('yyyy-MM-dd');
|
|
89
|
+
function getSomeMonthDays(year, month) {
|
|
90
|
+
let febDays = year % 4 === 0 ? 29 : 28
|
|
91
|
+
let days = [31, febDays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
92
|
+
if (month <= days.length) {
|
|
93
|
+
return days[month - 1]
|
|
94
|
+
}
|
|
95
|
+
return ''
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return typeof date === 'string' && getSomeMonthDays.call(this, Number(date.split('-')[0]), Number(date.split('-')[1])) || ''
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/**Date原型增加last函数,上num个时间last(type,timeFormat,num)
|
|
104
|
+
* @param type :上一个时间刻度类型
|
|
105
|
+
* @param timeFormat : 时间格式
|
|
106
|
+
* @param num : 数量
|
|
107
|
+
*/
|
|
108
|
+
//例如:new Date().last('month','yyyy-MM-dd',3)//3个月前日期
|
|
109
|
+
Object.defineProperty(Date.prototype, 'last', {
|
|
110
|
+
|
|
111
|
+
value(type, formatType,num=1) {
|
|
112
|
+
const oneDayStamp = 24 * 60 * 60 * 1000;
|
|
113
|
+
let _timeStamp = this instanceof Date ? this.getTime() : new Date().getTime();
|
|
114
|
+
function last(type, timeStamp = _timeStamp) {
|
|
115
|
+
if (typeof timeStamp === 'number' && !isNaN(timeStamp)) {
|
|
116
|
+
let res = '';
|
|
117
|
+
let dateTime = new Date(timeStamp).format('yyyy-MM-dd HH:mm:ss')
|
|
118
|
+
let arr = dateTime.split(' ')[0].split('-')
|
|
119
|
+
let year = Number(arr[0])
|
|
120
|
+
let month = Number(arr[1])
|
|
121
|
+
let date = Number(arr[2]);
|
|
122
|
+
|
|
123
|
+
switch (type) {
|
|
124
|
+
//上num年
|
|
125
|
+
case 'year':
|
|
126
|
+
if(month===2&&date===29&&new Date(`${year-num}-${month}`).getMonthDays()<29){//2月29号特殊处理
|
|
127
|
+
res = new Date(`${year-num}-${month}-28${dateTime.substring(10)}`).format(formatType)
|
|
128
|
+
}
|
|
129
|
+
else{
|
|
130
|
+
res = new Date(`${year - num}${dateTime.substring(4)}`).format(formatType)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
break;
|
|
134
|
+
//上num月
|
|
135
|
+
case 'month':
|
|
136
|
+
let minuYear=parseInt(num/12);
|
|
137
|
+
let minuMonth=num%12;
|
|
138
|
+
let minuDate=month-minuMonth > 0 ? `${year-minuYear}-${month-minuMonth}`:`${year-minuYear-1}-${12+month-minuMonth}`
|
|
139
|
+
let total = new Date(minuDate).getMonthDays()
|
|
140
|
+
res = new Date(`${minuDate}-${Math.min(date, total)} ${dateTime.split(' ')[1]}`).format(formatType)
|
|
141
|
+
break;
|
|
142
|
+
//num天前
|
|
143
|
+
case 'day':
|
|
144
|
+
res = new Date(timeStamp - num*oneDayStamp).format(formatType)
|
|
145
|
+
break;
|
|
146
|
+
case 'hour':
|
|
147
|
+
res = new Date(timeStamp -num* 60 * 60 * 1000).format(formatType)
|
|
148
|
+
break;
|
|
149
|
+
case 'minute':
|
|
150
|
+
res = new Date(timeStamp - num*60 * 1000).format(formatType)
|
|
151
|
+
break;
|
|
152
|
+
case 'second':
|
|
153
|
+
res = new Date(timeStamp -num*1000).format(formatType)
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
res = new Date(timeStamp -num* oneDayStamp).format(formatType)
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
return res
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
return this
|
|
164
|
+
}
|
|
165
|
+
return last.call(this, type)
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
/**Date原型增加next函数,下num个时间next(type,timeFormat,num)
|
|
171
|
+
* @param type :上一个时间刻度类型
|
|
172
|
+
* @param timeFormat : 时间格式
|
|
173
|
+
* @param num : 数量
|
|
174
|
+
*/
|
|
175
|
+
//例如:new Date().next('month','yyyy-MM-dd',3)//3个月后日期
|
|
176
|
+
Object.defineProperty(Date.prototype, 'next', {
|
|
177
|
+
|
|
178
|
+
value(type, formatType,num=1) {
|
|
179
|
+
const oneDayStamp = 24 * 60 * 60 * 1000;
|
|
180
|
+
let _timeStamp = this instanceof Date ? this.getTime() : new Date().getTime();
|
|
181
|
+
function next(type, timeStamp = _timeStamp) {
|
|
182
|
+
if (typeof timeStamp === 'number' && !isNaN(timeStamp)) {
|
|
183
|
+
let res = '';
|
|
184
|
+
let dateTime = new Date(timeStamp).format('yyyy-MM-dd HH:mm:ss')
|
|
185
|
+
let arr = dateTime.split(' ')[0].split('-')
|
|
186
|
+
let year = Number(arr[0])
|
|
187
|
+
let month = Number(arr[1])
|
|
188
|
+
let date = Number(arr[2]);
|
|
189
|
+
switch (type) {
|
|
190
|
+
//下num年
|
|
191
|
+
case 'year':
|
|
192
|
+
if(month===2&&date===29&&new Date(`${year+num}-${month}`).getMonthDays()<29){//2月29号特殊处理
|
|
193
|
+
res = new Date(`${year+num}-${month}-28${dateTime.substring(10)}`).format(formatType)
|
|
194
|
+
}
|
|
195
|
+
else{
|
|
196
|
+
res = new Date(`${year + num}${dateTime.substring(4)}`).format(formatType)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
break;
|
|
200
|
+
//下num月
|
|
201
|
+
case 'month':
|
|
202
|
+
let plusYear=parseInt(num/12);
|
|
203
|
+
let plusMonth=num%12;
|
|
204
|
+
let plusDate=month+plusMonth < 13 ? `${year+plusYear}-${month+plusMonth}`:`${year+plusYear+1}-${month+plusMonth-12}`
|
|
205
|
+
let total = new Date(plusDate).getMonthDays()
|
|
206
|
+
res = new Date(`${plusDate}-${Math.min(date, total)} ${dateTime.split(' ')[1]}`).format(formatType)
|
|
207
|
+
break;
|
|
208
|
+
//下num天
|
|
209
|
+
case 'day':
|
|
210
|
+
res = new Date(timeStamp +num* oneDayStamp).format(formatType)
|
|
211
|
+
break;
|
|
212
|
+
case 'hour':
|
|
213
|
+
res = new Date(timeStamp + num*60 * 60 * 1000).format(formatType)
|
|
214
|
+
break;
|
|
215
|
+
case 'minute':
|
|
216
|
+
res = new Date(timeStamp + num*60 * 1000).format(formatType)
|
|
217
|
+
break;
|
|
218
|
+
case 'second':
|
|
219
|
+
res = new Date(timeStamp + num*1000).format(formatType)
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
res = new Date(timeStamp + num*oneDayStamp).format(formatType)
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
return res
|
|
227
|
+
|
|
228
|
+
}
|
|
229
|
+
return this
|
|
230
|
+
}
|
|
231
|
+
return next.call(this, type)
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
}
|
|
237
|
+
module.exports = day
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 深度克隆
|
|
3
|
+
* Object原型增加深度克隆方法,调用object.deepClone()
|
|
4
|
+
*/
|
|
5
|
+
const deepClone = () => {
|
|
6
|
+
Object.defineProperty(Object.prototype, 'deepClone', {
|
|
7
|
+
value() {
|
|
8
|
+
function isNullOrUndefined(val) {
|
|
9
|
+
return val === null || val === undefined
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(this)) { //数组类型
|
|
12
|
+
return this.map(item => {
|
|
13
|
+
return isNullOrUndefined(item) ? item : item.deepClone()//null和undefined无原型直接返回
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
} else if (Object.prototype.toString.call(this) === '[object Object]') {//对象类型
|
|
17
|
+
let obj = {}
|
|
18
|
+
for (let key in this) {
|
|
19
|
+
if (this.hasOwnProperty(key)) {
|
|
20
|
+
obj[key] = isNullOrUndefined(this[key]) ? this[key] : this[key].deepClone()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return obj
|
|
24
|
+
|
|
25
|
+
}
|
|
26
|
+
return this.valueOf()//其他类型(基本类型或函数)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
module.exports =deepClone
|
|
32
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// utils/prototype/index.js
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 自动执行 prototype 目录下所有 .js 文件的默认导出方法(除了 index.js)
|
|
7
|
+
*/
|
|
8
|
+
function loadAndExecutePrototypes() {
|
|
9
|
+
const currentDir = __dirname;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// 读取当前目录下的所有文件
|
|
13
|
+
const files = fs.readdirSync(currentDir);
|
|
14
|
+
|
|
15
|
+
// 过滤出 .js 文件,排除 index.js
|
|
16
|
+
const jsFiles = files.filter((file) => {
|
|
17
|
+
return (
|
|
18
|
+
file.endsWith(".js") && file !== "index.js" && !file.startsWith(".") // 排除隐藏文件
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
// 按顺序执行每个文件的默认导出方法
|
|
25
|
+
jsFiles.forEach((file) => {
|
|
26
|
+
const filePath = path.join(currentDir, file);
|
|
27
|
+
const moduleName = path.basename(file, ".js");
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// 导入模块
|
|
31
|
+
const module = require(filePath);
|
|
32
|
+
|
|
33
|
+
// 检查是否有默认导出方法
|
|
34
|
+
if (module && typeof module.default === "function") {
|
|
35
|
+
module.default();
|
|
36
|
+
} else if (module && typeof module === "function") {
|
|
37
|
+
// 如果模块本身就是一个函数,也执行
|
|
38
|
+
module();
|
|
39
|
+
} else {
|
|
40
|
+
console.warn(
|
|
41
|
+
`[Prototype Loader] ${moduleName} has no default function to execute`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(
|
|
46
|
+
`[Prototype Loader] Error executing ${file}:`,
|
|
47
|
+
error.message
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(
|
|
54
|
+
"[Prototype Loader] Error loading prototype files:",
|
|
55
|
+
error.message
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 导出加载函数
|
|
61
|
+
module.exports = { loadAndExecutePrototypes };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* 响应工具类
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 成功
|
|
7
|
+
const success = (ctx, data = null, message = 'success', code = 200) => {
|
|
8
|
+
ctx.body = {
|
|
9
|
+
code,
|
|
10
|
+
message,
|
|
11
|
+
data,
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// 失败
|
|
16
|
+
const error = (ctx, message = 'error', code = 500) => {
|
|
17
|
+
ctx.body = {
|
|
18
|
+
code,
|
|
19
|
+
message,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
success,
|
|
25
|
+
error
|
|
26
|
+
};
|