smoke-app-member-resources.mjs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. #!/usr/bin/env node
  2. import { randomBytes } from 'node:crypto';
  3. const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
  4. const MASTER_KEY = process.env.XIAOSHU_MASTER_KEY || '';
  5. const PARSE_URL = (process.env.XIAOSHU_PARSE_URL || 'https://server.xiaoshu.pro/parse').replace(/\/$/, '');
  6. const FUNCTION_URL = (process.env.XIAOSHU_FUNCTION_URL || 'https://server.xiaoshu.pro/api/functions').replace(/\/$/, '');
  7. const COMPANY_ID = '7pIbDBJmKx';
  8. if (!MASTER_KEY) throw new Error('缺少 XIAOSHU_MASTER_KEY');
  9. const appHeaders = { 'X-Parse-Application-Id': APP_ID, 'Content-Type': 'application/json' };
  10. const masterHeaders = { ...appHeaders, 'X-Parse-Master-Key': MASTER_KEY };
  11. const username = `codex_member_resource_${Date.now()}`;
  12. const password = randomBytes(24).toString('base64url');
  13. const legacyUserId = 970000000 + Math.floor(Date.now() / 1000) % 20000000;
  14. let userObjectId = '';
  15. let membershipTargetUserId = '';
  16. let commerceProductId = 0;
  17. const created = [];
  18. async function request(url, init = {}, expected = 200) {
  19. const response = await fetch(url, init);
  20. const payload = await response.json().catch(() => ({}));
  21. if (response.status !== expected) throw new Error(`${url} 期望 ${expected},实际 ${response.status}: ${payload.retmsg || payload.error || ''}`);
  22. return payload;
  23. }
  24. async function master(path, init = {}, expected = 200) {
  25. return request(`${PARSE_URL}${path}`, { ...init, headers: { ...masterHeaders, ...(init.headers || {}) } }, expected);
  26. }
  27. async function cloud(token, action, params = {}, expected = 200) {
  28. return request(`${FUNCTION_URL}/xiaoshu/app/gateway`, {
  29. method: 'POST', headers: appHeaders, body: JSON.stringify({ token, params: { action, uid: legacyUserId, ...params } }),
  30. }, expected);
  31. }
  32. async function hardDelete(className, objectId) {
  33. if (!objectId) return;
  34. await master(`/classes/${className}/${objectId}`, { method: 'DELETE' }).catch(() => undefined);
  35. }
  36. try {
  37. const company = { __type: 'Pointer', className: 'Company', objectId: COMPANY_ID };
  38. const createdUser = await master('/users', {
  39. method: 'POST',
  40. body: JSON.stringify({ username, password, company, legacyUserId, legacyGroupId: 1, nickname: '会员资料云函数冒烟', legacyUserData: { UserID: legacyUserId, GroupID: 1, HoneyName: '会员资料云函数冒烟' } }),
  41. }, 201);
  42. userObjectId = createdUser.objectId;
  43. const login = await request(`${PARSE_URL}/login`, { method: 'POST', headers: appHeaders, body: JSON.stringify({ username, password }) });
  44. const token = login.sessionToken;
  45. await master(`/users/${userObjectId}`, {
  46. method: 'PUT',
  47. body: JSON.stringify({ legacyUserData: { UserID: legacyUserId, GroupID: 1, HoneyName: '会员资料云函数冒烟', SilverCoin: 200 } }),
  48. });
  49. const completedReviewStats = await cloud(token, 'e_get_21list_tj');
  50. if (!Array.isArray(completedReviewStats.result) || Number(completedReviewStats.result[0]?.num) !== 0) throw new Error('本人已完成复习任务统计异常');
  51. const address = await cloud(token, 'receaddr_add', { provinceName: '江西省', cityName: '南昌市', countyName: '青山湖区', userName: '测试收货人', telNumber: '13800138000', detailInfo: '测试路 1 号', postalCode: '330000' });
  52. const addressId = String(address.result?.ID || ''); created.push(['ReceivingAddress', addressId]);
  53. if (!addressId || address.result?.ReceivName !== '测试收货人') throw new Error('收货地址新增响应异常');
  54. const addressList = await cloud(token, 'receaddr_list', { cpage: 1, psize: 20 });
  55. if (addressList.page?.itemCount !== 1 || addressList.result?.[0]?.ID !== addressId) throw new Error('收货地址列表异常');
  56. const addressUpdated = await cloud(token, 'receaddr_upd', { id: addressId, provinceName: '江西省', cityName: '南昌市', countyName: '青山湖区', userName: '更新收货人', telNumber: '13800138000', detailInfo: '测试路 2 号', postalCode: '330000' });
  57. if (addressUpdated.result?.ReceivName !== '更新收货人') throw new Error('收货地址更新异常');
  58. const addressGet = await cloud(token, 'receaddr_get', { id: addressId });
  59. if (addressGet.result?.Street !== '测试路 2 号') throw new Error('收货地址详情异常');
  60. const invoice = await cloud(token, 'invoice_add', { aud: 1, name: '测试企业', idnum: '91360100TEST000001', mobile: '13800138000', email: 'billing@example.test', address: '测试注册地址', bankname: '测试银行', bankusr: '6222020000000000000' });
  61. const invoiceId = String(invoice.result?.ID || ''); created.push(['InvoiceProfile', invoiceId]);
  62. if (!invoiceId || invoice.result?.InvHead !== '测试企业') throw new Error('发票资料新增响应异常');
  63. const invoiceUpdated = await cloud(token, 'invoice_upd', { id: invoiceId, aud: 1, name: '更新测试企业', idnum: '91360100TEST000001', mobile: '13800138000', email: 'billing@example.test', address: '测试注册地址', bankname: '测试银行', bankusr: '6222020000000000000' });
  64. if (invoiceUpdated.result?.InvHead !== '更新测试企业') throw new Error('发票资料更新异常');
  65. const invoiceList = await cloud(token, 'invoice_list', { cpage: 1, psize: 20 });
  66. if (invoiceList.page?.itemCount !== 1 || invoiceList.result?.[0]?.ID !== invoiceId) throw new Error('发票资料列表异常');
  67. const bank = await cloud(token, 'user_bank_add', { CardType: '银行卡', BankName: '测试银行', PeopleName: '测试开户人', CardNum: '6222020000000000000', Remark: '首次创建' });
  68. const bankId = String(bank.result?.ID || ''); created.push(['UserBankAccount', bankId]);
  69. if (!bankId || bank.result?.PeopleName !== '测试开户人') throw new Error('提现账户新增响应异常');
  70. const bankUpdated = await cloud(token, 'user_bank_add', { id: bankId, CardType: '银行卡', BankName: '测试银行', PeopleName: '更新开户人', CardNum: '6222020000000000000', Remark: '已更新' });
  71. if (bankUpdated.result?.PeopleName !== '更新开户人') throw new Error('提现账户更新异常');
  72. const bankGet = await cloud(token, 'user_bank_get', { id: bankId });
  73. if (bankGet.result?.Remark !== '已更新') throw new Error('提现账户详情异常');
  74. const bankList = await cloud(token, 'user_bank_list', { cpage: 1, psize: 20 });
  75. if (bankList.page?.itemCount !== 1 || bankList.result?.[0]?.ID !== bankId) throw new Error('提现账户列表异常');
  76. const ask = await cloud('', 'vote_ask', { id: 1 });
  77. const questions = await cloud('', 'vote_question', { askid: 1 });
  78. if (ask.result?.[0]?.Title !== '第一组问题' || Number(questions.result?.length) !== 48 || !questions.result?.[0]?.QOption) throw new Error('测评问卷或题目读取异常');
  79. const examStamp = `2099-01-01 00:${String(Date.now() % 60).padStart(2, '0')}:${String(Date.now() % 60).padStart(2, '0')}`;
  80. const examAnswers = questions.result.slice(0, 2).map((item) => ({ qid: item.ID, qtype: item.QType, answer: '', result: 2 }));
  81. const answer = await cloud(token, 'vote_add', { mu: JSON.stringify({ AskID: 1, Answer: JSON.stringify(examAnswers), UserID: legacyUserId, CDate: examStamp }) });
  82. if (!Number(answer.result)) throw new Error('测评答题记录新增异常');
  83. const answerSourceKey = `app:vote:${legacyUserId}:1:${examStamp}`;
  84. const answerRows = await master(`/classes/DesignAnswer?where=${encodeURIComponent(JSON.stringify({ sourceKey: answerSourceKey }))}&limit=2`);
  85. if (answerRows.results?.length !== 1 || Number(answerRows.results[0].id) !== Number(answer.result)) throw new Error('测评答题记录写后读或幂等键异常');
  86. created.push(['DesignAnswer', answerRows.results[0].objectId]);
  87. const repeatedAnswer = await cloud(token, 'vote_add', { mu: JSON.stringify({ AskID: 1, Answer: JSON.stringify(examAnswers), UserID: legacyUserId, CDate: examStamp }) });
  88. if (Number(repeatedAnswer.result) !== Number(answer.result)) throw new Error('测评答题记录重复提交未保持幂等');
  89. const unit = await cloud(token, 'unit_record_update', { userId: legacyUserId, bookId: 16, nids: '40', wordIds: JSON.stringify({ 40: '2505,2506' }) });
  90. if (unit.result?.updated !== 1 || unit.result?.items?.[0]?.learnedCount !== 2) throw new Error('单元学习进度保存异常');
  91. created.push(['UnitLearningRecord', unit.result.items[0].objectId]);
  92. const unitRepeat = await cloud(token, 'unit_record_update', { userId: legacyUserId, bookId: 16, nids: '40', wordIds: JSON.stringify({ 40: '2505' }) });
  93. if (unitRepeat.result?.items?.[0]?.objectId !== unit.result.items[0].objectId || unitRepeat.result?.items?.[0]?.learnedCount !== 1) throw new Error('单元学习进度幂等更新异常');
  94. const favoriteMissing = await cloud(token, 'user_star_is', { id: 'future-product-1', type: 2 });
  95. if (String(favoriteMissing.result) !== '0') throw new Error('收藏初始状态异常');
  96. await cloud(token, 'user_star_add', { id: 'future-product-1', type: 2, title: '未来商品', url: '/Shop/future-product-1' });
  97. const favoriteAdded = await cloud(token, 'user_star_is', { id: 'future-product-1', type: 2 });
  98. if (String(favoriteAdded.result) !== '1') throw new Error('收藏新增后状态异常');
  99. const favoriteRows = await master(`/classes/UserFavorite?where=${encodeURIComponent(JSON.stringify({ sourceKey: `app:favorite:${legacyUserId}:2:future-product-1` }))}&limit=2`);
  100. if (favoriteRows.results?.length !== 1) throw new Error('收藏关系未写入规范类');
  101. created.push(['UserFavorite', favoriteRows.results[0].objectId]);
  102. await cloud(token, 'user_star_del', { id: 'future-product-1', type: 2 });
  103. const favoriteDeleted = await cloud(token, 'user_star_is', { id: 'future-product-1', type: 2 });
  104. if (String(favoriteDeleted.result) !== '0') throw new Error('取消收藏后状态异常');
  105. const supplierRequest = await cloud(token, 'pub_add', { pubid: 8, model: JSON.stringify({ PubTitle: '冒烟供应商', PubContent: '申请入驻', BankAccount: 'TEST-ONLY', PubIsTrue: 1 }) });
  106. const supplierRequestId = String(supplierRequest.result?.ID || '');
  107. if (!supplierRequestId || supplierRequest.result?.Category !== 'supplier-application') throw new Error('供应商服务申请新增异常');
  108. created.push(['ServiceRequest', supplierRequestId]);
  109. const supplierRequests = await cloud(token, 'pub_list', { pubid: 8, cpage: 1, psize: 20 });
  110. if (supplierRequests.page?.itemCount !== 1 || supplierRequests.result?.[0]?.ID !== supplierRequestId || supplierRequests.result?.[0]?.BankAccount !== 'TEST-ONLY') throw new Error('供应商服务申请列表异常');
  111. const returnRequest = await cloud(token, 'pub_add', { pubid: 10, model: JSON.stringify({ PubTitle: '冒烟退货', PubContent: '商品异常', PubContentid: 'order-smoke', PubIsTrue: 1 }) });
  112. const returnRequestId = String(returnRequest.result?.ID || '');
  113. if (!returnRequestId) throw new Error('售后服务申请新增异常');
  114. created.push(['ServiceRequest', returnRequestId]);
  115. const returnRequests = await cloud(token, 'pub_list', { pubid: 10, cid: 'order-smoke', cpage: 1, psize: 20 });
  116. if (returnRequests.page?.itemCount !== 1 || returnRequests.result?.[0]?.ID !== returnRequestId) throw new Error('售后服务申请按订单筛选异常');
  117. await cloud(token, 'pub_add', { pubid: 6, model: JSON.stringify({ PubTitle: '无效申请' }) }, 400);
  118. const productName = `冒烟商品-${Date.now()}`;
  119. const product = await cloud(token, 'product_add', {
  120. model: JSON.stringify({ Proname: productName, Nodeid: 7, LinPrice: 12.5, ShiPrice: 20, ProUnit: '件', Stock: 10, Thumbnails: 'https://example.test/product.png', Proinfo: '初始简介', Procontent: '<p>商品正文</p>' }),
  121. addon: JSON.stringify({ pics: JSON.stringify([{ url: 'https://example.test/product-1.png' }]) }),
  122. });
  123. const productId = Number(product.result);
  124. commerceProductId = productId;
  125. if (!productId) throw new Error('商品新增未返回规范编号');
  126. const productRows = await master(`/classes/CommerceProduct?where=${encodeURIComponent(JSON.stringify({ productId }))}&limit=2`);
  127. if (productRows.results?.length !== 1) throw new Error('商品主记录未写入规范类');
  128. const productObjectId = productRows.results[0].objectId;
  129. created.push(['CommerceProduct', productObjectId]);
  130. const draftProducts = await cloud(token, 'product_list', { uids: legacyUserId, mode: 'unaudit', skey: productName, cpage: 1, psize: 20 });
  131. if (draftProducts.page?.itemCount !== 1 || Number(draftProducts.result?.[0]?.ID) !== productId) throw new Error('待审核商品列表异常');
  132. const ownProduct = await cloud(token, 'product_get', { id: productId });
  133. if (ownProduct.result?.[0]?.Proname !== productName || Number(ownProduct.result?.[0]?.Stock) !== 10) throw new Error('商品详情投影异常');
  134. await cloud(token, 'product_upd', { id: productId, model: JSON.stringify({ ID: productId, Proinfo: '更新简介' }), addon: JSON.stringify({}) });
  135. const updatedProduct = await cloud(token, 'product_get', { id: productId });
  136. if (updatedProduct.result?.[0]?.Proinfo !== '更新简介') throw new Error('商品更新异常');
  137. await master(`/classes/CommerceProduct/${productObjectId}`, { method: 'PUT', body: JSON.stringify({ status: 'published' }) });
  138. const publicProducts = await cloud('', 'product_list', { uid: '', skey: productName, cpage: 1, psize: 20 });
  139. if (publicProducts.page?.itemCount !== 1 || Number(publicProducts.result?.[0]?.ID) !== productId) throw new Error('已发布商品公共列表异常');
  140. const publicProduct = await cloud('', 'product_get', { uid: '', id: productId });
  141. if (publicProduct.result?.[0]?.Proname !== productName) throw new Error('已发布商品公共详情异常');
  142. const stockIn = await cloud(token, 'product_stock_change', { proid: productId, num: 5, remark: '冒烟入库' });
  143. if (Number(stockIn.result?.Stock) !== 15) throw new Error('商品入库原子更新异常');
  144. const stockOut = await cloud(token, 'product_stock_change', { proid: productId, num: -2, remark: '冒烟出库' });
  145. if (Number(stockOut.result?.Stock) !== 13) throw new Error('商品出库原子更新异常');
  146. await cloud(token, 'product_stock_change', { proid: productId, num: -999, remark: '禁止负库存' }, 409);
  147. const stockLogs = await cloud(token, 'product_stock_list', { proid: productId, cpage: 1, psize: 20 });
  148. if (stockLogs.page?.itemCount !== 2 || !stockLogs.result?.some((item) => Number(item.StockNow) === 13)) throw new Error(`库存流水异常:${JSON.stringify(stockLogs)}`);
  149. for (const row of (await master(`/classes/ProductStockLedger?where=${encodeURIComponent(JSON.stringify({ productId }))}&limit=100`)).results || []) created.push(['ProductStockLedger', row.objectId]);
  150. await cloud(token, 'cart_add', { proID: productId, proNum: 1 });
  151. await cloud(token, 'cart_add', { proID: productId, proNum: 2 });
  152. const cart = await cloud(token, 'cart_list', { ids: '0' });
  153. if (cart.result?.CartDT?.length !== 1 || Number(cart.result.CartDT[0]?.Pronum) !== 3 || Number(cart.result.TotalMoney) !== 37.5) throw new Error('购物车合并或金额投影异常');
  154. const cartId = String(cart.result.CartDT[0]?.objectId || '');
  155. if (!cartId) throw new Error('购物车记录 ID 缺失');
  156. created.push(['ShoppingCartItem', cartId]);
  157. const selectedCart = await cloud(token, 'cart_list', { ids: cartId });
  158. if (selectedCart.result?.CartDT?.length !== 1) throw new Error('购物车按 ID 筛选异常');
  159. const couponId = 910000000 + Math.floor(Date.now() / 1000) % 80000000;
  160. const couponDefinition = await master('/classes/CouponDefinition', {
  161. method: 'POST',
  162. body: JSON.stringify({
  163. company,
  164. couponId,
  165. name: '冒烟满减券',
  166. amount: 5,
  167. minAmount: 20,
  168. maxAmount: 100,
  169. status: 'active',
  170. totalQuantity: 10,
  171. claimedQuantity: 0,
  172. sourceKey: `app:coupon-smoke:${couponId}`,
  173. }),
  174. }, 201);
  175. created.push(['CouponDefinition', couponDefinition.objectId]);
  176. const coupons = await cloud(token, 'coupon_list', { cpage: 1, psize: 100 });
  177. if (!coupons.result?.some((item) => Number(item.Flow) === couponId)) throw new Error('可领取优惠券列表异常');
  178. const claimedCoupon = await cloud(token, 'coupon_usrgot_add', { flowid: couponId });
  179. const userCouponId = String(claimedCoupon.result?.ID || '');
  180. if (!userCouponId || Number(claimedCoupon.result?.Flow) !== couponId) throw new Error('优惠券领取异常');
  181. created.push(['UserCoupon', userCouponId]);
  182. const claimedAgain = await cloud(token, 'coupon_usrgot_add', { flowid: couponId });
  183. if (String(claimedAgain.result?.ID) !== userCouponId) throw new Error('重复领取优惠券未保持幂等');
  184. const idleCoupons = await cloud(token, 'coupon_usrgot_list', { fast: 'idle', cpage: 1, psize: 20 });
  185. if (!idleCoupons.result?.some((item) => String(item.ID) === userCouponId)) throw new Error('用户未使用优惠券列表异常');
  186. const checkout = await cloud(token, 'payment_cart', {
  187. cartIds: cartId,
  188. use_sicon: 1,
  189. rece: JSON.stringify({ userName: '测试收货人', telNumber: '13800138000', provinceName: '江西省', cityName: '南昌市', countyName: '青山湖区', detailInfo: '测试路 1 号', postalCode: '330000' }),
  190. fareDT: '[]',
  191. invoice: '',
  192. couponid: userCouponId,
  193. });
  194. const orderId = Number(checkout.addon?.orderIds);
  195. if (!orderId || String(checkout.addon?.cartIds) !== cartId) throw new Error('学习豆订单结算响应异常');
  196. const repeatedCheckout = await cloud(token, 'payment_cart', {
  197. cartIds: cartId,
  198. use_sicon: 1,
  199. rece: JSON.stringify({ detailInfo: '幂等重试' }),
  200. fareDT: '[]',
  201. couponid: userCouponId,
  202. });
  203. if (Number(repeatedCheckout.addon?.orderIds) !== orderId) throw new Error('订单重复提交未保持幂等');
  204. await cloud(token, 'payment_success', { orderIds: orderId });
  205. await cloud(token, 'payment_success', { orderIds: orderId });
  206. await cloud(token, 'payment_cart_again', { orderId, mode: 1 });
  207. await cloud(token, 'payment_cart_again', { orderId, mode: 2 }, 501);
  208. const paidMember = await cloud(token, 'user_get');
  209. if (Number(paidMember.addon?.SilverCoin) !== 167.5) throw new Error(`学习豆扣减异常:${paidMember.addon?.SilverCoin}`);
  210. const emptyCartAfterCheckout = await cloud(token, 'cart_list', { ids: '0' });
  211. if (emptyCartAfterCheckout.result?.CartDT?.length) throw new Error('已结算购物车仍可见');
  212. const usedCoupons = await cloud(token, 'coupon_usrgot_list', { fast: 'used', cpage: 1, psize: 20 });
  213. if (!usedCoupons.result?.some((item) => String(item.ID) === userCouponId)) throw new Error('已使用优惠券状态异常');
  214. const order = await cloud(token, 'order_get', { id: orderId });
  215. if (Number(order.result?.ID) !== orderId || Number(order.result?.Ordersamount) !== 32.5 || order.addon?.cart?.length !== 1) throw new Error(`订单详情投影异常:${JSON.stringify(order)}`);
  216. const buyerOrders = await cloud(token, 'order_list', { fast: 'all', cpage: 1, psize: 20 });
  217. if (!buyerOrders.result?.some((item) => Number(item.ID) === orderId)) throw new Error('买家订单列表异常');
  218. const sellerOrders = await cloud(token, 'user_shop_order', { fast: 'paid', cpage: 1, psize: 20 });
  219. if (!sellerOrders.result?.some((item) => Number(item.ID) === orderId)) throw new Error('商户待发货订单列表异常');
  220. const sales = await cloud(token, 'user_shop_sales', { fast: 'month', cpage: 1, psize: 5 });
  221. if (!sales.result?.some((item) => Number(item.ID) === productId) || Number(sales.addon) !== 37.5) throw new Error('商户销售汇总异常');
  222. await cloud(token, 'order_delivery', { id: orderId, expComp: '顺丰快递', expNo: 'SF-TEST-0001', expRemind: '冒烟测试' });
  223. const shipped = await cloud(token, 'order_get', { id: orderId });
  224. if (Number(shipped.result?.StateLogistics) !== 1 || shipped.addon?.express?.ExpNo !== 'SF-TEST-0001') throw new Error('订单发货状态异常');
  225. await cloud(token, 'order_signfor', { id: orderId });
  226. const signed = await cloud(token, 'order_get', { id: orderId });
  227. if (Number(signed.result?.Signed) !== 1 || Number(signed.result?.StateLogistics) !== 2) throw new Error('订单签收状态异常');
  228. await cloud(token, 'order_comment_add', { orderId, proId: productId, score: 5, title: '冒烟好评', content: '订单链路验证通过', images: JSON.stringify([{ url: 'https://example.test/comment.png' }]) });
  229. await cloud(token, 'order_comment_add', { orderId, proId: productId, score: 5, title: '重复评价' }, 409);
  230. const publicComments = await cloud('', 'order_comment_list', { uid: '', proId: productId, rate: 'good', hasImgOnly: 1, cpage: 1, psize: 20 });
  231. if (publicComments.page?.itemCount !== 1 || publicComments.result?.[0]?.Title !== '冒烟好评') throw new Error('商品评价公共列表异常');
  232. const afterOrderProduct = await cloud(token, 'product_get', { id: productId });
  233. if (Number(afterOrderProduct.result?.[0]?.Stock) !== 10) throw new Error('订单库存扣减异常');
  234. for (const className of ['CommerceOrder', 'CommerceOrderItem', 'CommercePayment', 'ProductComment']) {
  235. const orderRows = await master(`/classes/${className}?where=${encodeURIComponent(JSON.stringify(className === 'ProductComment' ? { productId } : { orderId }))}&limit=100`);
  236. for (const row of orderRows.results || []) created.push([className, row.objectId]);
  237. }
  238. const membershipTargetLegacyId = legacyUserId + 1;
  239. const membershipTarget = await master('/users', {
  240. method: 'POST',
  241. body: JSON.stringify({
  242. username: `${username}_agent_target`,
  243. password: randomBytes(24).toString('base64url'),
  244. company,
  245. legacyUserId: membershipTargetLegacyId,
  246. legacyGroupId: 1,
  247. nickname: '代理库存目标会员',
  248. legacyUserData: { UserID: membershipTargetLegacyId, GroupID: 1, ParentUserID: legacyUserId, HoneyName: '代理库存目标会员', boffExp: 0 },
  249. }),
  250. }, 201);
  251. membershipTargetUserId = membershipTarget.objectId;
  252. await master(`/users/${userObjectId}`, {
  253. method: 'PUT',
  254. body: JSON.stringify({ legacyGroupId: 4, legacyUserData: { UserID: legacyUserId, GroupID: 4, HoneyName: '会员资料云函数冒烟', SilverCoin: 167.5, boffExp: 5000 } }),
  255. });
  256. const upgradeKey = `smoke-upgrade-${Date.now()}`;
  257. await cloud(token, 'user_group_usr_upgrade', { upusr: membershipTargetLegacyId, gid: 2, idempotencyKey: upgradeKey });
  258. await cloud(token, 'user_group_usr_upgrade', { upusr: membershipTargetLegacyId, gid: 2, idempotencyKey: upgradeKey });
  259. const supplyKey = `smoke-supply-${Date.now()}`;
  260. await cloud(token, 'user_group_usr_supply', { upusr: membershipTargetLegacyId, munber: 250, idempotencyKey: supplyKey });
  261. const operatorAfterStock = await master(`/users/${userObjectId}`);
  262. const targetAfterStock = await master(`/users/${membershipTargetUserId}`);
  263. if (Number(operatorAfterStock.legacyUserData?.boffExp) !== 3750 || Number(targetAfterStock.legacyGroupId) !== 2 || Number(targetAfterStock.legacyUserData?.boffExp) !== 1250) throw new Error('代理招募或库存批发原子余额异常');
  264. const membershipLedgers = await master(`/classes/MembershipStockLedger?where=${encodeURIComponent(JSON.stringify({ operatorLegacyId: legacyUserId }))}&limit=20`);
  265. if (membershipLedgers.results?.length !== 2) throw new Error('代理库存流水数量或幂等异常');
  266. for (const row of membershipLedgers.results || []) created.push(['MembershipStockLedger', row.objectId]);
  267. await cloud(token, 'product_sale_change', { id: productId, status: 0 });
  268. await cloud('', 'product_get', { uid: '', id: productId }, 404);
  269. await cloud(token, 'product_sale_change', { id: productId, status: 1 });
  270. await cloud(token, 'product_del', { id: productId });
  271. const emptyCartAfterRecycle = await cloud(token, 'cart_list', { ids: '0' });
  272. if (emptyCartAfterRecycle.result?.CartDT?.length) throw new Error('已回收商品仍出现在购物车');
  273. await cloud(token, 'receaddr_del', { id: addressId });
  274. await cloud(token, 'invoice_del', { id: invoiceId });
  275. await cloud(token, 'user_bank_del', { id: bankId });
  276. const emptyAddress = await cloud(token, 'receaddr_list', { cpage: 1, psize: 20 });
  277. const emptyInvoice = await cloud(token, 'invoice_list', { cpage: 1, psize: 20 });
  278. const emptyBank = await cloud(token, 'user_bank_list', { cpage: 1, psize: 20 });
  279. if (emptyAddress.page?.itemCount || emptyInvoice.page?.itemCount || emptyBank.page?.itemCount) throw new Error('软删除后的会员资料仍出现在列表');
  280. const auditWhere = encodeURIComponent(JSON.stringify({ legacyUserId }));
  281. const audits = await master(`/classes/AppAuditLog?where=${auditWhere}&count=1&limit=0`);
  282. if (Number(audits.count) < 30) throw new Error(`会员资料、商城与学习操作审计数量不足:${audits.count || 0}`);
  283. const direct = await fetch(`${PARSE_URL}/classes/ReceivingAddress?limit=1`, { headers: { 'X-Parse-Application-Id': APP_ID } });
  284. if (direct.ok) throw new Error('会员敏感资料类未关闭浏览器直接读取权限');
  285. const directServiceRequest = await fetch(`${PARSE_URL}/classes/ServiceRequest?limit=1`, { headers: { 'X-Parse-Application-Id': APP_ID } });
  286. if (directServiceRequest.ok) throw new Error('服务申请类未关闭浏览器直接读取权限');
  287. for (const className of ['CommerceProduct', 'ShoppingCartItem', 'ProductStockLedger', 'ProductComment', 'CommerceOrder', 'CommerceOrderItem', 'CommercePayment', 'CouponDefinition', 'UserCoupon', 'MembershipStockLedger']) {
  288. const directCommerce = await fetch(`${PARSE_URL}/classes/${className}?limit=1`, { headers: { 'X-Parse-Application-Id': APP_ID } });
  289. if (directCommerce.ok) throw new Error(`${className} 未关闭浏览器直接读取权限`);
  290. }
  291. console.log(JSON.stringify({ passed: true, reviewStats: true, address: true, invoice: true, bank: true, vote: true, unitProgress: true, favorite: true, serviceRequests: true, commerceProducts: true, stockLedger: true, cart: true, coupons: true, orders: true, internalPayment: true, comments: true, sellerSales: true, membershipStock: true, audit: Number(audits.count), directClassAccessDenied: true }, null, 2));
  292. } finally {
  293. for (const [className, objectId] of created.reverse()) await hardDelete(className, objectId);
  294. if (commerceProductId) {
  295. const productWhere = encodeURIComponent(JSON.stringify({ productId: commerceProductId }));
  296. for (const className of ['ShoppingCartItem', 'ProductStockLedger', 'ProductComment', 'CommerceOrderItem', 'CommerceProduct']) {
  297. const rows = await master(`/classes/${className}?where=${productWhere}&limit=1000`).catch(() => ({ results: [] }));
  298. for (const row of rows.results || []) await hardDelete(className, row.objectId);
  299. }
  300. }
  301. if (legacyUserId) {
  302. const where = encodeURIComponent(JSON.stringify({ legacyUserId }));
  303. for (const className of ['CommerceOrder', 'CommercePayment', 'UserCoupon']) {
  304. const rows = await master(`/classes/${className}?where=${where}&limit=1000`).catch(() => ({ results: [] }));
  305. for (const row of rows.results || []) await hardDelete(className, row.objectId);
  306. }
  307. const logs = await master(`/classes/AppAuditLog?where=${where}&limit=1000`).catch(() => ({ results: [] }));
  308. for (const row of logs.results || []) await hardDelete('AppAuditLog', row.objectId);
  309. const membershipWhere = encodeURIComponent(JSON.stringify({ operatorLegacyId: legacyUserId }));
  310. const membershipRows = await master(`/classes/MembershipStockLedger?where=${membershipWhere}&limit=1000`).catch(() => ({ results: [] }));
  311. for (const row of membershipRows.results || []) await hardDelete('MembershipStockLedger', row.objectId);
  312. }
  313. if (userObjectId) {
  314. const pointer = { __type: 'Pointer', className: '_User', objectId: userObjectId };
  315. const where = encodeURIComponent(JSON.stringify({ user: pointer }));
  316. const sessions = await master(`/sessions?where=${where}&limit=1000`).catch(() => ({ results: [] }));
  317. for (const row of sessions.results || []) await master(`/sessions/${row.objectId}`, { method: 'DELETE' }).catch(() => undefined);
  318. await master(`/users/${userObjectId}`, { method: 'DELETE' }).catch(() => undefined);
  319. }
  320. if (membershipTargetUserId) await master(`/users/${membershipTargetUserId}`, { method: 'DELETE' }).catch(() => undefined);
  321. }