Files
zt-closet-manage-frontend/.codebuddy/plans/goods-virtualpay-register_260b5ea9.md
T
2026-09-14 01:16:41 +08:00

8.8 KiB
Raw Blame History

name, overview, todos
name overview todos
goods-virtualpay-register 管理后台与管理前端支持登记/修改微信虚拟支付道具信息(product_id、product_status),发布状态仅支持未发布/已发布两态,运营无需再手动改数据库。
id content status
backend-goods-register 扩展管理后端:dto 增道具字段、errcode 增错误码、DAO 增按道具 ID 查询、Service 增写入与合并态校验 completed
id content status dependencies
frontend-goods-form 扩展管理前端:类型与常量增道具字段,GoodsListView 表单增校验、改价提醒与列表展示 completed
backend-goods-register
id content status dependencies
docs-and-verify 更新管理后端 README 道具登记说明;执行 go build 与前端 type-check、build 验证 completed
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)。
  1. README.md:核心功能中「只读展示」改为「可登记(未发布/已发布两态)」;商品接口表下补充运营约定(道具 ID / 名称 / 价格须与微信后台一致、改价须同步微信侧、道具 ID 唯一)。

管理前端

  1. src/types/goods.ts:GoodsCreateRequest / GoodsUpdateRequest 增加 product_id?: string、product_status?: number。
  2. src/utils/constants.ts:新增 PRODUCT_STATUS_EDITABLE = enumMap([{value:0,label:'未发布'},{value:2,label:'已发布',tagType:'success'}]),仅供表单下拉;原 PRODUCT_STATUS 四态枚举保留给列表展示,兼容历史数据。
  3. 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 小字(超长省略)。
  1. 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")
}