run-api-tests-v3.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. /**
  2. * CFC API Integration Test Runner v3
  3. * 环境: http://cfc.iwintrue.com:80
  4. * 覆盖: NEW-01/02/03修复验证 + ISSUE-001/002/003回归 + 新增API测试
  5. *
  6. * 运行: node tests/run-api-tests-v3.js
  7. */
  8. const BASE = 'http://cfc.iwintrue.com:80';
  9. const ADMIN = { phone: '13800138000', userId: 7, role: 'admin', name: '管理员' };
  10. const PARENT = { phone: '13701366188', userId: 8, familyId: 2, role: 'parent', name: '家长' };
  11. let adminToken = null;
  12. let testResults = [];
  13. let testStartTime = Date.now();
  14. function log(msg) { console.log(`[${new Date().toISOString()}] ${msg}`); }
  15. function pass(name, detail = '') {
  16. testResults.push({ name, result: 'PASS', detail, time: Date.now() });
  17. console.log(` ✅ PASS | ${name}${detail ? ' | ' + detail : ''}`);
  18. }
  19. function fail(name, err, detail = '') {
  20. testResults.push({ name, result: 'FAIL', detail: detail || err.message, time: Date.now() });
  21. console.log(` ❌ FAIL | ${name} | ${err.message}${detail ? ' | ' + detail : ''}`);
  22. }
  23. function skip(name, reason) {
  24. testResults.push({ name, result: 'SKIP', detail: reason, time: Date.now() });
  25. console.log(` ⏭️ SKIP | ${name} | ${reason}`);
  26. }
  27. async function api(path, body, headers = {}) {
  28. const url = `${BASE}${path}`;
  29. const opts = {
  30. method: 'POST',
  31. headers: { 'Content-Type': 'application/json', ...headers },
  32. body: body ? JSON.stringify(body) : undefined,
  33. };
  34. try {
  35. const resp = await fetch(url, opts);
  36. const text = await resp.text();
  37. let data;
  38. try { data = JSON.parse(text); } catch { data = null; }
  39. return { status: resp.status, data, ok: resp.ok, raw: text.substring(0, 300) };
  40. } catch (err) {
  41. throw new Error(`网络错误: ${err.message}`);
  42. }
  43. }
  44. async function adminApi(path, body) {
  45. if (!adminToken) throw new Error('无admin token');
  46. return api(path, body, { Authorization: `Bearer ${adminToken}` });
  47. }
  48. async function publicApi(path, body) {
  49. return api(path, body, {});
  50. }
  51. // ─────────────────────────────────────────
  52. // 1. 登录
  53. // ─────────────────────────────────────────
  54. async function login() {
  55. log('=== 1. 登录测试 ===');
  56. try {
  57. // 发送验证码(skip-captcha 已启用,任意验证码均可)
  58. await api('/api/admin-auth/send-code', { phone: ADMIN.phone });
  59. // 登录获取 token
  60. const resp = await api('/api/admin-auth/login', { phone: ADMIN.phone, code: '123456' });
  61. if (resp.data?.data?.token) {
  62. adminToken = resp.data.data.token;
  63. pass('Admin登录', `userId=${resp.data.data.adminId || ADMIN.userId}`);
  64. } else {
  65. fail('Admin登录', new Error('未获取到token'), `raw: ${resp.raw}`);
  66. }
  67. } catch (err) {
  68. fail('Admin登录', err);
  69. }
  70. skip('Parent登录(手机验证码)', '测试环境无法发短信,parent端需手动测试');
  71. }
  72. // ─────────────────────────────────────────
  73. // 2. 公共端点(无需认证)
  74. // ─────────────────────────────────────────
  75. async function testPublicEndpoints() {
  76. log('=== 2. 公共端点测试 ===');
  77. // 文章列表
  78. try {
  79. const resp = await publicApi('/api/articles/list', { page: 1, size: 5 });
  80. if (resp.ok || resp.data?.code === 200) {
  81. pass('文章列表(公开)', 'ok');
  82. } else {
  83. fail('文章列表(公开)', new Error(`code=${resp.data?.code}`));
  84. }
  85. } catch (err) { fail('文章列表(公开)', err); }
  86. // 活动列表
  87. try {
  88. const resp = await publicApi('/api/activity/list', { page: 1, size: 5 });
  89. if (resp.ok || resp.data?.code === 200) {
  90. pass('活动列表(公开)', 'ok');
  91. } else {
  92. fail('活动列表(公开)', new Error(`status=${resp.status}`));
  93. }
  94. } catch (err) { fail('活动列表(公开)', err); }
  95. }
  96. // ─────────────────────────────────────────
  97. // 3. NEW-01 验证:能量规则创建
  98. // 修复: AdminEnergyRuleController.create() 改用 Map<String,Object>
  99. // ─────────────────────────────────────────
  100. async function testNEW01_EnergyRuleCreate() {
  101. log('=== 3. NEW-01 验证:能量规则创建 ===');
  102. if (!adminToken) { skip('NEW-01 能量规则创建', '无admin token'); return; }
  103. // 先查看能量规则列表(确保维度存在)
  104. let dimensions = [];
  105. try {
  106. const resp = await adminApi('/api/admin/energy-rule/dimensions', {});
  107. if (resp.ok || resp.data?.code === 200) {
  108. dimensions = resp.data?.data || [];
  109. pass('能量维度列表', `count=${dimensions.length}`);
  110. } else {
  111. fail('能量维度列表', new Error(`code=${resp.data?.code}`));
  112. }
  113. } catch (err) { fail('能量维度列表', err); return; }
  114. // 创建能量规则(使用正确的 Map 参数格式)
  115. let ruleId = null;
  116. try {
  117. const resp = await adminApi('/api/admin/energy-rule/create', {
  118. ruleName: '自动化测试规则_NEW01_' + Date.now(),
  119. dimensionCode: 'body',
  120. eventType: 'checkin',
  121. energyValue: 5,
  122. status: 1,
  123. action: 'increase',
  124. priority: 0,
  125. });
  126. if (resp.ok || resp.data?.code === 200) {
  127. // 插入后 EnergyRule.id 会回填到 data
  128. ruleId = resp.data?.data?.id || resp.data?.id;
  129. pass('NEW-01 创建能量规则', `id=${ruleId} ✅ NEW-01已修复`);
  130. } else {
  131. fail('NEW-01 创建能量规则', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  132. }
  133. } catch (err) { fail('NEW-01 创建能量规则', err); }
  134. if (!ruleId) return;
  135. // 切换状态
  136. try {
  137. const resp = await adminApi('/api/admin/energy-rule/toggle', { id: ruleId });
  138. if (resp.ok || resp.data?.code === 200) {
  139. pass('能量规则切换状态', `id=${ruleId}`);
  140. } else {
  141. fail('能量规则切换状态', new Error(`code=${resp.data?.code}`));
  142. }
  143. } catch (err) { fail('能量规则切换状态', err); }
  144. }
  145. // ─────────────────────────────────────────
  146. // 4. NEW-02 验证:SKU列表
  147. // 修复: ProductSkuController.listByProduct 改用 Map + null检查
  148. // ─────────────────────────────────────────
  149. async function testNEW02_SkuList() {
  150. log('=== 4. NEW-02 验证:SKU列表 ===');
  151. if (!adminToken) { skip('NEW-02 SKU列表', '无admin token'); return; }
  152. // productId=1 在之前测试中应该是已存在的商品
  153. try {
  154. const resp = await adminApi('/api/admin/product/sku/list', { productId: 1 });
  155. if (resp.ok || resp.data?.code === 200) {
  156. const skus = resp.data?.data || [];
  157. pass('NEW-02 SKU列表', `count=${skus.length} ✅ NEW-02已修复`);
  158. } else if (resp.data?.code === 500) {
  159. fail('NEW-02 SKU列表', new Error('500 Internal Server Error'), '⚠️ NEW-02可能回归');
  160. } else {
  161. fail('NEW-02 SKU列表', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  162. }
  163. } catch (err) { fail('NEW-02 SKU列表', err); }
  164. }
  165. // ─────────────────────────────────────────
  166. // 5. NEW-03 验证:活动发布
  167. // 修复: ActivityController.publish() 改用 Map + try-catch
  168. // ─────────────────────────────────────────
  169. async function testNEW03_ActivityPublish() {
  170. log('=== 5. NEW-03 验证:活动发布 ===');
  171. if (!adminToken) { skip('NEW-03 活动发布', '无admin token'); return; }
  172. // 创建活动
  173. let activityId = null;
  174. try {
  175. const resp = await adminApi('/api/activity/create', {
  176. title: '自动化E2E测试活动_NEW03_' + Date.now(),
  177. description: 'NEW-03修复验证测试',
  178. dimensionCode: 'body',
  179. startTime: new Date(Date.now() + 86400000 * 7).toISOString().replace('T', ' ').split('.')[0],
  180. endTime: new Date(Date.now() + 86400000 * 8).toISOString().replace('T', ' ').split('.')[0],
  181. location: '线上直播',
  182. maxParticipants: 30,
  183. });
  184. if (resp.ok || resp.data?.code === 200) {
  185. activityId = resp.data?.data?.id || resp.data?.id;
  186. pass('创建测试活动', `id=${activityId}`);
  187. } else {
  188. fail('创建测试活动', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  189. }
  190. } catch (err) { fail('创建测试活动', err); }
  191. if (!activityId) return;
  192. // 发布活动(NEW-03 核心测试)
  193. try {
  194. const resp = await adminApi('/api/activity/publish', { id: activityId });
  195. if (resp.ok || resp.data?.code === 200) {
  196. pass('NEW-03 发布活动', `id=${activityId} ✅ NEW-03已修复`);
  197. } else if (resp.data?.code === 500) {
  198. fail('NEW-03 发布活动', new Error('500 Internal Server Error'), '⚠️ NEW-03可能回归');
  199. } else {
  200. fail('NEW-03 发布活动', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  201. }
  202. } catch (err) { fail('NEW-03 发布活动', err); }
  203. }
  204. // ─────────────────────────────────────────
  205. // 6. ISSUE-001 验证:供应商下架商品
  206. // ─────────────────────────────────────────
  207. async function testISSUE001() {
  208. log('=== 6. ISSUE-001 验证:供应商下架商品 ===');
  209. if (!adminToken) { skip('ISSUE-001 供应商下架', '无admin token'); return; }
  210. // 1) 审核供应商 81069(如尚未批准)
  211. try {
  212. const resp = await adminApi('/api/admin/vendor/review', {
  213. userId: 81069,
  214. action: 'approve',
  215. reason: '自动化测试',
  216. });
  217. if (resp.ok || resp.data?.code === 200) {
  218. pass('审核供应商81069', 'approved');
  219. } else {
  220. fail('审核供应商81069', new Error(`code=${resp.data?.code}`));
  221. }
  222. } catch (err) { fail('审核供应商81069', err); }
  223. // 2) 审核商品(如尚未批准)
  224. try {
  225. const resp = await adminApi('/api/admin/product/review', {
  226. productId: 7,
  227. action: 'approve',
  228. reason: '自动化测试',
  229. });
  230. if (resp.ok || resp.data?.code === 200) {
  231. pass('审核商品7', 'approved');
  232. } else {
  233. fail('审核商品7', new Error(`code=${resp.data?.code}`));
  234. }
  235. } catch (err) { fail('审核商品7', err); }
  236. // 3) 上架商品
  237. try {
  238. const resp = await adminApi('/api/admin/product/shelve', { productId: 7, shelve: true });
  239. if (resp.ok || resp.data?.code === 200) {
  240. pass('上架商品7', 'on_shelf');
  241. } else {
  242. fail('上架商品7', new Error(`code=${resp.data?.code}`));
  243. }
  244. } catch (err) { fail('上架商品7', err); }
  245. // 4) 供应商下架商品(核心验证)
  246. try {
  247. const resp = await api('/api/product/shelve', { productId: 7, action: 'unshelve' }, { 'X-User-Id': '81069' });
  248. if (resp.ok || resp.data?.code === 200) {
  249. pass('ISSUE-001 供应商下架商品', 'unshelved ✅ ISSUE-001已修复');
  250. } else if (resp.data?.message?.includes('未通过审核')) {
  251. fail('ISSUE-001 供应商下架商品', new Error('商品未通过审核/状态异常'), '需检查商品状态');
  252. } else {
  253. fail('ISSUE-001 供应商下架商品', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  254. }
  255. } catch (err) { fail('ISSUE-001 供应商下架商品', err); }
  256. }
  257. // ─────────────────────────────────────────
  258. // 7. ISSUE-002/003 回归验证
  259. // ─────────────────────────────────────────
  260. async function testIssue002_003() {
  261. log('=== 7. ISSUE-002/003 回归验证 ===');
  262. if (!adminToken) { skip('ISSUE-002/003', '无admin token'); return; }
  263. // ISSUE-003: 创建用户
  264. const phone = '139' + String(Math.floor(Math.random() * 1e9)).padStart(9, '0');
  265. let newUserId = null;
  266. try {
  267. const resp = await adminApi('/api/admin/users/create', {
  268. phone: phone,
  269. password: 'Test123456',
  270. name: '自动化测试用户_' + Date.now(),
  271. role: 'parent',
  272. });
  273. if (resp.ok || resp.data?.code === 200) {
  274. newUserId = resp.data?.data?.id || resp.data?.id;
  275. pass('ISSUE-003 创建用户', `userId=${newUserId} ✅ ISSUE-003稳定`);
  276. } else if (resp.data?.code === 500) {
  277. fail('ISSUE-003 创建用户', new Error('500 - ISSUE-003回归'), '⚠️ 需紧急修复');
  278. } else {
  279. fail('ISSUE-003 创建用户', new Error(`code=${resp.data?.code}`));
  280. }
  281. } catch (err) { fail('ISSUE-003 创建用户', err); }
  282. // ISSUE-002: 文章发布(检查 articleId=13 是否仍可发布)
  283. try {
  284. const resp = await adminApi('/api/article/13/publish', {});
  285. if (resp.ok || resp.data?.code === 200) {
  286. pass('ISSUE-002 文章发布', 'articleId=13 ✅ ISSUE-002稳定');
  287. } else {
  288. fail('ISSUE-002 文章发布', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  289. }
  290. } catch (err) { fail('ISSUE-002 文章发布', err); }
  291. }
  292. // ─────────────────────────────────────────
  293. // 8. 新增API测试(来自 recent commits)
  294. // dfcc285: 心愿兑换积分检查和扣除
  295. // 3e44f18: grantEnergy + deductForWish 接口
  296. // 8212e25: getRecordsByChild 成长记录
  297. // ─────────────────────────────────────────
  298. async function testNewAPIs() {
  299. log('=== 8. 新增API测试(recent commits)===');
  300. if (!adminToken) { skip('新增API测试', '无admin token'); return; }
  301. // 8a. EnergyService.grantEnergy + deductForWish
  302. // POST /api/energy/grantEnergy
  303. try {
  304. const resp = await adminApi('/api/energy/grantEnergy', {
  305. childId: 1,
  306. taskId: 1,
  307. amount: 10,
  308. dimensionCode: 'body',
  309. });
  310. if (resp.ok || resp.data?.code === 200) {
  311. pass('energy.grantEnergy', 'ok');
  312. } else {
  313. fail('energy.grantEnergy', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  314. }
  315. } catch (err) { fail('energy.grantEnergy', err); }
  316. // POST /api/energy/deductForWish
  317. try {
  318. const resp = await adminApi('/api/energy/deductForWish', {
  319. childId: 1,
  320. wishId: 1,
  321. amount: 5,
  322. });
  323. if (resp.ok || resp.data?.code === 200) {
  324. pass('energy.deductForWish', 'ok');
  325. } else {
  326. fail('energy.deductForWish', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  327. }
  328. } catch (err) { fail('energy.deductForWish', err); }
  329. // 8b. PointsService.checkAndGrantReward + deductForWish (dfcc285)
  330. // POST /api/points/checkAndGrantReward
  331. try {
  332. const resp = await adminApi('/api/points/checkAndGrantReward', {
  333. childId: 1,
  334. amount: 5,
  335. });
  336. // 可能返回 -1(余额不足)或成功
  337. if (resp.ok || resp.data?.code === 200 || resp.data?.data === -1) {
  338. pass('points.checkAndGrantReward', `result=${resp.data?.data}`);
  339. } else {
  340. fail('points.checkAndGrantReward', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  341. }
  342. } catch (err) { fail('points.checkAndGrantReward', err); }
  343. // POST /api/points/deductForWish
  344. try {
  345. const resp = await adminApi('/api/points/deductForWish', {
  346. childId: 1,
  347. amount: 5,
  348. });
  349. if (resp.ok || resp.data?.code === 200) {
  350. pass('points.deductForWish', `result=${resp.data?.data}`);
  351. } else {
  352. fail('points.deductForWish', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  353. }
  354. } catch (err) { fail('points.deductForWish', err); }
  355. // 8c. GrowthRecord - getRecordsByChild (8212e25)
  356. // GET /api/growth/record/child/{childId} 或 POST
  357. try {
  358. const resp = await adminApi('/api/growth/record/child/1', {});
  359. if (resp.ok || resp.data?.code === 200) {
  360. const records = resp.data?.data || [];
  361. pass('growth.getRecordsByChild', `count=${Array.isArray(records) ? records.length : 0}`);
  362. } else {
  363. fail('growth.getRecordsByChild', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  364. }
  365. } catch (err) { fail('growth.getRecordsByChild', err); }
  366. // 8d. Activity end
  367. try {
  368. const resp = await adminApi('/api/activity/end', { id: 18 });
  369. if (resp.ok || resp.data?.code === 200 || resp.data?.message?.includes('不存在')) {
  370. pass('activity.end', 'ok');
  371. } else {
  372. fail('activity.end', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
  373. }
  374. } catch (err) { fail('activity.end', err); }
  375. }
  376. // ─────────────────────────────────────────
  377. // 9. 能量规则维度查询
  378. // ─────────────────────────────────────────
  379. async function testEnergyRuleDimensions() {
  380. log('=== 9. 能量规则维度查询 ===');
  381. if (!adminToken) { skip('能量规则维度查询', '无admin token'); return; }
  382. try {
  383. const resp = await adminApi('/api/admin/energy-rule/dimensions', {});
  384. if (resp.ok || resp.data?.code === 200) {
  385. const dims = resp.data?.data || [];
  386. pass('能量规则维度列表', `count=${dims.length}`);
  387. dims.forEach(d => {
  388. console.log(` - ${d.dimensionCode || d.code}: ${d.name || d.dimensionName || JSON.stringify(d)}`);
  389. });
  390. } else {
  391. fail('能量规则维度列表', new Error(`code=${resp.data?.code}`));
  392. }
  393. } catch (err) { fail('能量规则维度列表', err); }
  394. }
  395. // ─────────────────────────────────────────
  396. // 10. 后台管理核心接口
  397. // ─────────────────────────────────────────
  398. async function testAdminCore() {
  399. log('=== 10. 后台管理核心接口 ===');
  400. if (!adminToken) { skip('后台管理核心接口', '无admin token'); return; }
  401. // 用户列表
  402. try {
  403. const resp = await adminApi('/api/admin/users', { page: 1, size: 10 });
  404. if (resp.ok || resp.data?.code === 200) {
  405. pass('用户列表', 'ok');
  406. } else {
  407. fail('用户列表', new Error(`code=${resp.data?.code}`));
  408. }
  409. } catch (err) { fail('用户列表', err); }
  410. // 供应商列表
  411. try {
  412. const resp = await adminApi('/api/admin/vendor/list', {});
  413. if (resp.ok || resp.data?.code === 200) {
  414. pass('供应商列表', 'ok');
  415. } else {
  416. fail('供应商列表', new Error(`code=${resp.data?.code}`));
  417. }
  418. } catch (err) { fail('供应商列表', err); }
  419. // 规划师申请列表
  420. try {
  421. const resp = await adminApi('/api/admin/guide/applications/pending', {});
  422. if (resp.ok || resp.data?.code === 200) {
  423. const apps = resp.data?.data || [];
  424. pass('规划师申请列表', `pending=${apps.length}`);
  425. } else {
  426. fail('规划师申请列表', new Error(`code=${resp.data?.code}`));
  427. }
  428. } catch (err) { fail('规划师申请列表', err); }
  429. }
  430. // ─────────────────────────────────────────
  431. // 报告生成
  432. // ─────────────────────────────────────────
  433. async function generateReport() {
  434. const duration = Date.now() - testStartTime;
  435. const passed = testResults.filter(r => r.result === 'PASS').length;
  436. const failed = testResults.filter(r => r.result === 'FAIL').length;
  437. const skipped = testResults.filter(r => r.result === 'SKIP').length;
  438. const total = testResults.length;
  439. console.log('\n' + '='.repeat(60));
  440. console.log(` API 测试报告 v3 — ${new Date().toLocaleString()}`);
  441. console.log(` 耗时: ${Math.round(duration / 1000)}s`);
  442. console.log(` 结果: ${passed} PASS / ${failed} FAIL / ${skipped} SKIP / ${total} TOTAL`);
  443. console.log('='.repeat(60));
  444. if (failed > 0) {
  445. console.log('\n❌ FAIL 明细:');
  446. testResults.filter(r => r.result === 'FAIL').forEach(r => {
  447. console.log(` - ${r.name}: ${r.detail}`);
  448. });
  449. }
  450. const report = {
  451. version: 'v3',
  452. timestamp: new Date().toISOString(),
  453. duration_ms: duration,
  454. summary: { total, passed, failed, skipped },
  455. results: testResults,
  456. };
  457. const fs = require('fs');
  458. const path = require('path');
  459. const reportPath = path.join(__dirname, 'TEST-RESULTS-v3.md');
  460. let md = `# API 测试报告 v3 — ${new Date().toLocaleString()}\n\n`;
  461. md += `| 结果 | 数量 |\n|------|------|\n`;
  462. md += `| ✅ PASS | ${passed} |\n| ❌ FAIL | ${failed} |\n| ⏭️ SKIP | ${skipped} |\n| 总计 | ${total} |\n\n`;
  463. md += `## FAIL 明细\n\n`;
  464. testResults.filter(r => r.result === 'FAIL').forEach(r => {
  465. md += `- **${r.name}**: ${r.detail}\n`;
  466. });
  467. md += `\n## 完整结果\n\n`;
  468. md += `| 用例 | 结果 | 详情 |\n|------|------|------|\n`;
  469. testResults.forEach(r => {
  470. md += `| ${r.name} | ${r.result} | ${r.detail} |\n`;
  471. });
  472. fs.writeFileSync(reportPath, md, 'utf8');
  473. console.log(`\n报告已保存: ${reportPath}`);
  474. }
  475. // ─────────────────────────────────────────
  476. // 主流程
  477. // ─────────────────────────────────────────
  478. async function main() {
  479. log('CFC API 测试 Runner v3 开始');
  480. log(`目标环境: ${BASE}`);
  481. await login();
  482. await testPublicEndpoints();
  483. await testNEW01_EnergyRuleCreate();
  484. await testNEW02_SkuList();
  485. await testNEW03_ActivityPublish();
  486. await testISSUE001();
  487. await testIssue002_003();
  488. await testEnergyRuleDimensions();
  489. await testAdminCore();
  490. await testNewAPIs();
  491. await generateReport();
  492. }
  493. main().catch(console.error);