Sfoglia il codice sorgente

fix: 对齐新品申请前后端与haha-sdk数据需求

- 修复前端字段名与后端实体不匹配导致数据静默丢失(productName→name, costPrice→cost)
- 新增 NewProductApplySubmitDTO 统一前后端数据契约,添加 brand/unit 字段
- Web管理后台重做表单:9张分类图片上传、商品类型/标品/货道参数等缺失字段
- 移动端补全商品类型/标品/货道参数/备注字段,图片上传改为分类标签位
- 新增 /upload 文件上传端点及静态资源映射
- vite代理配置添加 /upload 路径

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 settimane fa
parent
commit
0259689fe8

+ 10 - 0
haha-admin-mp/src/api/newProductApply.ts

@@ -21,6 +21,16 @@ export interface NewProductApplyItem {
   costPrice?: number;
   images?: string;
   description?: string;
+  productType?: number;
+  standard?: number;
+  stickerNum?: string;
+  length?: number;
+  width?: number;
+  height?: number;
+  exhibitHeight?: number;
+  columns?: number;
+  numbers?: number;
+  reason?: string;
   status?: number;
   applicantId?: number;
   applicantName?: string;

+ 331 - 63
haha-admin-mp/src/pages/products/apply-form.vue

@@ -130,36 +130,159 @@
 
       <view class="form-section">
         <view class="section-title">
-          <text class="section-title-text">商品图片</text>
+          <text class="section-title-text">加工图(5张,按顺序上传)</text>
         </view>
 
         <view class="image-upload-area">
-          <view class="image-list">
-            <view
-              class="image-item"
-              v-for="(img, index) in imageList"
-              :key="index"
-            >
-              <image
-                class="upload-image"
-                :src="img"
-                mode="aspectFill"
-                @click="previewImage(index)"
-              />
-              <view v-if="!isView" class="image-delete" @click="removeImage(index)">
+          <view class="labeled-image-list">
+            <view class="labeled-image-item" v-for="(item, index) in processedImageSlots" :key="'p'+index">
+              <text class="labeled-image-title">{{ item.label }}</text>
+              <view class="label-image-wrap">
+                <image
+                  v-if="item.url"
+                  class="upload-image"
+                  :src="item.url"
+                  mode="aspectFill"
+                  @click="previewLabeledImage(item.url)"
+                />
+                <view v-else-if="!isView" class="image-add" @click="chooseLabeledImage(index, 'processed')">
+                  <text class="add-icon">+</text>
+                </view>
+              </view>
+              <view v-if="!isView && item.url" class="label-image-delete" @click="removeLabeledImage(index, 'processed')">
                 <text class="delete-icon">×</text>
               </view>
             </view>
-            <view
-              v-if="!isView && imageList.length < maxImageCount"
-              class="image-add"
-              @click="chooseImage"
-            >
-              <text class="add-icon">+</text>
-              <text class="add-text">上传图片</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="form-section">
+        <view class="section-title">
+          <text class="section-title-text">原图(4张,按顺序上传)</text>
+        </view>
+
+        <view class="image-upload-area">
+          <view class="labeled-image-list">
+            <view class="labeled-image-item" v-for="(item, index) in originalImageSlots" :key="'o'+index">
+              <text class="labeled-image-title">{{ item.label }}</text>
+              <view class="label-image-wrap">
+                <image
+                  v-if="item.url"
+                  class="upload-image"
+                  :src="item.url"
+                  mode="aspectFill"
+                  @click="previewLabeledImage(item.url)"
+                />
+                <view v-else-if="!isView" class="image-add" @click="chooseLabeledImage(index, 'original')">
+                  <text class="add-icon">+</text>
+                </view>
+              </view>
+              <view v-if="!isView && item.url" class="label-image-delete" @click="removeLabeledImage(index, 'original')">
+                <text class="delete-icon">×</text>
+              </view>
             </view>
           </view>
-          <text class="image-tip" v-if="!isView">最多上传{{ maxImageCount }}张图片</text>
+        </view>
+      </view>
+
+      <!-- 货道参数 -->
+      <view class="form-section">
+        <view class="section-title">
+          <text class="section-title-text">货道参数</text>
+        </view>
+
+        <view class="form-row">
+          <view class="form-item form-item-half">
+            <text class="form-label">长度(mm)</text>
+            <view class="form-input-wrap">
+              <input class="form-input" type="number" v-model.number="formData.length" placeholder="长度" :disabled="isView" />
+            </view>
+          </view>
+          <view class="form-item form-item-half">
+            <text class="form-label">宽度(mm)</text>
+            <view class="form-input-wrap">
+              <input class="form-input" type="number" v-model.number="formData.width" placeholder="宽度" :disabled="isView" />
+            </view>
+          </view>
+        </view>
+        <view class="form-row">
+          <view class="form-item form-item-half">
+            <text class="form-label">高度(mm)</text>
+            <view class="form-input-wrap">
+              <input class="form-input" type="number" v-model.number="formData.height" placeholder="高度" :disabled="isView" />
+            </view>
+          </view>
+          <view class="form-item form-item-half">
+            <text class="form-label">陈列高度(mm)</text>
+            <view class="form-input-wrap">
+              <input class="form-input" type="number" v-model.number="formData.exhibitHeight" placeholder="陈列高度" :disabled="isView" />
+            </view>
+          </view>
+        </view>
+        <view class="form-row">
+          <view class="form-item form-item-half">
+            <text class="form-label">商品占列</text>
+            <view class="form-input-wrap">
+              <input class="form-input" type="number" v-model.number="formData.columns" placeholder="占列数" :disabled="isView" />
+            </view>
+          </view>
+          <view class="form-item form-item-half">
+            <text class="form-label">每货道数量</text>
+            <view class="form-input-wrap">
+              <input class="form-input" type="number" v-model.number="formData.numbers" placeholder="数量" :disabled="isView" />
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 其他信息 -->
+      <view class="form-section">
+        <view class="section-title">
+          <text class="section-title-text">其他信息</text>
+        </view>
+
+        <view class="form-item">
+          <text class="form-label">商品类型</text>
+          <view class="form-input-wrap">
+            <picker :range="productTypeOptions" :range-key="'label'" :value="formData.productType || 0" @change="onProductTypeChange" :disabled="isView">
+              <text class="picker-value" :class="{ 'picker-placeholder': formData.productType === undefined }">
+                {{ productTypeLabel }}
+              </text>
+            </picker>
+          </view>
+        </view>
+
+        <view class="form-item">
+          <text class="form-label">标品/非标品</text>
+          <view class="form-input-wrap">
+            <picker :range="standardOptions" :range-key="'label'" :value="formData.standard ? formData.standard - 1 : 0" @change="onStandardChange" :disabled="isView">
+              <text class="picker-value" :class="{ 'picker-placeholder': formData.standard === undefined }">
+                {{ standardLabel }}
+              </text>
+            </picker>
+          </view>
+        </view>
+
+        <view class="form-item">
+          <text class="form-label">学习机号</text>
+          <view class="form-input-wrap">
+            <input class="form-input" v-model="formData.stickerNum" placeholder="请输入学习机号" :maxlength="20" :disabled="isView" />
+          </view>
+        </view>
+
+        <view class="form-item textarea-item">
+          <text class="form-label">申请原因</text>
+          <textarea
+            class="form-textarea"
+            v-model="formData.reason"
+            placeholder="请输入申请原因"
+            :maxlength="500"
+            :disabled="isView"
+            auto-height
+            :style="{ minHeight: '120rpx' }"
+          />
+          <text v-if="!isView" class="char-count">{{ (formData.reason || '').length }}/500</text>
         </view>
       </view>
 
@@ -215,8 +338,26 @@ type PageMode = 'add' | 'edit' | 'view';
 const mode = ref<PageMode>('add');
 const editId = ref<number>(0);
 const barcodeWarning = ref(false);
-const maxImageCount = 9;
-const imageList = ref<string[]>([]);
+
+// 商品类型选项
+const productTypeOptions = [{ label: '静态', value: 0 }, { label: '动态', value: 1 }];
+const standardOptions = [{ label: '标品', value: 1 }, { label: '非标品', value: 2 }];
+
+const productTypeLabel = computed(() => {
+  const opt = productTypeOptions.find(o => o.value === formData.productType);
+  return opt ? opt.label : '请选择商品类型';
+});
+const standardLabel = computed(() => {
+  const opt = standardOptions.find(o => o.value === formData.standard);
+  return opt ? opt.label : '请选择';
+});
+
+// 9张分类图片 slots
+const processedLabels = ['正面图', '背面图', '侧面图', '顶部图', '称重图'];
+const originalLabels = ['正面原图', '背面原图', '侧面原图', '顶部原图'];
+
+const processedImageSlots = ref(processedLabels.map(label => ({ label, url: '' })));
+const originalImageSlots = ref(originalLabels.map(label => ({ label, url: '' })));
 
 const isEdit = computed(() => mode.value === 'edit');
 const isView = computed(() => mode.value === 'view');
@@ -236,7 +377,17 @@ const formData = reactive<NewProductApplyItem>({
   price: 0,
   costPrice: 0,
   images: '',
-  description: ''
+  description: '',
+  productType: 0,
+  standard: 1,
+  stickerNum: '',
+  length: undefined,
+  width: undefined,
+  height: undefined,
+  exhibitHeight: undefined,
+  columns: undefined,
+  numbers: undefined,
+  reason: ''
 });
 
 const validate = (): string | null => {
@@ -308,31 +459,61 @@ const onBarcodeBlur = async () => {
   }
 };
 
-const chooseImage = () => {
-  const remaining = maxImageCount - imageList.value.length;
-  if (remaining <= 0) return;
+const onProductTypeChange = (e: any) => {
+  formData.productType = productTypeOptions[e.detail.value].value;
+};
+
+const onStandardChange = (e: any) => {
+  formData.standard = standardOptions[e.detail.value].value;
+};
+
+// 分类图片上传
+const chooseLabeledImage = (index: number, type: 'processed' | 'original') => {
   uni.chooseImage({
-    count: remaining,
+    count: 1,
     sizeType: ['compressed'],
     sourceType: ['album', 'camera'],
     success: async (res) => {
-      for (const tempPath of res.tempFilePaths) {
-        try {
-          const uploadedUrl = await uploadImage(tempPath);
-          imageList.value.push(uploadedUrl);
-        } catch (error) {
-          console.error('上传图片失败', error);
-          imageList.value.push(tempPath);
+      const tempPath = res.tempFilePaths[0];
+      try {
+        const uploadedUrl = await uploadImage(tempPath);
+        if (type === 'processed') {
+          processedImageSlots.value[index].url = uploadedUrl;
+        } else {
+          originalImageSlots.value[index].url = uploadedUrl;
         }
+        syncImagesToForm();
+      } catch (error) {
+        console.error('上传图片失败', error);
       }
-      formData.images = imageList.value.join(',');
-    },
-    fail: () => {
-      showToast('选择图片取消');
     }
   });
 };
 
+const removeLabeledImage = (index: number, type: 'processed' | 'original') => {
+  if (type === 'processed') {
+    processedImageSlots.value[index].url = '';
+  } else {
+    originalImageSlots.value[index].url = '';
+  }
+  syncImagesToForm();
+};
+
+const previewLabeledImage = (url: string) => {
+  uni.previewImage({
+    current: url.startsWith('http') ? url : `${IMAGE_BASE_URL}${url}`,
+    urls: [url.startsWith('http') ? url : `${IMAGE_BASE_URL}${url}`]
+  });
+};
+
+const syncImagesToForm = () => {
+  const allUrls = [
+    ...processedImageSlots.value.map(s => s.url),
+    ...originalImageSlots.value.map(s => s.url)
+  ];
+  formData.images = allUrls.join(',');
+};
+
 const uploadImage = (tempFilePath: string): Promise<string> => {
   return new Promise((resolve, reject) => {
     uni.uploadFile({
@@ -359,21 +540,6 @@ const uploadImage = (tempFilePath: string): Promise<string> => {
   });
 };
 
-const removeImage = (index: number) => {
-  imageList.value.splice(index, 1);
-  formData.images = imageList.value.join(',');
-};
-
-const previewImage = (index: number) => {
-  const urls = imageList.value.map(img => {
-    if (img.startsWith('http')) return img;
-    return `${IMAGE_BASE_URL}${img}`;
-  });
-  uni.previewImage({
-    current: urls[index],
-    urls
-  });
-};
 
 const handleSaveDraft = async () => {
   const error = validate();
@@ -423,25 +589,48 @@ const handleSubmit = async () => {
 const loadDetail = async (id: number) => {
   try {
     uni.showLoading({ title: '加载中...', mask: true });
-    const res = await getNewProductApplyById(id);
+    const res: any = await getNewProductApplyById(id);
     uni.hideLoading();
     if (res) {
       Object.assign(formData, {
-        productName: res.productName || '',
+        productName: res.productName || res.name || '',
         barcode: res.barcode || '',
         category: res.category || '',
         brand: res.brand || '',
         specification: res.specification || '',
         unit: res.unit || '',
         price: res.price || 0,
-        costPrice: res.costPrice || 0,
+        costPrice: res.costPrice || res.cost || 0,
         images: res.images || '',
-        description: res.description || ''
+        description: res.description || '',
+        productType: res.productType ?? 0,
+        standard: res.standard ?? 1,
+        stickerNum: res.stickerNum || '',
+        length: res.length ?? undefined,
+        width: res.width ?? undefined,
+        height: res.height ?? undefined,
+        exhibitHeight: res.exhibitHeight ?? undefined,
+        columns: res.columns ?? undefined,
+        numbers: res.numbers ?? undefined,
+        reason: res.reason || ''
       });
-      if (res.images) {
-        imageList.value = res.images.split(',').filter((url: string) => url.trim());
-      } else {
-        imageList.value = [];
+
+      // 解析图片到分类slots:优先从独立字段,其次从逗号分隔字符串
+      const allUrls: string[] = [];
+      const directFields = [res.imageUrl, res.imageUrl2, res.imageUrl3, res.imageUrl4, res.imageUrl5,
+        res.originalImageUrl1, res.originalImageUrl2, res.originalImageUrl3, res.originalImageUrl4];
+      const hasDirect = directFields.some((f: string) => f);
+      if (hasDirect) {
+        for (const f of directFields) allUrls.push(f || '');
+      } else if (res.images) {
+        const parts = res.images.split(',');
+        for (const p of parts) allUrls.push((p || '').trim());
+      }
+      for (let i = 0; i < 5 && i < allUrls.length; i++) {
+        processedImageSlots.value[i].url = allUrls[i] || '';
+      }
+      for (let i = 0; i < 4 && i < allUrls.length - 5; i++) {
+        originalImageSlots.value[i].url = allUrls[5 + i] || '';
       }
     }
   } catch (error) {
@@ -599,6 +788,64 @@ onMounted(() => {
   padding: 20rpx 0;
 }
 
+.labeled-image-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.labeled-image-item {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8rpx;
+}
+
+.labeled-image-title {
+  font-size: 22rpx;
+  color: $text-color-muted;
+}
+
+.label-image-wrap {
+  width: 150rpx;
+  height: 150rpx;
+  border-radius: $radius-base;
+  overflow: hidden;
+  background: #FAFAFA;
+}
+
+.label-image-wrap .upload-image {
+  width: 150rpx;
+  height: 150rpx;
+  border-radius: $radius-base;
+  background: $bg-color-page;
+}
+
+.label-image-wrap .image-add {
+  width: 150rpx;
+  height: 150rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #FAFAFA;
+
+  &:active { opacity: 0.8; }
+}
+
+.label-image-delete {
+  position: absolute;
+  top: 18rpx;
+  right: 2rpx;
+  width: 36rpx;
+  height: 36rpx;
+  background: $error-color;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
 .image-list {
   display: flex;
   flex-wrap: wrap;
@@ -672,6 +919,27 @@ onMounted(() => {
   display: block;
 }
 
+// 货道参数双列布局
+.form-row {
+  display: flex;
+  gap: 20rpx;
+}
+
+.form-item-half {
+  flex: 1;
+  min-width: 0;
+}
+
+// picker 文本样式
+.picker-value {
+  font-size: $font-size-base;
+  color: $text-color-primary;
+}
+
+.picker-placeholder {
+  color: $text-color-placeholder;
+}
+
 .bottom-spacer {
   height: 140rpx;
 }

+ 88 - 0
haha-admin-web/src/views/product/new-apply/index.vue

@@ -216,3 +216,91 @@ const {
   }
 }
 </style>
+
+<style lang="scss">
+.new-apply-form {
+  max-height: 560px;
+  overflow-y: auto;
+  padding-right: 8px;
+
+  .form-section-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 14px;
+    padding-left: 8px;
+    border-left: 3px solid #409eff;
+  }
+
+  .image-upload-grid {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+  }
+
+  .image-upload-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 6px;
+
+    .upload-card {
+      width: 100px;
+      height: 100px;
+      border: 1px dashed #d9d9d9;
+      border-radius: 6px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      position: relative;
+      overflow: hidden;
+      background: #fafafa;
+      transition: border-color .2s;
+
+      &:hover {
+        border-color: #409eff;
+      }
+    }
+
+    .upload-card-img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      display: block;
+    }
+
+    .upload-card-remove {
+      position: absolute;
+      top: -6px;
+      right: -6px;
+      width: 18px;
+      height: 18px;
+      background: #f56c6c;
+      color: #fff;
+      border-radius: 50%;
+      font-size: 14px;
+      line-height: 18px;
+      text-align: center;
+      cursor: pointer;
+      display: none;
+    }
+
+    .upload-card:hover .upload-card-remove {
+      display: block;
+    }
+
+    .upload-card-loading {
+      color: #909399;
+      font-size: 12px;
+    }
+
+    .upload-label {
+      font-size: 12px;
+      color: #909399;
+      text-align: center;
+      white-space: nowrap;
+    }
+  }
+}
+</style>

+ 246 - 33
haha-admin-web/src/views/product/new-apply/utils/hook.tsx

@@ -21,7 +21,7 @@ import {
   ElSelect,
   ElOption,
   ElInputNumber,
-  ElMessageBox
+  ElDivider
 } from "element-plus";
 import {
   type Ref,
@@ -32,6 +32,7 @@ import {
   defineComponent,
   onMounted
 } from "vue";
+import { http } from "@/utils/http";
 
 export function useNewProductApply(tableRef: Ref) {
   const form = reactive({
@@ -65,7 +66,7 @@ export function useNewProductApply(tableRef: Ref) {
     },
     {
       label: "商品名称",
-      prop: "productName",
+      prop: "name",
       minWidth: 150
     },
     {
@@ -174,19 +175,38 @@ export function useNewProductApply(tableRef: Ref) {
       props: {
         formInline: {
           id: row?.id,
-          productName: row?.productName ?? "",
+          productName: row?.productName ?? row?.name ?? "",
           barcode: row?.barcode ?? "",
           category: row?.category ?? "",
           brand: row?.brand ?? "",
           specification: row?.specification ?? "",
           unit: row?.unit ?? "",
           price: row?.price ?? 0,
-          costPrice: row?.costPrice ?? 0,
+          costPrice: row?.costPrice ?? row?.cost ?? 0,
+          description: row?.description ?? "",
+          productType: row?.productType ?? 0,
+          standard: row?.standard ?? 1,
+          stickerNum: row?.stickerNum ?? "",
+          length: row?.length ?? null,
+          width: row?.width ?? null,
+          height: row?.height ?? null,
+          exhibitHeight: row?.exhibitHeight ?? null,
+          columns: row?.columns ?? null,
+          numbers: row?.numbers ?? null,
+          reason: row?.reason ?? "",
           images: row?.images ?? "",
-          description: row?.description ?? ""
+          imageUrl: row?.imageUrl ?? "",
+          imageUrl2: row?.imageUrl2 ?? "",
+          imageUrl3: row?.imageUrl3 ?? "",
+          imageUrl4: row?.imageUrl4 ?? "",
+          imageUrl5: row?.imageUrl5 ?? "",
+          originalImageUrl1: row?.originalImageUrl1 ?? "",
+          originalImageUrl2: row?.originalImageUrl2 ?? "",
+          originalImageUrl3: row?.originalImageUrl3 ?? "",
+          originalImageUrl4: row?.originalImageUrl4 ?? ""
         }
       },
-      width: "600px",
+      width: "900px",
       draggable: true,
       closeOnClickModal: false,
       contentRenderer: () => h(EditForm, { ref: formRef }),
@@ -279,6 +299,87 @@ export function useNewProductApply(tableRef: Ref) {
   };
 }
 
+// 上传图片辅助函数
+async function uploadFile(options: any): Promise<string> {
+  const formData = new FormData();
+  formData.append("file", options.file);
+  // 用 beforeRequestCallback 清除默认的 Content-Type: application/json,
+  // 让浏览器自动设置 multipart/form-data 及正确的 boundary
+  const res: any = await http.request("post", "/upload", { data: formData }, {
+    beforeRequestCallback: (config: any) => {
+      delete config.headers["Content-Type"];
+    }
+  } as any);
+  if (res.code === 200 && res.data) {
+    return res.data.url || res.data;
+  }
+  throw new Error(res.message || "上传失败");
+}
+
+// 图片上传组件(纯自定义,避免 ElUpload 的 fileList 与 httpRequest 状态冲突)
+const ImageUploadItem = defineComponent({
+  props: {
+    label: { type: String, required: true },
+    modelValue: { type: String, default: "" }
+  },
+  emits: ["update:modelValue"],
+  setup(props, { emit }) {
+    const uploading = ref(false);
+    const inputRef = ref<HTMLInputElement>();
+
+    const handleClick = () => {
+      if (props.modelValue) return; // 已有图片,不触发上传
+      inputRef.value?.click();
+    };
+
+    const handleFileChange = async (e: Event) => {
+      const file = (e.target as HTMLInputElement).files?.[0];
+      if (!file) return;
+      uploading.value = true;
+      try {
+        const url = await uploadFile({ file });
+        emit("update:modelValue", url);
+      } catch {
+        // 上传失败,静默处理
+      } finally {
+        uploading.value = false;
+        // 重置 input,允许重复选择同一文件
+        if (inputRef.value) inputRef.value.value = "";
+      }
+    };
+
+    const handleRemove = (e: Event) => {
+      e.stopPropagation();
+      emit("update:modelValue", "");
+    };
+
+    return () => (
+      <div class="image-upload-item">
+        <div class="upload-card" onClick={handleClick}>
+          {props.modelValue ? (
+            <>
+              <img class="upload-card-img" src={props.modelValue} />
+              <span class="upload-card-remove" onClick={handleRemove}>×</span>
+            </>
+          ) : uploading.value ? (
+            <span class="upload-card-loading">...</span>
+          ) : (
+            <el-icon><i class="ri-add-line" /></el-icon>
+          )}
+          <input
+            ref={inputRef}
+            type="file"
+            accept="image/*"
+            style="display:none"
+            onChange={handleFileChange}
+          />
+        </div>
+        <span class="upload-label">{props.label}</span>
+      </div>
+    );
+  }
+});
+
 const EditForm = defineComponent({
   props: {
     formInline: {
@@ -288,60 +389,172 @@ const EditForm = defineComponent({
   },
   setup(props) {
     const ruleFormRef = ref();
+
+    // 从已有数据中解析图片URL
+    const p = props.formInline as any;
+    const parseImg = (idx: number) => {
+      // 优先从独立的 imageUrl 字段获取
+      const directFields = [p.imageUrl, p.imageUrl2, p.imageUrl3, p.imageUrl4, p.imageUrl5,
+        p.originalImageUrl1, p.originalImageUrl2, p.originalImageUrl3, p.originalImageUrl4];
+      if (directFields[idx]) return directFields[idx];
+      // 从逗号分隔的 images 字符串获取
+      if (p.images) {
+        const parts = p.images.split(",");
+        return (parts[idx] || "").trim();
+      }
+      return "";
+    };
+
     const model = reactive({
-      id: props.formInline.id,
-      productName: props.formInline.productName,
-      barcode: props.formInline.barcode,
-      category: props.formInline.category,
-      brand: props.formInline.brand,
-      specification: props.formInline.specification,
-      unit: props.formInline.unit,
-      price: props.formInline.price,
-      costPrice: props.formInline.costPrice,
-      images: props.formInline.images,
-      description: props.formInline.description
+      id: p.id,
+      productName: p.productName || p.name || "",
+      barcode: p.barcode || "",
+      category: p.category || "",
+      brand: p.brand || "",
+      specification: p.specification || "",
+      unit: p.unit || "",
+      price: p.price ?? 0,
+      costPrice: p.costPrice ?? p.cost ?? 0,
+      description: p.description || "",
+      productType: p.productType ?? 0,
+      standard: p.standard ?? 1,
+      stickerNum: p.stickerNum || "",
+      length: p.length ?? null,
+      width: p.width ?? null,
+      height: p.height ?? null,
+      exhibitHeight: p.exhibitHeight ?? null,
+      columns: p.columns ?? null,
+      numbers: p.numbers ?? null,
+      reason: p.reason || "",
+      pic1: parseImg(0), pic2: parseImg(1), pic3: parseImg(2), pic4: parseImg(3), pic5: parseImg(4),
+      oriPic1: parseImg(5), oriPic2: parseImg(6), oriPic3: parseImg(7), oriPic4: parseImg(8)
     });
+
     const rules = {
       productName: [{ required: true, message: "请输入商品名称", trigger: "blur" }],
       barcode: [{ required: true, message: "请输入条形码", trigger: "blur" }]
     };
 
     const getRef = () => ruleFormRef.value;
-    const getForm = () => model;
+    const getForm = () => {
+      const images = [
+        model.pic1, model.pic2, model.pic3, model.pic4, model.pic5,
+        model.oriPic1, model.oriPic2, model.oriPic3, model.oriPic4
+      ].map(s => s || "").join(",");
+      const { pic1, pic2, pic3, pic4, pic5, oriPic1, oriPic2, oriPic3, oriPic4, ...rest } = model;
+      return { ...rest, images };
+    };
 
     return () => (
-      <ElForm ref={ruleFormRef} model={model} rules={rules} label-width="100px">
+      <ElForm ref={ruleFormRef} model={model} rules={rules} label-width="110px" class="new-apply-form">
+        {/* 基本信息 */}
+        <div class="form-section-title">基本信息</div>
         <ElFormItem label="商品名称" prop="productName">
-          <ElInput v-model={model.productName} placeholder="请输入商品名称" />
+          <ElInput v-model={model.productName} placeholder="请输入商品名称" maxlength={50} />
         </ElFormItem>
         <ElFormItem label="条形码" prop="barcode">
-          <ElInput v-model={model.barcode} placeholder="请输入条形码" />
+          <ElInput v-model={model.barcode} placeholder="请输入条形码" maxlength={30} />
         </ElFormItem>
         <ElFormItem label="分类" prop="category">
-          <ElInput v-model={model.category} placeholder="请输入分类" />
+          <ElInput v-model={model.category} placeholder="请输入分类" maxlength={20} />
         </ElFormItem>
         <ElFormItem label="品牌" prop="brand">
-          <ElInput v-model={model.brand} placeholder="请输入品牌" />
+          <ElInput v-model={model.brand} placeholder="请输入品牌" maxlength={30} />
         </ElFormItem>
         <ElFormItem label="规格" prop="specification">
-          <ElInput v-model={model.specification} placeholder="请输入规格" />
+          <ElInput v-model={model.specification} placeholder="请输入规格" maxlength={30} />
         </ElFormItem>
         <ElFormItem label="单位" prop="unit">
-          <ElInput v-model={model.unit} placeholder="请输入单位" />
+          <ElInput v-model={model.unit} placeholder="请输入单位" maxlength={10} />
+        </ElFormItem>
+
+        <ElDivider />
+
+        {/* 商品类型 */}
+        <div class="form-section-title">商品类型</div>
+        <ElFormItem label="商品类型" prop="productType">
+          <ElSelect v-model={model.productType} placeholder="请选择商品类型">
+            <ElOption label="静态" value={0} />
+            <ElOption label="动态" value={1} />
+          </ElSelect>
         </ElFormItem>
+        <ElFormItem label="标品/非标品" prop="standard">
+          <ElSelect v-model={model.standard} placeholder="请选择">
+            <ElOption label="标品" value={1} />
+            <ElOption label="非标品" value={2} />
+          </ElSelect>
+        </ElFormItem>
+        <ElFormItem label="学习机号" prop="stickerNum">
+          <ElInput v-model={model.stickerNum} placeholder="请输入学习机号" maxlength={20} />
+        </ElFormItem>
+
+        <ElDivider />
+
+        {/* 价格信息 */}
+        <div class="form-section-title">价格信息</div>
         <ElFormItem label="售价(元)" prop="price">
-          <ElInputNumber v-model={model.price} precision={2} min={0} />
+          <ElInputNumber v-model={model.price} precision={2} min={0} style="width:100%" />
         </ElFormItem>
         <ElFormItem label="成本价(元)" prop="costPrice">
-          <ElInputNumber v-model={model.costPrice} precision={2} min={0} />
+          <ElInputNumber v-model={model.costPrice} precision={2} min={0} style="width:100%" />
+        </ElFormItem>
+
+        <ElDivider />
+
+        {/* 商品图片 */}
+        <div class="form-section-title">加工图(5张)</div>
+        <ElFormItem label=" " label-width="0">
+          <div class="image-upload-grid">
+            <ImageUploadItem label="正面图" v-model={model.pic1} />
+            <ImageUploadItem label="背面图" v-model={model.pic2} />
+            <ImageUploadItem label="侧面图" v-model={model.pic3} />
+            <ImageUploadItem label="顶部图" v-model={model.pic4} />
+            <ImageUploadItem label="称重图" v-model={model.pic5} />
+          </div>
+        </ElFormItem>
+
+        <div class="form-section-title">原图(4张)</div>
+        <ElFormItem label=" " label-width="0">
+          <div class="image-upload-grid">
+            <ImageUploadItem label="正面原图" v-model={model.oriPic1} />
+            <ImageUploadItem label="背面原图" v-model={model.oriPic2} />
+            <ImageUploadItem label="侧面原图" v-model={model.oriPic3} />
+            <ImageUploadItem label="顶部原图" v-model={model.oriPic4} />
+          </div>
+        </ElFormItem>
+
+        <ElDivider />
+
+        {/* 货道参数 */}
+        <div class="form-section-title">货道参数</div>
+        <ElFormItem label="长度(mm)" prop="length">
+          <ElInputNumber v-model={model.length} min={0} style="width:100%" />
+        </ElFormItem>
+        <ElFormItem label="宽度(mm)" prop="width">
+          <ElInputNumber v-model={model.width} min={0} style="width:100%" />
+        </ElFormItem>
+        <ElFormItem label="高度(mm)" prop="height">
+          <ElInputNumber v-model={model.height} min={0} style="width:100%" />
+        </ElFormItem>
+        <ElFormItem label="陈列高度(mm)" prop="exhibitHeight">
+          <ElInputNumber v-model={model.exhibitHeight} min={0} style="width:100%" />
+        </ElFormItem>
+        <ElFormItem label="商品占列" prop="columns">
+          <ElInputNumber v-model={model.columns} min={0} style="width:100%" />
+        </ElFormItem>
+        <ElFormItem label="每货道数量" prop="numbers">
+          <ElInputNumber v-model={model.numbers} min={0} style="width:100%" />
+        </ElFormItem>
+
+        <ElDivider />
+
+        {/* 备注与描述 */}
+        <div class="form-section-title">其他信息</div>
+        <ElFormItem label="申请原因" prop="reason">
+          <ElInput v-model={model.reason} type="textarea" rows={2} placeholder="请输入申请原因" maxlength={500} show-word-limit />
         </ElFormItem>
         <ElFormItem label="商品描述" prop="description">
-          <ElInput
-            v-model={model.description}
-            type="textarea"
-            rows={3}
-            placeholder="请输入商品描述"
-          />
+          <ElInput v-model={model.description} type="textarea" rows={3} placeholder="请输入商品描述" maxlength={500} show-word-limit />
         </ElFormItem>
       </ElForm>
     );

+ 2 - 1
haha-admin-web/vite.config.ts

@@ -41,7 +41,8 @@ export default async ({ mode }: ConfigEnv): Promise<UserConfigExport> => {
     "/distribution",
     "/refund",
     "/replenishers",
-    "/menu"
+    "/menu",
+    "/upload"
   ];
   
   // 动态生成代理配置

+ 23 - 0
haha-admin/src/main/java/com/haha/admin/config/WebMvcConfig.java

@@ -4,10 +4,15 @@ import com.haha.common.config.LogProperties;
 import com.haha.common.constant.LogConstants;
 import com.haha.common.interceptor.RequestLogInterceptor;
 import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
 /**
  * Web MVC 配置类
  * 用于配置拦截器、跨域、静态资源等
@@ -22,6 +27,24 @@ public class WebMvcConfig implements WebMvcConfigurer {
 
     private final LogProperties logProperties;
 
+    @Value("${upload.path:./upload}")
+    private String uploadPath;
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        // 映射上传目录为静态资源,解析为绝对路径
+        Path path = Paths.get(uploadPath);
+        if (!path.isAbsolute()) {
+            path = Paths.get(System.getProperty("user.dir")).resolve(uploadPath).normalize();
+        }
+        String absolutePath = path.toAbsolutePath().toString().replace("\\", "/");
+        if (!absolutePath.endsWith("/")) {
+            absolutePath += "/";
+        }
+        registry.addResourceHandler("/upload/**")
+                .addResourceLocations("file:" + absolutePath);
+    }
+
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         // 注册请求日志拦截器

+ 8 - 20
haha-admin/src/main/java/com/haha/admin/controller/NewProductApplyController.java

@@ -4,9 +4,9 @@ import cn.dev33.satoken.stp.StpUtil;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.haha.admin.annotation.RequirePermission;
 import com.haha.entity.dto.NewProductApplyQueryDTO;
+import com.haha.entity.dto.NewProductApplySubmitDTO;
 import com.haha.common.annotation.Log;
 import com.haha.common.enums.ApplyStatus;
-import com.haha.common.constant.ResponseEnum;
 import com.haha.common.enums.OperationType;
 import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
@@ -76,13 +76,11 @@ public class NewProductApplyController {
     @RequirePermission("product-apply:create")
     @Log(module = "新品申请", operation = OperationType.INSERT, summary = "提交新品申请")
     @PostMapping("/submit")
-    public Result<Long> submit(@RequestBody NewProductApply apply) {
+    public Result<Long> submit(@RequestBody NewProductApplySubmitDTO dto) {
         try {
-            // 获取当前登录用户
             Long userId = StpUtil.getLoginIdAsLong();
             String userName = StpUtil.getLoginIdAsString();
-            
-            Long id = newProductApplyService.submitApply(apply, userId, userName);
+            Long id = newProductApplyService.submitApply(dto, userId, userName);
             return Result.success("新品申请已提交,请等待审核", id);
         } catch (Exception e) {
             log.error("提交新品申请失败", e);
@@ -98,18 +96,12 @@ public class NewProductApplyController {
     @RequirePermission("product-apply:create")
     @Log(module = "新品申请", operation = OperationType.INSERT, summary = "保存新品申请草稿")
     @PostMapping("/save-draft")
-    public Result<Long> saveDraft(@RequestBody NewProductApply apply) {
+    public Result<Long> saveDraft(@RequestBody NewProductApplySubmitDTO dto) {
         try {
-            // 获取当前登录用户
             Long userId = StpUtil.getLoginIdAsLong();
             String userName = StpUtil.getLoginIdAsString();
-            
-            apply.setStatus(ApplyStatus.DRAFT.getCode()); // 待提交状态
-            apply.setApplicantId(userId);
-            apply.setApplicantName(userName);
-            
-            newProductApplyService.save(apply);
-            return Result.success("草稿保存成功", apply.getId());
+            Long id = newProductApplyService.saveDraft(dto, userId, userName);
+            return Result.success("草稿保存成功", id);
         } catch (Exception e) {
             log.error("保存草稿失败", e);
             return Result.error(e.getMessage());
@@ -125,20 +117,16 @@ public class NewProductApplyController {
     @RequirePermission("product-apply:update")
     @Log(module = "新品申请", operation = OperationType.UPDATE, summary = "更新新品申请")
     @PutMapping("/{id}")
-    public Result<Boolean> update(@PathVariable Long id, @RequestBody NewProductApply apply) {
+    public Result<Boolean> update(@PathVariable Long id, @RequestBody NewProductApplySubmitDTO dto) {
         try {
             NewProductApply existing = newProductApplyService.getById(id);
             if (existing == null) {
                 return Result.error("申请记录不存在");
             }
-            
-            // 只有待提交状态才能修改
             if (existing.getStatus() != ApplyStatus.DRAFT.getCode()) {
                 return Result.error("只有待提交状态的申请才能修改");
             }
-            
-            apply.setId(id);
-            newProductApplyService.updateById(apply);
+            newProductApplyService.updateApply(id, dto);
             return Result.success("更新成功", true);
         } catch (Exception e) {
             log.error("更新申请失败", e);

+ 89 - 0
haha-admin/src/main/java/com/haha/admin/controller/UploadController.java

@@ -0,0 +1,89 @@
+package com.haha.admin.controller;
+
+import com.haha.common.vo.Result;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 文件上传控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/upload")
+public class UploadController {
+
+    @Value("${upload.path:./upload}")
+    private String rawUploadPath;
+
+    private Path uploadBasePath;
+
+    /** 懒加载解析上传路径(避免 @PostConstruct 在 Spring Boot 4.x 的兼容问题) */
+    private Path getUploadBasePath() {
+        if (uploadBasePath == null) {
+            synchronized (this) {
+                if (uploadBasePath == null) {
+                    Path path = Paths.get(rawUploadPath);
+                    if (!path.isAbsolute()) {
+                        path = Paths.get(System.getProperty("user.dir")).resolve(rawUploadPath).normalize();
+                    }
+                    uploadBasePath = path;
+                    log.info("上传文件存储路径: {}", uploadBasePath.toAbsolutePath());
+                }
+            }
+        }
+        return uploadBasePath;
+    }
+
+    @PostMapping
+    public Result<Map<String, Object>> upload(@RequestParam("file") MultipartFile file) {
+        if (file.isEmpty()) {
+            return Result.error("上传文件不能为空");
+        }
+
+        try {
+            // 按日期分目录
+            String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+            Path uploadDir = getUploadBasePath().resolve("images").resolve(dateDir);
+            Files.createDirectories(uploadDir);
+
+            // 生成唯一文件名
+            String originalName = file.getOriginalFilename();
+            String ext = "";
+            if (originalName != null && originalName.contains(".")) {
+                ext = originalName.substring(originalName.lastIndexOf("."));
+            }
+            String fileName = UUID.randomUUID().toString().replace("-", "") + ext;
+
+            // 保存文件
+            Path targetPath = uploadDir.resolve(fileName);
+            file.transferTo(targetPath.toFile());
+
+            // 返回访问URL
+            String url = "/upload/images/" + dateDir + "/" + fileName;
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("url", url);
+
+            log.info("文件上传成功: {} -> {}", originalName, targetPath);
+            return Result.success("上传成功", data);
+        } catch (IOException e) {
+            log.error("文件上传失败", e);
+            return Result.error("文件上传失败: " + e.getMessage());
+        }
+    }
+}

+ 2 - 0
haha-admin/src/main/resources/sql/new_product_apply.sql

@@ -5,6 +5,8 @@ CREATE TABLE IF NOT EXISTS `t_new_product_apply` (
     `barcode` VARCHAR(50) DEFAULT NULL COMMENT '商品条形码',
     `name` VARCHAR(200) NOT NULL COMMENT '商品名称',
     `category` VARCHAR(50) DEFAULT NULL COMMENT '商品分类',
+    `brand` VARCHAR(100) DEFAULT NULL COMMENT '品牌',
+    `unit` VARCHAR(20) DEFAULT NULL COMMENT '单位',
     `specification` VARCHAR(100) DEFAULT NULL COMMENT '商品规格',
     `price` DECIMAL(10, 2) DEFAULT NULL COMMENT '建议售价',
     `cost` DECIMAL(10, 2) DEFAULT NULL COMMENT '成本价',

+ 10 - 0
haha-entity/src/main/java/com/haha/entity/NewProductApply.java

@@ -47,6 +47,16 @@ public class NewProductApply implements Serializable {
      */
     private String category;
 
+    /**
+     * 品牌
+     */
+    private String brand;
+
+    /**
+     * 单位
+     */
+    private String unit;
+
     /**
      * 商品规格
      */

+ 112 - 0
haha-entity/src/main/java/com/haha/entity/dto/NewProductApplySubmitDTO.java

@@ -0,0 +1,112 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+/**
+ * 新品申请提交/编辑 DTO
+ * 字段名与前端 JSON 对齐
+ */
+@Data
+public class NewProductApplySubmitDTO {
+
+    /**
+     * 商品名称
+     */
+    private String productName;
+
+    /**
+     * 条形码
+     */
+    private String barcode;
+
+    /**
+     * 分类
+     */
+    private String category;
+
+    /**
+     * 品牌
+     */
+    private String brand;
+
+    /**
+     * 规格
+     */
+    private String specification;
+
+    /**
+     * 单位
+     */
+    private String unit;
+
+    /**
+     * 售价
+     */
+    private BigDecimal price;
+
+    /**
+     * 成本价
+     */
+    private BigDecimal costPrice;
+
+    /**
+     * 图片URL(逗号分隔,顺序:pic_1~pic_5, ori_pic_1~ori_pic_4)
+     */
+    private String images;
+
+    /**
+     * 商品描述
+     */
+    private String description;
+
+    /**
+     * 商品类型:0静态,1动态
+     */
+    private Integer productType;
+
+    /**
+     * 是否标品:1标品,2非标品
+     */
+    private Integer standard;
+
+    /**
+     * 学习机号
+     */
+    private String stickerNum;
+
+    /**
+     * 长度(mm)
+     */
+    private Integer length;
+
+    /**
+     * 宽度(mm)
+     */
+    private Integer width;
+
+    /**
+     * 高度(mm)
+     */
+    private Integer height;
+
+    /**
+     * 陈列高度(mm)
+     */
+    private Integer exhibitHeight;
+
+    /**
+     * 商品占列
+     */
+    private Integer columns;
+
+    /**
+     * 每货道数量
+     */
+    private Integer numbers;
+
+    /**
+     * 申请原因/备注
+     */
+    private String reason;
+}

+ 30 - 0
haha-service/src/main/java/com/haha/service/NewProductApplyService.java

@@ -3,6 +3,7 @@ package com.haha.service;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.haha.entity.dto.NewProductApplyQueryDTO;
+import com.haha.entity.dto.NewProductApplySubmitDTO;
 import com.haha.entity.NewProductApply;
 
 import java.util.Map;
@@ -22,6 +23,35 @@ public interface NewProductApplyService extends IService<NewProductApply> {
      */
     IPage<NewProductApply> getPage(int page, int pageSize, NewProductApplyQueryDTO queryDTO);
 
+    /**
+     * 提交新品申请(DTO版本)
+     * 将DTO转为实体,保存到本地数据库,并调用哈哈平台API提交申请
+     *
+     * @param dto             申请信息
+     * @param applicantId     申请人ID
+     * @param applicantName   申请人名称
+     * @return 申请记录ID
+     */
+    Long submitApply(NewProductApplySubmitDTO dto, Long applicantId, String applicantName);
+
+    /**
+     * 保存草稿(DTO版本)
+     *
+     * @param dto             申请信息
+     * @param applicantId     申请人ID
+     * @param applicantName   申请人名称
+     * @return 申请记录ID
+     */
+    Long saveDraft(NewProductApplySubmitDTO dto, Long applicantId, String applicantName);
+
+    /**
+     * 更新申请(DTO版本)
+     *
+     * @param id   申请ID
+     * @param dto  申请信息
+     */
+    void updateApply(Long id, NewProductApplySubmitDTO dto);
+
     /**
      * 提交新品申请
      * 将申请信息保存到本地数据库,并调用哈哈平台API提交申请

+ 74 - 0
haha-service/src/main/java/com/haha/service/impl/NewProductApplyServiceImpl.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.haha.entity.dto.NewProductApplyQueryDTO;
+import com.haha.entity.dto.NewProductApplySubmitDTO;
 import com.haha.common.enums.ApplyStatus;
 import com.haha.common.exception.BusinessException;
 import com.haha.entity.NewProductApply;
@@ -20,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
 import java.time.LocalDateTime;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -64,6 +66,78 @@ public class NewProductApplyServiceImpl extends ServiceImpl<NewProductApplyMappe
         return page(new Page<>(page, pageSize), wrapper);
     }
 
+    // ========== DTO → Entity 转换 ==========
+
+    private NewProductApply dtoToEntity(NewProductApplySubmitDTO dto) {
+        NewProductApply entity = new NewProductApply();
+        entity.setName(dto.getProductName());
+        entity.setBarcode(dto.getBarcode());
+        entity.setCategory(dto.getCategory());
+        entity.setBrand(dto.getBrand());
+        entity.setUnit(dto.getUnit());
+        entity.setSpecification(dto.getSpecification());
+        entity.setPrice(dto.getPrice());
+        entity.setCost(dto.getCostPrice());
+        entity.setDescription(dto.getDescription());
+        entity.setProductType(dto.getProductType());
+        entity.setStandard(dto.getStandard());
+        entity.setStickerNum(dto.getStickerNum());
+        entity.setLength(dto.getLength());
+        entity.setWidth(dto.getWidth());
+        entity.setHeight(dto.getHeight());
+        entity.setExhibitHeight(dto.getExhibitHeight());
+        entity.setColumns(dto.getColumns());
+        entity.setNumbers(dto.getNumbers());
+        entity.setReason(dto.getReason());
+
+        // 拆分逗号分隔的图片字符串到9个独立字段
+        if (StringUtils.hasText(dto.getImages())) {
+            String[] urls = dto.getImages().split(",");
+            if (urls.length > 0) entity.setImageUrl(urls[0].trim());
+            if (urls.length > 1) entity.setImageUrl2(urls[1].trim());
+            if (urls.length > 2) entity.setImageUrl3(urls[2].trim());
+            if (urls.length > 3) entity.setImageUrl4(urls[3].trim());
+            if (urls.length > 4) entity.setImageUrl5(urls[4].trim());
+            if (urls.length > 5) entity.setOriginalImageUrl1(urls[5].trim());
+            if (urls.length > 6) entity.setOriginalImageUrl2(urls[6].trim());
+            if (urls.length > 7) entity.setOriginalImageUrl3(urls[7].trim());
+            if (urls.length > 8) entity.setOriginalImageUrl4(urls[8].trim());
+        }
+
+        return entity;
+    }
+
+    // ========== DTO 版本的服务方法 ==========
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long submitApply(NewProductApplySubmitDTO dto, Long applicantId, String applicantName) {
+        NewProductApply apply = dtoToEntity(dto);
+        return submitApply(apply, applicantId, applicantName);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long saveDraft(NewProductApplySubmitDTO dto, Long applicantId, String applicantName) {
+        NewProductApply apply = dtoToEntity(dto);
+        apply.setStatus(ApplyStatus.DRAFT.getCode());
+        apply.setApplicantId(applicantId);
+        apply.setApplicantName(applicantName);
+        apply.setCreateTime(LocalDateTime.now());
+        save(apply);
+        return apply.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateApply(Long id, NewProductApplySubmitDTO dto) {
+        NewProductApply apply = dtoToEntity(dto);
+        apply.setId(id);
+        apply.setUpdateTime(LocalDateTime.now());
+        updateById(apply);
+        log.info("新品申请更新成功,ID: {}", id);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long submitApply(NewProductApply apply, Long applicantId, String applicantName) {