init from origin + update
45
.eslintrc.js
Normal 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
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
.idea
|
5
.serverrc.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
port: 8044,
|
||||
mongoAddr: process.env.MONGO_ADDR || 'localhost',
|
||||
mongoPort: 27017,
|
||||
}
|
26
.vscode/launch.json
vendored
Normal 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
@ -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
@ -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
@ -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
@ -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
52
package.json
Normal 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
@ -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
@ -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
@ -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
@ -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
|
34
server/routers/basket/auth.js
Normal 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
|
25
server/routers/basket/categories.js
Normal 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
|
510
server/routers/basket/controller.js
Normal 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,
|
||||
}
|
54
server/routers/basket/dashboard.js
Normal 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
|
9
server/routers/basket/index.js
Normal 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
|
10
server/routers/basket/json/auth/sign-in.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"data": {
|
||||
"token": "qwert",
|
||||
"user": {
|
||||
"id": "1234",
|
||||
"login": "eldar",
|
||||
"email": "www@www.ru"
|
||||
}
|
||||
}
|
||||
}
|
23
server/routers/basket/json/categories/add/success.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"success": true,
|
||||
"data":
|
||||
{
|
||||
"category":[
|
||||
{
|
||||
"id":1,
|
||||
"name": "Продукты",
|
||||
"color": "#08AE0F"
|
||||
},
|
||||
{
|
||||
"id":2,
|
||||
"name": "Бытовая химия",
|
||||
"color": "#3414F5"
|
||||
},
|
||||
{
|
||||
"id":3,
|
||||
"name": "Одежда",
|
||||
"color": "#FA8803"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
18
server/routers/basket/json/categories/current/success.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"success": true,
|
||||
"data":
|
||||
{
|
||||
"category":[
|
||||
{
|
||||
"id":1,
|
||||
"name": "Продукты",
|
||||
"color": "#08AE0F"
|
||||
},
|
||||
{
|
||||
"id":2,
|
||||
"name": "Бытовая химия",
|
||||
"color": "#3414F5"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
3
server/routers/basket/json/dashboard/common/error.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"error": "Не получилось..."
|
||||
}
|
8
server/routers/basket/json/dashboard/common/success.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "vrgbtrgbtrbryn",
|
||||
"listName": "2 список",
|
||||
"purchased": 1,
|
||||
"total": 5
|
||||
}
|
||||
}
|
4
server/routers/basket/json/dashboard/list/empty.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": []
|
||||
}
|
4
server/routers/basket/json/dashboard/list/error.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"success": false,
|
||||
"error": "Список не загрузился"
|
||||
}
|
23
server/routers/basket/json/dashboard/list/success.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
4
server/routers/basket/json/landing/error.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"success": false,
|
||||
"error": "Ошибка получения данных для Landing"
|
||||
}
|
80
server/routers/basket/json/landing/success.json
Normal 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": "Задача организации, в особенности же консультация с широким активом обеспечивает широкому кругу специалистов новых принципов формирования материально-технической и кадровой базы."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
3
server/routers/basket/json/listItem/item/error.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"error": "Не удалось изменить..."
|
||||
}
|
15
server/routers/basket/json/listItem/item/success.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"success": true,
|
||||
"data":
|
||||
{
|
||||
"id": 23,
|
||||
"categoryId": 1,
|
||||
"text": "Курица",
|
||||
"count": 17,
|
||||
"bought": false,
|
||||
"createdBy": "",
|
||||
"createdDt": "",
|
||||
"modifiedBy": "",
|
||||
"modifiedDt": ""
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"success": false,
|
||||
"error": "Ошибка получения данных"
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
5
server/routers/basket/key.js
Normal file
@ -0,0 +1,5 @@
|
||||
const BASKET_JWT_TOKEN = 'super super secret key'
|
||||
|
||||
module.exports = {
|
||||
BASKET_JWT_TOKEN,
|
||||
}
|
7
server/routers/basket/landing.js
Normal file
@ -0,0 +1,7 @@
|
||||
const router = require('express').Router()
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.send(require('./json/landing/success.json'))
|
||||
})
|
||||
|
||||
module.exports = router
|
67
server/routers/basket/listItem.js
Normal 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
|
162
server/routers/bushou/episodes/success.json
Normal 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"
|
||||
}
|
||||
]
|
BIN
server/routers/bushou/images/60px-%E4%B8%80-bigseal.svg.png
Normal file
After Width: | Height: | Size: 377 B |
BIN
server/routers/bushou/images/60px-%E4%B8%80-bronze.svg.png
Normal file
After Width: | Height: | Size: 470 B |
BIN
server/routers/bushou/images/60px-%E4%B8%80-clerical.svg.png
Normal file
After Width: | Height: | Size: 578 B |
BIN
server/routers/bushou/images/60px-%E4%B8%80-oracle.svg.png
Normal file
After Width: | Height: | Size: 481 B |
BIN
server/routers/bushou/images/60px-%E4%B8%80-seal.svg.png
Normal file
After Width: | Height: | Size: 368 B |
BIN
server/routers/bushou/images/60px-%E4%B8%80-silk.svg.png
Normal file
After Width: | Height: | Size: 593 B |
BIN
server/routers/bushou/images/60px-%E4%B8%80-slip.svg.png
Normal file
After Width: | Height: | Size: 539 B |
BIN
server/routers/bushou/images/60px-%E4%B8%A8-bigseal.svg.png
Normal file
After Width: | Height: | Size: 414 B |
BIN
server/routers/bushou/images/60px-%E4%B8%A8-seal.svg.png
Normal file
After Width: | Height: | Size: 414 B |
BIN
server/routers/bushou/images/60px-%E4%B8%B6-bronze.svg.png
Normal file
After Width: | Height: | Size: 739 B |
BIN
server/routers/bushou/images/60px-%E4%B8%B6-seal.svg.png
Normal file
After Width: | Height: | Size: 686 B |
BIN
server/routers/bushou/images/60px-%E4%B8%BF-bigseal.svg.png
Normal file
After Width: | Height: | Size: 750 B |
BIN
server/routers/bushou/images/60px-%E4%B8%BF-seal.svg.png
Normal file
After Width: | Height: | Size: 687 B |
BIN
server/routers/bushou/images/60px-%E4%B9%99-bigseal.svg.png
Normal file
After Width: | Height: | Size: 706 B |
BIN
server/routers/bushou/images/60px-%E4%B9%99-bronze.svg.png
Normal file
After Width: | Height: | Size: 776 B |
BIN
server/routers/bushou/images/60px-%E4%B9%99-oracle.svg.png
Normal file
After Width: | Height: | Size: 628 B |
BIN
server/routers/bushou/images/60px-%E4%B9%99-seal.svg.png
Normal file
After Width: | Height: | Size: 742 B |
BIN
server/routers/bushou/images/60px-%E4%B9%99-silk.svg.png
Normal file
After Width: | Height: | Size: 853 B |
BIN
server/routers/bushou/images/60px-%E4%BA%85-seal.svg.png
Normal file
After Width: | Height: | Size: 911 B |
BIN
server/routers/bushou/images/60px-%E4%BA%8C-bigseal.svg.png
Normal file
After Width: | Height: | Size: 601 B |
BIN
server/routers/bushou/images/60px-%E4%BA%8C-bronze.svg.png
Normal file
After Width: | Height: | Size: 648 B |
BIN
server/routers/bushou/images/60px-%E4%BA%8C-oracle.svg.png
Normal file
After Width: | Height: | Size: 709 B |
BIN
server/routers/bushou/images/60px-%E4%BA%8C-seal.svg.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
server/routers/bushou/images/60px-%E4%BA%8C-silk.svg.png
Normal file
After Width: | Height: | Size: 858 B |
BIN
server/routers/bushou/images/60px-%E4%BA%8C-slip.svg.png
Normal file
After Width: | Height: | Size: 718 B |
BIN
server/routers/bushou/images/60px-%E4%BA%A0-seal.svg.png
Normal file
After Width: | Height: | Size: 1015 B |
BIN
server/routers/bushou/images/60px-%E4%BA%BA-bigseal.svg.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
server/routers/bushou/images/60px-%E4%BA%BA-bronze-shang.svg.png
Normal file
After Width: | Height: | Size: 811 B |
After Width: | Height: | Size: 872 B |
After Width: | Height: | Size: 659 B |
BIN
server/routers/bushou/images/60px-%E4%BA%BA-bronze.svg.png
Normal file
After Width: | Height: | Size: 767 B |
BIN
server/routers/bushou/images/60px-%E4%BA%BA-oracle.svg.png
Normal file
After Width: | Height: | Size: 719 B |
BIN
server/routers/bushou/images/60px-%E4%BA%BA-seal.svg.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
server/routers/bushou/images/60px-%E4%BA%BA-silk.svg.png
Normal file
After Width: | Height: | Size: 812 B |
BIN
server/routers/bushou/images/60px-%E4%BA%BA-slip.svg.png
Normal file
After Width: | Height: | Size: 760 B |
BIN
server/routers/bushou/images/60px-%E4%BB%8C-bigseal.svg.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
server/routers/bushou/images/60px-%E4%BB%8C-bronze.svg.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
server/routers/bushou/images/60px-%E4%BB%8C-oracle.svg.png
Normal file
After Width: | Height: | Size: 955 B |
BIN
server/routers/bushou/images/60px-%E4%BB%8C-seal.svg.png
Normal file
After Width: | Height: | Size: 908 B |
BIN
server/routers/bushou/images/60px-%E5%84%BF-bigseal.svg.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
server/routers/bushou/images/60px-%E5%84%BF-bronze-shang.svg.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
server/routers/bushou/images/60px-%E5%84%BF-bronze.svg.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
server/routers/bushou/images/60px-%E5%84%BF-oracle.svg.png
Normal file
After Width: | Height: | Size: 1016 B |
BIN
server/routers/bushou/images/60px-%E5%84%BF-seal.svg.png
Normal file
After Width: | Height: | Size: 997 B |
BIN
server/routers/bushou/images/60px-%E5%85%A5-bigseal.svg.png
Normal file
After Width: | Height: | Size: 856 B |
BIN
server/routers/bushou/images/60px-%E5%85%A5-bronze.svg.png
Normal file
After Width: | Height: | Size: 693 B |
BIN
server/routers/bushou/images/60px-%E5%85%A5-oracle.svg.png
Normal file
After Width: | Height: | Size: 711 B |
BIN
server/routers/bushou/images/60px-%E5%85%A5-seal.svg.png
Normal file
After Width: | Height: | Size: 766 B |
BIN
server/routers/bushou/images/60px-%E5%85%A5-silk.svg.png
Normal file
After Width: | Height: | Size: 952 B |
BIN
server/routers/bushou/images/60px-%E5%85%AB-bigseal.svg.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
server/routers/bushou/images/60px-%E5%85%AB-bronze.svg.png
Normal file
After Width: | Height: | Size: 843 B |
BIN
server/routers/bushou/images/60px-%E5%85%AB-oracle.svg.png
Normal file
After Width: | Height: | Size: 951 B |
BIN
server/routers/bushou/images/60px-%E5%85%AB-seal.svg.png
Normal file
After Width: | Height: | Size: 967 B |
BIN
server/routers/bushou/images/60px-%E5%85%AB-silk.svg.png
Normal file
After Width: | Height: | Size: 923 B |
BIN
server/routers/bushou/images/60px-%E5%85%AB-slip.svg.png
Normal file
After Width: | Height: | Size: 725 B |
BIN
server/routers/bushou/images/60px-%E5%86%82-bigseal.svg.png
Normal file
After Width: | Height: | Size: 935 B |
BIN
server/routers/bushou/images/60px-%E5%86%82-bronze.svg.png
Normal file
After Width: | Height: | Size: 820 B |
BIN
server/routers/bushou/images/60px-%E5%86%82-seal.svg.png
Normal file
After Width: | Height: | Size: 841 B |
BIN
server/routers/bushou/images/60px-%E5%86%88-bronze-shang.svg.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.5 KiB |
BIN
server/routers/bushou/images/60px-%E5%86%88-bronze.svg.png
Normal file
After Width: | Height: | Size: 1.4 KiB |