From db05b73e5b45bd1b4de61e661124d11143dde47c Mon Sep 17 00:00:00 2001 From: spiduler Date: Tue, 13 Jul 2021 16:53:42 +0900 Subject: [PATCH] first commit --- .gitignore | 5 ++ config/index.js | 71 +++++++++++++++ config/params.json | 4 + config/xmlParserConfig.js | 20 +++++ index.js | 60 +++++++++++++ package.json | 31 +++++++ src/component/GodoClient.js | 53 +++++++++++ src/component/GodoUpdater.js | 103 +++++++++++++++++++++ src/component/RestClient.js | 53 +++++++++++ src/controller/ProductController.js | 32 +++++++ src/exception/TokenExpiredException.js | 16 ++++ src/service/CatalogService.js | 31 +++++++ src/service/ProductService.js | 118 +++++++++++++++++++++++++ test.js | 27 ++++++ 14 files changed, 624 insertions(+) create mode 100644 .gitignore create mode 100644 config/index.js create mode 100644 config/params.json create mode 100644 config/xmlParserConfig.js create mode 100644 index.js create mode 100644 package.json create mode 100644 src/component/GodoClient.js create mode 100644 src/component/GodoUpdater.js create mode 100644 src/component/RestClient.js create mode 100644 src/controller/ProductController.js create mode 100644 src/exception/TokenExpiredException.js create mode 100644 src/service/CatalogService.js create mode 100644 src/service/ProductService.js create mode 100644 test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d7b832 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +package-lock.json +.env +storage/html/* +!storage/html/.gitignore \ No newline at end of file diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..18e3642 --- /dev/null +++ b/config/index.js @@ -0,0 +1,71 @@ +const commandLineArgs = require('command-line-args') + , xmlParserConfig = require('./xmlParserConfig') + , godoParams = require('./params.json') + , logger = require("logops"); + +class Config { + + constructor() { + this.options = {} + this.requires = ['env', 'gateway', 'quix24', 'mongo'] + this.init() + this.validate() + this.finalize() + } + + init() { + try { + this.options = commandLineArgs([ + { name: 'env', alias: 'e', type: String, defaultValue: ['production'] }, + { name: 'host', type: String }, + { name: 'port', type: Number }, + { name: 'database', alias: 'd', type: String }, + { name: 'username', alias: 'u', type: String }, + { name: 'password', alias: 'p', type: String }, + { name: 'mongo', alias: 'm', type: String }, + { name: 'redis', alias: 'r', type: String }, + { name: 'level', alias: 'l', type: String }, + { name: 'gateway', alias: 'g', type: String }, + { name: 'quix24', alias: 'q', type: String } + ]); + } catch (e) { + logger.debug('Command line arguments interpret failed :', e.message) + logger.debug('expected arguments : ', JSON.stringify(this.requires)) + process.exit(1) + } + } + + finalize() { + if (this.options.env == 'development') { + this.options.level = this.options.level ? this.options.level : "DEBUG" + logger.formatters.dev.omit = ['pid', 'port', 'hostname', 'app']; + // logger.format = logger.formatters.dev; + } else { + this.options.level = this.options.level ? this.options.level : "WARN" + } + this.options.xmlParser = xmlParserConfig + this.options.godoParams = godoParams + logger.formatters.json.omit = ['pid', 'port', 'hostname', 'app']; + logger.setLevel(this.options.level) + logger.debug(this.options, 'Environment setting options') + } + + validate() { + for (let key in this.options) { + if (this.options[key]) { + delete this.requires[this.requires.indexOf(key)] + } + } + this.requires = this.requires.filter(function (el) { + return el != null; + }) + if (this.requires.length) { + logger.debug('Process terminated invalid required arguments: ', JSON.stringify(this.requires)) + process.exit(1) + } + } + +} + +let config = new Config; +module.exports = config.options \ No newline at end of file diff --git a/config/params.json b/config/params.json new file mode 100644 index 0000000..aa29b7d --- /dev/null +++ b/config/params.json @@ -0,0 +1,4 @@ +{ + "partner_key": "JUFDJUIzJTAyJTVEJTk5JTE5JUYwRw==", + "key": "JTVFJTE0JUZCJUVGJUQ2SSUxNiU4RCVCMyVGOSU3RSVGRCVGNSVGNyUxNSU3RCVBM0YlRDAlMkNwJUNDJUQ2JUNDVSU5MCUzRWklN0YlOEIlQUQlRTMlQURqYSVCQiUwOSUwMCVDMCUwOCVFRiVGQyVDQk0=" +} \ No newline at end of file diff --git a/config/xmlParserConfig.js b/config/xmlParserConfig.js new file mode 100644 index 0000000..b13bca6 --- /dev/null +++ b/config/xmlParserConfig.js @@ -0,0 +1,20 @@ +const he = require('he') + +module.exports = { + "attributeNamePrefix": "@_", + "attrNodeName": "attr", //default is 'false' + "textNodeName": "#text", + "ignoreAttributes": true, + "ignoreNameSpace": false, + "allowBooleanAttributes": false, + "parseNodeValue": true, + "parseAttributeValue": false, + "trimValues": true, + // "cdataTagName": "__cdata", //default is 'false' + "cdataPositionChar": "\\c", + "parseTrueNumberOnly": false, + "arrayMode": false, //"strict" + "attrValueProcessor": (val, attrName) => he.decode(val, { isAttributeValue: true }),//default is a=>a + "tagValueProcessor": (val, tagName) => he.decode(val), //default is a=>a + "stopNodes": ["parse-me-as-string"] +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..012893b --- /dev/null +++ b/index.js @@ -0,0 +1,60 @@ +const config = require('./config') + , express = require("express") + , expressLogging = require("express-logging") + , logger = require("logops") + , { StatusCodes, getReasonPhrase } = require('http-status-codes') + , hostname = require('os').hostname() + , port = 32105 + , godoUpdater = require('./src/component/GodoUpdater') + , controllers = {}; + + +process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; + +/** + * Express web server starting + * + */ +logger.getContext = function getContext() { + return { + // hostname: hostname, + // pid: process.pid, + // port: port, + app: 'Godo' + }; +} +var app = express(); +app.use(express.json()); +app.use(expressLogging(logger)); +app.use((req, res, next) => { + logger.debug(Object.assign(req.body, req.query, req.params), 'Request params') + next() +}) +app.listen(port, () => { + logger.info('Service started') + if(config.env == 'production') + godoUpdater.start() +}); + +/** + * Request handlers + * + */ +app.all('*', (req, res, next) => { + let urlSegments = req.url.split('/'); + req.params.service = urlSegments[1] + req.params.what = urlSegments.length > 2 ? urlSegments[2] : '' + req.params.args = urlSegments.length > 3 ? urlSegments.slice(3) : [] + + if(!controllers.hasOwnProperty(req.params.service)) { + let controller = req.params.service.charAt(0).toUpperCase() + req.params.service.slice(1) + 'Controller'; + controllers[req.params.service] = require('./src/controller/' + controller) + logger.info('%s has been loaded', controller) + } + controllers[req.params.service][req.params.service](req, result => { + let body = { code: StatusCodes.OK, message: getReasonPhrase(StatusCodes.OK), data: result } + if(result.code) body = result + res.json(body) + }) +}) + diff --git a/package.json b/package.json new file mode 100644 index 0000000..dc24bfa --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "spd-app-godo", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://spiduler:GD4eYvJMHUkXmajs@github.com/spiduler/spd-app-godo.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/spiduler/spd-app-godo/issues" + }, + "homepage": "https://github.com/spiduler/spd-app-godo#readme", + "dependencies": { + "command-line-args": "^5.1.1", + "express": "^4.17.1", + "express-logging": "^1.1.1", + "fast-xml-parser": "^3.17.4", + "form-data": "^3.0.0", + "he": "^1.2.0", + "http-status-codes": "^2.1.4", + "logops": "^2.1.1", + "mongodb": "^3.6.2", + "node-fetch": "^2.6.1" + } +} diff --git a/src/component/GodoClient.js b/src/component/GodoClient.js new file mode 100644 index 0000000..7732d29 --- /dev/null +++ b/src/component/GodoClient.js @@ -0,0 +1,53 @@ +const config = require('../../config') + , fetch = require('node-fetch') + , parser = require('fast-xml-parser') + , logger = require('logops') + , FormData = require('form-data') + , { getReasonPhrase, StatusCodes } = require('http-status-codes') + + +class GodoClient { + constructor() { + this.host = 'https://openhub.godo.co.kr/godomall5/' + } + + convertObj2FormData(params) { + let form = new FormData + Object.keys(params).forEach(key => { + form.append(key, params[key]) + }) + return form + } + + formatResult(data) { + return data + for (let k in data) { + if (data[k].__cdata) data[k] = data[k] + } + return data + } + + request(url, params) { + params = Object.assign(config.godoParams, params) + logger.debug(params, 'params') + return fetch(this.host + url, { + method: 'POST', + body: this.convertObj2FormData(params) + }).then(res => res.text()).then(body => { + let data = null + if (parser.validate(body) === true) { + data = parser.parse(body, config.xmlParser); + } else { + logger.debug({ body: body, url: url }, 'Invalid godo respose body') + } + return this.formatResult(data) + }).catch(err => { + logger.error(err.message, 'Unexpected error occured while requesting') + return { code: StatusCodes.PRECONDITION_FAILED, message: getReasonPhrase(StatusCodes.PRECONDITION_FAILED) } + }); + } + + +} + +module.exports = new GodoClient \ No newline at end of file diff --git a/src/component/GodoUpdater.js b/src/component/GodoUpdater.js new file mode 100644 index 0000000..efcbfc8 --- /dev/null +++ b/src/component/GodoUpdater.js @@ -0,0 +1,103 @@ +const { StatusCodes, getReasonPhrase } = require('http-status-codes') + , restClient = require('./RestClient') + , godoClient = require('../component/GodoClient') + , fetch = require("node-fetch") + , productService = require('../service/ProductService') + , logger = require('logops') + + +class GodoUpdater { + constructor() { + this.endDate = null + this.config = { + catalogSearch: { + skip: 0, + limit: 10 + } + } + } + + sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } + + start() { + this.startUpdate() + // setInterval(() => this.startUpdate(), 1000 * 3 * this.config.catalogSearch.limit) + } + + startUpdate() { + productService.searchCatalogId(this.config.catalogSearch).then(async res => { + logger.debug({resultCode: res.code}, 'Search Catalogs and Images %s', res.message) + this.config.catalogSearch.skip = this.config.catalogSearch.skip + this.config.catalogSearch.limit + if (res.code == StatusCodes.OK) { + this.update(res.data.rows).then(godoUpdateResult => { + logger.debug(godoUpdateResult, 'Godo product update result') + if (godoUpdateResult.length) { + this.updateCatalog(godoUpdateResult).then(updateCatalgResult => { + logger.debug({ + modifiedCount: updateCatalgResult.map(r => { + if (r.data.modifiedCount > 0) return true + }).filter(r => r).length + }, 'Update catalog adding godo goodsNo result count : %d', updateCatalgResult.length) + this.startUpdate() + }) + }else { + this.startUpdate() + } + }) + + } else if (res.code == StatusCodes.NOT_FOUND) { + this.resetCatalogSearchQuery() + await this.sleep(1000 * 2) + this.startUpdate() + } else if (res.code == StatusCodes.NO_CONTENT) { + await this.sleep(1000 * 2) + this.startUpdate() + } + }) + } + + async update(catalogsId) { + let results = [] + let promises = [] + for (let ck in catalogsId) { + await this.sleep(1000 * 1) + let c = catalogsId[ck] + promises.push(productService.post({ id: c.id }, result => { + if (result.header.code === 0) { + results.push(Object.assign(c, { godoId: result.return.goods_data.data.goodsno })) + } else { + logger.debug({ code: result.header.code, reason: result.header.msg }, 'Godo update failed') + } + })) + } + await Promise.all(promises) + return results + } + + async updateCatalog(godoUpdateResult) { + let updateCatalogResult = [] + let promises = [] + for (let k in godoUpdateResult) { + await this.sleep(1000 * 1) + let result = godoUpdateResult[k] + let params = { clause: { site: result.site, ean: result.ean }, data: { godoId: result.godoId, legacyId: result.id } } + promises.push(productService.updateCatalog(params).then(res => { + if (res.code == StatusCodes.OK) { + updateCatalogResult.push(res) + } + })) + } + await Promise.all(promises) + return updateCatalogResult + } + + resetCatalogSearchQuery() { + this.config.catalogSearch.skip = 0 + } +} + +module.exports = new GodoUpdater \ No newline at end of file diff --git a/src/component/RestClient.js b/src/component/RestClient.js new file mode 100644 index 0000000..6360ce3 --- /dev/null +++ b/src/component/RestClient.js @@ -0,0 +1,53 @@ +const fetch = require("node-fetch") + , logger = require("logops") + , TokenExpiredException = require('../exception/TokenExpiredException') + , config = require('../../config'); + + +class RestClient { + + constructor() { + this.token = null + this.gateway = 'http://' + config.gateway + '/api/v1/' + this.clientId = '7tmK4rF8As8CtRw5' + this.clientSecret = 'cgzLcVFku5zEvXkCKNZsAbxXQENFqPDx2kfjnMGd3m9BczehmFt2pw9r9MdASSFt' + } + + auth() { + let url = this.gateway + 'oauth/token?client_id=' + this.clientId + '&grant_type=client_credentials&client_secret=' + this.clientSecret + return fetch(url, { method: 'GET', headers: { 'content-type': 'application/json' } }).then(res => res.json()).then(res => { + logger.debug(res, 'Token from %s', this.gateway) + this.token = res.access_token + return res.access_token + }) + } + + headers(additionalheaders) { + let headers = { + "Authorization": "Bearer " + this.token, + "Content-Type": "application/json" + } + return additionalheaders ? Object.assign(headers, additionalheaders) : headers + } + + response(res) { + if (res.code == 406) { + throw new TokenExpiredException("Token expired " + this.gateway) + } + return res; + } + + async request(fetchable) { + if (!this.token) await this.auth() + return fetchable().catch(error => { + logger.error({ message: error.message }, 'Fetchable failed with error') + if (error.status == 406) { + logger.info('Requesting token %s', this.gateway) + return this.auth().then(() => fetchable()) + } + }) + } +} + + +module.exports = RestClient; \ No newline at end of file diff --git a/src/controller/ProductController.js b/src/controller/ProductController.js new file mode 100644 index 0000000..32e8232 --- /dev/null +++ b/src/controller/ProductController.js @@ -0,0 +1,32 @@ +const logger = require("logops") + , productService = require('../service/ProductService'); + +class ProductController { + getMethodName(req) { + return req.method.toLowerCase() + + req.params.what.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('') + } + + product(req, callback) { + let fn = this.getMethodName(req); + let params = req.body + logger.info('Invoking method ProductService.%s', fn) + if (typeof this[fn] === 'function') { + this[fn](params, callback) + } else callback({ code: 500, message: 'Method not found' }) + } + + get(params, callback) { + productService.get(params, callback); + } + + post(params, callback) { + productService.post(params, callback); + } + + postSummary(params, callback) { + productService.searchCatalogId(params).then(result => callback(result)); + } +} + +module.exports = new ProductController; \ No newline at end of file diff --git a/src/exception/TokenExpiredException.js b/src/exception/TokenExpiredException.js new file mode 100644 index 0000000..ace02ac --- /dev/null +++ b/src/exception/TokenExpiredException.js @@ -0,0 +1,16 @@ +class TokenExpiredException extends Error { + constructor (message) { + super(message) + + // assign the error class name in your custom error (as a shortcut) + this.name = this.constructor.name + + // capturing the stack trace keeps the reference to your error class + Error.captureStackTrace(this, this.constructor); + + // you may also assign additional properties to your error + this.status = 406 + } + } + + module.exports = TokenExpiredException \ No newline at end of file diff --git a/src/service/CatalogService.js b/src/service/CatalogService.js new file mode 100644 index 0000000..8c9ecaf --- /dev/null +++ b/src/service/CatalogService.js @@ -0,0 +1,31 @@ +const fetch = require("node-fetch") + , logger = require("logops") + , RestClient = require('../component/RestClient') + + + +class ProductService { + + constructor() { + this.restClient = new RestClient + } + + post(params, additionalheaders) { + return this.restClient.request(() => fetch(this.restClient.gateway + 'catalog', { + method: 'POST', + headers: this.restClient.headers(additionalheaders), + body: JSON.stringify(params) + }).then(res => res.json()).then(res => this.restClient.response(res))).then(res => res.data) + } + + get(params, additionalheaders) { + return this.restClient.request(() => fetch(this.restClient.gateway + 'catalog/search', { + method: 'POST', + headers: this.restClient.headers(additionalheaders), + body: JSON.stringify(params) + }).then(res => res.json()).then(res => this.restClient.response(res))).then(res => res.data) + } +} + + +module.exports = new ProductService; \ No newline at end of file diff --git a/src/service/ProductService.js b/src/service/ProductService.js new file mode 100644 index 0000000..35a1d2e --- /dev/null +++ b/src/service/ProductService.js @@ -0,0 +1,118 @@ +const fetch = require("node-fetch") + , logger = require("logops") + , godoClient = require('../component/GodoClient') + , RestClient = require("../component/RestClient") + , { StatusCodes, getReasonPhrase } = require('http-status-codes') + + +class ProductService extends RestClient { + + constructor() { + super() + } + + get(params, callback) { + return godoClient.request('goods/Goods_Search.php', {}).then(data => { + callback(data) + }) + } + + post(params, callback) { + return godoClient.request('goods/Goods_Insert.php', { data_url: 'http://www.ibspot.eu/app/open/godo/product/' + params.id + '.xml' }).then(data => { + callback(data.data) + }) + } + + updateCatalog(data) { + return this.request(() => fetch(this.gateway + 'ext/catalog', { + method: 'PUT', + headers: this.headers(), + body: JSON.stringify(data) + }).then(res => res.json()).then(res => this.response(res))).catch(err => { + logger.error({ reason: err.message }, 'Catalog search failed') + return { code: 500, message: err.message } + }) + } + + searchCatalog(data) { + return this.request(() => fetch(this.gateway + 'ext/catalog/search', { + method: 'POST', + headers: this.headers(), + body: JSON.stringify(data) + }).then(res => res.json()).then(res => this.response(res))).catch(err => { + logger.error({ reason: err.message }, 'Catalog search failed') + return { code: 500, message: err.message } + }) + } + + findCatalog(data) { + return this.request(() => fetch(this.gateway + 'ext/catalog/find', { + method: 'POST', + headers: this.headers(), + body: JSON.stringify(data) + }).then(res => res.json()).then(res => this.response(res))).catch(err => { + logger.error({ reason: err.message }, 'Catalog find failed') + return { code: 500, message: err.message } + }) + } + + searchImage(data) { + return this.request(() => fetch(this.gateway + 'ext/catalog/image_search', { + method: 'POST', + headers: this.headers(), + body: JSON.stringify(data) + }).then(res => res.json()).then(res => this.response(res))).catch(err => { + logger.error({ reason: err.message }, 'Catalog image search failed') + return { code: 500, message: err.message } + }) + } + + searchCatalogId(params) { + return this.searchImage(params).then(imageRes => { + if (imageRes.code == StatusCodes.OK && imageRes.data.length) { + let images = imageRes.data + // let barcodes = [] + // images.map(image => { + // barcodes = barcodes.concat(image.ean) + // }) + // multi ean product has array of ean in image collection so, should treat as one product + let barcodes = images.map(image => { + return image.ean[0] + }) + return this.searchCatalog({ ean: { $in: barcodes }, godoId: { $exists: false } }).then(catalogRes => { + if (catalogRes.code == StatusCodes.OK && catalogRes.data.length) { + let summary = catalogRes.data.map(c => { + for (let ik in images) { + if (images[ik].ean.includes(c.ean) && c.site == images[ik].site) { + c.image = images[ik] + return c + } + } + }) + return this.findCatalog({ barcode: barcodes }).then(res => { + if (res.code == StatusCodes.OK && res.data.length) { + let catalogsId = res.data.map(c => { + for (let s in summary) { + if (c.Barcode == summary[s].ean && c.SpiderSite == summary[s].site) { + return { id: c.Id, site: c.SpiderSite, ean: c.Barcode } + } + } + }).filter(id => id) + return { code: StatusCodes.OK, message: getReasonPhrase(StatusCodes.OK), data: { rows: catalogsId, total: catalogsId.length } } + } + }) + } else { + return { code: StatusCodes.NO_CONTENT, message: getReasonPhrase(StatusCodes.NO_CONTENT) + ' Catalog' } + } + }) + } else { + return { code: StatusCodes.NOT_FOUND, message: getReasonPhrase(StatusCodes.NOT_FOUND) + ' Image' } + } + }) + } +} + + + + +module.exports = new ProductService; \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..3f28d68 --- /dev/null +++ b/test.js @@ -0,0 +1,27 @@ +// new Promise(resolve => { +// resolve('abc') +// }).then(abc => { +// throw new Error("aaaa") +// }).then(a => { +// console.log('adsf') +// }).catch(err => { +// console.log(err.message) +// }).then(d => console.log(d)) + + + +goodsNm: a +goodsNo: a +goodsCd: a +makerNm: a +originNm: a +goodsSearchWord: a +goodsModelNo: a +companyNm: a +searchDateType: a +startDate: a +endDate: a +page: a +size: a +scmNo: a +cateCd: a