├── 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

{props.text}

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 | --------------------------------------------------------------------------------