init from origin + update

This commit is contained in:
Primakov Alexandr Alexandrovich 2023-08-01 13:14:02 +03:00
commit 321dc4c3c5
1109 changed files with 16019 additions and 0 deletions

45
.eslintrc.js Normal file
View File

@ -0,0 +1,45 @@
module.exports = {
env: {
browser: true,
commonjs: true,
es2021: true,
},
extends: [
'airbnb-base',
],
parserOptions: {
ecmaVersion: 12,
},
rules: {
indent: ['error', 4],
semi: ['warn', 'never'],
'object-curly-newline': ['warn', {
ObjectExpression: 'always',
ObjectPattern: {
multiline: true,
},
ImportDeclaration: 'never',
ExportDeclaration: {
multiline: true, minProperties: 3,
},
}],
'consistent-return': [0],
'prefer-const': [0],
'no-unused-vars': [0],
'no-console': [0],
'global-require': [0],
'no-plusplus': [0],
'no-underscore-dangle': [0],
'import/no-dynamic-require': [0],
'no-shadow': ['warn'],
'no-restricted-syntax': ['warn'],
'max-len': ['warn'],
'linebreak-style': [0],
'prefer-destructuring': [0],
'imoprt-order': [0],
'no-param-reassign': [1],
'no-await-in-loop': [1],
'no-return-assign': [1],
'spaced-comment': [1],
},
}

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
.env
.idea

1
.npmrc Normal file
View File

@ -0,0 +1 @@
package-lock=true

5
.serverrc.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
port: 8044,
mongoAddr: process.env.MONGO_ADDR || 'localhost',
mongoPort: 27017,
}

26
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,26 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/server/index.js"
},
{
"name": "attach",
"type": "pwa-node",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"processId": "${command:PickProcess}"
}
]
}

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM 'node:18'
RUN mkdir -p /usr/src/app/server/
WORKDIR /usr/src/app/
COPY ./server /usr/src/app/server
COPY ./package.json /usr/src/app/package.json
COPY ./.serverrc.js /usr/src/app/.serverrc.js
# COPY ./.env /usr/src/app/.env
RUN npm ci --only=prod
EXPOSE 8044
CMD ["npm", "run", "up:prod"]

37
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,37 @@
pipeline {
agent {
docker {
image 'node:16'
}
}
stages {
stage('install') {
steps {
sh 'node -v'
sh 'npm -v'
sh 'npm install'
}
}
stage('eslint') {
steps {
sh 'npm run eslint'
}
}
stage('test') {
steps {
sh 'npm run test:start'
}
}
stage('clean-all') {
steps {
sh 'rm -rf .[!.]*'
sh 'rm -rf ./*'
sh 'ls -a'
}
}
}
}

6
d-scripts/rerun.sh Normal file
View File

@ -0,0 +1,6 @@
#!/bin/sh
docker stop ms-mongo
docker volume remove ms_volume
docker volume create ms_volume
docker run --rm -v ms_volume:/data/db --name ms-mongo -p 27017:27017 -d mongo:4.4.13

21
docker-compose.yaml Normal file
View File

@ -0,0 +1,21 @@
version: "3"
volumes:
ms_volume:
services:
mongoDb:
image: mongo:4.4.13
volumes:
- ms_volume:/data/db
restart: always
# ports:
# - 27017:27017
multy-stubs:
build: .
restart: always
ports:
- 8044:8044
environment:
- TZ=Europe/Moscow
- MONGO_ADDR=mongodb

4670
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "multi-stub",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "npx nodemon ./server",
"up:prod": "cross-env NODE_ENV=\"production\" node ./server",
"deploy:d:stop": "docker-compose down",
"deploy:d:build": "docker-compose build",
"deploy:d:up": "docker-compose up -d",
"redeploy": "npm run deploy:d:stop && npm run deploy:d:build && npm run deploy:d:up",
"eslint": "npx eslint ./server",
"eslint:fix": "npx eslint ./server --fix",
"test": "echo \"test complete\"",
"test:start": "start-server-and-test up:prod 8043 test"
},
"repository": {
"type": "git",
"url": "git+ssh://git@bitbucket.org/online-mentor/multi-stub.git"
},
"keywords": [],
"author": "",
"license": "MIT",
"homepage": "https://bitbucket.org/online-mentor/multi-stub#readme",
"dependencies": {
"bcrypt": "^5.1.0",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"crypto-js": "^4.1.1",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-jwt": "^8.4.1",
"express-session": "^1.17.3",
"jsonwebtoken": "^8.5.1",
"mongodb": "^3.6.8",
"mysql": "^2.18.1",
"pbkdf2-password": "^1.2.1",
"socket.io": "^4.7.1",
"start-server-and-test": "^1.13.1",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/node": "18.17.1",
"eslint": "8.46.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-plugin-import": "2.28.0",
"nodemon": "3.0.1"
}
}

12
server/error.js Normal file
View File

@ -0,0 +1,12 @@
const noToken = 'No authorization token was found'
module.exports = (err, req, res, next) => {
if (err.message === noToken) {
res.status(400).send({
success: false, error: 'Токен авторизации не найден',
})
}
res.status(400).send({
success: false, error: err.message || 'Что-то пошло не так',
})
}

64
server/index.js Normal file
View File

@ -0,0 +1,64 @@
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const session = require('express-session')
const app = express()
const cors = require('cors')
require('dotenv').config()
const config = require('../.serverrc')
const { setIo } = require('./io')
app.use(cookieParser())
app.options('*', cors())
app.use(cors())
const server = setIo(app)
const sess = {
secret: 'super-secret-key',
resave: true,
saveUninitialized: true,
cookie: {
},
}
if (app.get('env') === 'production') {
app.set('trust proxy', 1)
sess.cookie.secure = true
}
app.use(session(sess))
app.use(bodyParser.json({
limit: '50mb',
}))
app.use(bodyParser.urlencoded({
limit: '50mb',
extended: true,
}))
app.use(require('./root'))
/**
* Добавляйте сюда свои routers.
*/
app.use('/example', require('./routers/example'))
// app.use('/coder', require('./routers/coder'))
app.use('/stc-21-03', require('./routers/stc-21-03'))
app.use('/stc-21', require('./routers/stc'))
app.use('/stc-22-24', require('./routers/stc-22-24'))
// app.use('/bushou-api', require('./routers/bushou'))
// app.use('/uryndyklar-api', require('./routers/uryndyklar'))
// app.use('/neptunium', require('./routers/neptunium'))
// app.use('/music-learn', require('./routers/music-learn'))
// app.use('/publicium', require('./routers/publicium'))
// app.use('/task-boss', require('./routers/task-boss'))
// app.use('/car-wash', require('./routers/car-wash'))
app.use('/zoom-bar', require('./routers/zoom-bar'))
app.use('/basket', require('./routers/basket'))
app.use('/easy-project', require('./routers/easy-project'))
app.use('/sugarbun', require('./routers/sugarbun'))
require('./routers/hub-video')
app.use(require('./error'))
server.listen(config.port, () => console.log(`Listening on http://localhost:${config.port}`))

13
server/io.js Normal file
View File

@ -0,0 +1,13 @@
const { Server } = require('socket.io')
const { createServer } = require('http')
let io = null
module.exports.setIo = (app) => {
const server = createServer(app)
io = new Server(server, {})
return server
}
module.exports.getIo = () => io

17
server/root.js Normal file
View File

@ -0,0 +1,17 @@
const router = require('express').Router()
const fs = require('fs')
const path = require('path')
const folderPath = path.resolve(__dirname, './routers')
const folders = fs.readdirSync(folderPath)
router.get('/', (req, res) => {
res.send(`
<h1>multy stub is working</h1>
<ul>
${folders.map((f) => `<li>${f}</li>`).join('')}
</ul>
`)
})
module.exports = router

View File

@ -0,0 +1,34 @@
const router = require('express').Router()
const checkPwd = require('pbkdf2-password')()
const jwt = require('jsonwebtoken')
const { BASKET_JWT_TOKEN } = require('./key')
const { getResponse, signUp, getUser, _idToId, requiredFields } = require('./controller')
router.post('/sign-in', requiredFields(['email', 'password']), async (req, res) => {
try {
const user = await getUser(req.body)
// eslint-disable-next-line max-len
checkPwd({ password: req.body.password, salt: user.salt }, async (err, pass, salt, hash) => {
if (err) throw new Error(err)
if (user.pwd === hash) {
const { pwd, salt: _salt, ...rest } = user
const token = jwt.sign(_idToId(rest), BASKET_JWT_TOKEN)
res.send(getResponse(null, { token, user: _idToId(rest) }))
} else {
res.status(400).send(getResponse('Неправильный email или пароль'))
}
})
} catch (e) {
res.status(400).send(getResponse(e.message))
}
})
router.post('/sign-up', requiredFields(['email', 'login', 'password']), async (req, res) => {
let error = null
const data = await signUp(req.body).catch((e) => error = e.message)
res.status(error ? 400 : 200).send(getResponse(error, data))
})
module.exports = router

View File

@ -0,0 +1,25 @@
const router = require('express').Router()
const { expressjwt } = require('express-jwt')
const ObjectId = require('mongodb').ObjectID
const { BASKET_JWT_TOKEN } = require('./key')
const { getResponse, getCategory, postCategory } = require('./controller')
router.use(expressjwt({ secret: BASKET_JWT_TOKEN, algorithms: ['HS256'] }))
router.get('/', async (req, res) => {
const userId = new ObjectId(req.auth.id)
let error = null
const categoryData = await getCategory({ userId }).catch((e) => error = e.message)
res.send(getResponse(error, categoryData))
})
router.post('/', async (req, res) => {
const userId = new ObjectId(req.auth.id)
let error = null
const categoryData = await postCategory({ userId, ...req.body }).catch((e) => error = e.message)
res.send(getResponse(error, categoryData))
})
module.exports = router

View File

@ -0,0 +1,510 @@
const ObjectId = require('mongodb').ObjectID
const getHash = require('pbkdf2-password')()
const { getDB } = require('../../utils/mongo')
const USERS_COLLECTION = 'users'
const LISTS_COLLECTION = 'lists'
const CATEGORY_COLLECTION = 'default_categories'
const USER_CATEGROY_COLLECTION = 'user_categories'
const ITEM_COLLECTION = 'items'
const fakeUserId = 'fakeUserId'
let db = null
const connect = async () => {
db = await getDB('basket')
}
const init = async () => {
await connect()
const categoriesCollection = db.collection(CATEGORY_COLLECTION)
const findData = await categoriesCollection.find({
}).toArray()
if (findData.length === 0) {
await categoriesCollection.insertMany([
{
name: 'Продукты',
color: '#08AE0F',
},
{
name: 'Одежда',
color: '#9D79B9',
},
{
name: 'Бытовая химия',
color: '#B11F1F',
},
{
name: 'Лекарства',
color: '#3414F5',
},
])
}
}
init()
const _idToId = (data) => {
const { _id, ...rest } = data
return {
id: _id,
...rest,
}
}
const _idToIdArray = (data) => {
const _idToIdMap = data.map((item) => _idToId(item))
return _idToIdMap
}
const getResponse = (error, data, success = true) => {
if (error) {
return {
success: false,
error,
}
}
return {
success,
data,
}
}
const signUp = async ({ email, login, password }) => {
if (db === null) throw new Error('no db connection')
try {
const usersCollection = db.collection(USERS_COLLECTION)
const userData = await usersCollection.findOne({
$or: [{
login,
}, {
email,
}],
})
if (userData?.login === login) {
throw new Error('Логин занят')
}
if (userData?.email === email) {
throw new Error('Email занят')
}
getHash({ password }, async (err, pass, salt, hash) => {
if (err) throw new Error(err)
// eslint-disable-next-line max-len
const { insertedCount } = await usersCollection.insertOne({ email, login, pwd: hash, salt })
if (!insertedCount) throw new Error('insert error')
})
return {}
} catch (e) {
throw new Error(e)
}
}
const getUser = async ({ email }) => {
if (db === null) throw new Error('no db connection')
try {
const usersCollection = db.collection(USERS_COLLECTION)
const userData = await usersCollection.findOne(
{
email,
},
)
if (userData) return userData
throw new Error('Неправильный email или пароль')
} catch (e) {
throw new Error(e)
}
}
const addList = async ({ userId = fakeUserId, ...data }) => {
if (db === null) throw new Error('no db connection')
try {
const listsCollection = db.collection(LISTS_COLLECTION)
const insertData = await listsCollection.insertOne({
userId,
timeStamp: Date.now(),
...data,
})
const { insertedCount, ops } = insertData
if (insertedCount) { return _idToId(ops[0]) }
throw new Error('insert error')
} catch (e) {
throw new Error(e)
}
}
const getLists = async ({ userId = fakeUserId }) => {
if (db === null) throw new Error('no db connection')
try {
const listsCollection = db.collection(LISTS_COLLECTION)
const itemsCollection = db.collection(ITEM_COLLECTION)
let newLists = []
const data = await listsCollection.find({
userId,
}).toArray()
await Promise.all(data.map(async (element) => {
const total = await itemsCollection.countDocuments({
parentId: element._id,
})
const purchased = await itemsCollection.countDocuments({
parentId: element._id,
bought: true,
})
newLists.push({
...element, total, purchased,
})
}))
newLists.sort((a, b) => (b.timeStamp - a.timeStamp))
return _idToIdArray(newLists)
} catch (e) {
throw new Error(e)
}
}
/* добавил логику рекурсивного удаления дочерних документов */
const deleteDoc = async ({ id, tag = false }) => {
if (db === null) throw new Error('no db connection')
try {
const listsCollection = db.collection(LISTS_COLLECTION)
const itemsCollection = db.collection(ITEM_COLLECTION)
const findData = await itemsCollection.find({
parentId: new ObjectId(id),
}).toArray()
findData.forEach(async (element) => {
await deleteDoc({
id: element._id,
tag: true,
})
})
let delData = null
if (tag) {
delData = await itemsCollection.deleteOne({
_id: new ObjectId(id),
})
} else {
delData = await listsCollection.deleteOne({
_id: new ObjectId(id),
})
}
const { deletedCount } = delData
if (deletedCount) {
return {
}
} throw new Error('no data to delete')
} catch (e) {
throw new Error(e)
}
}
const renameList = async ({ id, listName }) => {
if (db === null) throw new Error('no db connection')
try {
const listsCollection = db.collection(LISTS_COLLECTION)
const data = await listsCollection.updateOne({
_id: new ObjectId(id),
},
{
$set: {
listName,
},
})
const { matchedCount } = data
if (matchedCount) {
const findData = await listsCollection.findOne({
_id: new ObjectId(id),
})
return _idToId(findData)
} throw new Error('no data to rename')
} catch (e) {
throw new Error(e)
}
}
const duplicateList = async ({ id, parentId = null }) => {
if (db === null) throw new Error('no db connection')
try {
const listsCollection = db.collection(LISTS_COLLECTION)
const itemsCollection = db.collection(ITEM_COLLECTION)
let addListData = null
let newId = null
if (parentId) {
const findData = await itemsCollection.findOne(
{
_id: new ObjectId(id),
},
)
const { _id, ...item } = findData
item.parentId = parentId
const insertData = await itemsCollection.insertOne({
...item,
})
const { insertedCount } = insertData
if (!insertedCount) throw new Error('insert new item error')
} else {
const findData = await listsCollection.findOne(
{
_id: new ObjectId(id),
},
)
const { _id, timeStamp, ...item } = findData
item.listName = `(КОПИЯ) ${item.listName}`
addListData = await addList({
...item,
})
newId = addListData.id
}
const childData = await itemsCollection.find({
parentId: new ObjectId(id),
}).toArray()
childData.forEach(async (element) => {
await duplicateList({
id: element._id, parentId: newId,
})
})
if (addListData) return _idToId(addListData)
} catch (e) {
throw new Error(e)
}
}
const getCategory = async ({ userId }) => {
if (db === null) throw new Error('no db connection')
try {
const categoriesCollection = db.collection(CATEGORY_COLLECTION)
const defaultCategories = await categoriesCollection.find({
}).toArray()
const defaultCategoriesData = _idToIdArray(defaultCategories).map((dc) => ({
...dc, userId,
}))
const userCollection = db.collection(USER_CATEGROY_COLLECTION)
const userCategoriesFilter = {}
if (userId) {
userCategoriesFilter.userId = userId
}
const userFindData = await userCollection.find(userCategoriesFilter).toArray()
return [...defaultCategoriesData, ..._idToIdArray(userFindData)]
} catch (e) {
throw new Error(e)
}
}
const postCategory = async ({ userId = fakeUserId, ...categoryData }) => {
if (db === null) throw new Error('no db connection')
try {
const userCollection = db.collection(USER_CATEGROY_COLLECTION)
const insertData = await userCollection.insertOne({
userId, ...categoryData,
})
// const {insertedCount, ops} = insertData
// if (insertedCount)
// _idToId(ops[0])
// else
// throw new Error('insert error')
const userFindData = await userCollection.find({
userId,
}).toArray()
return _idToIdArray(userFindData)
} catch (e) {
throw new Error(e)
}
}
const getShoppingList = async ({ userId = fakeUserId, id }) => {
if (db === null) throw new Error('no db connection')
try {
const listsCollection = db.collection(ITEM_COLLECTION)
const itemsList = await listsCollection.find({
parentId: new ObjectId(id),
}).toArray()
const categoryList = await getCategory({ })
const coloredItemsList = itemsList.map((item) => ({
...item,
// eslint-disable-next-line max-len
color: categoryList.find((category) => String(category.id) === String(item.categoryId))?.color,
}))
return _idToIdArray(coloredItemsList)
} catch (e) {
throw new Error(e)
}
}
const addListItem = async ({ userId = fakeUserId, listId, categoryId, text }) => {
if (db === null) throw new Error('no db connection')
try {
const dataToInsert = {
parentId: new ObjectId(listId),
categoryId: new ObjectId(categoryId),
text,
count: 1,
bought: false,
createdBy: userId,
createdDt: Date.now(),
modifiedBy: userId,
modifiedDt: Date.now(),
}
const itemCollection = db.collection(ITEM_COLLECTION)
await itemCollection.insertOne(dataToInsert)
return _idToId(dataToInsert)
} catch (e) {
throw new Error(e)
}
}
const boughtItem = async ({ userId = fakeUserId, itemId, bought }) => {
if (db === null) throw new Error('no db connection')
try {
const itemCollection = db.collection(ITEM_COLLECTION)
const chengedData = await itemCollection.findOneAndUpdate({
_id: new ObjectId(itemId),
},
[{
$set: {
bought: { $eq: [false, '$bought'] },
modifiedBy: userId,
modifiedDt: Date.now(),
},
}])
return _idToId(chengedData)
} catch (e) {
throw new Error(e)
}
}
const incCountItem = async ({ userId = fakeUserId, itemId, count }) => {
if (db === null) throw new Error('no db connection')
try {
const itemCollection = db.collection(ITEM_COLLECTION)
const chengedData = await itemCollection.findOneAndUpdate({
_id: new ObjectId(itemId),
},
{
$inc: {
count,
},
},
{
$set: {
modifiedBy: userId,
modifiedDt: Date.now(),
},
})
const chengeData = await itemCollection.findOneAndUpdate({
_id: new ObjectId(itemId),
count: {
$lt: 1,
},
},
{
$set: {
count: 1,
modifiedBy: userId,
modifiedDt: Date.now(),
},
})
return _idToId(chengedData || chengeData)
} catch (e) {
throw new Error(e)
}
}
const deleteItem = async ({ itemId }) => {
if (db === null) throw new Error('no db connection')
try {
const itemCollection = db.collection(ITEM_COLLECTION)
const findItemData = await itemCollection.find({
_id: new ObjectId(itemId),
})
findItemData.forEach((item) => {
deleteItem({
id: item._id,
})
})
const deleteItemData = await itemCollection.deleteOne({
_id: new ObjectId(itemId),
})
const { deletedButton } = deleteItemData
if (deletedButton) {
return {
}
}
} catch (e) {
throw new Error(e)
}
}
const requiredFields = (fields) => (req, res, next) => {
// eslint-disable-next-line no-restricted-syntax
for (const fieldName of fields) {
if (!req.body[fieldName]) {
throw new Error(`Параметр ${fieldName} не установлен`)
}
}
next()
}
module.exports = {
getResponse,
addList,
getLists,
deleteDoc,
renameList,
duplicateList,
getCategory,
postCategory,
getShoppingList,
addListItem,
boughtItem,
deleteItem,
incCountItem,
signUp,
getUser,
_idToId,
requiredFields,
}

View File

@ -0,0 +1,54 @@
const router = require('express').Router()
const { expressjwt } = require('express-jwt')
const ObjectId = require('mongodb').ObjectID
const { BASKET_JWT_TOKEN } = require('./key')
const {
getResponse, addList,
getLists, deleteDoc, renameList, duplicateList,
} = require('./controller')
const wait = (req, res, next) => setTimeout(next, 0)
router.use(expressjwt({ secret: BASKET_JWT_TOKEN, algorithms: ['HS256'] }))
/* получить списки покупок*/
router.get('/list', wait, async (req, res) => {
const userId = new ObjectId(req.auth.id)
let error = null
const listData = await getLists({ userId }).catch((e) => error = e.message)
res.status(error ? 400 : 200).send(getResponse(error, listData))
})
/* удалить список*/
router.delete('/list', wait, async (req, res) => {
let error = null
const listData = await deleteDoc(req.body).catch((e) => error = e.message)
res.status(error ? 400 : 200).send(getResponse(error, listData))
})
/* добавить новый список*/
router.post('/list', wait, async (req, res) => {
const userId = new ObjectId(req.auth.id)
let error = null
// eslint-disable-next-line max-len
const listData = await addList({ userId, ...req.body }).catch((e) => error = e.message)
res.status(error ? 400 : 200).send(getResponse(error, listData))
})
/* переименовать список*/
router.put('/list', wait, async (req, res) => {
let error = null
const listData = await renameList(req.body).catch((e) => error = e.message)
res.status(error ? 400 : 200).send(getResponse(error, listData))
})
/* дублировать список*/
router.post('/list/duplicate', wait, async (req, res) => {
let error = null
const listData = await duplicateList(req.body).catch((e) => error = e.message)
res.status(error ? 400 : 200).send(getResponse(error, listData))
})
module.exports = router

View File

@ -0,0 +1,9 @@
const router = require('express').Router()
router.use('/dashboard', require('./dashboard'))
router.use('/landing', require('./landing'))
router.use('/categories', require('./categories'))
router.use('/shoppingList', require('./listItem'))
router.use('/auth', require('./auth'))
module.exports = router

View File

@ -0,0 +1,10 @@
{
"data": {
"token": "qwert",
"user": {
"id": "1234",
"login": "eldar",
"email": "www@www.ru"
}
}
}

View File

@ -0,0 +1,23 @@
{
"success": true,
"data":
{
"category":[
{
"id":1,
"name": "Продукты",
"color": "#08AE0F"
},
{
"id":2,
"name": "Бытовая химия",
"color": "#3414F5"
},
{
"id":3,
"name": "Одежда",
"color": "#FA8803"
}
]
}
}

View File

@ -0,0 +1,18 @@
{
"success": true,
"data":
{
"category":[
{
"id":1,
"name": "Продукты",
"color": "#08AE0F"
},
{
"id":2,
"name": "Бытовая химия",
"color": "#3414F5"
}
]
}
}

View File

@ -0,0 +1,3 @@
{
"error": "Не получилось..."
}

View File

@ -0,0 +1,8 @@
{
"data": {
"id": "vrgbtrgbtrbryn",
"listName": "2 список",
"purchased": 1,
"total": 5
}
}

View File

@ -0,0 +1,4 @@
{
"success": true,
"data": []
}

View File

@ -0,0 +1,4 @@
{
"success": false,
"error": "Список не загрузился"
}

View File

@ -0,0 +1,23 @@
{
"success": true,
"data": [
{
"id": "uuid1",
"listName": "Состоялась 94-я церемония вручения премии «Оскар»: награда за лучший фильм присуждена картине...",
"purchased": 0,
"total": 5
},
{
"id": "uuid2",
"listName": "Второй список",
"purchased": 1,
"total": 5
},
{
"id": "uuid3",
"listName": "Первый список",
"purchased": 5,
"total": 5
}
]
}

View File

@ -0,0 +1,4 @@
{
"success": false,
"error": "Ошибка получения данных для Landing"
}

View File

@ -0,0 +1,80 @@
{
"success": true,
"data": [
{
"features": [
{
"id": "1",
"nameImg": "image-shopping-list.png",
"altImg": "image-shopping-list",
"title": "Список покупок",
"text": "Отличный интерфейс, удобно и понятно для использования. Удобно! Если нет нужного в списке, можно добавить и оно сохраниться."
},
{
"id": "2",
"nameImg": "image-shared-access.png",
"altImg": "image-shared-access",
"title": "Общий доступ",
"text": "Делитесь списками покупок, чтобы планировать вместе с другими. Синхронизация для нескольких покупателей — это вещь!"
},
{
"id": "3",
"nameImg": "image-messaging.png",
"altImg": "image-messaging",
"title": "Обмен сообщениями",
"text": "Хотите что-то обсудить при составление списка покупок, то есть возможность обмениваться сообщениями в режиме реального времени."
},
{
"id": "4",
"nameImg": "image-share-photos.png",
"altImg": "mage-share-photos",
"title": "Делитесь фотографиями",
"text": "Не перепутайте товар при покупки — загружайте фотографии и обменивайтесь ими с другими пользователями. Это очень удобно!"
},
{
"id": "5",
"nameImg": "image-prices.png",
"altImg": "image-prices",
"title": "Цены",
"text": "Расходы под контролем — вводите цены в спиок покупок. Приложение оценит стоимость и будет известно, чего ожидать на кассе."
},
{
"id": "6",
"nameImg": "image-ecology.png",
"altImg": "image-ecology",
"title": "Экология",
"text": "Бумажные списки покупок — это деревья, которые могли бы еще расти. Вместо тысяч слов — возьми с собой приложение!"
},
{
"id": "7",
"nameImg": "image-buy.png",
"altImg": "image-buy",
"title": "Покупайте",
"text": "97% пользователей поробывали приложение и теперь совершают меньше лишних и ненужных покупок. Покупайте нужное!"
}
],
"helps": [
{
"id": "1",
"title": "Как работает вМагазин?",
"text": "Задача организации, в особенности же консультация с широким активом обеспечивает широкому кругу специалистов новых принципов формирования материально-технической и кадровой базы."
},
{
"id": "2",
"title": "Условия оплаты",
"text": "Задача организации, в особенности же консультация с широким активом обеспечивает широкому кругу специалистов новых принципов формирования материально-технической и кадровой базы."
},
{
"id": "3",
"title": "Как с нами связаться",
"text": "Задача организации, в особенности же консультация с широким активом обеспечивает широкому кругу специалистов новых принципов формирования материально-технической и кадровой базы."
},
{
"id": "4",
"title": "Текст",
"text": "Задача организации, в особенности же консультация с широким активом обеспечивает широкому кругу специалистов новых принципов формирования материально-технической и кадровой базы."
}
]
}
]
}

View File

@ -0,0 +1,3 @@
{
"error": "Не удалось изменить..."
}

View File

@ -0,0 +1,15 @@
{
"success": true,
"data":
{
"id": 23,
"categoryId": 1,
"text": "Курица",
"count": 17,
"bought": false,
"createdBy": "",
"createdDt": "",
"modifiedBy": "",
"modifiedDt": ""
}
}

View File

@ -0,0 +1,4 @@
{
"success": false,
"error": "Ошибка получения данных"
}

View File

@ -0,0 +1,34 @@
{
"success": true,
"data":
{
"id": 8,
"listName":"Мой список",
"data":[
{
"id": 1,
"categoryId": 1,
"text": "Курица",
"count": 2,
"bought": true,
"delete":false
},
{
"id": 2,
"categoryId":1,
"text": "Хлеб",
"count": 1,
"bought":false,
"delete":true
},
{
"id": 3,
"categoryId": "3",
"text": "Шампунь",
"count": 1,
"bought":true,
"delete":false
}
]
}
}

View File

@ -0,0 +1,5 @@
const BASKET_JWT_TOKEN = 'super super secret key'
module.exports = {
BASKET_JWT_TOKEN,
}

View File

@ -0,0 +1,7 @@
const router = require('express').Router()
router.get('/', (req, res) => {
res.send(require('./json/landing/success.json'))
})
module.exports = router

View File

@ -0,0 +1,67 @@
const router = require('express').Router()
const { expressjwt } = require('express-jwt')
const ObjectId = require('mongodb').ObjectID
const { BASKET_JWT_TOKEN } = require('./key')
const { getShoppingList, deleteItem, boughtItem, incCountItem, getResponse, addListItem } = require('./controller')
router.use(expressjwt({ secret: BASKET_JWT_TOKEN, algorithms: ['HS256'] }))
router.get('/:id', async (req, res) => {
const userId = new ObjectId(req.auth.id)
let error = null
// eslint-disable-next-line no-return-assign
const { id } = req.params
// eslint-disable-next-line no-return-assign
const listData = await getShoppingList({ userId, id }).catch((e) => error = e.message)
res.send(getResponse(error, listData))
})
router.post('/item/:id', async (req, res) => {
const userId = new ObjectId(req.auth.id)
let error = null
// eslint-disable-next-line no-return-assign
const { id } = req.params
const { categoryId, text } = req.body
const shoppingListData = await addListItem({
userId, listId: id, categoryId, text,
// eslint-disable-next-line no-return-assign
}).catch((e) => error = e.message)
res.send(getResponse(error, shoppingListData))
})
router.patch('/item/:id', async (req, res) => {
const userId = new ObjectId(req.auth.id)
let error = null
// eslint-disable-next-line no-return-assign
const { id } = req.params
const { bought } = req.body.item
// eslint-disable-next-line no-return-assign
// eslint-disable-next-line max-len
const itemData = await boughtItem({ userId, itemId: id, bought }).catch((e) => error = e.message)
res.send(getResponse(error, itemData))
})
router.put('/item/:id', async (req, res) => {
const userId = new ObjectId(req.auth.id)
let error = null
// eslint-disable-next-line no-return-assign
const { id } = req.params
const { count } = req.body
// eslint-disable-next-line no-return-assign
// eslint-disable-next-line max-len
const itemData = await incCountItem({ userId, itemId: id, count }).catch((e) => error = e.message)
res.send(getResponse(error, itemData))
})
router.delete('/item/:id', async (req, res) => {
let error = null
// eslint-disable-next-line no-return-assign
const { id } = req.params
// eslint-disable-next-line no-return-assign
const itemData = await deleteItem({ itemId: id }).catch((e) => error = e.message)
res.send(getResponse(error, itemData))
})
module.exports = router

View File

@ -0,0 +1,162 @@
[
{
"id": "6d6883d86aa44db784436894a0b30881",
"title": "听新闻学汉语 2021年03月31日",
"description": "中国-世卫组织新冠病毒溯源联合研究报告正式发布\n世界卫生组织30日在日内瓦正式发布中国-世卫组织新冠病毒溯源联合研究报告。报告认为新冠病毒“极不可能”通过实验室传人。今年1月14日至2月10日17名中方专家和17名外方专家组成联合专家组分为流行病学、分子溯源、动物与环境3个小组在武汉开展了为期28天的全球溯源研究中国部分工作在此基础上撰写了研究报告。联合专家组评估了关于病毒引入人类的4个路径认为新冠病毒“比较可能至非常可能”经中间宿主传人“可能至比较可能”直接传人“可能”通过冷链食品传人“极不可能”通过实验室传人。报告提出了联合专家组下步研究的建议包括建立全球统一的数据库在全球更广范围内继续寻找可能的早期病例由全球科学家在多国多地寻找可能成为病毒宿主的动物物种进一步了解冷链和冷冻食品在病毒传播过程中的作用等。\n\n货轮搁浅6天终于脱困 苏伊士运河恢复通航\n苏伊士运河因“长赐”号货轮搁浅被堵多日货轮解困进展牵动全球目光。经过了长达一周的救援当地时间3月29日搁浅货轮已经完全脱困苏伊士运河恢复通航。当地时间3月29日救援队成功让搁浅在苏伊士运河长达一周的巨型货轮脱困。过去7天这艘搁浅货轮阻止了价值数十亿美元的货物通过苏伊士运河。苏伊士运河是世界上最繁忙的海运通道之一。受雇参与救援的荷兰宝斯卡利斯公司的总监皮特•伯尔道斯基表示为了让货轮脱困挖掘机挖走了3万立方米的泥沙并动用了13艘拖船。\n\n威廉王子获评“世界最性感光头男人”\n上周六3月27日英国《太阳报》发布报道称威廉王子是“世界最性感的秃顶男人”该报道基于提供植发服务的整容公司的一项调查。据《太阳报》报道该调查是通过分析“博客、报道和谷歌搜索页面”中“性感”一词的出现频率来得出结果的。《太阳报》写道调查发现威廉王子的名字被提及1760万次。这篇文章还提到了调查结果中入选前十位的其他名人。迈克•泰森仅次于威廉王子排在第二位紧随其后的是《速度与激情》主演杰森•斯坦森、说唱歌手皮普保罗、迈克尔•乔丹、拳击手弗洛伊德•梅威瑟、约翰•特拉沃尔塔、布鲁斯•威利斯、“巨石”强森和范•迪塞尔。",
"pub_date_ms": 1617163200000,
"audio": "https://www.listennotes.com/e/p/6d6883d86aa44db784436894a0b30881/",
"audio_length_sec": 553,
"listennotes_url": "https://www.listennotes.com/e/6d6883d86aa44db784436894a0b30881/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/6d6883d86aa44db784436894a0b30881/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "b82bf190efbc59a11323fba954a88939"
},
{
"id": "e8f2f82ac8b645c18ba3cc401cc3b9e0",
"title": "中国人的问候方式",
"description": "问候是交际中最常用的日常礼节,人与人之间的交际都是从互致问候开始的。由于习俗不同,各个国家,各个民族的问候方式差异很大。\n首先在问候类别上中国人的问候方式包括问好型、询问型、也有评论型。问好型最常用的是早、你早。中国人现在也越来越多用“你好”来打招呼。这种问候方式比较正式。汉语中还有“干吗去啊”“吃了吗”等询问型问候语以表示对别人的关心并不是打探隐私。\n其次问候语涉及的话题也不同。汉语的问候语大多由包含信息的问句组成并且大多涉及别人的起居寒暖给人亲切关心的感觉。中国人往往会互相问起对方的年龄、收入、婚姻和健康等非常大众化的话题并且问得越详细越能体现出对别人的关心。\n除了语言问候方式中国人的非语言问候方式也有自己的特点。中国人尤其是年轻人之间通常会点头或挥手表示问候而在旧时人们见面还要鞠躬以示问候。中国人的问候方式一般比较含蓄即使是久别重逢也仅仅是握握手绝不会亲吻一般男女之间连拥抱都很少有最多握握手。",
"pub_date_ms": 1617076800000,
"audio": "https://www.listennotes.com/e/p/e8f2f82ac8b645c18ba3cc401cc3b9e0/",
"audio_length_sec": 945,
"listennotes_url": "https://www.listennotes.com/e/e8f2f82ac8b645c18ba3cc401cc3b9e0/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/e8f2f82ac8b645c18ba3cc401cc3b9e0/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "e0b0bedbc76befceae54d01abdfb4742"
},
{
"id": "a7fa065d4f3e43f4af68b642f2ae7331",
"title": "不求有功,但求无过",
"description": "张路:大家好,我是张路。\n珺慧大家好我是珺慧。欢迎大家按时收听我们的节目。\n张路珺慧今天你给大家带来的俗语是哪一个呢\n珺慧今天我想给大家讲讲“不求有功但求无过”这个俗语。\n张路好啊那么你先来说说它的意思吧。\n珺慧“功”在这里是功绩、立功的意思“过”是指错误、过错的意思“求”是指要求、希望。所以“不求有功但求无过”这个俗语的表面意思就是不要求立功只希望没有错误。\n张路我觉得这个俗语其实也反映了一些人对待事情的态度。\n珺慧什么态度呢\n张路比如说我吧我就非常希望成功做事情的时候即使要冒险我也不怕只要能成功就行。可是我姐姐跟我就不一样她就是那种“不求有功但求无过”的人她做事特别小心谨慎不太追求成功只要没有大的错误就行。\n珺慧没错所以这个俗语有时也比喻不求上进或安分守己的态度或做法。\n张路下面我们就来听三段对话一起来学习一下这个俗语的意思和用法。",
"pub_date_ms": 1616817600000,
"audio": "https://www.listennotes.com/e/p/a7fa065d4f3e43f4af68b642f2ae7331/",
"audio_length_sec": 404,
"listennotes_url": "https://www.listennotes.com/e/a7fa065d4f3e43f4af68b642f2ae7331/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/a7fa065d4f3e43f4af68b642f2ae7331/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "722cdf994f21362230a5081106b82a3a"
},
{
"id": "94f99709224a4cd98f92ae4399ee1327",
"title": "听新闻学汉语 2021年03月26日",
"description": "83天200亿件快递业务量增长再提速\n国家邮政局实时监测数据显示截至3月24日今年我国快递业务量已突破200亿件日均业务量超过2.4亿件日均服务用户接近5亿人次服务民生作用更加凸显。今年以来邮政快递业继续保持高速增长态势预计全年快递业务量将超过950亿件。值得注意的是今年快递业务量突破200亿件用时仅83天比2020年提前了45天又一次刷新了我国快递业发展纪录。今年邮政快递业更贴近民生七件实事提出要提高建制村快递服务通达率东部地区基本实现快递服务直投到村中、西部地区分别达到80%和60%。\n\n欧盟正式实施健康欧盟计划\n欧盟委员会26日宣布2021年至2027年健康欧盟计划即日起正式实施旨在增强欧盟有效应对未来卫生危机的能力。欧委会当天在一份声明中说欧盟将为该计划拨付51亿欧元的财政预算有助于新冠疫情后健全卫生系统增强有效应对跨境健康威胁及未来卫生危机的能力促进人口健康。根据声明健康欧盟计划指导小组将与欧盟成员国协商后推出2021年第一个工作方案。健康欧盟计划将由新成立的健康和数字执行机构负责实施该机构将于4月1日开始运行。欧盟委员会负责卫生和食品安全事务的委员基里亚基季斯表示健康欧盟计划为欧盟在公共卫生领域进行长期性变革提供了手段可以通过规模空前的财政预算进行有针对性的投资以加强危机准备建立更强大、更有韧性和更易获得的卫生系统。欧委会于去年5月提出2021年至2027年健康欧盟计划旨在向欧盟成员国、卫生组织和非政府组织等提供资金支持提高应对卫生领域各种威胁的能力。该计划于本月9日经欧洲议会表决通过17日获欧洲理事会批准。\n\n研究不爱运动的人更容易失眠\n日前中国睡眠研究会发布了《2021年运动与睡眠白皮书》。数据显示当下中国有超3亿人存在睡眠障碍而运动人群失眠困扰比例仅为10%。运动人群中以广东运动人数最多,并且睡眠充足比例位列各省份第一。研究报告指出,新冠疫情期间,不运动人群和定期运动人群睡眠状态两极分化,旅行限制令使得久坐人群减少锻炼,作息更不规律。中国睡眠研究会理事、北京朝阳医院呼吸睡眠中心主任郭兮恒称,随着国内疫情得到控制,回归常规生活的重要一步就是多出门走走,定期锻炼,以解决日益普遍的睡眠问题。",
"pub_date_ms": 1616731200000,
"audio": "https://www.listennotes.com/e/p/94f99709224a4cd98f92ae4399ee1327/",
"audio_length_sec": 617,
"listennotes_url": "https://www.listennotes.com/e/94f99709224a4cd98f92ae4399ee1327/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/94f99709224a4cd98f92ae4399ee1327/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "7ed62a5606b169765dcc29d339b6e2d2"
},
{
"id": "244fe98dbb8d47cdb090041db48db674",
"title": "我对摇滚乐不感兴趣",
"description": "林娜:音乐声太吵了,赶快关掉。\n表妹这首歌多好听啊听完再关吧。\n林娜这有什么好听的我真不明白你为什么这么喜欢摇滚乐。\n表妹这有什么不明白的原因很简单啊我一听摇滚乐就特别兴奋。\n林娜我对摇滚乐不感兴趣完全没办法欣赏。拜托你下次听摇滚乐的时候小声一点儿不要影响别人。\n表妹好吧好吧我戴上耳机听绝对不会影响到你你放心吧。",
"pub_date_ms": 1616644800001,
"audio": "https://www.listennotes.com/e/p/244fe98dbb8d47cdb090041db48db674/",
"audio_length_sec": 910,
"listennotes_url": "https://www.listennotes.com/e/244fe98dbb8d47cdb090041db48db674/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/244fe98dbb8d47cdb090041db48db674/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "7bda2555f80feee77ea3c2f0b48d3034"
},
{
"id": "38cef39a6f64477488fa0ff88bed6e17",
"title": "听新闻学汉语 2021年03月24日",
"description": "教育部印发《职业教育专业目录2021年》 共设置1349个专业\n记者22日从教育部获悉其近日印发的《职业教育专业目录2021年专业设置对接现代产业体系服务产业基础高级化、产业链现代化共包含1349个专业。此次新版目录面向集成电路技术、生物信息技术、新能源材料应用技术等9大重点领域服务国家战略性新兴产业发展此外回应社会民生关切加强紧缺领域人才培养如设置婴幼儿托育服务与管理、智慧健康养老服务与管理等专业。\n\n澳大利亚东部遭遇罕见暴雨洪灾\n澳大利亚官员21日说东部沿海地带最近几天连降暴雨部分区域遭遇50年来最严重洪灾数以千计居民紧急疏散数以百计房屋损毁。新南威尔士州有800万人口是澳大利亚人口最多的州。州长格拉迪丝•贝雷吉克利安21日在一场新闻发布会上说全州范围遭遇暴雨情形比先前预期得更糟糕州首府悉尼西北部地势低洼地区洪灾尤其严重。“我们昨天预期将迎来20年一遇的洪灾”她说“但如今看来这是50年一遇的洪灾。”贝雷吉克利安说由于暴雨洪水摧毁房屋不少居民20日夜间至21日凌晨紧急疏散另有1000人21日晚些时候接到疏散令今后几天可能还将有大约4000人接到疏散令。气象部门预测澳大利亚东部地区暴雨还将持续数日。据法新社报道自18日开始降雨以来澳大利亚紧急情况应对部门已经接到7000多个求助电话。\n\n日本孩子长大后最想干啥男孩公司职员 女孩:糕点师\n据日本《朝日新闻》中文网报道日本“第一生命保险”不久前公布了对小学3至6年级儿童询问“长大后最想成为什么”的调查结果发现男生首选“公司职员”女生选择最多的是“糕点师”。根据调查结果男生选择的第1位是“公司职员”“油管主播”(YouTuber)以微弱之差位居其后。女生选择的第1位是“糕点师”第2位是“教师”。该调查以日本全国的约1100人为对象于2020年12月实施。此次是第32次。男生选择的第1位从上次的“足球选手”转为了“公司职员”。“第一生命保险”的负责人对这一结果解释称“在因疫情而引入远程工作的情况下或许是因为看到父母在家里工作的样子而产生了一种亲近感吧”。",
"pub_date_ms": 1616558400002,
"audio": "https://www.listennotes.com/e/p/38cef39a6f64477488fa0ff88bed6e17/",
"audio_length_sec": 536,
"listennotes_url": "https://www.listennotes.com/e/38cef39a6f64477488fa0ff88bed6e17/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/38cef39a6f64477488fa0ff88bed6e17/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "f28e8823de7317903c9a994e562f02a6"
},
{
"id": "5b8527abc26a47aea5d79849c1577bbe",
"title": "寒食节的传说(二)",
"description": "晋文公多次请介子推出来做官,可介子推每次都不愿意。晋文公很想念他,最后就想了一个办法:他想,介子推藏在山里,我怎么也找不到他。如果我在山里放火,他怕被火烧死,一定会跑出来,这样我就一定能见到他了。可是他没想到,介子推宁愿烧死也不愿意出来做官,晋文公放了火,介子推最后被大火烧死了。晋文公万万没想到他得到的是这样一个悲剧的结果。他因为想念自己的朋友而杀死了自己的朋友,又难过又后悔,生了一场大病,自己也差一点儿死了。\n晋文公为了纪念自己的好朋友介子推下了一个命令在每年的农历三月初三这一天全国的老百姓都不能用火甚至不能做饭只能吃做好的食品。因为这一天是他放火烧死自己朋友的日子他永远后悔这一天。因为法律不准用火人们只能吃凉的东西所以历史上这一天就被称作“寒食节”。",
"pub_date_ms": 1616472000003,
"audio": "https://www.listennotes.com/e/p/5b8527abc26a47aea5d79849c1577bbe/",
"audio_length_sec": 913,
"listennotes_url": "https://www.listennotes.com/e/5b8527abc26a47aea5d79849c1577bbe/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/5b8527abc26a47aea5d79849c1577bbe/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "f57e66faffeff030727d85932435c52e"
},
{
"id": "8fdae88892734896b79395817c061221",
"title": "寒食节的传说(一)",
"description": "传说在中国古代有一个晋国。这个国家遇到了危险,国家的王子逃了出去。在王子的身边有一些很好的朋友和忠诚的读书人陪着他,这些人爱自己的国家,他们支持王子,希望他能努力奋斗,回去救自己的国家。他们在外边受了很多苦,一直逃亡了十九年。这就是历史上的晋文公。\n在国外逃亡期间晋文公有一个最忠诚的朋友叫介子推。他是一个优秀的读书人为了自己热爱的祖国为了帮助晋文公重新回国做国王做了很多努力。他们成功以后晋文公让伴随他逃亡的人都做了大官。可是没想到他最好的朋友介子推却坚决拒绝他的邀请这让晋文公感到很苦恼。无论他怎么劝说介子推就是不肯出来做官。为了表示自己不愿意出来做官的决心介子推背着自己的母亲到了山上去隐居。",
"pub_date_ms": 1616212800004,
"audio": "https://www.listennotes.com/e/p/8fdae88892734896b79395817c061221/",
"audio_length_sec": 863,
"listennotes_url": "https://www.listennotes.com/e/8fdae88892734896b79395817c061221/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/8fdae88892734896b79395817c061221/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "a2df1174ce43a2f18a71d288ecc646f7"
},
{
"id": "0932fa791c6243f3a03ba42da3397369",
"title": "听新闻学汉语 2021年03月19日",
"description": "外交部:疫苗好不好要看是否安全可靠 反对搞疫苗民族主义\n3月15日外交部发言人赵立坚主持外交部例行记者会。有记者提问新加坡总理李显龙接受媒体采访时针对日益政治化的新冠病毒疫苗问题他表示疫苗不分国籍没有任何依据可以凭生产国就断言中国疫苗必定好或者不好。他还表示中国有非常优秀的科学家、生物医药和疫苗研究人员相信他们有能力生产好的疫苗。中方有何回应赵立坚表示疫苗是抗击病毒的利器是拯救生命的希望应当服务全世界、造福全人类。无论是哪个国家的疫苗只要安全可靠就是好疫苗。中方将继续同各方一道反对搞疫苗民族主义不接受制造“免疫鸿沟”努力推进疫苗在全球范围内的公平分配携手各国共同战胜疫情。\n\n朝鲜宣布与马来西亚断交\n据朝中社19日报道朝鲜外务省当天发表声明正式宣布完全断绝与马来西亚的外交关系。声明表示马来西亚当局17日以涉嫌“非法洗钱”为由将一名无辜的朝鲜公民强行引渡至美国彻底破坏了两国关系中相互尊重主权的基础鉴于当前的严重事态朝方因此决定断绝与“屈服美国强权而对朝鲜做出特大敌对行为”的马来西亚的外交关系。声明称该朝鲜公民多年在新加坡从事合法对外贸易活动其涉嫌洗钱是荒唐无稽的捏造和阴谋马来西亚方面也从未提出过任何确凿证据。声明指出马来西亚当局盲目追随美国不当的压力甚至无视公认的国际法把朝鲜公民当作美国敌视政策的牺牲品。从现在起马来西亚将要对双方之间可能发生的任何后果负全部责任。声明还警告美国作为这起事件的幕后操纵者也将付出应有的代价。据悉朝鲜和马来西亚1973年正式建交。\n\n20天逾4万次地震冰岛人被震到失眠\n据外媒报道自2月24日冰岛西南部的雷克雅内斯半岛发生5.6级地震以来该地区余震不断。过去20天已记录下4万多次地震活动。据路透社16日报道自2月24日以来雷克雅内斯半岛发生了40000多次地震超过了2020年在那里记录的地震总数。冰岛气象局(IMO)火山灾害协调员萨拉•巴索蒂表示:“我们从未见过如此多的地震活动。”如此频繁的地震活动,严重影响了当地居民的睡眠。“这里的每个人都很累,”当地一位名叫古德蒙兹多蒂尔的教师说。“当我晚上上床睡觉的时候,我想的都是:我今晚能睡得着吗?”",
"pub_date_ms": 1616126400000,
"audio": "https://www.listennotes.com/e/p/0932fa791c6243f3a03ba42da3397369/",
"audio_length_sec": 580,
"listennotes_url": "https://www.listennotes.com/e/0932fa791c6243f3a03ba42da3397369/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/0932fa791c6243f3a03ba42da3397369/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "9563923aacc3c0ced1bde1af9c578a14"
},
{
"id": "7ea1b07350204827a31c9b0995d9ce22",
"title": "我想预订一个双人间",
"description": "服务员:早上好。这里是假日酒店,很高兴为您服务。\n林 娜:早上好。我想预订一个双人间,你们有下周的空房吗?\n服务员请稍等。让我查一下房间的预订情况。是的我们有剩余的双人间。请问您要什么样的房间\n林 娜:我想要一个能看到漂亮风景的房间。\n服务员我们有一个带漂亮花园风景的房间可以吗\n林 娜:棒极了,我就要这间。多少钱?\n服务员每晚880元包含早餐。\n林 娜:好的,我们下周一下午两点左右入住,周四早上离开。\n服务员知道了。请留下您的名字和电话号码。\n林 娜:林娜,一三九五八一六二五一二。",
"pub_date_ms": 1616040000000,
"audio": "https://www.listennotes.com/e/p/7ea1b07350204827a31c9b0995d9ce22/",
"audio_length_sec": 971,
"listennotes_url": "https://www.listennotes.com/e/7ea1b07350204827a31c9b0995d9ce22/",
"image": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"thumbnail": "https://cdn-images-1.listennotes.com/podcasts/learn-chinese-culture-imandarinpodcom-Ac3CZvRgnzL.300x300.jpg",
"maybe_audio_invalid": false,
"listennotes_edit_url": "https://www.listennotes.com/e/7ea1b07350204827a31c9b0995d9ce22/#edit",
"explicit_content": false,
"link": "http://www.imandarinpod.com?utm_source=listennotes.com&utm_campaign=Listen+Notes&utm_medium=website",
"guid_from_rss": "9a3124609346ebf3c52cafbe8d0d1b4a"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show More