feat: 技术支持工单/企微权限/开发版本管理/消息推送等迭代
This commit is contained in:
@@ -1,89 +0,0 @@
|
||||
/**
|
||||
* 为 OSS Bucket 配置浏览器直传所需的 CORS 规则。
|
||||
* 使用 server/dukang-api/.env 中的 OSS 凭证。
|
||||
*
|
||||
* 用法:pnpm oss:cors
|
||||
* 可选环境变量 OSS_CORS_ORIGINS(逗号分隔),默认包含本地 H5 / admin 端口。
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const apiRoot = resolve(__dirname, '../server/dukang-api');
|
||||
const require = createRequire(resolve(apiRoot, 'package.json'));
|
||||
const OSS = require('ali-oss');
|
||||
const envPath = resolve(apiRoot, '.env');
|
||||
|
||||
function loadEnvFile(path: string) {
|
||||
if (!existsSync(path)) return;
|
||||
for (const line of readFileSync(path, 'utf8').split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq <= 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (process.env[key] === undefined) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFile(envPath);
|
||||
|
||||
const {
|
||||
OSS_ACCESS_KEY_ID: accessKeyId,
|
||||
OSS_ACCESS_KEY_SECRET: accessKeySecret,
|
||||
OSS_BUCKET: bucket,
|
||||
OSS_REGION: region = 'oss-cn-hangzhou',
|
||||
OSS_CORS_ORIGINS,
|
||||
} = process.env;
|
||||
|
||||
if (!accessKeyId || !accessKeySecret || !bucket) {
|
||||
console.error('请在 server/dukang-api/.env 配置 OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_BUCKET');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const defaultOrigins = [
|
||||
'http://localhost:5173',
|
||||
'http://localhost:5174',
|
||||
'http://localhost:5175',
|
||||
'http://127.0.0.1:5173',
|
||||
'http://127.0.0.1:5174',
|
||||
'http://127.0.0.1:5175',
|
||||
'http://localhost:5173/user',
|
||||
'http://localhost:5174/shop',
|
||||
'http://localhost:5175/partner',
|
||||
'https://user.runxian.top',
|
||||
];
|
||||
|
||||
const allowedOrigin = (OSS_CORS_ORIGINS ?? defaultOrigins.join(','))
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const client = new OSS({
|
||||
region,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
bucket,
|
||||
});
|
||||
|
||||
const rules = [
|
||||
{
|
||||
allowedOrigin,
|
||||
allowedMethod: ['GET', 'POST', 'PUT', 'HEAD'],
|
||||
allowedHeader: ['*'],
|
||||
exposeHeader: ['ETag', 'x-oss-request-id'],
|
||||
maxAgeSeconds: 600,
|
||||
},
|
||||
];
|
||||
|
||||
console.log(`配置 Bucket「${bucket}」CORS,允许来源:`);
|
||||
for (const origin of allowedOrigin) console.log(` - ${origin}`);
|
||||
|
||||
await client.putBucketCORS(bucket, rules);
|
||||
console.log('CORS 规则已写入。若 H5 端仍直传 OSS,请确认来源域名已包含在列表中。');
|
||||
@@ -1,167 +0,0 @@
|
||||
/**
|
||||
* mini-user H5 本地预览:
|
||||
* Taro Vite `--watch` 在本机易 OOM(峰值 >8GB),改为:
|
||||
* 1) 先完整 build(约 30s)
|
||||
* 2) 静态托管 dist + /api 代理
|
||||
* 3) 监听 src 变更后防抖重建
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const APP_ROOT = path.resolve(__dirname, '../apps/mini-user');
|
||||
const DIST = path.resolve(APP_ROOT, 'dist');
|
||||
const PORT = Number(process.env.PORT || 5177);
|
||||
const API_ORIGIN = (process.env.VITE_API_TARGET ?? 'http://localhost:3000').replace(/\/$/, '');
|
||||
const TARO_BIN = path.resolve(APP_ROOT, 'node_modules/@tarojs/cli/bin/taro');
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'application/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.map': 'application/json',
|
||||
};
|
||||
|
||||
function runBuild() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
['--max-old-space-size=8192', TARO_BIN, 'build', '--type', 'h5'],
|
||||
{
|
||||
cwd: APP_ROOT,
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
},
|
||||
);
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`taro build failed with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendFile(res, filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
}
|
||||
|
||||
function proxyApi(req, res) {
|
||||
const target = new URL(req.url || '/', API_ORIGIN);
|
||||
const lib = target.protocol === 'https:' ? https : http;
|
||||
const headers = { ...req.headers, host: target.host };
|
||||
delete headers['accept-encoding'];
|
||||
const upstream = lib.request(
|
||||
{
|
||||
protocol: target.protocol,
|
||||
hostname: target.hostname,
|
||||
port: target.port || undefined,
|
||||
path: target.pathname + target.search,
|
||||
method: req.method,
|
||||
headers,
|
||||
},
|
||||
(up) => {
|
||||
res.writeHead(up.statusCode || 502, up.headers);
|
||||
up.pipe(res);
|
||||
},
|
||||
);
|
||||
upstream.on('error', (err) => {
|
||||
res.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end(`API proxy error: ${err.message}`);
|
||||
});
|
||||
req.pipe(upstream);
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
const server = http.createServer((req, res) => {
|
||||
const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
|
||||
if (urlPath.startsWith('/api')) {
|
||||
proxyApi(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
path.join(DIST, urlPath),
|
||||
path.join(DIST, urlPath, 'index.html'),
|
||||
path.join(DIST, 'index.html'),
|
||||
];
|
||||
const file = candidates.find((p) => fs.existsSync(p) && fs.statSync(p).isFile());
|
||||
if (!file) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Not Found');
|
||||
return;
|
||||
}
|
||||
sendFile(res, file);
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`\n mini-user H5 preview: http://localhost:${PORT}`);
|
||||
console.log(` API proxy -> ${API_ORIGIN}`);
|
||||
console.log(' Watching apps/mini-user/src for changes...\n');
|
||||
});
|
||||
}
|
||||
|
||||
function watchSrc() {
|
||||
const srcDir = path.join(APP_ROOT, 'src');
|
||||
let timer = null;
|
||||
let building = false;
|
||||
let queued = false;
|
||||
|
||||
const schedule = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(async () => {
|
||||
if (building) {
|
||||
queued = true;
|
||||
return;
|
||||
}
|
||||
building = true;
|
||||
try {
|
||||
console.log('\n[dev] src changed, rebuilding...');
|
||||
await runBuild();
|
||||
console.log('[dev] rebuild done — refresh browser');
|
||||
} catch (e) {
|
||||
console.error('[dev] rebuild failed:', e instanceof Error ? e.message : e);
|
||||
} finally {
|
||||
building = false;
|
||||
if (queued) {
|
||||
queued = false;
|
||||
schedule();
|
||||
}
|
||||
}
|
||||
}, 800);
|
||||
};
|
||||
|
||||
fs.watch(srcDir, { recursive: true }, (_event, filename) => {
|
||||
if (!filename) return;
|
||||
if (/\.(tsx?|jsx?|css|scss|json|png|jpg|svg)$/i.test(filename)) {
|
||||
schedule();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(TARO_BIN)) {
|
||||
throw new Error(`Taro CLI not found: ${TARO_BIN}`);
|
||||
}
|
||||
console.log('[dev] initial H5 build...');
|
||||
await runBuild();
|
||||
startServer();
|
||||
watchSrc();
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 总部 H5 静态预览:托管 dist 并将 /api 代理到后端(与 admin-web vite proxy 行为一致)
|
||||
*/
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { platform } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DIST = path.resolve(__dirname, '../apps/mini-hq/dist');
|
||||
const PORT = Number(process.env.HQ_PREVIEW_PORT || 5176);
|
||||
const API_TARGET = (process.env.VITE_API_TARGET || 'http://localhost:3000').replace(/\/$/, '');
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'application/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
};
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/** 释放预览端口(pnpm preview:hq 重复执行时自动重启) */
|
||||
function freePort(port) {
|
||||
try {
|
||||
if (platform() === 'win32') {
|
||||
const out = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8' });
|
||||
const pids = new Set();
|
||||
for (const line of out.split('\n')) {
|
||||
if (!line.includes('LISTENING')) continue;
|
||||
const pid = line.trim().split(/\s+/).pop();
|
||||
if (pid && /^\d+$/.test(pid)) pids.add(pid);
|
||||
}
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null || true`, {
|
||||
shell: true,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
} catch {
|
||||
/* 端口可能本就空闲 */
|
||||
}
|
||||
}
|
||||
|
||||
function sendFile(res, filePath) {
|
||||
const ext = path.extname(filePath);
|
||||
const type = MIME[ext] || 'application/octet-stream';
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': type });
|
||||
res.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
function proxyApi(req, res) {
|
||||
const target = new URL(req.url, API_TARGET);
|
||||
const headers = { ...req.headers, host: target.host };
|
||||
const proxyReq = http.request(
|
||||
{
|
||||
hostname: target.hostname,
|
||||
port: target.port || (target.protocol === 'https:' ? 443 : 80),
|
||||
path: target.pathname + target.search,
|
||||
method: req.method,
|
||||
headers,
|
||||
},
|
||||
(proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
},
|
||||
);
|
||||
proxyReq.on('error', () => {
|
||||
res.writeHead(502, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ code: 502, message: 'API 不可达,请先启动 pnpm dev:api' }));
|
||||
});
|
||||
req.pipe(proxyReq);
|
||||
}
|
||||
|
||||
function createServer() {
|
||||
return http.createServer((req, res) => {
|
||||
const urlPath = req.url?.split('?')[0] || '/';
|
||||
|
||||
if (urlPath.startsWith('/api')) {
|
||||
proxyApi(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
let filePath = path.join(DIST, urlPath === '/' ? 'index.html' : urlPath);
|
||||
if (!filePath.startsWith(DIST)) {
|
||||
res.writeHead(403);
|
||||
res.end('Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
fs.stat(filePath, (err, stat) => {
|
||||
if (!err && stat.isFile()) {
|
||||
sendFile(res, filePath);
|
||||
return;
|
||||
}
|
||||
sendFile(res, path.join(DIST, 'index.html'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function listen(server, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(port, () => {
|
||||
server.off('error', reject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(DIST)) {
|
||||
console.error('未找到 apps/mini-hq/dist,请先执行:pnpm --filter @dukang/mini-hq build');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
freePort(PORT);
|
||||
await sleep(400);
|
||||
|
||||
const server = createServer();
|
||||
try {
|
||||
await listen(server, PORT);
|
||||
} catch (err) {
|
||||
if (err && err.code === 'EADDRINUSE') {
|
||||
console.error(`端口 ${PORT} 仍被占用,请手动结束进程后重试,或设置 HQ_PREVIEW_PORT 换端口。`);
|
||||
process.exit(1);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log(`mini-hq preview: http://localhost:${PORT}`);
|
||||
console.log(`API proxy: ${API_TARGET}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import { loginWithSms, resolveSmsCode } from './sms-test-helper.mjs';
|
||||
|
||||
const API = 'http://localhost:3000/api/v1';
|
||||
|
||||
async function req(clientApp, path, options = {}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': clientApp,
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
};
|
||||
const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body });
|
||||
const json = await res.json();
|
||||
if (json.code !== 0) throw new Error(`${path}: ${json.message}`);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
async function sendSms(clientApp, phone, scene, sendPath = '/auth/sms/send') {
|
||||
await req(clientApp, sendPath, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone, scene }),
|
||||
});
|
||||
}
|
||||
|
||||
async function loginSms(clientApp, phone, scene, loginPath = '/auth/login/sms', sendPath = '/auth/sms/send') {
|
||||
return loginWithSms(clientApp, phone, scene, loginPath, sendPath);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('1. Health');
|
||||
await req('USER_H5', '/health');
|
||||
|
||||
console.log('2. User login');
|
||||
const userLogin = await loginSms('USER_H5', '13800000001', 'USER_LOGIN');
|
||||
const userToken = userLogin.accessToken;
|
||||
|
||||
console.log('3. Catalog');
|
||||
const products = await req('USER_H5', '/catalog/products');
|
||||
if (products.length < 4) throw new Error('Expected 4 products');
|
||||
|
||||
console.log('4. Create address + order + mock pay');
|
||||
const addr = await req('USER_H5', '/user/addresses', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({
|
||||
receiverName: '测试',
|
||||
phone: '13800000001',
|
||||
province: '河南省',
|
||||
city: '郑州市',
|
||||
district: '金水区',
|
||||
detail: '冒烟测试地址1号',
|
||||
isDefault: true,
|
||||
}),
|
||||
});
|
||||
const order = await req('USER_H5', '/trade/orders', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }),
|
||||
});
|
||||
await req('USER_H5', `/trade/orders/${order.id}/pay`, { method: 'POST', token: userToken });
|
||||
|
||||
console.log('5. Shop login + redeem preview');
|
||||
const shopLogin = await loginSms(
|
||||
'SHOP_H5',
|
||||
'13910000001',
|
||||
'STORE_LOGIN',
|
||||
'/shop/auth/login/sms',
|
||||
'/shop/auth/sms/send',
|
||||
);
|
||||
const coupons = await req('USER_H5', '/benefit/coupons', { token: userToken });
|
||||
if (!coupons.length) throw new Error('No coupons');
|
||||
const tokenRes = await req('USER_H5', '/redeem/tokens', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({ amount: 50 }),
|
||||
});
|
||||
await req('SHOP_H5', '/shop/redeem/preview', {
|
||||
method: 'POST',
|
||||
token: shopLogin.accessToken,
|
||||
body: JSON.stringify({ token: tokenRes.token }),
|
||||
});
|
||||
|
||||
console.log('6. Partner login');
|
||||
await loginSms(
|
||||
'PARTNER_H5',
|
||||
'13700000001',
|
||||
'PARTNER_LOGIN',
|
||||
'/partner/auth/login/sms',
|
||||
'/partner/auth/sms/send',
|
||||
);
|
||||
|
||||
console.log('preV1 smoke OK');
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,285 +0,0 @@
|
||||
import { loginWithSms } from './sms-test-helper.mjs';
|
||||
|
||||
const API = process.env.SMOKE_API ?? 'http://localhost:3000/api/v1';
|
||||
|
||||
async function req(clientApp, path, options = {}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': clientApp,
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
};
|
||||
const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body });
|
||||
const json = await res.json();
|
||||
if (json.code !== 0) throw new Error(`${path}: ${json.message}`);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
async function expectFail(clientApp, path, options = {}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': clientApp,
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
};
|
||||
const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body });
|
||||
const json = await res.json();
|
||||
if (json.code === 0) throw new Error(`${path}: expected failure`);
|
||||
return json.message;
|
||||
}
|
||||
|
||||
|
||||
async function sendSms(clientApp, phone, scene, sendPath = '/auth/sms/send') {
|
||||
await req(clientApp, sendPath, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone, scene }),
|
||||
});
|
||||
}
|
||||
|
||||
async function loginSms(clientApp, phone, scene, loginPath = '/auth/login/sms', sendPath = '/auth/sms/send') {
|
||||
return loginWithSms(clientApp, phone, scene, loginPath, sendPath);
|
||||
}
|
||||
|
||||
async function adminLogin() {
|
||||
return loginSms(
|
||||
'HQ_WEB',
|
||||
'13600000001',
|
||||
'HQ_LOGIN',
|
||||
'/admin/auth/login/sms',
|
||||
'/admin/auth/sms/send',
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('1. Health');
|
||||
await req('USER_H5', '/health');
|
||||
|
||||
console.log('2. User login');
|
||||
const userLogin = await loginSms('USER_H5', '13800000001', 'USER_LOGIN');
|
||||
const userToken = userLogin.accessToken;
|
||||
|
||||
console.log('3. Cities + products');
|
||||
const cities = await req('USER_H5', '/catalog/cities');
|
||||
const cityCode = cities[0]?.code ?? 'ZZ';
|
||||
const products = await req('USER_H5', `/catalog/products?cityCode=${cityCode}`);
|
||||
if (products.length < 4) throw new Error(`Expected >=4 products, got ${products.length}`);
|
||||
|
||||
console.log('4. Min purchase boundaries');
|
||||
const addr = await req('USER_H5', '/user/addresses', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({
|
||||
receiverName: '测试',
|
||||
phone: '13800000001',
|
||||
province: '河南省',
|
||||
city: '郑州市',
|
||||
district: '金水区',
|
||||
detail: 'V3冒烟地址',
|
||||
isDefault: true,
|
||||
}),
|
||||
});
|
||||
await expectFail('USER_H5', '/trade/orders/preview', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({ productId: products[0].id, quantity: 1, addressId: addr.id }),
|
||||
});
|
||||
|
||||
const preview2 = await req('USER_H5', '/trade/orders/preview', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }),
|
||||
});
|
||||
if (preview2.payAmount <= 0) throw new Error('Preview qty=2 failed');
|
||||
|
||||
console.log('5. Create order + pay + benefit');
|
||||
const order = await req('USER_H5', '/trade/orders', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }),
|
||||
});
|
||||
await req('USER_H5', `/trade/orders/${order.id}/pay`, { method: 'POST', token: userToken });
|
||||
const paidOrder = await req('USER_H5', `/trade/orders/${order.id}`, { token: userToken });
|
||||
if (paidOrder.status !== 'PENDING_SHIP') throw new Error('Order not PENDING_SHIP after pay');
|
||||
const coupons = await req('USER_H5', '/benefit/coupons', { token: userToken });
|
||||
if (!coupons.length) throw new Error('No coupon after pay');
|
||||
const summary = await req('USER_H5', '/benefit/summary', { token: userToken });
|
||||
|
||||
console.log('6. Direct redeem (no couponId)');
|
||||
const directAmount = Math.min(100, Number(summary.totalBalance ?? summary.balance ?? 100));
|
||||
const directToken = await req('USER_H5', '/redeem/tokens', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({ amount: directAmount }),
|
||||
});
|
||||
if (!directToken.token) throw new Error('Direct redeem token missing');
|
||||
|
||||
console.log('7. Coupon redeem cap');
|
||||
await expectFail('USER_H5', '/redeem/tokens', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({ couponId: coupons[0].id, amount: Number(coupons[0].balance) + 1 }),
|
||||
});
|
||||
|
||||
console.log('7b. Shop sms guard + session');
|
||||
const unboundMsg = await expectFail('SHOP_H5', '/shop/auth/sms/send', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone: '13899999999', scene: 'STORE_LOGIN' }),
|
||||
});
|
||||
if (!unboundMsg.includes('未绑定门店')) {
|
||||
throw new Error(`Expected unbound store message, got: ${unboundMsg}`);
|
||||
}
|
||||
|
||||
console.log('8. Shop confirm redeem');
|
||||
const shopLogin = await loginSms(
|
||||
'SHOP_H5',
|
||||
'13910000001',
|
||||
'STORE_LOGIN',
|
||||
'/shop/auth/login/sms',
|
||||
'/shop/auth/sms/send',
|
||||
);
|
||||
let shopToken = shopLogin.accessToken;
|
||||
let shopMe = await req('SHOP_H5', '/shop/auth/me', { token: shopToken });
|
||||
if (!shopMe?.phone) throw new Error('Shop /shop/auth/me failed');
|
||||
const refreshed = await req('SHOP_H5', '/shop/auth/token/refresh', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ refreshToken: shopLogin.refreshToken }),
|
||||
});
|
||||
if (!refreshed?.accessToken) throw new Error('Shop token refresh failed');
|
||||
shopToken = refreshed.accessToken;
|
||||
shopMe = await req('SHOP_H5', '/shop/auth/me', { token: shopToken });
|
||||
if (!shopMe.storeId && shopMe.stores?.[0]?.storeId) {
|
||||
const selected = await req('SHOP_H5', '/shop/auth/select-store', {
|
||||
method: 'POST',
|
||||
token: shopToken,
|
||||
body: JSON.stringify({ storeId: shopMe.stores[0].storeId }),
|
||||
});
|
||||
shopToken = selected.accessToken || shopToken;
|
||||
shopMe = await req('SHOP_H5', '/shop/auth/me', { token: shopToken });
|
||||
}
|
||||
if (!shopMe.storeId) throw new Error('Shop storeId missing after select-store');
|
||||
const preview = await req('SHOP_H5', '/shop/redeem/preview', {
|
||||
method: 'POST',
|
||||
token: shopToken,
|
||||
body: JSON.stringify({ token: directToken.token }),
|
||||
});
|
||||
if (!preview.amount) throw new Error('Redeem preview failed');
|
||||
await req('SHOP_H5', '/shop/redeem/confirm', {
|
||||
method: 'POST',
|
||||
token: shopToken,
|
||||
body: JSON.stringify({ token: directToken.token }),
|
||||
});
|
||||
|
||||
console.log('9. Store withdraw (OPT-010)');
|
||||
const admin = await adminLogin();
|
||||
const shopStoreId = shopMe.storeId || shopMe.stores?.[0]?.storeId;
|
||||
if (!shopStoreId) throw new Error('Shop storeId missing after login');
|
||||
|
||||
const withdrawSummary = await req('SHOP_H5', '/shop/withdraw/summary', {
|
||||
token: shopToken,
|
||||
});
|
||||
if (!(withdrawSummary.availableAmount > 0)) {
|
||||
throw new Error('Expected available withdraw amount after redeem');
|
||||
}
|
||||
|
||||
const applied = await req('SHOP_H5', '/shop/withdraw', {
|
||||
method: 'POST',
|
||||
token: shopToken,
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (applied.status !== 'PENDING_REVIEW') {
|
||||
throw new Error(`Expected PENDING_REVIEW withdraw, got ${applied.status}`);
|
||||
}
|
||||
|
||||
const pendingDup = await expectFail('SHOP_H5', '/shop/withdraw', {
|
||||
method: 'POST',
|
||||
token: shopToken,
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (!String(pendingDup).includes('待审核')) {
|
||||
throw new Error(`Expected pending-request reject, got: ${pendingDup}`);
|
||||
}
|
||||
|
||||
// 锁定中的明细不可再出账(T+1 排除 withdrawItem)
|
||||
const summaryAfter = await req('SHOP_H5', '/shop/withdraw/summary', {
|
||||
token: shopToken,
|
||||
});
|
||||
if (summaryAfter.availableAmount !== 0) {
|
||||
throw new Error('Expected availableAmount=0 while withdraw pending');
|
||||
}
|
||||
|
||||
await req('HQ_WEB', `/admin/store-withdrawals/${applied.id}/approve`, {
|
||||
method: 'POST',
|
||||
token: admin.accessToken,
|
||||
body: JSON.stringify({ paymentRef: 'smoke-withdraw' }),
|
||||
});
|
||||
|
||||
const withdrawList = await req('SHOP_H5', '/shop/withdraw/requests?status=PAID', {
|
||||
token: shopToken,
|
||||
});
|
||||
const settled = withdrawList.items?.find((w) => w.id === applied.id);
|
||||
if (!settled || settled.status !== 'PAID') {
|
||||
throw new Error('Expected settled withdraw request on shop side');
|
||||
}
|
||||
|
||||
const overdue = await req('HQ_WEB', '/admin/store-withdrawals/overdue-summary', {
|
||||
token: admin.accessToken,
|
||||
});
|
||||
if (typeof overdue.pendingCount !== 'number') {
|
||||
throw new Error('overdue-summary missing pendingCount');
|
||||
}
|
||||
|
||||
console.log('10. Refund ticket flow');
|
||||
const order2 = await req('USER_H5', '/trade/orders', {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }),
|
||||
});
|
||||
await req('USER_H5', `/trade/orders/${order2.id}/pay`, { method: 'POST', token: userToken });
|
||||
await req('USER_H5', `/trade/orders/${order2.id}/refund-requests`, {
|
||||
method: 'POST',
|
||||
token: userToken,
|
||||
body: JSON.stringify({ remark: 'smoke refund' }),
|
||||
});
|
||||
const tickets = await req('HQ_WEB', '/admin/tickets?ticketType=REFUND', {
|
||||
token: admin.accessToken,
|
||||
});
|
||||
const refundTicket = tickets.items?.find((t) => t.refId === order2.id);
|
||||
if (!refundTicket) throw new Error('Refund ticket not created');
|
||||
await req('HQ_WEB', `/admin/tickets/${refundTicket.id}/approve`, {
|
||||
method: 'POST',
|
||||
token: admin.accessToken,
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const refundedOrder = await req('USER_H5', `/trade/orders/${order2.id}`, { token: userToken });
|
||||
if (refundedOrder.status !== 'REFUNDED' || refundedOrder.payStatus !== 'REFUNDED') {
|
||||
throw new Error(
|
||||
`Refund order status expected REFUNDED/REFUNDED, got ${refundedOrder.status}/${refundedOrder.payStatus}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('11. Partner phone gate');
|
||||
const unknownPartner = await expectFail('PARTNER_H5', '/partner/auth/phone/check', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone: '13899999999' }),
|
||||
});
|
||||
if (!unknownPartner.includes('未找到合伙人账号')) {
|
||||
throw new Error(`Expected partner phone gate, got: ${unknownPartner}`);
|
||||
}
|
||||
|
||||
console.log('12. Partner bill');
|
||||
const partners = await req('HQ_WEB', '/admin/partners', { token: admin.accessToken });
|
||||
const partnerId = partners.items?.[0]?.id;
|
||||
if (partnerId) {
|
||||
const now = new Date();
|
||||
await req('HQ_WEB', '/admin/partner-bills/generate', {
|
||||
method: 'POST',
|
||||
token: admin.accessToken,
|
||||
body: JSON.stringify({ partnerId, year: now.getFullYear(), month: now.getMonth() + 1 }),
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n✅ V3 smoke passed');
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('❌ V3 smoke failed:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const API = process.env.SMOKE_API ?? 'http://localhost:3000/api/v1';
|
||||
const REDIS_CONTAINER = process.env.REDIS_CONTAINER ?? 'dukang-v1-redis';
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function fetchJson(path, options = {}) {
|
||||
const res = await fetch(`${API}${path}`, options);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function readSmsCodeFromRedis(phone, scene) {
|
||||
const key = `dukang:sms:${scene}:${phone}`;
|
||||
try {
|
||||
const code = execSync(`docker exec ${REDIS_CONTAINER} redis-cli GET "${key}"`, {
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
if (code && code !== '(nil)') return code;
|
||||
} catch {
|
||||
/* docker/redis unavailable */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadClientConfig() {
|
||||
const json = await fetchJson('/common/client-config');
|
||||
if (json.code !== 0) throw new Error(`client-config: ${json.message}`);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export async function resolveSmsCode(phone, scene) {
|
||||
const fromRedis = readSmsCodeFromRedis(phone, scene);
|
||||
if (fromRedis) return fromRedis;
|
||||
|
||||
throw new Error(
|
||||
`SMS code not found for ${phone} (${scene}); send SMS first and ensure Redis is reachable`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function sendSmsOnce(clientApp, sendPath, phone, scene) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': clientApp,
|
||||
};
|
||||
const res = await fetch(`${API}${sendPath}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ phone, scene }),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function sendSmsWithCooldown(clientApp, sendPath, phone, scene) {
|
||||
let json = await sendSmsOnce(clientApp, sendPath, phone, scene);
|
||||
if (json.code !== 0 && String(json.message).includes('过于频繁')) {
|
||||
await sleep(65_000);
|
||||
json = await sendSmsOnce(clientApp, sendPath, phone, scene);
|
||||
}
|
||||
if (json.code !== 0) {
|
||||
const existing = readSmsCodeFromRedis(phone, scene);
|
||||
if (existing) return { reusedCode: true };
|
||||
throw new Error(`${sendPath}: ${json.message}`);
|
||||
}
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export async function loginWithSms(clientApp, phone, scene, loginPath, sendPath) {
|
||||
await sendSmsWithCooldown(clientApp, sendPath, phone, scene);
|
||||
const code = await resolveSmsCode(phone, scene);
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': clientApp,
|
||||
};
|
||||
const res = await fetch(`${API}${loginPath}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ phone, code }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.code !== 0) throw new Error(`${loginPath}: ${json.message}`);
|
||||
return json.data;
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/**
|
||||
* Sync Stitch shared project "4端1.0版本" prototypes to pages/
|
||||
* Usage: STITCH_API_KEY=xxx node scripts/sync-stitch.mjs [--force]
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const PAGES = path.join(ROOT, 'pages');
|
||||
const PROJECT_ID = process.env.STITCH_PROJECT_ID || '9513024892894556686';
|
||||
const FORCE = process.argv.includes('--force');
|
||||
|
||||
const API_KEY =
|
||||
process.env.STITCH_API_KEY ||
|
||||
(() => {
|
||||
try {
|
||||
const mcp = JSON.parse(fs.readFileSync(path.join(process.env.USERPROFILE || '', '.cursor', 'mcp.json'), 'utf8'));
|
||||
return mcp?.mcpServers?.stitch?.headers?.['X-Goog-Api-Key'];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error('Missing STITCH_API_KEY');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function mcpCall(tool, args = {}) {
|
||||
const res = await fetch('https://stitch.googleapis.com/mcp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Goog-Api-Key': API_KEY },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method: 'tools/call',
|
||||
params: { name: tool, arguments: args },
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.error) throw new Error(json.error.message || JSON.stringify(json.error));
|
||||
if (json.result?.isError) throw new Error(json.result.content?.[0]?.text || 'MCP error');
|
||||
return json.result.structuredContent ?? JSON.parse(json.result.content?.[0]?.text || '{}');
|
||||
}
|
||||
|
||||
async function download(url, dest) {
|
||||
if (!url) return false;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return false;
|
||||
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
||||
return true;
|
||||
}
|
||||
|
||||
function classify(title) {
|
||||
const t = title || '';
|
||||
if (/\.md$|logo\.png|全案交付|Stitch提示词/i.test(t)) return 'asset';
|
||||
if (/总部|开城管理|开城页面|城市参数|商品管理页|拦截配送|数据报表|预警详情|补发处理页面|跨城发货处理|财务结算中心|财务对账|合同管理|子账号|我的员工|推广码|数据周报|总部管理|在线客服对话页.*总部|客服中心.*跨城|申请打款|资产明细|账单明细详情|超时订单|订单售后详情.*补发与退款|补发订单详情|添加商品|编辑城市页面|门店审核管理|门店审核记录|添加子账号|新增开城|生成推广码|推广码详情|推广码管理|合伙人贡献榜|合伙人待确认账单.*(?!合伙人中心)/i.test(t)) {
|
||||
if (/合伙人待确认账单|合伙人中心|合伙人订单|录入新门店|合伙人门店|合伙人工作台|城市合伙人登录|城市合伙人快捷|城市合伙人订单/.test(t)) {
|
||||
// keep partner-specific
|
||||
} else if (/总部|开城|拦截|数据报表|预警|补发处理页|跨城发货|财务结算|财务对账|合同|子账号|我的员工|推广码|数据周报|申请打款|资产明细|账单明细|超时订单|添加商品|编辑城市|门店审核管理|门店审核记录|添加子账号|新增开城|商品管理页/.test(t)) {
|
||||
return 'skip';
|
||||
}
|
||||
}
|
||||
if (/总部|开城管理|拦截配送管理|数据报表中心|预警详情|补发处理页面|跨城发货处理|财务结算中心|财务对账|合同管理|子账号|我的员工|推广码管理|数据周报|总部管理|申请打款|资产明细|账单明细详情|超时订单详情|添加商品页面|编辑城市页面|门店审核管理|门店审核记录|添加子账号|新增开城|生成推广码|推广码详情|合伙人贡献榜|商品管理页|订单中心管理页|订单售后详情|补发订单详情|在线客服对话页|总部客服|总部端订单|总部管理中心|总部管理登录|总部管理一键|拦截配送|城市参数配置/i.test(t)) {
|
||||
return 'skip';
|
||||
}
|
||||
if (/门店端|门店管理|S-0|门店核销|门店营业|门店登录|门店管理登录|门店管理首页|门店管理一键|门店核销成功|门店核销确认|门店核销记录/i.test(t) && !/合伙人门店|录入新门店/.test(t)) {
|
||||
return 'shop';
|
||||
}
|
||||
if (/合伙人|录入新门店|城市合伙人/i.test(t)) return 'partner';
|
||||
if (/C端|小程序|用户登录|商品详情|确认订单|地址|我的订单|订单详情|好客权益|个人中心|餐券核销|核销码|可用门店|门店详情|微信支付|定位授权|微信|核销成功及评价|核销成功.*评价/i.test(t)) {
|
||||
return 'user';
|
||||
}
|
||||
if (/门店详情页$/.test(t) && !/编辑/.test(t)) return 'user';
|
||||
if (/门店详情编辑/.test(t)) return 'partner';
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
function slug(title, screenId) {
|
||||
const base = (title || 'screen')
|
||||
.replace(/[^\w\u4e00-\u9fa5]+/g, '_')
|
||||
.replace(/^_|_$/g, '')
|
||||
.slice(0, 40);
|
||||
return `${base}_${screenId.slice(0, 8)}`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Fetching project...');
|
||||
const project = await mcpCall('get_project', { name: `projects/${PROJECT_ID}` });
|
||||
const designMd = project.designTheme?.designMd;
|
||||
if (designMd) {
|
||||
const designPath = path.join(PAGES, 'stitch_4_1.0', 'DESIGN.md');
|
||||
fs.mkdirSync(path.dirname(designPath), { recursive: true });
|
||||
fs.writeFileSync(designPath, designMd, 'utf8');
|
||||
console.log('Updated DESIGN.md');
|
||||
}
|
||||
|
||||
console.log('Listing screens...');
|
||||
const { screens = [] } = await mcpCall('list_screens', { projectId: PROJECT_ID });
|
||||
console.log(`Found ${screens.length} screens`);
|
||||
|
||||
const manifest = {
|
||||
projectId: PROJECT_ID,
|
||||
projectTitle: '4端1.0版本',
|
||||
syncedAt: new Date().toISOString(),
|
||||
screens: [],
|
||||
};
|
||||
|
||||
let idx = { user: 0, shop: 0, partner: 0, skip: 0, asset: 0 };
|
||||
|
||||
for (const screen of screens) {
|
||||
const screenId = screen.name?.replace(/.*\/screens\//, '') || screen.screenId;
|
||||
const title = screen.title || screen.displayName || '';
|
||||
const category = classify(title);
|
||||
idx[category] = (idx[category] || 0) + 1;
|
||||
|
||||
const entry = {
|
||||
screenId,
|
||||
title,
|
||||
category,
|
||||
route: null,
|
||||
priority: category === 'skip' || category === 'asset' ? 'skip' : 'P1',
|
||||
};
|
||||
|
||||
if (category === 'asset') {
|
||||
const assetDir = path.join(PAGES, '_assets');
|
||||
fs.mkdirSync(assetDir, { recursive: true });
|
||||
manifest.screens.push(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (category === 'skip') {
|
||||
const archiveDir = path.join(PAGES, '_archive', slug(title, screenId));
|
||||
fs.mkdirSync(archiveDir, { recursive: true });
|
||||
entry.dir = path.relative(ROOT, archiveDir);
|
||||
manifest.screens.push(entry);
|
||||
if (FORCE || !fs.existsSync(path.join(archiveDir, 'meta.json'))) {
|
||||
try {
|
||||
const detail = await mcpCall('get_screen', { name: `projects/${PROJECT_ID}/screens/${screenId}`, projectId: PROJECT_ID, screenId });
|
||||
fs.writeFileSync(path.join(archiveDir, 'meta.json'), JSON.stringify({ title, screenId, syncedAt: new Date().toISOString() }, null, 2));
|
||||
if (detail.htmlCode?.downloadUrl) await download(detail.htmlCode.downloadUrl, path.join(archiveDir, 'code.html'));
|
||||
if (detail.screenshot?.downloadUrl) await download(detail.screenshot.downloadUrl, path.join(archiveDir, 'screen.png'));
|
||||
} catch (e) {
|
||||
console.warn(` skip download failed: ${title}`, e.message);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const num = String(++idx[category]).padStart(2, '0');
|
||||
const dir = path.join(PAGES, category, `${num}_${slug(title, screenId)}`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
entry.dir = path.relative(ROOT, dir);
|
||||
|
||||
const metaPath = path.join(dir, 'meta.json');
|
||||
if (!FORCE && fs.existsSync(metaPath) && fs.existsSync(path.join(dir, 'code.html'))) {
|
||||
manifest.screens.push(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const detail = await mcpCall('get_screen', {
|
||||
name: `projects/${PROJECT_ID}/screens/${screenId}`,
|
||||
projectId: PROJECT_ID,
|
||||
screenId,
|
||||
});
|
||||
const meta = { title, screenId, category, syncedAt: new Date().toISOString() };
|
||||
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
||||
|
||||
if (detail.htmlCode?.downloadUrl) {
|
||||
const ok = await download(detail.htmlCode.downloadUrl, path.join(dir, 'code.html'));
|
||||
if (!ok) console.warn(` HTML download failed: ${title}`);
|
||||
}
|
||||
if (detail.screenshot?.downloadUrl) {
|
||||
await download(detail.screenshot.downloadUrl, path.join(dir, 'screen.png'));
|
||||
}
|
||||
console.log(` [${category}] ${title}`);
|
||||
} catch (e) {
|
||||
console.warn(` Failed: ${title}`, e.message);
|
||||
}
|
||||
manifest.screens.push(entry);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(PAGES, 'SCREEN_MAP.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
||||
console.log('\nDone.');
|
||||
console.log(` user: ${manifest.screens.filter((s) => s.category === 'user').length}`);
|
||||
console.log(` shop: ${manifest.screens.filter((s) => s.category === 'shop').length}`);
|
||||
console.log(` partner: ${manifest.screens.filter((s) => s.category === 'partner').length}`);
|
||||
console.log(` skip/archive: ${manifest.screens.filter((s) => s.category === 'skip').length}`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* 合伙人登录 / 手机号校验专项冒烟(需 API 已启动)
|
||||
* 用法: node scripts/test-partner-auth.mjs
|
||||
*/
|
||||
import { loginWithSms } from './sms-test-helper.mjs';
|
||||
|
||||
const API = process.env.SMOKE_API ?? 'http://localhost:3000/api/v1';
|
||||
|
||||
async function req(clientApp, path, options = {}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': clientApp,
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
};
|
||||
const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body });
|
||||
const json = await res.json();
|
||||
if (json.code !== 0) throw new Error(`${path}: ${json.message}`);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
async function expectFail(clientApp, path, options = {}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': clientApp,
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
};
|
||||
const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body });
|
||||
const json = await res.json();
|
||||
if (json.code === 0) throw new Error(`${path}: expected failure`);
|
||||
return json.message;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('1. PARTNER phone/check unknown');
|
||||
const unknownCheck = await expectFail('PARTNER_H5', '/partner/auth/phone/check', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone: '13899999999' }),
|
||||
});
|
||||
if (!unknownCheck.includes('未找到合伙人账号')) throw new Error(`unexpected: ${unknownCheck}`);
|
||||
|
||||
console.log('2. PARTNER sms/send unknown');
|
||||
const unknownSms = await expectFail('PARTNER_H5', '/partner/auth/sms/send', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone: '13899999999', scene: 'PARTNER_LOGIN' }),
|
||||
});
|
||||
if (!unknownSms.includes('未找到合伙人账号')) throw new Error(`unexpected: ${unknownSms}`);
|
||||
|
||||
console.log('3. PARTNER sms/login bound phone');
|
||||
const login = await loginWithSms(
|
||||
'PARTNER_H5',
|
||||
'13700000001',
|
||||
'PARTNER_LOGIN',
|
||||
'/partner/auth/login/sms',
|
||||
'/partner/auth/sms/send',
|
||||
);
|
||||
if (!login.accessToken) throw new Error('login missing token');
|
||||
|
||||
console.log('4. PARTNER /partner/me after login');
|
||||
const me = await req('PARTNER_H5', '/partner/me', { token: login.accessToken });
|
||||
if (!me?.id || !me?.phone) throw new Error('partner/me missing profile');
|
||||
|
||||
console.log('5. Admin partner logs list');
|
||||
const admin = await req('HQ_WEB', '/admin/auth/login/password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
loginName: process.env.SUPER_ADMIN_LOGIN ?? 'admin',
|
||||
password: process.env.SUPER_ADMIN_PASSWORD ?? 'dukang@123!',
|
||||
}),
|
||||
});
|
||||
const logs = await req('HQ_WEB', '/admin/logs/partners?page=1&pageSize=10&category=login', {
|
||||
token: admin.accessToken,
|
||||
});
|
||||
if (!Array.isArray(logs.items)) throw new Error('partner logs missing items');
|
||||
const hasLogin = logs.items.some((r) => r.eventName === 'partner_sms_login' || r.eventName === 'partner_login_success');
|
||||
if (!hasLogin) throw new Error('partner login log not found');
|
||||
|
||||
console.log('\n✅ partner-auth tests passed');
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* 门店登录 / 建店短信校验专项冒烟(需 API 已启动且 MOCK_SMS 开启)
|
||||
* 用法: node scripts/test-shop-auth.mjs
|
||||
*/
|
||||
import { resolveSmsCode, sendSmsWithCooldown } from './sms-test-helper.mjs';
|
||||
|
||||
const API = process.env.SMOKE_API ?? 'http://localhost:3000/api/v1';
|
||||
|
||||
async function req(clientApp, path, options = {}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': clientApp,
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
};
|
||||
const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body });
|
||||
const json = await res.json();
|
||||
if (json.code !== 0) throw new Error(`${path}: ${json.message}`);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
async function expectFail(clientApp, path, options = {}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': clientApp,
|
||||
...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),
|
||||
};
|
||||
const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body });
|
||||
const json = await res.json();
|
||||
if (json.code === 0) throw new Error(`${path}: expected failure`);
|
||||
return json.message;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('1. STORE_LOGIN unbound phone');
|
||||
const unbound = await expectFail('SHOP_H5', '/shop/auth/sms/send', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone: '13899999999', scene: 'STORE_LOGIN' }),
|
||||
});
|
||||
if (!unbound.includes('未绑定门店')) throw new Error(`unexpected: ${unbound}`);
|
||||
|
||||
console.log('2. STORE_LOGIN bound phone');
|
||||
await sendSmsWithCooldown('SHOP_H5', '/shop/auth/sms/send', '13910000001', 'STORE_LOGIN');
|
||||
const code = await resolveSmsCode('13910000001', 'STORE_LOGIN');
|
||||
const login = await req('SHOP_H5', '/shop/auth/login/sms', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone: '13910000001', code }),
|
||||
});
|
||||
if (!login.accessToken || !login.refreshToken) throw new Error('login missing tokens');
|
||||
|
||||
console.log('3. Shop session me + refresh');
|
||||
const me = await req('SHOP_H5', '/shop/auth/me', { token: login.accessToken });
|
||||
if (me.phone !== '13910000001') throw new Error('me phone mismatch');
|
||||
const refreshed = await req('SHOP_H5', '/shop/auth/token/refresh', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ refreshToken: login.refreshToken }),
|
||||
});
|
||||
if (!refreshed.accessToken) throw new Error('refresh failed');
|
||||
|
||||
console.log('4. Admin create store rejects occupied phone');
|
||||
await sendSmsWithCooldown('HQ_WEB', '/admin/auth/sms/send', '13600000001', 'HQ_LOGIN');
|
||||
const adminCode = await resolveSmsCode('13600000001', 'HQ_LOGIN');
|
||||
const admin = await req('HQ_WEB', '/admin/auth/login/sms', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone: '13600000001', code: adminCode }),
|
||||
});
|
||||
const occupied = await expectFail('HQ_WEB', '/admin/stores', {
|
||||
method: 'POST',
|
||||
token: admin.accessToken,
|
||||
body: JSON.stringify({
|
||||
partnerId: '1',
|
||||
cityId: '1',
|
||||
name: '重复手机号测试店',
|
||||
phone: '13910000001',
|
||||
district: '测试区',
|
||||
address: '测试地址1号',
|
||||
}),
|
||||
});
|
||||
if (!occupied.includes('已绑定')) throw new Error(`unexpected: ${occupied}`);
|
||||
|
||||
console.log('\n✅ shop-auth tests passed');
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user