优化管理后台
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: goods-virtualpay-register
|
||||||
|
overview: 管理后台与管理前端支持登记/修改微信虚拟支付道具信息(product_id、product_status),发布状态仅支持未发布/已发布两态,运营无需再手动改数据库。
|
||||||
|
todos:
|
||||||
|
- id: backend-goods-register
|
||||||
|
content: 扩展管理后端:dto 增道具字段、errcode 增错误码、DAO 增按道具 ID 查询、Service 增写入与合并态校验
|
||||||
|
status: completed
|
||||||
|
- id: frontend-goods-form
|
||||||
|
content: 扩展管理前端:类型与常量增道具字段,GoodsListView 表单增校验、改价提醒与列表展示
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- backend-goods-register
|
||||||
|
- id: docs-and-verify
|
||||||
|
content: 更新管理后端 README 道具登记说明;执行 go build 与前端 type-check、build 验证
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- backend-goods-register
|
||||||
|
- frontend-goods-form
|
||||||
|
---
|
||||||
|
|
||||||
|
## 用户需求
|
||||||
|
|
||||||
|
管理后台(zt-closet-manage-backend)与管理前端(zt-closet-manage-frontend)当前缺少微信虚拟支付道具信息的填写能力:商品表已有 product_id(道具 ID)与 product_status(发布状态)两列,但管理端仅只读展示,运营在微信后台创建道具后只能人工改库。本次需补上登记能力,让商品被小程序虚拟支付下单不再依赖人工改库。
|
||||||
|
|
||||||
|
## 产品概述
|
||||||
|
|
||||||
|
商品新增/编辑表单增加「道具 ID」与「道具发布状态」两个可编辑项,运营填写并标记「已发布」后,该商品即可被小程序虚拟支付下单;列表可直观核对道具登记情况;改价时给出同步微信侧的价格风险提醒。
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
- 表单填写:新增/编辑商品时填写道具 ID(最长 64 字)并选择发布状态(仅「未发布 / 已发布」两态)。
|
||||||
|
- 校验:发布状态选「已发布」时必须填写道具 ID;道具 ID 不得与其他商品重复。
|
||||||
|
- 列表展示:商品列表「虚拟支付」列展示发布状态标签与道具 ID,便于与微信后台核对。
|
||||||
|
- 风险提醒:编辑已发布商品且修改售价时,提交前弹确认提醒——必须同步微信后台道具价格,否则用户付款后无法发货。
|
||||||
|
- 提示文案:明确需与微信后台保持一致的三项为「道具 ID / 道具名称 / 道具价格」。
|
||||||
|
- 兼容历史数据:旧的「发布中 / 发布失败」状态在编辑表单中归一显示为「未发布」。
|
||||||
|
|
||||||
|
## 约束边界
|
||||||
|
|
||||||
|
- 仅做登记与状态标记,不调用微信接口自动创建/发布/改价道具(运营仍在微信后台手动操作)。
|
||||||
|
- 不改动小程序业务服务及下单、发货链路。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- 管理后端(/root/zt-closet-manage-backend):Go + Gin + GORM,四层架构(router → service → dao → model)、注册式错误码、统一响应;无测试文件,验证以 go build / go vet 为主。
|
||||||
|
- 管理前端(/root/zt-closet-manage-frontend):Vue 3 + TypeScript + Element Plus + Vite;表单校验用 Element Plus FormRules,枚举统一维护在 src/utils/constants.ts。
|
||||||
|
- 约束参考(不改动):业务服务下单校验要求 product_id 非空且 product_status=2 才可下单(zt-closet-backend internal/service/virtualpay.go),本次改动与该约束对齐。
|
||||||
|
|
||||||
|
## 实现方案
|
||||||
|
|
||||||
|
沿用两端现有分层与表单模式:后端「新增可写字段 + 合并态校验 + 唯一性校验」,前端「新增表单字段 + 校验 + 价格同步提醒」。
|
||||||
|
|
||||||
|
### 管理后端
|
||||||
|
|
||||||
|
1. internal/dto/goods.go:GoodsCreateRequest 增加 ProductID string(binding:"omitempty,max=64")、ProductStatus int8(binding:"omitempty,oneof=0 2");GoodsUpdateRequest 增加 ProductID *string、ProductStatus *int8(同 binding,指针区分「未传」)。
|
||||||
|
2. pkg/errcode/admin_code.go 商品域新增:GoodsProductInvalid=17003「虚拟支付道具信息不合法」、GoodsProductIDExists=17004「该道具 ID 已被其他商品使用」。
|
||||||
|
3. internal/mysqldao/goods.go 新增 GoodsGetByProductID(ctx, productID):默认作用域(排除软删)按 product_id 等值查询,未找到返回 gorm.ErrRecordNotFound,复用 isNotFound 判定。
|
||||||
|
4. internal/service/goods.go:
|
||||||
|
|
||||||
|
- Create:trim 后写入 ProductID / ProductStatus;校验「ProductStatus=2 且 ProductID 为空 → GoodsProductInvalid」;ProductID 非空时查唯一性,被占用则返回 GoodsProductIDExists。
|
||||||
|
- Update:捕获已查出的现有商品,用「现有值 + 本次改动」的合并态校验(effective status=2 时 effective product_id 必填);仅当 ProductID 有传且与现有不同时做唯一性查询(排除自身);随后写入 updates map(product_id / product_status)。
|
||||||
|
|
||||||
|
5. README.md:核心功能中「只读展示」改为「可登记(未发布/已发布两态)」;商品接口表下补充运营约定(道具 ID / 名称 / 价格须与微信后台一致、改价须同步微信侧、道具 ID 唯一)。
|
||||||
|
|
||||||
|
### 管理前端
|
||||||
|
|
||||||
|
6. src/types/goods.ts:GoodsCreateRequest / GoodsUpdateRequest 增加 product_id?: string、product_status?: number。
|
||||||
|
7. src/utils/constants.ts:新增 PRODUCT_STATUS_EDITABLE = enumMap([{value:0,label:'未发布'},{value:2,label:'已发布',tagType:'success'}]),仅供表单下拉;原 PRODUCT_STATUS 四态枚举保留给列表展示,兼容历史数据。
|
||||||
|
8. src/views/goods/GoodsListView.vue:
|
||||||
|
|
||||||
|
- GoodsForm 与 form 增加 product_id / product_status;openCreate 重置为 '' / 0;openEdit 回填(product_status === 2 ? 2 : 0,历史 1/3 归一为 0)。
|
||||||
|
- 表单在「售价」后插入「道具 ID」输入框(maxlength=64)与「道具发布状态」下拉(enumOptions(PRODUCT_STATUS_EDITABLE)),各带 form-tip 提示。
|
||||||
|
- rules 增加校验:product_id 自定义 validator(发布状态为已发布时必填、≤64 字);product_status 自定义 validator(=2 且 product_id 为空报错)。
|
||||||
|
- submitForm:create / update payload 带上 product_id(trim 后)与 product_status;编辑时若原状态为 2 且售价变化,先 ElMessageBox.confirm 提醒同步微信侧价格,取消则不提交。
|
||||||
|
- 列表「虚拟支付」列宽 110→140,有 product_id 时展示状态 tag + 道具 ID 小字(超长省略)。
|
||||||
|
|
||||||
|
9. src/api/goods.ts 不改(请求类型透传,自动生效)。
|
||||||
|
|
||||||
|
## 实现要点
|
||||||
|
|
||||||
|
- 一致性关键:Update 必须用合并态校验,避免「只改其中一个字段」绕过(如单独把 product_status 改为 2 而 product_id 为空)。
|
||||||
|
- 性能:goods 为运营级小表,唯一性查询为单表等值查询,开销可忽略,无需新增索引或迁移。
|
||||||
|
- 兼容性:列表展示继续用四态枚举;表单归一化历史 1/3 值,不改存量数据。
|
||||||
|
- 爆炸半径:仅管理端商品模块与商品表单;不动下单/发货/退款链路、不动业务服务、不动前端 api 层。
|
||||||
|
- 错误处理:统一走注册式错误码,前端请求层已有统一提示,无需新增日志。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
/root/zt-closet-manage-backend/
|
||||||
|
├── internal/dto/goods.go # [MODIFY] 创建/更新请求增加 product_id、product_status 字段与 binding 校验
|
||||||
|
├── internal/service/goods.go # [MODIFY] Create/Update 写入道具字段;合并态校验(已发布必须有道具 ID);道具 ID 唯一性校验
|
||||||
|
├── internal/mysqldao/goods.go # [MODIFY] 新增 GoodsGetByProductID(排除软删),供唯一性校验
|
||||||
|
├── pkg/errcode/admin_code.go # [MODIFY] 商品域新增 17003/17004 错误码
|
||||||
|
└── README.md # [MODIFY] 商品管理说明改为可登记两态,补充道具运营约定
|
||||||
|
|
||||||
|
/root/zt-closet-manage-frontend/
|
||||||
|
├── src/types/goods.ts # [MODIFY] 创建/更新请求类型增加 product_id、product_status
|
||||||
|
├── src/utils/constants.ts # [MODIFY] 新增 PRODUCT_STATUS_EDITABLE(0/2 两态)供表单下拉
|
||||||
|
└── src/views/goods/GoodsListView.vue # [MODIFY] 表单新增道具 ID 与发布状态、校验、提交、改价提醒、列表列展示道具 ID
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键代码结构
|
||||||
|
|
||||||
|
```
|
||||||
|
// internal/dto/goods.go —— 请求新增字段
|
||||||
|
// GoodsCreateRequest
|
||||||
|
ProductID string `json:"product_id" binding:"omitempty,max=64"`
|
||||||
|
ProductStatus int8 `json:"product_status" binding:"omitempty,oneof=0 2"`
|
||||||
|
// GoodsUpdateRequest
|
||||||
|
ProductID *string `json:"product_id" binding:"omitempty,max=64"`
|
||||||
|
ProductStatus *int8 `json:"product_status" binding:"omitempty,oneof=0 2"`
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
// internal/service/goods.go —— Update 合并态校验思路
|
||||||
|
existing, err := s.goodsDao.GoodsGetByID(ctx, id) // 捕获返回值参与校验
|
||||||
|
// ...
|
||||||
|
effectiveProductID := existing.ProductID
|
||||||
|
if request.ProductID != nil { effectiveProductID = strings.TrimSpace(*request.ProductID) }
|
||||||
|
effectiveStatus := existing.ProductStatus
|
||||||
|
if request.ProductStatus != nil { effectiveStatus = *request.ProductStatus }
|
||||||
|
if effectiveStatus == 2 && effectiveProductID == "" {
|
||||||
|
return errcode.GoodsProductInvalid.WithDetails("发布状态为已发布时必须填写道具 ID")
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -50,6 +50,11 @@ jobs:
|
|||||||
SSH_HOST: ${{ vars.ZT_CLOSET_PROD_SSH_HOST }}
|
SSH_HOST: ${{ vars.ZT_CLOSET_PROD_SSH_HOST }}
|
||||||
SSH_USER: ${{ vars.ZT_CLOSET_PROD_SSH_USER }}
|
SSH_USER: ${{ vars.ZT_CLOSET_PROD_SSH_USER }}
|
||||||
run: |
|
run: |
|
||||||
|
# runner 默认未安装 rsync,先装上(生产机一般自带,无需处理)
|
||||||
|
if ! command -v rsync >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -y && sudo apt-get install -y rsync
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||||
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
|
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
|
||||||
chmod 600 ~/.ssh/deploy_key
|
chmod 600 ~/.ssh/deploy_key
|
||||||
@@ -97,3 +102,27 @@ jobs:
|
|||||||
- name: 部署结果
|
- name: 部署结果
|
||||||
if: always()
|
if: always()
|
||||||
run: echo "前端部署流程结束(目标 ${{ env.DEPLOY_DIR }})"
|
run: echo "前端部署流程结束(目标 ${{ env.DEPLOY_DIR }})"
|
||||||
|
|
||||||
|
- name: 通知钉钉
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
DINGTALK_TOKEN: ${{ secrets.ZT_CLOSET_DINGTALK_TOKEN }}
|
||||||
|
JOB_RESULT: ${{ jobs.build-and-deploy.result }}
|
||||||
|
run: |
|
||||||
|
if [ "$JOB_RESULT" = "success" ]; then
|
||||||
|
STATUS="✅ 前端部署成功"
|
||||||
|
COLOR="info"
|
||||||
|
else
|
||||||
|
STATUS="❌ 前端部署失败"
|
||||||
|
COLOR="warning"
|
||||||
|
fi
|
||||||
|
curl -s -X POST "https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_TOKEN}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "{
|
||||||
|
\"msgtype\": \"markdown\",
|
||||||
|
\"markdown\": {
|
||||||
|
\"title\": \"前端部署通知\",
|
||||||
|
\"text\": \"## 前端部署通知\n\n**结果**:<font color=\\\"${COLOR}\\\">${STATUS}</font>\n\n**触发人**:${{ gitea.actor }}\n**分支/标签**:${{ gitea.ref_name }}\n**提交**:[${{ gitea.sha }}](${{ gitea.server_url }}/${{ gitea.repository }}/commit/${{ gitea.sha }})\n**流水线**:[${{ gitea.run_id }}](${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }})\n"
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
echo "钉钉通知已发送:${STATUS}"
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export interface GoodsCreateRequest {
|
|||||||
valid_days?: number
|
valid_days?: number
|
||||||
description?: string
|
description?: string
|
||||||
status?: number
|
status?: number
|
||||||
|
/** 虚拟支付道具 ID(需与微信后台一致) */
|
||||||
|
product_id?: string
|
||||||
|
/** 虚拟支付道具发布状态 0=未发布 2=已发布 */
|
||||||
|
product_status?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoodsUpdateRequest {
|
export interface GoodsUpdateRequest {
|
||||||
@@ -54,6 +58,10 @@ export interface GoodsUpdateRequest {
|
|||||||
outfit_share_quota?: number
|
outfit_share_quota?: number
|
||||||
valid_days?: number
|
valid_days?: number
|
||||||
description?: string
|
description?: string
|
||||||
|
/** 虚拟支付道具 ID(传空串表示解除登记) */
|
||||||
|
product_id?: string
|
||||||
|
/** 虚拟支付道具发布状态 0=未发布 2=已发布 */
|
||||||
|
product_status?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoodsStatusRequest {
|
export interface GoodsStatusRequest {
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ export const PRODUCT_STATUS = enumMap([
|
|||||||
{ value: 3, label: '发布失败', tagType: 'danger' },
|
{ value: 3, label: '发布失败', tagType: 'danger' },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/** 虚拟支付道具发布状态(表单可选项:仅未发布/已发布两态) */
|
||||||
|
export const PRODUCT_STATUS_EDITABLE = enumMap([
|
||||||
|
{ value: 0, label: '未发布', tagType: 'info' },
|
||||||
|
{ value: 2, label: '已发布', tagType: 'success' },
|
||||||
|
])
|
||||||
|
|
||||||
/* ---------------- 订单 / 退款 ---------------- */
|
/* ---------------- 订单 / 退款 ---------------- */
|
||||||
export const ORDER_STATUS = enumMap([
|
export const ORDER_STATUS = enumMap([
|
||||||
{ value: 1, label: '待支付', tagType: 'warning' },
|
{ value: 1, label: '待支付', tagType: 'warning' },
|
||||||
|
|||||||
@@ -6,7 +6,15 @@ import SearchFormCard from '@/components/SearchFormCard.vue'
|
|||||||
import PaginationBar from '@/components/PaginationBar.vue'
|
import PaginationBar from '@/components/PaginationBar.vue'
|
||||||
import { createGoods, deleteGoods, getGoodsList, updateGoods, updateGoodsStatus } from '@/api/goods'
|
import { createGoods, deleteGoods, getGoodsList, updateGoods, updateGoodsStatus } from '@/api/goods'
|
||||||
import type { GoodsItem } from '@/types/goods'
|
import type { GoodsItem } from '@/types/goods'
|
||||||
import { GOODS_STATUS, GOODS_TYPE, PRODUCT_STATUS, enumLabel, enumOptions, enumTag } from '@/utils/constants'
|
import {
|
||||||
|
GOODS_STATUS,
|
||||||
|
GOODS_TYPE,
|
||||||
|
PRODUCT_STATUS,
|
||||||
|
PRODUCT_STATUS_EDITABLE,
|
||||||
|
enumLabel,
|
||||||
|
enumOptions,
|
||||||
|
enumTag,
|
||||||
|
} from '@/utils/constants'
|
||||||
import { formatDate, formatMoney, formatText, yuanToCents } from '@/utils/format'
|
import { formatDate, formatMoney, formatText, yuanToCents } from '@/utils/format'
|
||||||
|
|
||||||
/* ---------------- 列表与筛选 ---------------- */
|
/* ---------------- 列表与筛选 ---------------- */
|
||||||
@@ -60,6 +68,8 @@ const dialogVisible = ref(false)
|
|||||||
const dialogLoading = ref(false)
|
const dialogLoading = ref(false)
|
||||||
const editId = ref<number | null>(null)
|
const editId = ref<number | null>(null)
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
/** 编辑前的售价快照(分),用于已发布道具的改价提醒 */
|
||||||
|
const originalDiscountPrice = ref<number | null>(null)
|
||||||
|
|
||||||
interface GoodsForm {
|
interface GoodsForm {
|
||||||
goods_code: string
|
goods_code: string
|
||||||
@@ -74,6 +84,10 @@ interface GoodsForm {
|
|||||||
outfit_share_quota: number
|
outfit_share_quota: number
|
||||||
valid_days: number
|
valid_days: number
|
||||||
description: string
|
description: string
|
||||||
|
/** 虚拟支付道具 ID(需与微信后台一致) */
|
||||||
|
product_id: string
|
||||||
|
/** 虚拟支付道具发布状态 0=未发布 2=已发布 */
|
||||||
|
product_status: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = reactive<GoodsForm>({
|
const form = reactive<GoodsForm>({
|
||||||
@@ -89,6 +103,8 @@ const form = reactive<GoodsForm>({
|
|||||||
outfit_share_quota: 0,
|
outfit_share_quota: 0,
|
||||||
valid_days: 0,
|
valid_days: 0,
|
||||||
description: '',
|
description: '',
|
||||||
|
product_id: '',
|
||||||
|
product_status: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
@@ -103,10 +119,29 @@ const rules: FormRules = {
|
|||||||
goods_type: [{ required: true, message: '请选择商品类型', trigger: 'change' }],
|
goods_type: [{ required: true, message: '请选择商品类型', trigger: 'change' }],
|
||||||
price: [{ required: true, message: '请输入原价', trigger: 'blur' }],
|
price: [{ required: true, message: '请输入原价', trigger: 'blur' }],
|
||||||
discount_price: [{ required: true, message: '请输入售价', trigger: 'blur' }],
|
discount_price: [{ required: true, message: '请输入售价', trigger: 'blur' }],
|
||||||
|
product_id: [
|
||||||
|
{ max: 64, message: '道具 ID 不超过 64 字', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (form.product_status === 2 && !String(value ?? '').trim()) {
|
||||||
|
callback(new Error('发布状态为「已发布」时必须填写道具 ID'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发布状态变更时联动重校验道具 ID(校验结果由表单项自身展示) */
|
||||||
|
function handleProductStatusChange() {
|
||||||
|
formRef.value?.validateField('product_id', () => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
editId.value = null
|
editId.value = null
|
||||||
|
originalDiscountPrice.value = null
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
goods_code: '',
|
goods_code: '',
|
||||||
goods_name: '',
|
goods_name: '',
|
||||||
@@ -120,12 +155,15 @@ function openCreate() {
|
|||||||
outfit_share_quota: 0,
|
outfit_share_quota: 0,
|
||||||
valid_days: 0,
|
valid_days: 0,
|
||||||
description: '',
|
description: '',
|
||||||
|
product_id: '',
|
||||||
|
product_status: 0,
|
||||||
})
|
})
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(row: GoodsItem) {
|
function openEdit(row: GoodsItem) {
|
||||||
editId.value = row.id
|
editId.value = row.id
|
||||||
|
originalDiscountPrice.value = row.discount_price
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
goods_code: row.goods_code,
|
goods_code: row.goods_code,
|
||||||
goods_name: row.goods_name,
|
goods_name: row.goods_name,
|
||||||
@@ -139,6 +177,9 @@ function openEdit(row: GoodsItem) {
|
|||||||
outfit_share_quota: row.outfit_share_quota,
|
outfit_share_quota: row.outfit_share_quota,
|
||||||
valid_days: row.valid_days,
|
valid_days: row.valid_days,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
product_id: row.product_id ?? '',
|
||||||
|
// 历史脏值(1 发布中 / 3 发布失败,旧自动同步任务遗留)统一归一为「未发布」
|
||||||
|
product_status: row.product_status === 2 ? 2 : 0,
|
||||||
})
|
})
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -146,10 +187,27 @@ function openEdit(row: GoodsItem) {
|
|||||||
async function submitForm() {
|
async function submitForm() {
|
||||||
const valid = await formRef.value?.validate().catch(() => false)
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
|
|
||||||
|
const price = yuanToCents(form.price)
|
||||||
|
const discountPrice = yuanToCents(form.discount_price)
|
||||||
|
|
||||||
|
// 已发布道具改价提醒:微信按道具价格扣款,未同步会因发货金额校验失败导致用户付款拿不到权益
|
||||||
|
if (
|
||||||
|
editId.value !== null &&
|
||||||
|
form.product_status === 2 &&
|
||||||
|
originalDiscountPrice.value !== null &&
|
||||||
|
discountPrice !== originalDiscountPrice.value
|
||||||
|
) {
|
||||||
|
const confirmed = await ElMessageBox.confirm(
|
||||||
|
'该商品已登记为「已发布」的微信虚拟支付道具。修改售价后,请务必同步更新微信后台的道具价格,否则用户付款后将无法发货。确认继续保存吗?',
|
||||||
|
'改价提醒',
|
||||||
|
{ type: 'warning', confirmButtonText: '继续保存', cancelButtonText: '取消' },
|
||||||
|
).catch(() => false)
|
||||||
|
if (!confirmed) return
|
||||||
|
}
|
||||||
|
|
||||||
dialogLoading.value = true
|
dialogLoading.value = true
|
||||||
try {
|
try {
|
||||||
const price = yuanToCents(form.price)
|
|
||||||
const discountPrice = yuanToCents(form.discount_price)
|
|
||||||
if (editId.value === null) {
|
if (editId.value === null) {
|
||||||
await createGoods({
|
await createGoods({
|
||||||
goods_code: form.goods_code,
|
goods_code: form.goods_code,
|
||||||
@@ -164,6 +222,8 @@ async function submitForm() {
|
|||||||
outfit_share_quota: form.outfit_share_quota,
|
outfit_share_quota: form.outfit_share_quota,
|
||||||
valid_days: form.valid_days,
|
valid_days: form.valid_days,
|
||||||
description: form.description || undefined,
|
description: form.description || undefined,
|
||||||
|
product_id: form.product_id.trim() || undefined,
|
||||||
|
product_status: form.product_status,
|
||||||
})
|
})
|
||||||
ElMessage.success('商品创建成功')
|
ElMessage.success('商品创建成功')
|
||||||
} else {
|
} else {
|
||||||
@@ -178,6 +238,8 @@ async function submitForm() {
|
|||||||
outfit_share_quota: form.outfit_share_quota,
|
outfit_share_quota: form.outfit_share_quota,
|
||||||
valid_days: form.valid_days,
|
valid_days: form.valid_days,
|
||||||
description: form.description || undefined,
|
description: form.description || undefined,
|
||||||
|
product_id: form.product_id.trim(),
|
||||||
|
product_status: form.product_status,
|
||||||
})
|
})
|
||||||
ElMessage.success('商品更新成功')
|
ElMessage.success('商品更新成功')
|
||||||
}
|
}
|
||||||
@@ -277,11 +339,14 @@ async function handleDelete(row: GoodsItem) {
|
|||||||
<el-table-column label="AI 额度" width="90" align="right">
|
<el-table-column label="AI 额度" width="90" align="right">
|
||||||
<template #default="{ row }: { row: GoodsItem }">{{ row.ai_recommend_quota }}</template>
|
<template #default="{ row }: { row: GoodsItem }">{{ row.ai_recommend_quota }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="虚拟支付" width="110">
|
<el-table-column label="虚拟支付" width="150">
|
||||||
<template #default="{ row }: { row: GoodsItem }">
|
<template #default="{ row }: { row: GoodsItem }">
|
||||||
<el-tag v-if="row.product_id" :type="enumTag(PRODUCT_STATUS, row.product_status)" size="small">
|
<template v-if="row.product_id">
|
||||||
{{ enumLabel(PRODUCT_STATUS, row.product_status) }}
|
<el-tag :type="enumTag(PRODUCT_STATUS, row.product_status)" size="small">
|
||||||
</el-tag>
|
{{ enumLabel(PRODUCT_STATUS, row.product_status) }}
|
||||||
|
</el-tag>
|
||||||
|
<div class="product-id-text" :title="row.product_id">{{ row.product_id }}</div>
|
||||||
|
</template>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -389,6 +454,28 @@ async function handleDelete(row: GoodsItem) {
|
|||||||
<el-input v-model="form.description" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
<el-input v-model="form.description" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-divider content-position="left">微信虚拟支付道具</el-divider>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="道具 ID" prop="product_id">
|
||||||
|
<el-input v-model="form.product_id" maxlength="64" placeholder="微信后台的道具 ID,如 monthly_vip" />
|
||||||
|
<div class="form-tip">需与微信后台保持一致:道具 ID、道具名称、售价</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="发布状态" prop="product_status">
|
||||||
|
<el-select v-model="form.product_status" style="width: 100%" @change="handleProductStatusChange">
|
||||||
|
<el-option
|
||||||
|
v-for="opt in enumOptions(PRODUCT_STATUS_EDITABLE)"
|
||||||
|
:key="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<div class="form-tip">设为「已发布」后小程序才可下单;改价后必须同步微信侧</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -419,4 +506,13 @@ async function handleDelete(row: GoodsItem) {
|
|||||||
color: #9aa3c0;
|
color: #9aa3c0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-id-text {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aa3c0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user