Просмотр исходного кода

chore: document blocked vendor migration

彭峰 1 месяц назад
Родитель
Сommit
1830e5692a

+ 2 - 1
docs/migration/admin-migration-status.md

@@ -45,8 +45,9 @@
 - 旧客服后台 `Default/Seat/SeatAdd/SeatAdd_Submit/CodeAdd/CodeAdd_Submit/Seat_API/CodeList` 已映射到 `ServiceSeat` 与 `Temp(useType=12)`:`serviceSeats/saveServiceSeat` 支持帐套隔离列表、现有编辑、绑定同帐套有效账号、默认席位互斥、排序与原子生成 `sId/sourceKey`,`serviceCodes/saveServiceCode` 只读取并白名单维护客服欢迎语的名称、HTML 内容和主题,`serviceBatch` 覆盖席位及欢迎语删除。Angular 新增客服席位、客服欢迎语入口、账号绑定控件和批量删除。旧逻辑在用户名不存在时以固定密码 `123456` 自动开户,该弱密码行为不迁移:云函数明确 501,并要求先走现有安全开户流程。`Temp.id` 与 Parse 保留字段冲突且省略 ID 无法生成旧编号,因此欢迎语新增明确阻塞。目标 Schema 完全缺少 `ChatMsg` 或等价消息类,`MsgEx/MsgInfo/MsgList` 以及 `Seat_API.chat_del` 由 `legacyServiceChatBlocker` 返回逐项数据阻塞原因。
 - 旧店铺后台 `StoreManage/Store_API/StoreStyle/Style_API/StoreStyleAdd/StoreStyleAdd_Submit/StoreUpdate/StoreUpdate_Submit` 已映射到规范化 `StoreApplication/StoreStyle`:店铺按状态和关键词筛选,支持审核、取消审核、推荐、取消推荐,现有资料可维护同帐套所属账号、样式、联系方式、地址和展示内容;样式可维护三类模板路径、缩略图和说明,删除前检查店铺引用。Angular 新增店铺、店铺样式入口、状态工具和归属/样式选择。店铺与样式旧数字 ID 都不能通过兼容层安全新增,店铺创建还依赖已丢失的模型 6 动态字段链,因此新增明确 501。旧 `ZL_Commodities` 商品主表完全缺失,目标 `Product/StoreProduct` 只有图片等扩展碎片,无法核验店铺商品引用,所以真实删除店铺与 `Product` 页面均明确数据阻塞,未冒险制造孤儿商品。
 - 旧内容附加功能 `TagKey/TagKey_Submit` 已迁移为 Angular 全局内容标签词库:保留管道符分隔、移除空白/引号/反斜杠和原顺序语义,改由 `saveContentTagKeys` 管理云函数写入 Parse Config,不再依赖旧 ASP.NET 服务器的 `/js/ICMS/tags.json` 本地文件。线上完整 Schema 核验不存在 `Comment/ContentAudit/ContentVerBak` 或等价类,因此 `Comment/Commont_API`、4 个内容审核流动作以及 `VerBak/VerBak_Del` 均由 `legacyContentAddonBlocker` 返回逐项数据阻塞原因,未用 `CommonModel` 状态伪造丢失的审批历史。
+- 旧商城供应商 `Vendor/Vendor_API/VendorAdd/VendorAdd_Submit` 全部依赖未迁入的 `ZL_Shop_Vendor`:线上 Schema 与签约数据库文档均没有 `Vendor/ShopVendor/Manufacturers`。现有 `Agency` 只有机构编号/名称/负责人,`DeliveryCenter` 只有交付中心编号/负责人,都缺供应商地址、手机、传真、邮箱和状态,不能等价映射。`legacyShopVendorBlocker` 已按动作返回数据阻塞原因,未擅自新建未签约数据类或污染机构数据。
 - `_User`、`CommonModel`、`Node` 与 `Special` 表格已补齐当前页全选和批量工具栏,可直接批量停用/解锁/移动用户组,审核、回收、恢复、移动内容节点,回收、恢复、移动栏目,以及移动专题;翻页或重新查询会清空选择,避免误操作隐藏页记录。
-- 迁移状态页已接入从反编译基线自动生成的 1,042 项旧后台动作台账,可按模块、action、原因与状态搜索筛选;当前保守登记为 91 项已实现、59 项部分实现、135 项数据阻塞、203 项基础设施阻塞、554 项待逐项核验。生成器和 `admin-coverage:check` 会阻止台账与源码基线漂移。
+- 迁移状态页已接入从反编译基线自动生成的 1,042 项旧后台动作台账,可按模块、action、原因与状态搜索筛选;当前保守登记为 91 项已实现、59 项部分实现、139 项数据阻塞、203 项基础设施阻塞、550 项待逐项核验。生成器和 `admin-coverage:check` 会阻止台账与源码基线漂移。
 - Parse 登录只在浏览器保存当前 `sessionToken`;`masterKey` 只在部署进程中使用。
 - 云函数网关强制校验管理员身份、`company` 帐套、类白名单、字段白名单和敏感字段过滤。
 - `_Session` 不开放,`Function` 源码不开放通用查询或编辑;用户密码只允许专用重置操作。

+ 8 - 0
scripts/deploy-admin-functions.mjs

@@ -1208,6 +1208,14 @@ async function handler(request, response) {
         VerBak_Del: '目标 Schema 缺少可定位和删除的旧 ZL_Content_VerBak 版本记录'
       }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的内容附加功能动作'); fail(501, 'migration_blocked: ' + blockers[action]);
     }
+    if (operation === 'legacyShopVendorBlocker') {
+      const action = String(input.action || ''); const blockers = {
+        Vendor: '目标 Schema 缺少旧 ZL_Shop_Vendor 供应商主表与历史供应商数据',
+        Vendor_API: '目标没有可定位和删除的供应商记录;Agency/DeliveryCenter 与供应商语义不等价',
+        VendorAdd: '目标 Schema 没有可承载供应商名称、地址、手机、传真、邮箱和状态的签约数据类',
+        VendorAdd_Submit: '按既定数据库设计不能擅自新建 ShopVendor 类,也不能把供应商字段错写到 Agency 或 DeliveryCenter'
+      }; if (!Object.prototype.hasOwnProperty.call(blockers, action)) fail(400, '不支持的旧供应商动作'); fail(501, 'migration_blocked: ' + blockers[action]);
+    }
     if (operation === 'saveExamClass') {
       const objectId = String(input.objectId || '');
       const payload = input.fields && typeof input.fields === 'object' ? input.fields : {};

+ 4 - 0
scripts/generate-admin-coverage.mjs

@@ -155,6 +155,10 @@ const explicit = new Map([
   ['Content / Addon/TagKey_Submit', ['implemented', '已由 saveContentTagKeys 清理标签并持久化到 Parse Config']],
   ['Content / Addon/VerBak', ['blocked-data', '目标 Schema 缺少旧 ZL_Content_VerBak 内容/商品版本快照与历史数据']],
   ['Content / Addon/VerBak_Del', ['blocked-data', '目标无可定位和删除的旧 ZL_Content_VerBak 版本记录']],
+  ['Shop / Addon/Vendor', ['blocked-data', '目标 Schema 缺少旧 ZL_Shop_Vendor 供应商主表与历史数据']],
+  ['Shop / Addon/Vendor_API', ['blocked-data', '目标没有可定位和删除的供应商记录;Agency/DeliveryCenter 语义不等价']],
+  ['Shop / Addon/VendorAdd', ['blocked-data', '目标无签约数据类承载名称、地址、手机、传真、邮箱和状态']],
+  ['Shop / Addon/VendorAdd_Submit', ['blocked-data', '按既定数据库设计不能擅自新建 ShopVendor,也不能错写 Agency/DeliveryCenter']],
   ['FontPic/Index', ['blocked-data', '目标 Schema 缺少旧 FontPic 成品字体图主表与历史数据']],
   ['FontPic/FontPicInfo', ['blocked-data', '目标 Schema 缺少旧 FontPic 成品字体图详情数据']],
   ['FontPic/FontPicInfo_Submit', ['blocked-data', '目标 Schema 缺少旧 FontPic 成品字体图持久化模型']],

+ 2 - 1
scripts/smoke-admin-functions.mjs

@@ -344,6 +344,7 @@ try {
   const contentTagSchema = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'schema', className: 'ContentTagKey' }); if (contentTagSchema.className !== 'ContentTagKey' || contentTagSchema.writable !== true || contentTagSchema.creatable !== false || contentTagSchema.fields?.some((field) => field.writable)) throw new Error('内容标签虚拟 Schema 异常');
   const contentTagPage = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'contentTagKeys', className: 'ContentTagKey', page: 1, pageSize: 20, search: '' }); const originalContentTags = Array.isArray(contentTagPage.results?.[0]?.tags) ? contentTagPage.results[0].tags.map(String) : []; contentTagsRestore = originalContentTags; const tagProbe = `angular-smoke-${Date.now()}`; const probeContentTags = originalContentTags.length >= 1000 ? [...originalContentTags.slice(0, 999), tagProbe] : [...originalContentTags, tagProbe]; const savedContentTags = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'saveContentTagKeys', className: 'ContentTagKey', objectId: 'global-content-tags', tags: probeContentTags }); if (savedContentTags.count !== probeContentTags.length || savedContentTags.tags?.at(-1) !== tagProbe) throw new Error('内容标签 Parse Config 保存异常'); const readContentTags = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'getContentTagKeys', className: 'ContentTagKey', objectId: 'global-content-tags' }); if (readContentTags.tags?.at(-1) !== tagProbe) throw new Error('内容标签 Parse Config 写后读失败'); await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'saveContentTagKeys', className: 'ContentTagKey', objectId: 'global-content-tags', tags: originalContentTags }); contentTagsRestore = null;
   for (const action of ['Comment','Commont_API','ConAudit_API','ConAudit_SJ_API','ConAudit','ConAuditDetail','VerBak','VerBak_Del']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyContentAddonBlocker', action }, 501); if (!String(blocked.message).startsWith('migration_blocked:')) throw new Error(`内容附加动作 ${action} 未返回明确数据阻塞`); }
+  for (const action of ['Vendor','Vendor_API','VendorAdd','VendorAdd_Submit']) { const blocked = await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'legacyShopVendorBlocker', action }, 501); if (!String(blocked.message).startsWith('migration_blocked:')) throw new Error(`旧供应商动作 ${action} 未返回明确数据阻塞`); }
   await callFunctionError('xiaoshu/admin/gateway', login.sessionToken, { operation: 'save', className: 'Node', fields: { nodeName: '禁止通用新增' } }, 400);
   const nodeSuffix = Date.now().toString(36);
   const rootNode = await callFunction('xiaoshu/admin/gateway', login.sessionToken, { operation: 'saveNode', className: 'Node', parentId: 0, fields: { nodeName: `云函数冒烟栏目-${nodeSuffix}`, nodeDir: `smoke-${nodeSuffix}`, nodeType: 1, contentModel: '54' } });
@@ -779,7 +780,7 @@ try {
   const productBlocked = await callLegacyFunction('', { action: 'product_list' }, 501);
   if (!String(productBlocked.retmsg).includes('ZL_Commodities')) throw new Error('缺商品主表的读取接口未明确阻塞');
 
-  console.log('Cloud smoke passed: admin auth/tenant/account lifecycle/group sync/node/special/guest-category/bar/exam-class/exam-point/knowledge/exam-teacher/dictionary/grade-dictionary/currency/holiday/search-navigation/advertising/font-shapes/surveys/customer-service/stores/content-tags/api-catalog/config-blockers/model/guestbook lifecycle/content workflow/CRUD reads, CMS projections, normalized learning joins, app action coverage, public content, session scope, explicit blocked APIs.');
+  console.log('Cloud smoke passed: admin auth/tenant/account lifecycle/group sync/node/special/guest-category/bar/exam-class/exam-point/knowledge/exam-teacher/dictionary/grade-dictionary/currency/holiday/search-navigation/advertising/font-shapes/surveys/customer-service/stores/content-tags/shop-vendor-blockers/api-catalog/config-blockers/model/guestbook lifecycle/content workflow/CRUD reads, CMS projections, normalized learning joins, app action coverage, public content, session scope, explicit blocked APIs.');
 } finally {
   if (adZoneRestore) await jsonRequest(`${PARSE_URL}/classes/AdZone/${adZoneRestore.objectId}`, { method: 'PUT', body: JSON.stringify({ remind: adZoneRestore.remind, zstatus: adZoneRestore.zstatus }) }, true).catch((error) => { console.error(`广告位冒烟数据恢复失败:${error.message}`); process.exitCode = 1; });
   if (adInfoRestore) await jsonRequest(`${PARSE_URL}/classes/AdInfo/${adInfoRestore.objectId}`, { method: 'PUT', body: JSON.stringify({ remind: adInfoRestore.remind, zstatus: adInfoRestore.zstatus }) }, true).catch((error) => { console.error(`广告内容冒烟数据恢复失败:${error.message}`); process.exitCode = 1; });

+ 3 - 0
src/app/admin/legacy-admin-coverage.generated.spec.ts

@@ -83,6 +83,9 @@ describe('legacy admin coverage inventory', () => {
     expect(byKey.get('Content / Addon/Comment')?.status).toBe('blocked-data');
     expect(byKey.get('Content / Addon/ConAudit')?.status).toBe('blocked-data');
     expect(byKey.get('Content / Addon/VerBak')?.status).toBe('blocked-data');
+    expect(byKey.get('Shop / Addon/Vendor')?.status).toBe('blocked-data');
+    expect(byKey.get('Shop / Addon/Vendor_API')?.status).toBe('blocked-data');
+    expect(byKey.get('Shop / Addon/VendorAdd_Submit')?.status).toBe('blocked-data');
     expect(byKey.get('Exam/Teacher_Add')?.status).toBe('implemented');
     expect(byKey.get('User/User_API')?.status).toBe('partial');
     expect(byKey.get('Order/OrderList')?.status).toBe('blocked-data');

+ 8 - 8
src/app/admin/legacy-admin-coverage.generated.ts

@@ -146,29 +146,29 @@ export const LEGACY_ADMIN_COVERAGE: readonly LegacyAdminCoverageEntry[] = [
     "module": "Shop / Addon",
     "action": "Vendor",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标 Schema 缺少旧 ZL_Shop_Vendor 供应商主表与历史数据"
   },
   {
     "module": "Shop / Addon",
     "action": "Vendor_API",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标没有可定位和删除的供应商记录;Agency/DeliveryCenter 语义不等价"
   },
   {
     "module": "Shop / Addon",
     "action": "VendorAdd",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "目标无签约数据类承载名称、地址、手机、传真、邮箱和状态"
   },
   {
     "module": "Shop / Addon",
     "action": "VendorAdd_Submit",
     "occurrence": 1,
-    "status": "pending",
-    "reason": "尚未逐项迁移或完成等价性核验"
+    "status": "blocked-data",
+    "reason": "按既定数据库设计不能擅自新建 ShopVendor,也不能错写 Agency/DeliveryCenter"
   },
   {
     "module": "User / Addon",