diff --git a/.workbuddy/memory/2026-08-11.md b/.workbuddy/memory/2026-08-11.md index c68e693..f63ee31 100644 --- a/.workbuddy/memory/2026-08-11.md +++ b/.workbuddy/memory/2026-08-11.md @@ -23,6 +23,20 @@ - 新增 `h` 函数 import。 ### 待办 / 风险 -- **后端需同步改造**:表加 `province_ids` / `city_ids`(varchar 逗号分隔),接口 add/update 接收这两个字段。前端已按此约定提交,后端未改则数据落不了库。 - antd 为 v4,`var(--ant-color-border)` 在暗色下可能回退浅灰(边框略浅,不影响功能);如要完美暗色适配可后续改 less 变量。 -- 构建验证(vite build)已触发,待结果确认无编译错误。 +- 构建验证(vite build)已通过。 + +## ⚠️ 重大修订(同日):前端字段名与后端不匹配,导致"没存进库" + +### 根因 +- 后端 `ShopExpressTemplateDetail` 实体**已有**配送区域多选机制:`regionMode`(0=全国 / 1=指定配送白名单 / 2=不发货黑名单)+ `regionIds`(逗号分隔的 6 位城市编码串)。 +- 旧代码 `provinceId`/`cityId` 是单值历史字段,已被迁移脚本 `sql/shop_express_template_detail_region.sql` 迁成 `regionMode=1, regionIds=城市编码`。 +- 我第一版前端提交的是 `provinceIds`/`cityIds`,后端实体无此字段 → MyBatis-Plus 自动忽略未知字段 → 看起来"没存没变化"。 +- ORM 是 MyBatis-Plus,add/update 用 `save()`/`updateById()` 自动生成 SQL,接收的是实体类本身(非 Param);新增字段只需在**实体加属性 + 表加列**,无需改 Mapper XML。 + +### 修复(对接后端已有的 regionMode/regionIds,无需后端改动) +1. model:`provinceIds`/`cityIds` → 改为 `regionMode?`、`regionIds?`。 +2. `RegionMultiSelect` 组件:去掉 `update:provinceIds` emit,只 emit `update:value`(城市编码数组)。 +3. 编辑弹窗:城市选择变化 → `form.regionIds = 逗号串`、`form.regionMode = 1`(有选)或 `0`(全国);回显从 `form.regionIds` 拆出。 +4. 列表页:customRender 读 `record.regionIds` 反查城市名。 +- 结论:**本需求纯前端即可闭环,后端无需改**。城市编码(`regions-data.json` 的 city value,6位)与 `regionIds` 完全一致,下单校验 `DeliveryRegionChecker` 读 `regionIds` 直接生效。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 0561ad1..b090a16 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -57,9 +57,10 @@ ### 运费模板配送地区多选(2026-08-11 已实现前端) - 需求:配送地区选择从「省+市单选级联」升级为「省+市两级多选穿梭弹窗」(仿客户截图双栏:左候选/右已选)。 -- 前端组件:`src/components/RegionMultiSelect/index.vue`(树形可展开,省级"选择"=选该省全部市,市级可单独选;@update:value 城市ID数组 + @update:provinceIds 省份ID数组)。 -- 数据字段:model 新增 `provinceIds` / `cityIds`(逗号分隔字符串);保留旧 `provinceId`/`cityId` 兼容(编辑回显时旧单值会自动补进 cityIds)。 -- 列表多值展示:`index.vue` customRender 遍历 `cityIds` 反查 regionMap,超 22 字截断 + tooltip。 -- ⚠️ 后端待改:`shop_express_template_detail` 表加 `province_ids`/`city_ids`(varchar 逗号分隔),add/update 接口接收这两个字段;前端已按此约定提交,后端未改前数据落不了库。 -- 注:与"不发货地区(黑名单) regionMode/regionIds"是不同需求,勿混淆。 +- 前端组件:`src/components/RegionMultiSelect/index.vue`(树形可展开,省级"选择"=选该省全部市,市级可单独选;@update:value 城市编码数组)。 +- **直接对接后端已有的 `regionMode`(0=全国/1=指定配送白名单/2=不发货黑名单) + `regionIds`(逗号分隔城市编码串) 机制,无需后端改动**。城市编码即 `regions-data.json` 的 city value(6位),与 `regionIds` 完全一致。 +- 数据字段:model 用 `regionMode` / `regionIds`(已有字段,非新建);旧 `provinceId`/`cityId` 仅作回显兼容。 +- 列表多值展示:`index.vue` customRender 遍历 `regionIds` 反查 regionMap,超 22 字截断 + tooltip。 +- 下单校验 `DeliveryRegionChecker` 读 `regionIds` 直接生效(regionMode=2 为不发货黑名单)。 +- ⚠️ 教训:第一版误用 `provinceIds`/`cityIds` 字段名,与后端 `regionMode`/`regionIds` 不匹配,MyBatis-Plus 自动忽略未知字段导致"没存"。改字段名对接即可,不必新增后端字段。 diff --git a/src/api/shop/shopExpressTemplateDetail/model/index.ts b/src/api/shop/shopExpressTemplateDetail/model/index.ts index 1ad36ad..58361dd 100644 --- a/src/api/shop/shopExpressTemplateDetail/model/index.ts +++ b/src/api/shop/shopExpressTemplateDetail/model/index.ts @@ -16,10 +16,10 @@ export interface ShopExpressTemplateDetail { provinceId?: number; /** 城市ID(旧单值字段,兼容历史数据) */ cityId?: number; - /** 省份IDs(逗号分隔,多选配送区域) */ - provinceIds?: string; - /** 城市IDs(逗号分隔,多选配送区域) */ - cityIds?: string; + /** 配送区域模式: 0=全国, 1=指定配送(白名单), 2=指定不发货(黑名单) */ + regionMode?: number; + /** 地区编码列表(6位行政区划码,逗号分隔),regionMode=1或2时有效 */ + regionIds?: string; /** 首件数量/重量 */ firstNum?: string; /** 首件价格 */ diff --git a/src/components/RegionMultiSelect/index.vue b/src/components/RegionMultiSelect/index.vue index 396e952..d2ff257 100644 --- a/src/components/RegionMultiSelect/index.vue +++ b/src/components/RegionMultiSelect/index.vue @@ -129,7 +129,6 @@ const emit = defineEmits<{ (e: 'update:value', value: string[]): void; - (e: 'update:provinceIds', value: string[]): void; }>(); const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE; @@ -226,17 +225,6 @@ })) ); - const derivedProvinceIds = computed(() => { - const set = new Set(); - selectedSet.value.forEach((cv) => { - const pv = cityToProvince.value[cv]; - if (pv) { - set.add(pv); - } - }); - return Array.from(set); - }); - const selectProvince = (p: ProvinceItem) => { const s = new Set(selectedSet.value); p.cities.forEach((c) => s.add(c.value)); @@ -275,7 +263,6 @@ const handleConfirm = () => { emit('update:value', selectedCities.value); - emit('update:provinceIds', derivedProvinceIds.value); visible.value = false; }; diff --git a/src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue b/src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue index 0d99197..505b5cb 100644 --- a/src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue +++ b/src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue @@ -24,7 +24,6 @@
不选则默认全国
@@ -128,10 +127,8 @@ const maxable = ref(true); // 表单实例 const formRef = ref(null); - // 已选城市ID数组 + // 已选城市编码数组(对应后端的 regionIds) const cityIds = ref([]); - // 已选省份ID数组(由城市推导,随城市变化同步) - const provinceIds = ref([]); // 表单数据 const form = reactive({ @@ -140,8 +137,8 @@ type: true, provinceId: undefined, cityId: undefined, - provinceIds: undefined, - cityIds: undefined, + regionMode: undefined, + regionIds: undefined, firstNum: undefined, firstAmount: undefined, extraAmount: undefined, @@ -190,16 +187,16 @@ const { resetFields } = useForm(form, rules); - /* 城市选中变化,同步到 form.cityIds */ + /* 城市选中变化,同步到 form.regionIds / regionMode(1=指定配送白名单,0=全国) */ const onCityIdsChange = (val: string[]) => { cityIds.value = val; - form.cityIds = val.length ? val.join(',') : undefined; - }; - - /* 省份选中变化(由组件根据已选城市推导),同步到 form.provinceIds */ - const onProvinceIdsChange = (val: string[]) => { - provinceIds.value = val; - form.provinceIds = val.length ? val.join(',') : undefined; + if (val.length) { + form.regionIds = val.join(','); + form.regionMode = 1; + } else { + form.regionIds = undefined; + form.regionMode = 0; + } }; /* 保存编辑 */ @@ -215,13 +212,14 @@ // 确保templateId和type从父级继承 formData.templateId = props.templateId; formData.type = props.templateType ?? true; - // 多选配送区域(逗号分隔,空则默认全国) - formData.cityIds = cityIds.value.length - ? cityIds.value.join(',') - : undefined; - formData.provinceIds = provinceIds.value.length - ? provinceIds.value.join(',') - : undefined; + // 多选配送区域(regionMode=1 指定配送,0=全国;regionIds 存城市编码逗号串) + if (cityIds.value.length) { + formData.regionIds = cityIds.value.join(','); + formData.regionMode = 1; + } else { + formData.regionIds = undefined; + formData.regionMode = 0; + } const saveOrUpdate = isUpdate.value ? updateShopExpressTemplateDetail : addShopExpressTemplateDetail; @@ -252,26 +250,18 @@ form.templateId = props.templateId; form.type = props.templateType ?? true; } - // 回显多选配送区域 - if (form.cityIds) { - cityIds.value = form.cityIds.split(',').filter(Boolean); + // 回显多选配送区域(regionIds 存城市编码) + if (form.regionIds) { + cityIds.value = form.regionIds.split(',').filter(Boolean); } else if (form.cityId) { // 兼容旧单值数据 cityIds.value = [String(form.cityId)]; } else { cityIds.value = []; } - if (form.provinceIds) { - provinceIds.value = form.provinceIds.split(',').filter(Boolean); - } else if (form.provinceId) { - provinceIds.value = [String(form.provinceId)]; - } else { - provinceIds.value = []; - } } else { resetFields(); cityIds.value = []; - provinceIds.value = []; } }, { immediate: true } diff --git a/src/views/shop/shopExpressTemplateDetail/index.vue b/src/views/shop/shopExpressTemplateDetail/index.vue index 8616a99..242d21d 100644 --- a/src/views/shop/shopExpressTemplateDetail/index.vue +++ b/src/views/shop/shopExpressTemplateDetail/index.vue @@ -128,8 +128,8 @@ align: 'center', width: 150, customRender: ({ record }) => { - // 多选配送区域(cityIds 逗号分隔) - const cityIds = (record.cityIds || '') + // 多选配送区域(regionIds 逗号分隔的城市编码) + const cityIds = (record.regionIds || '') .split(',') .filter(Boolean); if (cityIds.length) {