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,287 @@
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
+ function envelope(success, message, upstream, exitCode) {
9
+ const traceId = `local-${crypto.randomBytes(8).toString("hex")}`;
10
+ console.log(
11
+ JSON.stringify(
12
+ {
13
+ success,
14
+ code: success ? "OK" : "UPSTREAM_ERROR",
15
+ message,
16
+ data: { upstream },
17
+ traceId,
18
+ },
19
+ null,
20
+ 2,
21
+ ),
22
+ );
23
+ process.exit(exitCode ?? (success ? 0 : 2));
24
+ }
25
+
26
+ /**
27
+ * POST /mall-multishop/shop/member/list — 当前会员门店分页(Body: GetShopReq)
28
+ * @param {{ page?: string, pageSize?: string, name?: string, objectCode?: string }} opts
29
+ */
30
+ async function list(opts) {
31
+ const cfg = config.loadConfig();
32
+ if (!cfg?.token) {
33
+ console.error("cyy: not logged in. Run: cyy auth login");
34
+ process.exit(1);
35
+ }
36
+
37
+ const body = JSON.stringify({
38
+ pageNum: Number(opts.page || 1),
39
+ pageSize: Number(opts.pageSize || 20),
40
+ shopName: opts.name ?? "",
41
+ objectCode: Number(opts.objectCode ?? 3),
42
+ deliveryAddressProvince: "",
43
+ deliveryAddressCity: "",
44
+ deliveryAddressArea: "",
45
+ });
46
+
47
+ const url = http.moduleUrl("DEFAULT", "/shop/member/list");
48
+ const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
49
+
50
+ const { ok, json } = await http.request(url, {
51
+ method: "POST",
52
+ headers,
53
+ body,
54
+ });
55
+
56
+ const success = ok && biz.isBizSuccess(json);
57
+ envelope(success, success ? "success" : "shop list failed", json, success ? 0 : 2);
58
+ }
59
+
60
+ /**
61
+ * GET /mall-multishop/shop/member/store/list — 指定门店下的站点列表
62
+ * @param {{ shopId?: string, siteName?: string }} opts
63
+ */
64
+ async function sites(opts) {
65
+ const cfg = config.loadConfig();
66
+ if (!cfg?.token) {
67
+ console.error("cyy: not logged in. Run: cyy auth login");
68
+ process.exit(1);
69
+ }
70
+
71
+ const shopId = opts.shopId ?? cfg.shop_id;
72
+ if (shopId === undefined || shopId === null || String(shopId).trim() === "") {
73
+ console.error("cyy: provide --shop-id or set shop_id in ~/.cyymall/config.json");
74
+ process.exit(1);
75
+ }
76
+
77
+ const base = http.moduleUrl("DEFAULT", "/shop/member/store/list");
78
+ const u = new URL(base);
79
+ u.searchParams.set("shopId", String(shopId));
80
+ if (opts.siteName != null && String(opts.siteName).length > 0) {
81
+ u.searchParams.set("siteName", String(opts.siteName));
82
+ }
83
+
84
+ const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
85
+ delete headers["Content-Type"];
86
+
87
+ const { ok, json } = await http.request(u.toString(), {
88
+ method: "GET",
89
+ headers,
90
+ body: null,
91
+ });
92
+
93
+ const success = ok && biz.isBizSuccess(json);
94
+ envelope(success, success ? "success" : "site list failed", json, success ? 0 : 2);
95
+ }
96
+
97
+ /**
98
+ * @param {unknown} json
99
+ * @returns {Array<Record<string, unknown>>}
100
+ */
101
+ function parseSitesArray(json) {
102
+ if (!json || typeof json !== "object") return [];
103
+ const d = /** @type {{ data?: unknown }} */ (json).data;
104
+ return Array.isArray(d) ? /** @type {Array<Record<string, unknown>>} */ (d) : [];
105
+ }
106
+
107
+ /**
108
+ * @param {Record<string, unknown>} cfg
109
+ * @param {string|number} shopId
110
+ */
111
+ async function fetchSitesJson(cfg, shopId) {
112
+ const base = http.moduleUrl("DEFAULT", "/shop/member/store/list");
113
+ const u = new URL(base);
114
+ u.searchParams.set("shopId", String(shopId));
115
+ const headers = /** @type {Record<string, string>} */ (http.buildAuthHeaders(cfg));
116
+ delete headers["Content-Type"];
117
+ return http.request(u.toString(), { method: "GET", headers, body: null });
118
+ }
119
+
120
+ /**
121
+ * @param {Array<Record<string, unknown>>} sitesList
122
+ * @param {string|number} siteId
123
+ */
124
+ function siteIdInList(sitesList, siteId) {
125
+ const want = String(siteId);
126
+ return sitesList.some((row) => String(row.id) === want);
127
+ }
128
+
129
+ /**
130
+ * @param {Record<string, unknown>} cfg
131
+ * @param {string|number} shopId
132
+ * @returns {Promise<boolean>}
133
+ */
134
+ async function shopIdBelongsToMember(cfg, shopId) {
135
+ const target = Number(shopId);
136
+ if (Number.isNaN(target)) return false;
137
+
138
+ const url = http.moduleUrl("DEFAULT", "/shop/member/list");
139
+ const headers = /** @type {Record<string, string>} */ (http.buildAuthHeaders(cfg));
140
+
141
+ let page = 1;
142
+ const pageSize = 50;
143
+ for (;;) {
144
+ const body = JSON.stringify({
145
+ pageNum: page,
146
+ pageSize,
147
+ shopName: "",
148
+ objectCode: 3,
149
+ deliveryAddressProvince: "",
150
+ deliveryAddressCity: "",
151
+ deliveryAddressArea: "",
152
+ });
153
+
154
+ const { ok, json } = await http.request(url, {
155
+ method: "POST",
156
+ headers,
157
+ body,
158
+ });
159
+
160
+ if (!ok || !biz.isBizSuccess(json)) return false;
161
+ const data =
162
+ json && typeof json === "object" && "data" in json
163
+ ? /** @type {{ data?: { list?: unknown[]; hasNextPage?: boolean } }} */ (json).data
164
+ : null;
165
+ const list = data && Array.isArray(data.list) ? data.list : [];
166
+ for (const row of list) {
167
+ const r = /** @type {{ id?: unknown }} */ (row);
168
+ if (Number(r.id) === target) return true;
169
+ }
170
+ if (!data?.hasNextPage) return false;
171
+ page += 1;
172
+ if (page > 200) return false;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Set default shop_id only in ~/.cyymall/config.json (does not change site_id).
178
+ * Validates id appears in POST /shop/member/list for current member.
179
+ * @param {{ shopId?: string }} opts
180
+ */
181
+ async function useShop(opts) {
182
+ const cfg = config.loadConfig();
183
+ if (!cfg?.token) {
184
+ console.error("cyy: not logged in. Run: cyy auth login");
185
+ process.exit(1);
186
+ }
187
+ const shopId = opts.shopId;
188
+ if (shopId === undefined || shopId === null || String(shopId).trim() === "") {
189
+ console.error("cyy: shop use requires --shop-id");
190
+ process.exit(1);
191
+ }
192
+
193
+ const belongs = await shopIdBelongsToMember(cfg, shopId);
194
+ if (!belongs) {
195
+ console.error("cyy: shop id not in your shop list (see: cyy shop list)");
196
+ process.exit(2);
197
+ }
198
+
199
+ const prev = config.loadConfig() || {};
200
+ const saved = {
201
+ ...prev,
202
+ shop_id: Number(shopId),
203
+ };
204
+ config.saveConfig(saved);
205
+
206
+ const traceId = `local-${crypto.randomBytes(8).toString("hex")}`;
207
+ console.log(
208
+ JSON.stringify(
209
+ {
210
+ success: true,
211
+ code: "OK",
212
+ message:
213
+ "default shop_id updated; site_id unchanged — use shop sites + shop use-site if you need a different site",
214
+ data: {
215
+ shop_id: saved.shop_id,
216
+ site_id: saved.site_id,
217
+ },
218
+ traceId,
219
+ },
220
+ null,
221
+ 2,
222
+ ),
223
+ );
224
+ process.exit(0);
225
+ }
226
+
227
+ /**
228
+ * Switch default site only for current session shop_id.
229
+ * @param {{ siteId?: string }} opts
230
+ */
231
+ async function useSite(opts) {
232
+ const cfg = config.loadConfig();
233
+ if (!cfg?.token) {
234
+ console.error("cyy: not logged in. Run: cyy auth login");
235
+ process.exit(1);
236
+ }
237
+ const shopId = cfg.shop_id;
238
+ if (shopId === undefined || shopId === null || String(shopId).trim() === "") {
239
+ console.error("cyy: no shop_id in session; run: cyy shop use --shop-id <id>");
240
+ process.exit(1);
241
+ }
242
+ const siteId = opts.siteId;
243
+ if (siteId === undefined || siteId === null || String(siteId).trim() === "") {
244
+ console.error("cyy: shop use-site requires --site-id");
245
+ process.exit(1);
246
+ }
247
+
248
+ const { ok, json } = await fetchSitesJson(cfg, shopId);
249
+ if (!ok || !biz.isBizSuccess(json)) {
250
+ console.error("cyy: failed to load sites for current shop");
251
+ process.exit(2);
252
+ }
253
+
254
+ const sitesList = parseSitesArray(json);
255
+ if (!siteIdInList(sitesList, siteId)) {
256
+ console.error("cyy: --site-id is not valid for current shop; run: cyy shop sites");
257
+ process.exit(2);
258
+ }
259
+
260
+ const prev = config.loadConfig() || {};
261
+ const saved = {
262
+ ...prev,
263
+ site_id: String(siteId),
264
+ };
265
+ config.saveConfig(saved);
266
+
267
+ const traceId = `local-${crypto.randomBytes(8).toString("hex")}`;
268
+ console.log(
269
+ JSON.stringify(
270
+ {
271
+ success: true,
272
+ code: "OK",
273
+ message: "default site updated",
274
+ data: {
275
+ shop_id: saved.shop_id,
276
+ site_id: saved.site_id,
277
+ },
278
+ traceId,
279
+ },
280
+ null,
281
+ 2,
282
+ ),
283
+ );
284
+ process.exit(0);
285
+ }
286
+
287
+ module.exports = { list, sites, useShop, useSite };
package/src/config.js ADDED
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const DEFAULT_BASE_URL = "https://dhcmall.ifoodbuy.com";
8
+
9
+ /** BuildConfig-style module prefixes (app-api-cli-spec §1.2) */
10
+ const MODULE_MAP = {
11
+ DEFAULT: "/mall-multishop",
12
+ BIZ: "/mall-biz",
13
+ OSS: "/mall-oss",
14
+ ORDER: "/mall-order",
15
+ PLATFORM: "/mall-platform",
16
+ PRODUCT: "/mall-product",
17
+ PAYMENT: "/mall-payment",
18
+ };
19
+
20
+ function getBaseUrl() {
21
+ const u = process.env.CYY_BASE_URL || DEFAULT_BASE_URL;
22
+ return u.endsWith("/") ? u.slice(0, -1) : u;
23
+ }
24
+
25
+ function getConfigDir() {
26
+ return path.join(os.homedir(), ".cyymall");
27
+ }
28
+
29
+ function getConfigPath() {
30
+ return path.join(getConfigDir(), "config.json");
31
+ }
32
+
33
+ function ensureConfigDir() {
34
+ const dir = getConfigDir();
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ }
39
+
40
+ /**
41
+ * @returns {Record<string, unknown>|null}
42
+ */
43
+ function loadConfig() {
44
+ const p = getConfigPath();
45
+ if (!fs.existsSync(p)) return null;
46
+ try {
47
+ const raw = fs.readFileSync(p, "utf8");
48
+ return JSON.parse(raw);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param {Record<string, unknown>} data
56
+ */
57
+ function saveConfig(data) {
58
+ ensureConfigDir();
59
+ fs.writeFileSync(getConfigPath(), JSON.stringify(data, null, 2), "utf8");
60
+ }
61
+
62
+ /**
63
+ * Mask token for display
64
+ * @param {string} t
65
+ */
66
+ function maskToken(t) {
67
+ if (!t || typeof t !== "string") return "";
68
+ if (t.length <= 12) return "***";
69
+ return `${t.slice(0, 8)}...${t.slice(-4)}`;
70
+ }
71
+
72
+ module.exports = {
73
+ DEFAULT_BASE_URL,
74
+ MODULE_MAP,
75
+ getBaseUrl,
76
+ getConfigDir,
77
+ getConfigPath,
78
+ ensureConfigDir,
79
+ loadConfig,
80
+ saveConfig,
81
+ maskToken,
82
+ };
package/src/http.js ADDED
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+
3
+ const config = require("./config");
4
+
5
+ const DEFAULT_VERSION =
6
+ process.env.CYY_BOOTSTRAP_VERSION_CODE || "22118";
7
+
8
+ /** Defaults for /ua bootstrap before login (override via env; no secrets in repo) */
9
+ function bootstrapIds() {
10
+ return {
11
+ site_id: process.env.CYY_BOOTSTRAP_SITE_ID || "1",
12
+ shop_id: process.env.CYY_BOOTSTRAP_SHOP_ID || "",
13
+ member_id: process.env.CYY_BOOTSTRAP_MEMBER_ID || "",
14
+ token: process.env.CYY_BOOTSTRAP_TOKEN || "",
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Build auth headers from saved config (matches legacy App scripts)
20
+ * @param {Record<string, unknown>|null} cfg
21
+ */
22
+ function buildAuthHeaders(cfg) {
23
+ const ts = Date.now().toString();
24
+ const boot = bootstrapIds();
25
+ const siteId = cfg?.site_id ?? boot.site_id;
26
+ const shopId = cfg?.shop_id ?? boot.shop_id;
27
+ const memberId = cfg?.member_id ?? boot.member_id;
28
+ const versionCode = cfg?.version_code ?? DEFAULT_VERSION;
29
+ const base = {
30
+ loginType: "az.app.cyy",
31
+ "version-code": String(versionCode),
32
+ siteId: String(siteId ?? "1"),
33
+ shopId: String(shopId ?? ""),
34
+ memberId: String(memberId ?? ""),
35
+ timestamp: ts,
36
+ "Content-Type": "application/json;charset=UTF-8",
37
+ };
38
+ const token = cfg?.token ?? boot.token;
39
+ if (token) {
40
+ base.appToken = token;
41
+ base.Authorization = token;
42
+ }
43
+ return base;
44
+ }
45
+
46
+ /** Headers for unauthenticated calls (e.g. login) — still need default shop/site for gateway */
47
+ function buildDefaultHeaders() {
48
+ const cfg = config.loadConfig();
49
+ return buildAuthHeaders(cfg);
50
+ }
51
+
52
+ /**
53
+ * @param {string} url
54
+ * @param {object} options
55
+ * @param {string} options.method
56
+ * @param {Record<string,string>} [options.headers]
57
+ * @param {string|null} [options.body]
58
+ */
59
+ async function request(url, { method, headers, body }) {
60
+ const res = await fetch(url, {
61
+ method,
62
+ headers,
63
+ body: body ?? undefined,
64
+ redirect: "follow",
65
+ });
66
+ const text = await res.text();
67
+ let json = null;
68
+ try {
69
+ json = text ? JSON.parse(text) : null;
70
+ } catch {
71
+ json = { _raw: text };
72
+ }
73
+ return { ok: res.ok, status: res.status, json };
74
+ }
75
+
76
+ function moduleUrl(moduleKey, pathSuffix) {
77
+ const base = config.getBaseUrl();
78
+ const mod = config.MODULE_MAP[moduleKey];
79
+ if (!mod) {
80
+ throw new Error(`Unknown module: ${moduleKey}`);
81
+ }
82
+ if (!pathSuffix.startsWith("/")) {
83
+ throw new Error("--path must start with /");
84
+ }
85
+ return `${base}${mod}${pathSuffix}`;
86
+ }
87
+
88
+ module.exports = {
89
+ buildAuthHeaders,
90
+ buildDefaultHeaders,
91
+ request,
92
+ moduleUrl,
93
+ DEFAULT_VERSION,
94
+ };