cyymall-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,327 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const crypto = require("crypto");
5
+ const http = require("../http");
6
+ const config = require("../config");
7
+ const biz = require("../biz");
8
+
9
+ function envelope(success, message, data, exitCode) {
10
+ const traceId = `local-${crypto.randomBytes(8).toString("hex")}`;
11
+ console.log(
12
+ JSON.stringify(
13
+ {
14
+ success,
15
+ code: success ? "OK" : "UPSTREAM_ERROR",
16
+ message,
17
+ data,
18
+ traceId,
19
+ },
20
+ null,
21
+ 2,
22
+ ),
23
+ );
24
+ process.exit(exitCode ?? (success ? 0 : 2));
25
+ }
26
+
27
+ /**
28
+ * @param {{ bodyFile?: string, bodyJson?: string }} opts
29
+ */
30
+ async function preSettle(opts) {
31
+ const cfg = config.loadConfig();
32
+ if (!cfg?.token) {
33
+ console.error("cyy: not logged in.");
34
+ process.exit(1);
35
+ }
36
+ let raw = opts.bodyJson;
37
+ if (opts.bodyFile) raw = fs.readFileSync(opts.bodyFile, "utf8");
38
+ if (!raw) {
39
+ console.error("cyy: provide --body-file or --body-json");
40
+ process.exit(1);
41
+ }
42
+ const url = http.moduleUrl("ORDER", "/app/order/preSettleOrder");
43
+ const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
44
+ const { ok, json } = await http.request(url, {
45
+ method: "POST",
46
+ headers,
47
+ body: raw,
48
+ });
49
+ const success = ok && biz.isBizSuccess(json);
50
+ envelope(success, success ? "success" : "pre-settle failed", { upstream: json }, success ? 0 : 2);
51
+ }
52
+
53
+ /**
54
+ * @param {{ bodyFile?: string, bodyJson?: string }} opts
55
+ */
56
+ async function confirm(opts) {
57
+ const cfg = config.loadConfig();
58
+ if (!cfg?.token) {
59
+ console.error("cyy: not logged in.");
60
+ process.exit(1);
61
+ }
62
+ let raw = opts.bodyJson;
63
+ if (opts.bodyFile) raw = fs.readFileSync(opts.bodyFile, "utf8");
64
+ if (!raw) {
65
+ console.error("cyy: provide --body-file or --body-json");
66
+ process.exit(1);
67
+ }
68
+ const url = http.moduleUrl("ORDER", "/app/order/confirmOrder");
69
+ const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
70
+ const { ok, json } = await http.request(url, {
71
+ method: "POST",
72
+ headers,
73
+ body: raw,
74
+ });
75
+ const success = ok && biz.isBizSuccess(json);
76
+ envelope(success, success ? "success" : "confirm failed", { upstream: json }, success ? 0 : 2);
77
+ }
78
+
79
+ async function payUrl(opts) {
80
+ const cfg = config.loadConfig();
81
+ if (!cfg?.token) {
82
+ console.error("cyy: not logged in.");
83
+ process.exit(1);
84
+ }
85
+ const orderId = opts.orderId;
86
+ if (!orderId) {
87
+ console.error("cyy: --order-id required");
88
+ process.exit(1);
89
+ }
90
+
91
+ const url = http.moduleUrl("DEFAULT", "/member/getInfo/V2");
92
+ const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
93
+ delete headers["Content-Type"];
94
+
95
+ const { ok, json } = await http.request(url, { method: "GET", headers, body: null });
96
+
97
+ let phone = String(cfg.phone || "");
98
+ let nick = "";
99
+ let img = "";
100
+ if (ok && biz.isBizSuccess(json) && json && typeof json === "object") {
101
+ const d = /** @type {{data?:Record<string,unknown>}} */ (json).data;
102
+ if (d) {
103
+ phone = String(d.memberPhone || phone);
104
+ nick = String(d.memberNick || "");
105
+ img = String(d.headerImg || "");
106
+ }
107
+ }
108
+
109
+ const siteId = cfg.site_id;
110
+ const shopId = cfg.shop_id;
111
+ const enc = encodeURIComponent;
112
+ const payUrlStr = `https://dhcmall.ifoodbuy.com/H5/#/replacePay?phone=${enc(phone)}&orderid=${enc(String(orderId))}&img=${enc(img)}&nick=${enc(nick)}&siteId=${siteId}&shopId=${shopId}&share=true`;
113
+
114
+ envelope(true, "success", { payUrl: payUrlStr, orderId }, 0);
115
+ }
116
+
117
+ /**
118
+ * @param {object} opts
119
+ */
120
+ async function quick(opts) {
121
+ const cfg = config.loadConfig();
122
+ if (!cfg?.token) {
123
+ console.error("cyy: not logged in.");
124
+ process.exit(1);
125
+ }
126
+
127
+ const keyword = opts.keyword;
128
+ const quantity = Number(opts.quantity || 1);
129
+ const unit = opts.unit || "袋";
130
+ const shopId = Number(opts.shopId ?? cfg.shop_id);
131
+ const siteId = Number(opts.siteId ?? cfg.site_id);
132
+
133
+ const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
134
+
135
+ // 1) Search
136
+ const searchUrl = http.moduleUrl("PRODUCT", "/app/product/getSkuList");
137
+ const searchBody = JSON.stringify({
138
+ pageNum: 1,
139
+ pageSize: 10,
140
+ shopId,
141
+ siteId,
142
+ spuName: keyword,
143
+ stockFlag: "0",
144
+ });
145
+ let r = await http.request(searchUrl, {
146
+ method: "POST",
147
+ headers,
148
+ body: searchBody,
149
+ });
150
+ if (!r.ok || !biz.isBizSuccess(r.json)) {
151
+ envelope(false, "search failed", { upstream: r.json }, 2);
152
+ return;
153
+ }
154
+ const list =
155
+ r.json &&
156
+ typeof r.json === "object" &&
157
+ r.json.data &&
158
+ typeof r.json.data === "object"
159
+ ? /** @type {{list?:unknown[]}} */ (r.json.data).list
160
+ : [];
161
+ const products = Array.isArray(list) ? list : [];
162
+ if (!products.length) {
163
+ envelope(false, "no products found", {}, 2);
164
+ return;
165
+ }
166
+
167
+ const product = /** @type {Record<string,unknown>} */ (products[0]);
168
+ const skuId = product.id;
169
+ const skuCode = product.skuCode;
170
+ const skuName = product.spuName;
171
+ const stockQty = product.stockAllQuantity ?? 0;
172
+ const promotionList = Array.isArray(product.promotionList) ? product.promotionList : [];
173
+
174
+ let price = 0;
175
+ let unitRate = 1;
176
+ for (const p of promotionList) {
177
+ const row = /** @type {Record<string,unknown>} */ (p);
178
+ if (row.skuSaleUnitName === unit) {
179
+ price = Number(row.salePrice || 0);
180
+ unitRate = Number(row.unitRate ?? 1);
181
+ break;
182
+ }
183
+ }
184
+ if (!price) {
185
+ envelope(
186
+ false,
187
+ `unit "${unit}" not found in promotionList`,
188
+ { availableUnits: promotionList.map((x) => /** @type {{skuSaleUnitName?:string}} */ (x).skuSaleUnitName) },
189
+ 2,
190
+ );
191
+ return;
192
+ }
193
+
194
+ // 2) Cart
195
+ const cartBody = JSON.stringify({
196
+ cartItemList: [
197
+ {
198
+ skuCode,
199
+ skuName,
200
+ skuQty: quantity,
201
+ skuSaleUnit: unit,
202
+ listPrice: price,
203
+ salePrice: price,
204
+ unitRate,
205
+ selectFlag: true,
206
+ },
207
+ ],
208
+ cartModel: "append",
209
+ shopId,
210
+ siteId,
211
+ });
212
+ const cartUrl = http.moduleUrl("ORDER", "/app/order/cart");
213
+ r = await http.request(cartUrl, { method: "POST", headers, body: cartBody });
214
+ if (!r.ok || !biz.isBizSuccess(r.json)) {
215
+ envelope(false, "cart failed", { upstream: r.json }, 2);
216
+ return;
217
+ }
218
+ const cartData =
219
+ r.json && typeof r.json === "object" && "data" in r.json
220
+ ? /** @type {{data?:{cartItemList?:{addTime?:string}[]}}} */ (r.json).data
221
+ : undefined;
222
+ const addTime = cartData?.cartItemList?.[0]?.addTime;
223
+ if (!addTime) {
224
+ envelope(false, "missing addTime from cart response", { upstream: r.json }, 2);
225
+ return;
226
+ }
227
+
228
+ // 3) Pre-settle
229
+ const preBody = JSON.stringify({
230
+ headerVO: { shopId, siteId },
231
+ itemVOList: [
232
+ {
233
+ skuId,
234
+ skuCode,
235
+ skuName,
236
+ skuQty: quantity,
237
+ skuSaleUnit: unit,
238
+ salePrice: price,
239
+ listPrice: price,
240
+ unitRate,
241
+ addTime,
242
+ stockQty,
243
+ minSaleUnit: unit,
244
+ selectFlag: true,
245
+ status: 0,
246
+ canBuyFlag: true,
247
+ },
248
+ ],
249
+ balanceFlag: false,
250
+ userPointFlag: false,
251
+ });
252
+ const preUrl = http.moduleUrl("ORDER", "/app/order/preSettleOrder");
253
+ r = await http.request(preUrl, { method: "POST", headers, body: preBody });
254
+ if (!r.ok || !biz.isBizSuccess(r.json)) {
255
+ envelope(false, "pre-settle failed", { upstream: r.json }, 2);
256
+ return;
257
+ }
258
+ const pdata =
259
+ r.json && typeof r.json === "object" && r.json.data && typeof r.json.data === "object"
260
+ ? r.json.data
261
+ : null;
262
+ const preSettleId = pdata && /** @type {{preSettleId?:string}} */ (pdata).preSettleId;
263
+ const headerVO = pdata && /** @type {{headerVO?:unknown}} */ (pdata).headerVO;
264
+ const itemVOList = pdata && /** @type {{itemVOList?:unknown}} */ (pdata).itemVOList;
265
+ if (!preSettleId) {
266
+ envelope(false, "missing preSettleId", { upstream: r.json }, 2);
267
+ return;
268
+ }
269
+
270
+ // 4) Confirm
271
+ const confirmBody = JSON.stringify({
272
+ preSettleId,
273
+ headerVO,
274
+ itemVOList,
275
+ balanceFlag: false,
276
+ userPointFlag: false,
277
+ validateStockFlag: true,
278
+ repeatFlag: false,
279
+ });
280
+ const confirmUrl = http.moduleUrl("ORDER", "/app/order/confirmOrder");
281
+ r = await http.request(confirmUrl, { method: "POST", headers, body: confirmBody });
282
+ if (!r.ok || !biz.isBizSuccess(r.json)) {
283
+ envelope(false, "confirm failed", { upstream: r.json }, 2);
284
+ return;
285
+ }
286
+ const orderData =
287
+ r.json && typeof r.json === "object" && r.json.data && typeof r.json.data === "object"
288
+ ? r.json.data
289
+ : {};
290
+ const orderId = /** @type {{orderId?:string,payAmt?:unknown}} */ (orderData).orderId;
291
+ const payAmt = /** @type {{orderId?:string,payAmt?:unknown}} */ (orderData).payAmt;
292
+
293
+ // 5) Pay URL
294
+ const memberUrl = http.moduleUrl("DEFAULT", "/member/getInfo/V2");
295
+ const h2 = /** @type {Record<string,string>} */ ({ ...headers });
296
+ delete h2["Content-Type"];
297
+ const uinfo = await http.request(memberUrl, { method: "GET", headers: h2, body: null });
298
+ let phone = String(cfg.phone || "");
299
+ let nick = "";
300
+ let img = "";
301
+ if (uinfo.ok && biz.isBizSuccess(uinfo.json) && uinfo.json && typeof uinfo.json === "object") {
302
+ const d = /** @type {{data?:Record<string,unknown>}} */ (uinfo.json).data;
303
+ if (d) {
304
+ phone = String(d.memberPhone || phone);
305
+ nick = String(d.memberNick || "");
306
+ img = String(d.headerImg || "");
307
+ }
308
+ }
309
+ const enc = encodeURIComponent;
310
+ const payUrlStr = `https://dhcmall.ifoodbuy.com/H5/#/replacePay?phone=${enc(phone)}&orderid=${enc(String(orderId))}&img=${enc(img)}&nick=${enc(nick)}&siteId=${siteId}&shopId=${shopId}&share=true`;
311
+
312
+ envelope(
313
+ true,
314
+ "order placed",
315
+ {
316
+ product: skuName,
317
+ quantity,
318
+ unit,
319
+ orderId,
320
+ payAmt,
321
+ payUrl: payUrlStr,
322
+ },
323
+ 0,
324
+ );
325
+ }
326
+
327
+ module.exports = { preSettle, confirm, payUrl, quick };
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const http = require("../http");
5
+ const config = require("../config");
6
+ const biz = require("../biz");
7
+
8
+ /**
9
+ * @param {object} opts
10
+ */
11
+ async function search(opts) {
12
+ const cfg = config.loadConfig();
13
+ if (!cfg?.token) {
14
+ console.error("cyy: not logged in. Run: cyy auth login");
15
+ process.exit(1);
16
+ }
17
+
18
+ const shopId = Number(opts.shopId ?? cfg.shop_id);
19
+ const siteId = Number(opts.siteId ?? cfg.site_id);
20
+ const body = JSON.stringify({
21
+ pageNum: Number(opts.page || 1),
22
+ pageSize: Number(opts.pageSize || 10),
23
+ shopId,
24
+ siteId,
25
+ spuName: opts.keyword,
26
+ stockFlag: String(opts.stockFlag ?? "0"),
27
+ });
28
+
29
+ const url = http.moduleUrl("PRODUCT", "/app/product/getSkuList");
30
+ const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
31
+
32
+ const { ok, json } = await http.request(url, {
33
+ method: "POST",
34
+ headers,
35
+ body,
36
+ });
37
+
38
+ const traceId = `local-${crypto.randomBytes(8).toString("hex")}`;
39
+ const success = ok && biz.isBizSuccess(json);
40
+ console.log(
41
+ JSON.stringify(
42
+ {
43
+ success,
44
+ code: success ? "OK" : "UPSTREAM_ERROR",
45
+ message: success ? "success" : "search failed",
46
+ data: { upstream: json },
47
+ traceId,
48
+ },
49
+ null,
50
+ 2,
51
+ ),
52
+ );
53
+ process.exit(success ? 0 : 2);
54
+ }
55
+
56
+ module.exports = { search };
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+
3
+ const http = require("http");
4
+ const { executeApiCall } = require("./apiCall");
5
+
6
+ /**
7
+ * @param {{ port: number, host: string }} opts
8
+ */
9
+ function runServe(opts) {
10
+ const port = Number(opts.port ?? 8787);
11
+ const host = String(opts.host || "127.0.0.1");
12
+
13
+ const server = http.createServer(async (req, res) => {
14
+ if (req.method !== "POST" || req.url !== "/invoke") {
15
+ res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
16
+ res.end(JSON.stringify({ error: "not_found", hint: "POST /invoke only" }));
17
+ return;
18
+ }
19
+
20
+ let raw = "";
21
+ for await (const chunk of req) {
22
+ raw += chunk;
23
+ }
24
+ let payload = {};
25
+ try {
26
+ payload = raw ? JSON.parse(raw) : {};
27
+ } catch {
28
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
29
+ res.end(JSON.stringify({ error: "invalid_json" }));
30
+ return;
31
+ }
32
+
33
+ const {
34
+ method = "GET",
35
+ module: moduleKey = "DEFAULT",
36
+ path: pathSuffix,
37
+ body: bodyObj,
38
+ query: queryObj,
39
+ noAuth = false,
40
+ } = payload;
41
+
42
+ if (!pathSuffix || typeof pathSuffix !== "string") {
43
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
44
+ res.end(JSON.stringify({ error: "path_required" }));
45
+ return;
46
+ }
47
+
48
+ try {
49
+ const result = await executeApiCall({
50
+ method: String(method).toUpperCase(),
51
+ module: moduleKey,
52
+ path: pathSuffix,
53
+ bodyObj,
54
+ queryObj: queryObj && typeof queryObj === "object" ? queryObj : undefined,
55
+ query: undefined,
56
+ noAuth: Boolean(noAuth),
57
+ header: [],
58
+ });
59
+
60
+ if (result.error) {
61
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
62
+ res.end(JSON.stringify({ error: result.error }));
63
+ return;
64
+ }
65
+
66
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
67
+ res.end(JSON.stringify(result.envelope));
68
+ } catch (e) {
69
+ res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
70
+ res.end(
71
+ JSON.stringify({
72
+ error: "internal_error",
73
+ message: e instanceof Error ? e.message : String(e),
74
+ }),
75
+ );
76
+ }
77
+ });
78
+
79
+ server.listen(port, host, () => {
80
+ console.error(`cyy serve listening on http://${host}:${port} (POST /invoke)`);
81
+ });
82
+ }
83
+
84
+ module.exports = { runServe };