├── Procfile
├── views
├── index.jade
├── error.jade
└── layout.jade
├── public
├── assets
│ ├── img
│ │ ├── 201501171020455646.jpg
│ │ ├── 1.svg
│ │ ├── 7.svg
│ │ ├── 8.svg
│ │ ├── 13.svg
│ │ ├── 3.svg
│ │ ├── 11.svg
│ │ ├── 5.svg
│ │ ├── 10.svg
│ │ ├── 6.svg
│ │ ├── 4.svg
│ │ ├── 12.svg
│ │ ├── 9.svg
│ │ └── 2.svg
│ ├── css
│ │ └── main.css
│ └── js
│ │ └── main.js
└── index.html
├── routes
├── index.js
└── drawPrize.route.js
├── middlewares
├── checkMemberAuth.js
├── checkMemberRestPoint.js
├── checkFreeCode.js
├── checkMemberDrawRecord.js
├── checkMemberLuckyRecord.js
├── updateMemberDrawRecord.js
├── drawPrize.js
├── checkActivityDate.js
├── checkRestPrizePool.js
└── checkActivityLimit.js
├── package.json
├── models
├── prizeRecord.model.js
├── prizeContact.model.js
├── prizeActivity.model.js
└── prizePool.model.js
├── utils
└── index.js
├── .gitignore
├── config
├── mongodb.js
├── prizeData.js
├── updatedb.js
└── prize.json
├── app.js
├── README.md
├── bin
└── www
└── controllers
└── drawPrize.controller.js
/Procfile:
--------------------------------------------------------------------------------
1 | web: node ./bin/www
--------------------------------------------------------------------------------
/views/index.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= title
5 | p Welcome to #{title}
6 |
--------------------------------------------------------------------------------
/views/error.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/public/assets/img/201501171020455646.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anightrabbit/lottery-api/HEAD/public/assets/img/201501171020455646.jpg
--------------------------------------------------------------------------------
/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 | link(rel='stylesheet', href='/stylesheets/style.css')
6 | body
7 | block content
8 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | const Router = require('express');
2 | const drawPrizeRoute = require('./drawPrize.route');
3 |
4 | const routes = new Router();
5 |
6 | routes.use('/activity', drawPrizeRoute);
7 |
8 | module.exports = routes;
9 |
--------------------------------------------------------------------------------
/middlewares/checkMemberAuth.js:
--------------------------------------------------------------------------------
1 | // 需求:登陆会员才有抽奖机会
2 |
3 | async function checkMemberAuth (req, res, next) {
4 | try {
5 | next();
6 | }
7 | catch(e) {
8 | throw new Error(e);
9 | }
10 | }
11 |
12 | module.exports = checkMemberAuth;
--------------------------------------------------------------------------------
/middlewares/checkMemberRestPoint.js:
--------------------------------------------------------------------------------
1 | // 需求:登陆会员消耗一定的积分参与一次抽奖
2 |
3 | async function checkMemberRestPoint (req, res, next) {
4 | try {
5 | next();
6 | }
7 | catch(e) {
8 | throw new Error(e);
9 | }
10 | }
11 |
12 | module.exports = checkMemberRestPoint;
13 |
--------------------------------------------------------------------------------
/middlewares/checkFreeCode.js:
--------------------------------------------------------------------------------
1 | // 需求:带有推广渠道code,免费抽奖一次
2 |
3 | async function checkFreeCode(req, res, next) {
4 | try {
5 | const {
6 | code
7 | } = req.body;
8 | req.isFreeCode = req.activityInfo.freeCode && req.activityInfo.freeCode.indexOf(code) > -1;
9 | next();
10 | } catch (e) {
11 | throw new Error(e);
12 | }
13 | }
14 |
15 | module.exports = checkFreeCode;
--------------------------------------------------------------------------------
/middlewares/checkMemberDrawRecord.js:
--------------------------------------------------------------------------------
1 | // 需求:参与抽奖会员能查看自己的抽奖记录
2 |
3 | const prizeRecordModel = require('../models/prizeRecord.model');
4 |
5 | async function checkMemberDrawRecord(req, res, next) {
6 | try {
7 | const {
8 | memberId
9 | } = req.body;
10 | req.drawRecords = await prizeRecordModel.find({
11 | memberId
12 | });
13 | next();
14 | } catch (e) {
15 | throw new Error(e);
16 | }
17 | }
18 |
19 | module.exports = checkMemberDrawRecord;
--------------------------------------------------------------------------------
/middlewares/checkMemberLuckyRecord.js:
--------------------------------------------------------------------------------
1 | // 需求:参与抽奖会员能查看自己的中奖记录
2 | // 实现:从抽奖记录中过滤中奖记录
3 |
4 | async function checkMemberLuckyRecord(req, res, next) {
5 | try {
6 | const records = req.drawRecords;
7 | console.log('records', records);
8 | const luckyRecords = records.filter(item => item.prizeType !== 'lucky' || item.prizeType === undefined);
9 | res.send({
10 | data: luckyRecords,
11 | msg: '中奖纪录',
12 | });
13 | // next();
14 | } catch (e) {
15 | throw new Error(e);
16 | }
17 | }
18 |
19 | module.exports = checkMemberLuckyRecord;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "drawprize-api",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./bin/www",
7 | "db": "node ./config/updatedb"
8 | },
9 | "dependencies": {
10 | "body-parser": "~1.18.2",
11 | "connect-timeout": "^1.9.0",
12 | "cookie-parser": "~1.4.3",
13 | "debug": "~2.6.9",
14 | "express": "~4.15.5",
15 | "jade": "^1.11.0",
16 | "mathjs": "^6.6.1",
17 | "mongoose": "^5.9.3",
18 | "morgan": "~1.9.0",
19 | "serve-favicon": "~2.4.5"
20 | },
21 | "devDependencies": {
22 | "axios": "^0.19.2",
23 | "lodash": "^4.17.15"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | lucky
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/models/prizeRecord.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const PrizeRecordSchema = new Schema(
6 | {
7 | memberId: {
8 | type: Number,
9 | required: [true, 'memberId is required!'],
10 | },
11 | freeCode: {
12 | type: String,// 参与抽奖的免费code
13 | },
14 | prizeNo: {
15 | type: Number,
16 | required: [true, 'prizeNo is required'],
17 | default: 0, // 奖池奖品被领取完,默认分配参与奖
18 | },
19 | prizeType: {
20 | type: String, // 奖品类型,标记中奖后的领取方式,若lucky则未中奖
21 | },
22 | },
23 | {
24 | timestamps: {
25 | createdAt: 'created_at',
26 | updatedAt: 'updated_at',
27 | },
28 | },
29 | );
30 |
31 | module.exports = mongoose.model('prizeRecord', PrizeRecordSchema);
--------------------------------------------------------------------------------
/middlewares/updateMemberDrawRecord.js:
--------------------------------------------------------------------------------
1 | // 需求:记录会员抽奖结果
2 |
3 | const {
4 | updatePrizePool,
5 | createPrizeRecord
6 | } = require('../controllers/drawPrize.controller');
7 |
8 | async function updateMemberDrawRecord(req, res, next) {
9 | try {
10 | const {
11 | prizeNo,
12 | prizeType
13 | } = req.prizeInfo;
14 | const body = req.body;
15 | // 奖池信息更新
16 | updatePrizePool(prizeNo, {
17 | checkStatus: true
18 | });
19 | // 保存会员抽奖记录
20 | createPrizeRecord({
21 | memberId: body.memberId,
22 | prizeNo,
23 | prizeType
24 | });
25 | res.send({
26 | data: req.prizeInfo,
27 | msg: prizeNo > -1 ? '恭喜中奖' : '谢谢参与',
28 | todayDrawRest: req.todayDrawRest - 1, // 今日剩余抽奖次数
29 | });
30 | next();
31 | } catch (e) {
32 | throw new Error(e);
33 | }
34 | }
35 |
36 | module.exports = updateMemberDrawRecord;
--------------------------------------------------------------------------------
/middlewares/drawPrize.js:
--------------------------------------------------------------------------------
1 | // 需求:抽奖要随机性,公平
2 | // 实现:用随机数生成的一个pool范围内的number,对应奖池的prizeNo
3 | // https://mathjs.org/docs/reference/functions/pickRandom.html
4 |
5 | const math = require('mathjs');
6 |
7 | const pickRandom = (prizeTokenArray) => prizeTokenArray.length ?
8 | math.pickRandom(prizeTokenArray) : -1;
9 |
10 | async function drawPrize(req, res, next) {
11 | try {
12 | const restPrizeNoList = req.restPrizeNoList;
13 | if (restPrizeNoList.length) {
14 | const prizeToken = pickRandom(req.restPrizeNoList);
15 | const prizeInfo = req.restPrizePool.find(item => item.prizeNo === prizeToken);
16 | req.prizeInfo = prizeInfo;
17 | } else {
18 | // 奖池已空
19 | req.prizeInfo = {
20 | prizeNo: -1,
21 | prizeType: 'lucky'
22 | }
23 | }
24 | next();
25 | } catch (e) {
26 | throw new Error(e);
27 | }
28 | }
29 |
30 | module.exports = drawPrize;
--------------------------------------------------------------------------------
/utils/index.js:
--------------------------------------------------------------------------------
1 | // 需求:大数组中剔除一个小数组
2 | // 返回一个
3 | // prizeLimit['$and'] = [ { prizeNo: { $le: 1,$gt:6 } }];
4 |
5 |
6 | const formatPrizeSetting = (prizeSetting) => prizeSetting.filter(item => item.limit);
7 |
8 | const countPrizeTotal = (prizeSetting) => prizeSetting.reduce((act,cur) => [...act,cur.total],[]) ;
9 |
10 | const filterLimitPrize = (drawRecord,prizeSetting) => {
11 | const prizeSettingLimit = formatPrizeSetting(prizeSetting);
12 | const countPrize = countPrizeTotal(prizeSetting);
13 | const queryLimit = [];
14 | drawRecord.forEach(item => {
15 | if(prizeSettingLimit.includes(item.prizeLevel)) {
16 | queryLimit.push({
17 | prizeNo:{
18 | $lte:countPrize[item.level - 1],
19 | $gt:countPrize[item.level > 1 ? item.level -2 : level - 1]
20 | }
21 | });
22 | }
23 | });
24 | return queryLimit;
25 | }
26 |
27 | module.exports = {
28 | filterLimitPrize
29 | }
--------------------------------------------------------------------------------
/models/prizeContact.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const PrizeContactSchema = new Schema(
6 | {
7 | memberId: {
8 | type: Number,
9 | required: [true, 'memberId is required!'],
10 | unique: true,
11 | },
12 | prizeNo: {
13 | type: Number,
14 | required: [true, 'prizeNo is required!'],
15 | unique: true,
16 | },
17 | name: {
18 | type: String,
19 | required: [true, 'contact name is required!'],
20 | trim: false,
21 | },
22 | phone: {
23 | type: String,
24 | required: [true, 'contact phone is required!'],
25 | trim: true,
26 | unique: false,
27 | },
28 | },
29 | {
30 | timestamps: {
31 | createdAt: 'created_at',
32 | updatedAt: 'updated_at',
33 | },
34 | },
35 | );
36 |
37 | module.exports = mongoose.model('prizeContact', PrizeContactSchema);
--------------------------------------------------------------------------------
/middlewares/checkActivityDate.js:
--------------------------------------------------------------------------------
1 | // 需求:活动是否在有效期
2 | const moment = require('moment');
3 | const activityModel = require('../models/prizeActivity.model');
4 |
5 | async function checkActivityDate(req, res, next) {
6 | try {
7 | const body = req.body;
8 | if (!body.activity) {
9 | res.send({
10 | msg: '活动不存在',
11 | err: true,
12 | });
13 | } else {
14 | const activityInfo = await getActivity(body.activity);
15 | if (!activityInfo) {
16 | res.send({
17 | msg: '活动尚未开始或已经结束',
18 | err: true,
19 | });
20 | }
21 | req.activityInfo = activityInfo;
22 | }
23 | next();
24 | } catch (e) {
25 | throw new Error(e);
26 | }
27 | }
28 |
29 | const getActivity = async (activity) => {
30 | const today = moment(new Date()).format('YYYY-MM-DD');
31 | const query = {
32 | activity,
33 | startDate: {
34 | $lt: today
35 | },
36 | endDate: {
37 | $gt: today
38 | }
39 | };
40 | return activityModel.findOne(query);
41 | }
42 |
43 | module.exports = checkActivityDate;
--------------------------------------------------------------------------------
/middlewares/checkRestPrizePool.js:
--------------------------------------------------------------------------------
1 | // 需求:大奖中奖次数限制
2 | // 实现:从奖池里捞取所有checkStatus是false的奖品
3 |
4 | const prizePoolModel = require('../models/prizePool.model');
5 | const {filterLimitPrize} = require('../utils/');
6 |
7 | async function checkRestPrizePool(req, res, next) {
8 | try {
9 | let prizeLimit = {
10 | checkStatus: false,
11 | }
12 | console.log(req.drawRecords.length,req.activityInfo.prizeSettingLimit);
13 | if (req.drawRecords.length) {
14 | const limits = filterLimitPrize(req.drawRecords,req.activityInfo.prizeSettingLimit);
15 | if (limits.length) prizeLimit['$and'] = limits;
16 | }
17 | const restPrizePool = await prizePoolModel.find(prizeLimit);
18 | console.log('restPrizePool', restPrizePool.length);
19 | if (restPrizePool && restPrizePool.length) {
20 | req.restPrizePool = restPrizePool;
21 | req.restPrizeNoList = restPrizePool.map(item => item.prizeNo);
22 | } else {
23 | res.send({
24 | msg: '奖池已空,😢',
25 | });
26 | }
27 | next();
28 | } catch (e) {
29 | throw new Error(e);
30 | }
31 | }
32 |
33 | module.exports = checkRestPrizePool;
--------------------------------------------------------------------------------
/models/prizeActivity.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const PrizeActivitySchema = new Schema(
6 | {
7 | activity: {
8 | type: String,
9 | required: [true, 'activity is required!'],
10 | },
11 | startDate: {
12 | type: Date,
13 | required: [true, 'startDate is required!'],
14 | },
15 | endDate: {
16 | type: Date,
17 | required: [true, 'endDate is required!'],
18 | },
19 | drawLimitTotal: {
20 | type: Number, // 每个用户限制参与抽奖总次数
21 | },
22 | drawLimitDay: { // 每个用户限制参与抽奖次数每天
23 | type: Number,
24 | },
25 | drawLimitTime: {
26 | type: Number, // 每间隔多长时间允许抽奖一次,单位/分钟
27 | },
28 | drawLimitPoint: {
29 | type: Number, // 抽奖一次消耗多少积分
30 | },
31 | freeCode: String, // 参与抽奖的免费code
32 | prizeSettingLimit: {
33 | type: Array, // 中奖次数限制
34 | }
35 | },
36 | {
37 | timestamps: {
38 | createdAt: 'created_at',
39 | updatedAt: 'updated_at',
40 | },
41 | },
42 | );
43 |
44 | module.exports = mongoose.model('prizeActivitie', PrizeActivitySchema);
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
--------------------------------------------------------------------------------
/models/prizePool.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const PrizePoolSchema = new Schema(
6 | {
7 | prizeLevel: {
8 | type: Number,
9 | default: 0, // 未中奖
10 | required: [true, 'prizeLevel is required!'],
11 | },
12 | prizeType: {
13 | type: String,
14 | enum: ['form', 'coupon', 'point', 'lucky'],
15 | required: [true, 'prizeType is required!'],
16 | // 奖品领取方式:表单填写联系方式,发放优惠券券码,发放积分
17 | },
18 | prizeText: {
19 | type: String, // 奖品描述
20 | trim: true,
21 | required: [true, 'prizeText is required!'],
22 | },
23 | prizeNo: {
24 | type: Number,
25 | unique: true,
26 | required: [true, 'prizeNo is required!'],
27 | },
28 | checkStatus: {
29 | type: Boolean, // 奖品被领取后,要更新
30 | default: false,
31 | },
32 | couponCode: { // 优惠券券码,如果有
33 | type: String,
34 | unique: true,
35 | trim: true,
36 | }
37 | },
38 | {
39 | timestamps: {
40 | createdAt: 'created_at',
41 | updatedAt: 'updated_at',
42 | },
43 | },
44 | );
45 |
46 | module.exports = mongoose.model('prizePool', PrizePoolSchema);
--------------------------------------------------------------------------------
/config/mongodb.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 | mongoose.Promise = global.Promise;
3 |
4 | // 测试账户:test,test123456,read-only
5 | // 线上地址mongodb://:@ds155747.mlab.com:55747/demo
6 | // 本地地址mongodb://test:test123456@127.0.0.1:27017:/demo
7 | var DB_user = encodeURIComponent('test');
8 | var DB_password = encodeURIComponent('test123456');
9 | var DB_host_local = '127.0.0.1:27017'
10 | var DB_host = 'ds155747.mlab.com:55747';
11 | var DB_name = 'demo'
12 | var DB_url = `mongodb://${DB_user}:${DB_password}@${DB_host}/${DB_name}`;
13 | var options = {
14 | useNewUrlParser: true,
15 | autoIndex: false, // Don't build indexes
16 | poolSize: 10, // Maintain up to 10 socket connections
17 | // If not connected, return errors immediately rather than waiting for reconnect
18 | bufferMaxEntries: 0,
19 | connectTimeoutMS: 10000, // Give up initial connection after 10 seconds
20 | socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity
21 | family: 4, // Use IPv4, skip trying IPv6
22 | useUnifiedTopology: true
23 | }
24 | mongoose.connect(DB_url, options);
25 | var db = mongoose.connection;
26 | db.on('connected', function() {
27 | console.log('Successfully,Mongoose connection open to ' + DB_url);
28 | });
29 | db.on('error', function(err) {
30 | console.log('fail,Mongoose connection error: ' + err);
31 | });
32 | db.on('disconnected', function() {
33 | console.log('Mongoose connection disconnected');
34 | });
--------------------------------------------------------------------------------
/middlewares/checkActivityLimit.js:
--------------------------------------------------------------------------------
1 | // 需求:活动要求会员每天抽奖次数不能超过限制
2 | // 新增:抽奖总次数的限制不告知用户,实际抽奖次数超过这个限制后,都是返回谢谢参与
3 | // 同时,需要前台需要告知用户当天剩余抽奖次数
4 |
5 | const moment = require('moment');
6 |
7 | const {
8 | createPrizeRecord
9 | } = require('../controllers/drawPrize.controller');
10 |
11 | async function checkActivityLimit(req, res, next) {
12 | try {
13 | const {
14 | memberId
15 | } = req.body;
16 | const records = req.drawRecords;
17 | // 每天抽奖次数限制
18 | if (req.activityInfo.drawLimitDay) {
19 | const today = moment(new Date()).format('YYYYMMDD');
20 | const todays = records.filter(item => {
21 | const date = moment(item.created_at).format('YYYYMMDD');
22 | return date === today;
23 | });
24 | req.todayDrawRest = req.activityInfo.drawLimitDay - todays.length;
25 | }
26 | if (!req.todayDrawRest) {
27 | res.send({
28 | msg: '今日抽奖次数没有了,明天再来',
29 | data: null,
30 | todayDrawRest: 0
31 | });
32 | }
33 | if (records && records.length > req.activityInfo.drawLimitTotal) {
34 | // 保存会员抽奖记录
35 | createPrizeRecord({
36 | memberId,
37 | prizeNo: -1,
38 | prizeType: 'lucky'
39 | });
40 | res.send({
41 | msg: '谢谢参与',
42 | data: {
43 | prizeNo: -1,
44 | prizeType: 'lucky'
45 | },
46 | todayDrawRest: req.todayDrawRest - 1,
47 | });
48 | }
49 | next();
50 | } catch (e) {
51 | throw new Error(e);
52 | }
53 | }
54 |
55 | module.exports = checkActivityLimit;
--------------------------------------------------------------------------------
/config/prizeData.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "fuck3yue": [{
3 | // drawLimitTime: 2, // 2分钟内只允许抽奖一次
4 | freeCode: 'hello,github',
5 | total: 9421,
6 | drawLimitTotal: 10,// 中奖次数限制
7 | drawLimitDay: 3, // 每天参与抽奖次数限制
8 | drawLimitPoint: 100,// 参与抽奖每次需要消耗积分数量
9 | startDate: '2020-03-01',// 抽奖活动开始时间
10 | endDate: '2020-03-31',// 抽奖活动结束时间
11 | prizeSetting: [{
12 | level: 1,
13 | text: '免费全球游78天',
14 | type: 'form',
15 | total: 1,
16 | },{
17 | level: 2,
18 | text: '免费游轮5天4晚',
19 | type: 'form',
20 | total: 5,
21 | limit: 1, // 限制该奖每个会员最多只能中一次
22 | },{
23 | level: 3,
24 | text: '免费住酒店3晚',
25 | type: 'form',
26 | total: 15,
27 | limit: 1
28 | },{
29 | level: 4,
30 | text: '奖励5000积分',
31 | type: 'point',
32 | total: 400,
33 | limit: 1
34 | },{
35 | level: 5,
36 | text: '优惠券满1000减500',
37 | type: 'coupon',
38 | total: 600,
39 | },{
40 | level: 6,
41 | text: '优惠券满1500减500',
42 | type: 'coupon',
43 | total: 600,
44 | },{
45 | level: 7,
46 | text: '优惠券满2000减500',
47 | type: 'coupon',
48 | total: 800,
49 | },{
50 | level: 8,
51 | text: '奖励300积分',
52 | type: 'point',
53 | total: 2000,
54 | },{
55 | level: 9,
56 | text: '奖励100积分',
57 | type: 'point',
58 | total: 2000,
59 | },{
60 | level: 10,
61 | text: '参与奖',
62 | type: 'lucky',
63 | total: 3000,
64 | }],
65 | prizeSettingLimit: [0,1,1,1,0,0,0,0,0,0],
66 | }],
67 | };
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var path = require('path');
3 | var favicon = require('serve-favicon');
4 | var logger = require('morgan');
5 | var cookieParser = require('cookie-parser');
6 | var bodyParser = require('body-parser');
7 | var timeout = require('connect-timeout')
8 | var apiRoutes = require('./routes');
9 |
10 | var db = require('./config/mongodb');
11 |
12 | var app = express();
13 |
14 | // view engine setup
15 | app.set('views', path.join(__dirname, 'views'));
16 | app.set('view engine', 'jade');
17 |
18 |
19 | // uncomment after placing your favicon in /public
20 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
21 | app.use(timeout('20s'))
22 | app.use(logger('dev'));
23 | app.use(bodyParser.json());
24 | app.use(bodyParser.urlencoded({ extended: false }));
25 | app.use(cookieParser());
26 | app.use(express.static(path.join(__dirname, 'public')));
27 |
28 | app.use('/api', apiRoutes);
29 |
30 | // catch 404 and forward to error handler
31 | app.use(function(req, res, next) {
32 | var err = new Error('Not Found');
33 | err.status = 404;
34 | next(err);
35 | });
36 |
37 | // error handler
38 | app.use(function(err, req, res, next) {
39 | // set locals, only providing error in development
40 | res.locals.message = err.message;
41 | res.locals.error = req.app.get('env') === 'development' ? err : {};
42 |
43 | // render the error page
44 | res.status(err.status || 500);
45 | res.render('error');
46 | });
47 |
48 | module.exports = app;
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # h5营销抽奖活动API
2 |
3 | ## 功能实现
4 |
5 | ### config目录,API配置信息
6 | + prizeData.js文件配置奖池信息,各种需求限制自由配置,开启中间件,自由搭配。
7 | + mongodb配置本地开发环境 “mongodb://test:test123456@127.0.0.1:27017:/demo”
8 | + updatedb.js帮助导入奖池数据和抽奖活动数据
9 |
10 | ### models目录,MonogoDB数据模型
11 | + prizeActivity.model.js记录抽奖活动信息
12 | + prizeContact.model.js记录大奖用户领取联系信息
13 | + prizePool.model.js记录奖池信息
14 | + prizeRecord.model.js记录抽奖记录
15 |
16 | ### middlewares目录,中间件
17 | + checkMemberAuth.js检查用户抽奖权限,非登陆会员不可参与抽奖
18 | + checkActivityDate.js检查活动有效期
19 | + checkActivityLimit.js检查活动参与次数限制
20 | + checkFreeCode.js 检查渠道免费抽奖码
21 | + checkMemberDrawRecord.js检查会员抽奖记录,限制抽奖次数
22 | + checkMemberLuckyRecord.js检查会员中奖记录
23 | + checkMemberRestPoint.js检查会员剩余积分是否足够参与一次抽奖
24 | + drawPrize.js 生成随机数对应奖池的奖品编号,确定是否中奖
25 | + updateMemberDrawRecord.js 更新会员抽奖记录
26 |
27 | ### public目录
28 | 用react写了一个九宫格类型的抽奖demo,备注[目录下有隐藏福利]
29 |
30 |
31 | ### 抽奖请求示例
32 |
33 | 请求参数:
34 | | 参数 | 类型 |可选| 备注 |
35 | | -------- | -------- | -------- |-------- |
36 | | memberId|number|否|会员Id|
37 | | activity|string|否|活动名称|
38 | | code|string|是|渠道|
39 |
40 | {
41 | "memberId": 15,
42 | "activity":"fuck3yue",
43 | "code":"hello"
44 | }
45 |
46 | 返回参数:
47 |
48 | {
49 | "data": {
50 | "prizeLevel": 8,
51 | "checkStatus": false,
52 | "_id": "5e71caf9a1b3fc3522f23ad0",
53 | "prizeType": "point",
54 | "prizeText": "奖励300积分",
55 | "prizeNo": 3613,
56 | "__v": 0,
57 | "created_at": "2020-03-18T07:17:13.827Z",
58 | "updated_at": "2020-03-18T07:17:13.827Z"
59 | },
60 | "msg": "🎉🎉🎉,中奖了呢"
61 | }
62 |
--------------------------------------------------------------------------------
/routes/drawPrize.route.js:
--------------------------------------------------------------------------------
1 | const Router = require('express');
2 | const Controller = require('../controllers/drawPrize.controller');
3 | const checkMemberAuth = require('../middlewares/checkMemberAuth');
4 | const checkFreeCode = require('../middlewares/checkFreeCode');
5 | const checkRestPrizePool = require('../middlewares/checkRestPrizePool');
6 | const checkMemberRestPoint = require('../middlewares/checkMemberRestPoint');
7 | const drawPrize = require('../middlewares/drawPrize');
8 | const updateMemberDrawRecord = require('../middlewares/updateMemberDrawRecord');
9 | const checkActivityDate = require('../middlewares/checkActivityDate');
10 | const checkMemberDrawRecord = require('../middlewares/checkMemberDrawRecord');
11 | const checkActivityLimit = require('../middlewares/checkActivityLimit');
12 | const checkMemberLuckyRecord = require('../middlewares/checkMemberLuckyRecord');
13 |
14 | const routes = new Router();
15 |
16 | routes.route('/')
17 | .get((req,res,next) => res.send('good luck'));
18 |
19 | routes
20 | .route('/luckyDraw')
21 | .post(checkMemberAuth,// 先检查参与抽奖用户权限
22 | checkActivityDate, // 检查抽奖活动有效期
23 | checkFreeCode, // 检查参与用户渠道来源,是否需要消耗积分参与
24 | checkMemberDrawRecord, //检查用户抽奖记录
25 | checkActivityLimit, // 检查活动次数限制
26 | checkMemberRestPoint, // 检查用户积分是否足够,如足够,先扣积分,再参与
27 | checkRestPrizePool, // 检查奖池是否有剩余奖品,无奖品,则返回谢谢参与
28 | drawPrize, // 抽奖,产生一个随机数号码,对应奖池奖品编号,若有则中奖,若无,则表示未中奖
29 | updateMemberDrawRecord); // 最后更新抽奖记录
30 |
31 | routes
32 | .route('/updatePrizePool')
33 | .post(Controller.createAndUpdateForPrizePool);
34 |
35 | routes
36 | .route('/updateActivity')
37 | .post(Controller.createAndUpdateForActivity);
38 |
39 | routes
40 | .route('/luckylist')
41 | .post(checkMemberAuth,
42 | checkActivityDate,
43 | checkMemberDrawRecord,
44 | checkMemberLuckyRecord);
45 |
46 | module.exports = routes;
47 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('drawprize-api:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/controllers/drawPrize.controller.js:
--------------------------------------------------------------------------------
1 | const prizePoolModel = require('../models/prizePool.model');
2 | const prizeActivityModel = require('../models/prizeActivity.model');
3 | const prizeRecordModel = require('../models/prizeRecord.model');
4 |
5 | // res错误
6 | const handleError = (error, res) =>
7 | res.send({
8 | code: 500,
9 | hasError: true,
10 | error,
11 | });
12 | // res成功
13 | const handleSuccess = (params, res) => {
14 | const result = {
15 | code: 200,
16 | ...params,
17 | };
18 | res.send(result);
19 | };
20 | const createHandleActivity = async (req, res) => {
21 | const body = req.body;
22 | // 批量插入
23 | const record = await prizeActivityModel.insertMany(body);
24 | return !record ?
25 | handleError(record, res) :
26 | handleSuccess({
27 | msg: '添加成功',
28 | // data: record,
29 | },
30 | res,
31 | req,
32 | );
33 | };
34 | const createHandlePool = async (req, res) => {
35 | const body = req.body;
36 | // 批量插入
37 | const record = await prizePoolModel.insertMany(body);
38 | return !record ?
39 | handleError(record, res) :
40 | handleSuccess({
41 | msg: '添加成功',
42 | // data: record,
43 | },
44 | res,
45 | req,
46 | );
47 | };
48 |
49 | const createAndUpdateForPrizePool = async (req, res) => {
50 | try {
51 | createHandlePool(req, res);
52 | } catch (e) {
53 | handleError(e, res);
54 | }
55 | };
56 |
57 | const createAndUpdateForActivity = async (req, res) => {
58 | try {
59 | createHandleActivity(req, res);
60 | } catch (e) {
61 | handleError(e, res);
62 | }
63 | };
64 |
65 | const updatePrizePool = async (prizeNo, updateOptions) => {
66 | const result = await prizePoolModel.updateOne({
67 | prizeNo
68 | }, updateOptions);
69 | console.log(result.n, result.nModified);
70 | }
71 |
72 | const createPrizeRecord = async (data) => {
73 | const result = await prizeRecordModel.create(data);
74 | console.log(result);
75 | }
76 |
77 | module.exports = {
78 | createAndUpdateForPrizePool,
79 | createAndUpdateForActivity,
80 | updatePrizePool,
81 | createPrizeRecord
82 | };
--------------------------------------------------------------------------------
/config/updatedb.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const _ = require('lodash');
3 | const DATA = require('./prizeData');
4 | const CouponCode = require('./prize.json');
5 |
6 | const dbURL1 = 'http://localhost:3000/api/activity/updatePrizePool';
7 | const dbURL2 = 'http://localhost:3000/api/activity/updateActivity';
8 |
9 |
10 | const update = (URL, data) => axios.post(URL, data).then(res => {
11 | console.log('新增的' + data.length + '条数据更新完成。。。');
12 | }).catch(err => {
13 | console.log(err);
14 | });
15 |
16 | // 生成抽奖活动数据
17 | const generateActivity = (activityName) => {
18 | const activityModelData = [];
19 | DATA[activityName].forEach(activitySetting => {
20 | activityModelData.push({
21 | ...activitySetting,
22 | activity: activityName
23 | });
24 | });
25 | return activityModelData;
26 | };
27 |
28 | // 生成奖池数据
29 | const generatePrizePool = (poolConfig, activity) => {
30 | let num = 0;
31 | const pool = poolConfig[activity];
32 | const prizes = [];
33 | pool.map(activityItem => {
34 | activityItem.prizeSetting.map((prizeSettingItem, prizeSettingIndex) => {
35 | for (let i = 0; i < prizeSettingItem.total; i++) {
36 | num++;
37 | const item = {
38 | prizeLevel: prizeSettingItem.level,
39 | prizeType: prizeSettingItem.type,
40 | prizeText: prizeSettingItem.text,
41 | prizeNo: num,
42 | };
43 | if (CouponCode[prizeSettingItem.level] && CouponCode[prizeSettingItem.level].length) {
44 | item.couponCode = CouponCode[prizeSettingItem.level][i];
45 | }
46 | prizes.push(item);
47 | }
48 | });
49 | });
50 | return prizes;
51 | }
52 |
53 | const allPrize = generatePrizePool(DATA, 'fuck3yue');
54 | const allActivityData = generateActivity('fuck3yue');
55 |
56 | const chunkData = (url, data) => {
57 | // 切割数组
58 | const chunkArray = _.chunk(data, 100);
59 | console.log('chunkArray-->', chunkArray.length);
60 | // 批量插入数据
61 | _.forEach(chunkArray, (item, index) => {
62 | setTimeout(() => {
63 | axios.post(url, item).then(()=> {
64 | if (index === chunkArray.length - 1) {
65 | console.log('全部更新完成');
66 | }
67 | });
68 | }, 1000);
69 | });
70 | };
71 |
72 | update(dbURL2, allActivityData);
73 | chunkData(dbURL1, allPrize);
74 |
--------------------------------------------------------------------------------
/public/assets/css/main.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding:0;
4 | }
5 | body {
6 | background: #f5f5f5;
7 | }
8 | #app {
9 | max-width: 640px;
10 | min-width: 320px;
11 | width: 100%;
12 | margin: 0 auto;
13 | background: #FFEBBB;
14 | height: 100vh;
15 | }
16 | .title {
17 | font-size: 20px;
18 | margin-bottom: 20px;
19 | }
20 | .container {
21 | /*padding: 20px;*/
22 | /*background: red;*/
23 | }
24 | .container-inner {
25 | display: grid;
26 | justify-content: center;
27 | align-items: center;
28 | grid-template-columns: repeat(4, 62px);
29 | grid-template-rows: repeat(4, 53px);
30 | grid-row-gap: 4px;
31 | grid-column-gap: 4px;
32 | }
33 | .container-inner .inner-item {
34 | width: 62px;
35 | height: 53px;
36 | background-color:rgba(255,205,85,0.5);
37 | border-radius:7px;
38 | overflow: hidden;
39 | text-align: center;
40 | position: relative;
41 | }
42 |
43 | .inner-item::before {
44 | content:attr(data-order);
45 | position: absolute;
46 | left: 0px;
47 | top: 0px;
48 | text-align: center;
49 | font-size: 12px;
50 | line-height: 12px;
51 | font-style: italic;
52 | color: #FFF;
53 | width: 14px;
54 | height: 14px;
55 | border-radius: 7px;
56 | background: red;
57 | }
58 | .inner-item {
59 | width: 100%;
60 | background-size: 100%;
61 | background-repeat: no-repeat;
62 | background-position: center;
63 | display: block;
64 | /*opacity: .5;*/
65 | }
66 | .inner-item .text {
67 | display: none;
68 | font-size: 12px;
69 | font-family: all;
70 | color: #333;
71 | }
72 | .inner-item.item-center {
73 | grid-column-start: span 2;
74 | grid-row-start: span 2;
75 | margin: 0 auto;
76 | width: 120px;
77 | height: 96px;
78 | border-radius: 20px;
79 | display: flex;
80 | flex-direction:column;
81 | justify-content: center;
82 | align-items: center;
83 | background: none;
84 | font-size: 12px;
85 | color: red;
86 | }
87 | .inner-item.item-center::before {
88 | display: none;
89 | }
90 | .inner-item.highlight {
91 | background-color:rgba(255,205,85,1);
92 | }
93 |
94 | button {
95 | border:none;
96 | outline: none;
97 | }
98 | button.click-me {
99 | border: none;
100 | outline: none;
101 | position: relative;
102 | top: 0;
103 | color: rgba(255,255,255,1);
104 | text-decoration: none;
105 | background-color: rgba(219,87,5,1);
106 | font-weight: 500;
107 | font-size: 20px;
108 | display: block;
109 | padding: 4px;
110 | margin-bottom: 16px;
111 | -webkit-border-radius: 8px;
112 | -moz-border-radius: 8px;
113 | border-radius: 8px;
114 | -webkit-box-shadow: 0px 4px 0px rgba(219,31,5,1), 0px 4px 25px rgba(0,0,0,.2);
115 | -moz-box-shadow: 0px 4px 0px rgba(219,31,5,1), 0px 4px 25px rgba(0,0,0,.2);
116 | box-shadow: 0px 4px 0px rgba(219,31,5,1), 0px 4px 25px rgba(0,0,0,.2);
117 | width: 100px;
118 | text-align: center;
119 |
120 | -webkit-transition: all .1s ease;
121 | -moz-transition: all .1s ease;
122 | -ms-transition: all .1s ease;
123 | -o-transition: all .1s ease;
124 | transition: all .1s ease;
125 | }
126 |
127 | button.disable {
128 | opacity: .5;
129 | -webkit-box-shadow: 0px 3px 0px rgba(219,31,5,1), 0px 3px 6px rgba(0,0,0,.9);
130 | -moz-box-shadow: 0px 3px 0px rgba(219,31,5,1), 0px 3px 6px rgba(0,0,0,.9);
131 | box-shadow: 0px 3px 0px rgba(219,31,5,1), 0px 3px 6px rgba(0,0,0,.9);
132 | top: 2px;
133 | }
--------------------------------------------------------------------------------
/public/assets/js/main.js:
--------------------------------------------------------------------------------
1 | const root = document.getElementById('app');
2 |
3 | const { Fragment, useState, useEffect,memo } = React;
4 | const prizeData = [{
5 | level:1,
6 | text:'奖品',
7 | img: "/img/1.svg",
8 | order:1,
9 | },{
10 | level:2,
11 | text:'奖品',
12 | img: "/img/2.svg",
13 | order:2,
14 | },{
15 | level:3,
16 | text:'奖品',
17 | img: "/img/3.svg",
18 | order:3,
19 | },{
20 | level:4,
21 | text:'奖品',
22 | img: "/img/4.svg",
23 | order:4,
24 | },{
25 | level:5,
26 | text:'奖品',
27 | img: "/img/5.svg",
28 | order:12,
29 | },{
30 | level: 'center',
31 | text: '立即抽奖',
32 | img: "/img/13.svg"
33 | },{
34 | level:6,
35 | text:'奖品',
36 | img: "/img/6.svg",
37 | order:5,
38 | },{
39 | level:7,
40 | text:'奖品',
41 | img: "/img/7.svg",
42 | order:11,
43 | },{
44 | level:8,
45 | text:'奖品',
46 | img: "/img/8.svg",
47 | order:6
48 | },{
49 | level:9,
50 | text:'奖品',
51 | img: "/img/9.svg",
52 | order:10,
53 |
54 | },{
55 | level:10,
56 | text:'奖品',
57 | img: "/img/10.svg",
58 | order:9,
59 | },{
60 | level:11,
61 | text:'奖品',
62 | img: "/img/11.svg",
63 | order:8,
64 | },{
65 | level:12,
66 | text:'奖品',
67 | img: "/img/12.svg",
68 | order:7,
69 | }];
70 |
71 | // 容器组件
72 | const App = () =>
73 | 1、九宫格型抽奖
74 |
75 | ;
76 |
77 | // 标题
78 | const Title = (props) => {props.children}
;
79 |
80 | // 奖池
81 | const Pool = (props) => {
82 | const data = props.data;
83 | const [loading, setLoading] = useState(false); // 按钮状态
84 | const [selectedLevel,updateSelectedLevel] = useState(0);// 奖品选择
85 | const orderList = data.filter(item => typeof item.level === 'number'); // 过滤中间的button
86 | orderList.sort((a,b) => a.order > b.order ? 1 : a.order < b.order ? -1 : 0); // 排序
87 | const orderListLength = orderList.length;
88 |
89 | let timerId = null;
90 | let level = 0;
91 | const animationActionStart = (orderLength, loop) => {
92 | timerId = setInterval(()=>{
93 | level = level < orderLength ? level += 1 : !loop ? 1 : level;
94 | updateSelectedLevel(level);
95 | },150);
96 | };
97 | const animationActionEnd = (randomLevel) => {
98 | clearInterval(timerId);
99 | setLoading(false);
100 | console.log(`🎉恭喜中奖:${randomLevel}等奖`);
101 | }
102 | const getRandomNumber = (max) => Math.floor(Math.random() * Math.floor(max)) + 1;
103 | const fetchPrize = () => {
104 | setLoading(true);
105 | animationActionStart(orderListLength);
106 | const randomLevel = getRandomNumber(orderListLength - 1);
107 | setTimeout(()=>{
108 | animationActionEnd(randomLevel);
109 | },150 * randomLevel + 150 * orderListLength * 3);
110 | };
111 | return
112 |
113 | {
114 | prizeData.map(item => item.level === 'center'
115 | ?
116 | :
117 | )
118 | }
119 |
120 |
;
121 | }
122 |
123 | // 奖品
124 | const PrizeItem = (props) => {
125 | const className = `inner-item item-${props.level} ${props.className}`;
126 | const style = {
127 | backgroundImage: `url(assets${props.img})`,
128 | }
129 | return
130 | }
131 |
132 | // 抽奖按钮
133 | const ClickMe = (props) => {
134 | const btnClassName = props.loading ? 'click-me disable' : 'click-me';
135 | return
136 |
137 |
50积分/次
138 |
;
139 | }
140 |
141 | ReactDOM.render(,root);
--------------------------------------------------------------------------------
/public/assets/img/1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/8.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/13.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/11.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/10.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/prize.json:
--------------------------------------------------------------------------------
1 | {
2 | "3": ["TCMT20200401","TCMT20200402","TCMT20200403","TCMT20200404","TCMT20200405"],
3 | "6": ["XJShmaeyp060","XJPhmaeyq060","XJJhmaeyr060","XJYhmaeys060","XJBhmaeyt060","XJPhmaeyu060","XJYhmaeyv060","XJThmaeyw060","XJJhmaeyx060","XJWhmaeyy060","XJRhmaeyz060","XJGhmaez0060","XJBhmaez1060","XJLhmaez2060","XJRhmaez3060","XJKhmaez4060","XJJhmaez5060","XJVhmaez6060","XJChmaez7060","XJFhmaez8060","XJNhmaez9060","XJZhmaeza060","XJMhmaezb060","XJYhmaezc060","XJBhmaezd060","XJVhmaeze060","XJIhmaezf060","XJRhmaezg060","XJThmaezh060","XJIhmaezi060","XJUhmaezj060","XJLhmaezk060","XJQhmaezl060","XJMhmaezm060","XJBhmaezn060","XJBhmaezo060","XJThmaezp060","XJPhmaezq060","XJHhmaezr060","XJWhmaezs060","XJShmaezt060","XJChmaezu060","XJGhmaezv060","XJPhmaezw060","XJLhmaezx060","XJZhmaezy060","XJEhmaezz060","XJKhmaf00060","XJYhmaf01060","XJXhmaf02060","XJPhmaf03060","XJEhmaf04060","XJAhmaf05060","XJLhmaf06060","XJQhmaf07060","XJChmaf08060","XJGhmaf09060","XJOhmaf0a060","XJPhmaf0b060","XJIhmaf0c060","XJVhmaf0d060","XJYhmaf0e060","XJPhmaf0f060","XJYhmaf0g060","XJJhmaf0h060","XJDhmaf0i060","XJOhmaf0j060","XJGhmaf0k060","XJYhmaf0l060","XJGhmaf0m060","XJPhmaf0n060","XJPhmaf0o060","XJWhmaf0p060","XJOhmaf0q060","XJXhmaf0r060","XJKhmaf0s060","XJEhmaf0t060","XJXhmaf0u060","XJThmaf0v060","XJAhmaf0w060","XJChmaf0x060","XJKhmaf0y060","XJAhmaf0z060","XJAhmaf10060","XJZhmaf11060","XJLhmaf12060","XJChmaf13060","XJBhmaf14060","XJFhmaf15060","XJRhmaf16060","XJMhmaf17060","XJWhmaf18060","XJAhmaf19060","XJFhmaf1a060","XJJhmaf1b060","XJBhmaf1c060","XJKhmaf1d060","XJDhmaf1e060","XJThmaf1f060","XJWhmaf1g060","XJYhmaf1h060","XJUhmaf1i060","XJGhmaf1j060","XJMhmaf1k060","XJQhmaf1l060","XJXhmaf1m060","XJKhmaf1n060","XJMhmaf1o060","XJThmaf1p060","XJJhmaf1q060","XJVhmaf1r060","XJFhmaf1s060","XJVhmaf1t060","XJDhmaf1u060","XJRhmaf1v060","XJThmaf1w060","XJIhmaf1x060","XJGhmaf1y060","XJVhmaf1z060","XJQhmaf20060","XJKhmaf21060","XJAhmaf22060","XJRhmaf23060","XJMhmaf24060","XJKhmaf25060","XJBhmaf26060","XJThmaf27060","XJThmaf28060","XJFhmaf29060","XJKhmaf2a060","XJHhmaf2b060","XJHhmaf2c060","XJZhmaf2d060","XJDhmaf2e060","XJJhmaf2f060","XJHhmaf2g060","XJHhmaf2h060","XJThmaf2i060","XJShmaf2j060","XJMhmaf2k060","XJMhmaf2l060","XJBhmaf2m060","XJOhmaf2n060","XJUhmaf2o060","XJHhmaf2p060","XJZhmaf2q060","XJJhmaf2r060","XJKhmaf2s060","XJPhmaf2t060","XJLhmaf2u060","XJZhmaf2v060","XJFhmaf2w060","XJEhmaf2x060","XJOhmaf2y060","XJShmaf2z060","XJOhmaf30060","XJRhmaf31060","XJHhmaf32060","XJLhmaf33060","XJGhmaf34060","XJXhmaf35060","XJWhmaf36060","XJKhmaf37060","XJDhmaf38060","XJNhmaf39060","XJLhmaf3a060","XJWhmaf3b060","XJKhmaf3c060","XJGhmaf3d060","XJNhmaf3e060","XJYhmaf3f060","XJHhmaf3g060","XJThmaf3h060","XJShmaf3i060","XJShmaf3j060","XJIhmaf3k060","XJLhmaf3l060","XJRhmaf3m060","XJQhmaf3n060","XJChmaf3o060","XJMhmaf3p060","XJLhmaf3q060","XJUhmaf3r060","XJMhmaf3s060","XJJhmaf3t060","XJMhmaf3u060","XJYhmaf3v060","XJChmaf3w060","XJKhmaf3x060","XJFhmaf3y060","XJDhmaf3z060","XJMhmaf40060","XJShmaf41060","XJOhmaf42060","XJChmaf43060","XJFhmaf44060","XJZhmaf45060","XJQhmaf46060","XJHhmaf47060","XJQhmaf48060","XJShmaf49060","XJNhmaf4a060","XJEhmaf4b060","XJXhmaf4c060","XJQhmaf4d060","XJShmaf4e060","XJChmaf4f060","XJEhmaf4g060","XJDhmaf4h060","XJIhmaf4i060","XJQhmaf4j060","XJYhmaf4k060","XJVhmaf4l060","XJNhmaf4m060","XJEhmaf4n060","XJOhmaf4o060","XJFhmaf4p060","XJAhmaf4q060","XJEhmaf4r060","XJChmaf4s060","XJChmaf4t060","XJYhmaf4u060","XJNhmaf4v060","XJPhmaf4w060","XJLhmaf4x060","XJMhmaf4y060","XJAhmaf4z060","XJBhmaf50060","XJShmaf51060","XJQhmaf52060","XJPhmaf53060","XJXhmaf54060","XJAhmaf55060","XJJhmaf56060","XJXhmaf57060","XJShmaf58060","XJQhmaf59060","XJJhmaf5a060","XJThmaf5b060","XJChmaf5c060","XJWhmaf5d060","XJUhmaf5e060","XJLhmaf5f060","XJDhmaf5g060","XJAhmaf5h060","XJNhmaf5i060","XJUhmaf5j060","XJGhmaf5k060","XJRhmaf5l060","XJPhmaf5m060","XJNhmaf5n060","XJFhmaf5o060","XJUhmaf5p060","XJPhmaf5q060","XJPhmaf5r060","XJShmaf5s060","XJJhmaf5t060","XJOhmaf5u060","XJHhmaf5v060","XJGhmaf5w060","XJShmaf5x060","XJDhmaf5y060","XJNhmaf5z060","XJBhmaf60060","XJDhmaf61060","XJOhmaf62060","XJShmaf63060","XJIhmaf64060","XJZhmaf65060","XJXhmaf66060","XJIhmaf67060","XJPhmaf68060","XJHhmaf69060","XJZhmaf6a060","XJLhmaf6b060","XJFhmaf6c060","XJVhmaf6d060","XJBhmaf6e060","XJRhmaf6f060","XJShmaf6g060","XJMhmaf6h060","XJWhmaf6i060","XJQhmaf6j060","XJKhmaf6k060","XJOhmaf6l060","XJEhmaf6m060","XJBhmaf6n060","XJBhmaf6o060","XJBhmaf6p060","XJGhmaf6q060","XJThmaf6r060","XJJhmaf6s060","XJPhmaf6t060","XJYhmaf6u060","XJLhmaf6v060","XJFhmaf6w060","XJYhmaf6x060","XJMhmaf6y060","XJQhmaf6z060","XJRhmaf70060"],
4 | "8": ["VANiCYCQ6I6", "VANiX6WXDH7", "VANiEYJQABT", "VANiZ53WXAF", "VANiGEP5F31", "VANi159W22M", "VANiIDG4ZW8", "VANiLLJL2SE", "VANi2DQT0MK", "VANiNLAKNLM", "VANi4DWT4FS", "VANiQLGZRET", "VANi7D3SP8Z", "VANiSKNZW71", "VANi9S9RT07", "VANiCL98424", "VANiTDWG2FA", "VANiFLGN9UB", "VANiWDNG68Y", "VANiHL6MT7J", "VANiYDTFR15", "VANiJLDMY0R", "VANi0DZEVTD", "VANi6XHVEE4", "VANiNPONW8Q", "VANi8XOUJ7B", "VANiZL18SBD", "VANiUP71PMD", "VANi7UE3RYP", "VANiEQUAY1G", "VANiZHEHLGH", "VANiGQLPIUO", "VANi2H4GP9P", "VANiSLYAYXA", "VANi4XBVU1W", "VANiLPXORFJ", "VANiOXL5AR8", "VANi558D85U", "VANiQWRKFKG", "VANi75ECCY2", "VANiSWYJJD3", "VANi945CHQA", "VANiB4BB1JH", "VANiOTYDNWT", "VANiWPY0AEK", "VANiHXI7HDM", "VANiYP4ZE7S", "VANiJXO6L6T", "VANi0PBYJKF", "VANiLXU56Z1", "VANi251ENCN", "VANi5C5V6PD", "VANiM4BN43Z", "VANiWGY9FEE", "VANiH8IG2TZ", "VANiYGO806L", "VANiJ88F666", "VANi0GVN4ZT", "VANi88834M5", "VANi1CSWX4X", "VANiI4EPFXK", "VANi3CYV2W5", "VANiT0SABLQ", "VANiDF2ZRBI", "VANi6JMC49B", "VANi8JSSP1I", "VANi4GSMTL8", "VANiXKSZ620", "VANiSNIS4CH", "VANi9F5K26N", "VANiUNORP5O", "VANi8O6EE8N", "VANiAODEJ1U", "VANiCO3TNT2", "VANiTWQLLNO", "VANiEOASSMP", "VANiVWXKQGW", "VANiZOX10HT", "VANiGGJAYVZ", "VANi1O3HLA0", "VANiIGQ9JO6", "VANi3O9GP38", "VANiKGG8NGE", "VANiFKN1LQU", "VANiWSTTJK0", "VANiZZXQLH6", "VANiGR3JJAC", "VANiW3TI4KU", "VANiDV0B2D0", "VANiQZNDNQW", "VANiXW3KA83", "VANiSZTC8JJ", "VANi0VTZFLB", "VANiL3DQL0C", "VANi2V0YJEI", "VANiN3K56TJ", "VANi4V6YO6Q", "VANi73UU6JV", "VANiOBHN4XI", "VANi920UBC3", "VANiY3KH8C1", "VANiFB7A667", "VANi03QHD59", "VANiHBD9BZF", "VANi23XGXYG", "VANiJB4OFRM", "VANi53NF2QO", "VANiMBAO0KA", "VANiOIX52G0", "VANiFMRJR5L", "VANi0EBQYKM", "VANi2EHP2DU", "VANi5EN4N61", "VANiGI4R4Q5", "VANiXAA023C", "VANiIIU792D", "VANi86NLY7Y", "VANiUE7S46Z", "VANiAMUK206", "VANiWEERPZ7", "VANiDM0J7TD", "VANiJQI0PDK", "VANi0IPSN7R", "VANiLQ9ZU6S", "VANi2YVSSJY", "VANiNQFYEYZ", "VANi4Y2RCCM", "VANiPQMYJR7", "VANi0F210BB", "VANiHN8TYPH", "VANi3FS0L4J", "VANiKNF9JIP", "VANiVJSUYLB", "VANiONC7S34", "VANiRUFOAFA", "VANi822X8TG", "VANi36S963W", "VANiJYZI4X2", "VANi56JOAW4", "VANiMY6H8QQ", "VANi76POFPB", "VANiOYCGDIX", "VANiSQCXNKE", "VANi8YZ5LY1", "VANiUQIC8DM", "VANiBY55QQ8", "VANi32PIJ81", "VANiPU9P6N2", "VANi62WXO08", "VANi89JE6DE", "VANiI5Q0XOT", "VANiZDCSF2Z", "VANiK5WZ210", "VANiD9WDVYT", "VANiUH3LDCF", "VANiF9MS0R1", "VANiWH9KY5N", "VANi09P186K", "VANiH1WA60Q", "VANi29G0DZR", "VANiJ129BTY", "VANiE5TM83E", "VANiGCWIRZ3", "VANiBGMVP9K", "VANiH9TR44O", "VANiJ9ZR9WW", "VANiM9QQTP3", "VANiO9WPYIB", "VANiYW38EOY", "VANiOKXMNDJ", "VANiASGTUCL", "VANi2WG6N9D", "VANi5W76R2L", "VANiPH9V0HO", "VANi6PGNYBA", "VANi8ONMI4I", "VANiUW6TP3J", "VANiKK07YR4", "VANi5SJEL65", "VANiGWNIAL2", "VANiW4UQ8F9", "VANiR7036PP", "VANiKBKWJ7I", "VANi137PHL4", "VANiW7XHFB4", "VANiDZKACOQ", "VANiGSKRNQ7", "VANiX07ZLKT", "VANiJSQQ8JF", "VANiZ0DYPC1", "VANiS4XCJUU", "VANiDWHJ5TV", "VANi40AXEIG", "VANi77XUXUM", "VANiOFKMFOS", "VANi974T2NT", "VANiQFRLZHZ", "VANiIJAFDY8", "VANi4BU6ZXT", "VANiLJHEXRG", "VANiOBHV8SD", "VANi534N66J", "VANiRBNUCLK", "VANiHZH81A5", "VANi270F897", "VANiVB091QZ", "VANiC371ZK5", "VANiOMH4TSQ", "VANi5EOCRLW", "VANiYIOQ435", "VANiTMEI2D5", "VANiLQEWVVY", "VANiCURAKZZ", "VANiGN7RV1G", "VANi8A7K8IO", "VANiRIKCRP2", "VANi3EXX6TP", "VANi9ZFDPDW", "VANi2NZRIBO", "VANiJVMZGOV", "VANiEZCCEZB", "VANiURZKCSH", "VANiGZIBJRI", "VANiXR5JGLP", "VANi0J50RMM", "VANiHRSTP08", "VANi3JC0WFT", "VANiVNWTPX2", "VANiXN2SUP9", "VANiEVPLR3G", "VANiH2CIAFL", "VANi866WJ46", "VANiTYP36J8", "VANiA6CV3XE", "VANiM29GJ10", "VANi3AW8HFN", "VANiO2GF3U8", "VANi5A2O17U", "VANi8225C9B", "VANiZRWJLEC", "VANiKYFQ8DX", "VANi1Q2IP6K", "VANiUUMBJOC", "VANiWUSBNGK", "VANiZIGS6DQ", "VANiGA3047W", "VANi1HMRA6X", "VANiR5GLZVI", "VANiDDZC6AJ", "VANi5HZ5ZRC", "VANiM96YXLY", "VANi0ET0ZHU", "VANiG6F9HBG", "VANi9AZMAT9", "VANi4EQF839", "VANiL6C76WW", "VANiEAWLZEO", "VANiGH0I2QU", "VANi2PJPPQV", "VANi2LGX2U0", "VANiOD0OPT1", "VANiILKKWQ6", "VANiDPBWU0M", "VANiUXI5RTS", "VANiN1HILBL", "VANiHP8BJL1", "VANiSUOE05P", "VANi9MB7YZC", "VANi4Q1ZW9C", "VANiZTRCUJS", "VANiS5ONLLJ", "VANiV5EM6DQ", "VANiN9E0JVJ", "VANi61S711D", "VANiKPEANE9", "VANiM55987G", "VANiO5B9CZO", "VANiQ5IOHSV", "VANiA4SXXIN", "VANiKGYJ8TM", "VANiMG5YDMU", "VANi14I0ZBD", "VANiU8IEDT6", "VANiB0O6ANS", "VANiMC2R6QE", "VANiZFCA0YJ", "VANi1FI94RQ", "VANiMN2GRQS", "VANi3F88PKY", "VANiPNSFWJZ", "VANi6FF8TCL", "VANiJK2AVPH", "VANi0C8JT3O", "VANiLKSP02P", "VANi2CFIXVB", "VANiNKYP4UW", "VANi4CLH2OJ", "VANiPK5OPN4", "VANi6CCG6HQ", "VANiCWTXP1H", "VANi50TAJJQ", "VANi700ANBX", "VANi90Q9S45", "VANiUKTY0Z8", "VANiBS0QYDE", "VANi6GQJW3V", "VANiYKQXPLN", "VANi1KWCUDV", "VANiKZ71A3N", "VANi17DT8HT", "VANi47KTCA0", "VANiYVALAKH", "VANi1BHLFDO", "VANiC0XOCXD", "VANi2OQ2LMY", "VANi44H2PE5", "VANiXSGVJCE", "VANi93TGE00", "VANiS34PFQC", "VANiL7NJ8N5", "VANi6F7QVM6", "VANiXJ144BR", "VANiIAKBBQT", "VANiYFDDHGW", "VANi0F4C283", "VANiQ3XQAX5", "VANi1M1U0S2", "VANiMEK0NR3", "VANi3M7T4L9", "VANiYQXM2VP", "VANiQUXZWDI", "VANiHIBTK13", "VANiKARAV30", "VANiDEBN90T", "VANiUMXW6EF", "VANiFEHNTT0", "VANiWM4VR7M", "VANiHEN2Y6O", "VANiS2V5ECV", "VANiIQOJN1W", "VANiBU8WGYP", "VANiW283NXQ", "VANiY2Y2RQY", "VANiFULBP44", "VANiSI7DRGG", "VANi2UEJISV", "VANiXY4CG2V", "VANiEQR4YV1", "VANiZYBBKV2", "VANiGQIKI8P", "VANi91EFAAF", "VANiQ9LN73M", "VANiLXB05DM", "VANi25Y8378", "VANi45578KG", "VANiX9OL1H8", "VANiHURA9WB", "VANiJTI9EPZ", "VANi511GL4K", "VANiMTO9II7", "VANi718F5HS", "VANiOTV8NAE", "VANi0CPRXIJ", "VANiH5BJFC5", "VANi3CVQ1BQ", "VANiJKIIZ5C", "VANi5C2P64E", "VANiXGLJZL6", "VANiOKFX8QR", "VANiSDFEJRO", "VANi8526H5V", "VANi191ZAN3", "VANiWDSC8D4", "VANiYDIRCQB", "VANiF55KAJX", "VANiPO8NZYU", "VANiBGSUMDW", "VANi1KM8V2H", "VANiMS5F21I", "VANi3KS80VO", "VANiPSCEMUQ", "VANi5KI7KNW", "VANiG8ZA17K", "VANi2GIH86L", "VANiJO59608", "VANi4GPGTZT", "VANiLOCPQTF", "VANi6GVGXS1", "VANiNO2OVLN", "VANi3O6APHT", "VANiKWD3NVF", "VANiC0DWGS8", "VANiYSX3NR9", "VANiE03VL5F", "VANi0SN2RKG", "VANiH0AVPYN", "VANiKTQB0ZK", "VANi1LXKYTQ", "VANiNTGRKSR", "VANi3L3JIMX", "VANiPTNQPLZ", "VANiFXG4YPK", "VANi8L0IR7T", "VANiB83EA4I", "VANi1WXTJ8J", "VANiN4H0575", "VANi3WNS31R", "VANiP47ZA0C", "VANiY0UL1BR", "VANi2SU1CDO", "VANiJ0GUAQU", "VANi4S01G5W", "VANiVWTFPUH", "VANi98XF5BB", "VANiZWQTEGW", "VANi2JEAXC1", "VANi4JKA159", "VANiLB7IZZV", "VANi6JRP6YG", "VANiZNB2ZFP", "VANiGFXVX9V", "VANiK7DS8AS", "VANi0ZKK54Z", "VANiM74RC30", "VANi3FRJAH6", "VANiO7AQHW7", "VANi5FXIE9U", "VANiQ7HP1OF", "VANi7FNYZ21", "VANiAMRF2FR", "VANiCMHEM7E", "VANiTE4M4LL", "VANiOIUZ2VL", "VANiHMUSVTT", "VANiMF18QNY", "VANiHJR1OXY", "VANiYREU6BK", "VANi4VWUPBC", "VANiLNI2NPI", "VANiER2GG6Q", "VANiZZMNNLC", "VANiGR9FLZY", "VANi1ZSMREZ", "VANiS3M0G3K", "VANiOJMAKMA", "VANi5RS3IGG", "VANi7RZIN8O", "VANiTZI9T7P", "VANiJNC3IWA", "VANi32MCJ2M", "VANiV6M6CKV", "VANi72JR88H", "VANiTA3YEN2", "VANi92PQC0P", "VANiDV57N2M", "VANiU3CFLWS", "VANiFVW67VT", "VANi8ZW0LCM", "VANiP73SJ6S", "VANiAYMZ55T", "VANi12GDEAF", "VANi4AJAX6K", "VANiB6JH4PR", "VANiWE3OAOD", "VANiNIW2ZTE", "VANi8AG96SZ", "VANiPI314LL", "VANiA9M8AK7", "VANiRHTG8ET", "VANiVA9XJFQ", "VANiC2WPHTW", "VANiXAGW38Y", "VANiE2MP1M4", "VANiZA6W8L5", "VANiG2T46EB", "VANiSE6P1IY", "VANi96THZWK", "VANiCDGY29A", "VANiSL370MW", "VANiETNEM1H", "VANiVLT64F3", "VANiGTDDRU5", "VANiXL05P8B", "VANi6H6R0JQ", "VANiSPQYMIB", "VANi9HDQKBX", "VANiC9D7VDU", "VANiTH0GTR1", "VANi5DD18UN", "VANiMLZT6OT", "VANi7DJ0TNU", "VANiOLQSRH1", "VANi9DAZXG2", "VANiJPWLORH", "VANi0H3D65N", "VANi6MLUP5U", "VANiNU8MNJ0", "VANi82RTUY2", "VANiPUELRC8", "VANiA1YSEB9"]
5 | }
--------------------------------------------------------------------------------
/public/assets/img/12.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/9.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------