feat: 实现客户端换货系统(client-exchange-system)

新增完整换货生命周期管理:后台发起 → 客户端填收货信息 → 后台发货 → 确认完成(含可选全量迁移) → 旧资产转新再销售

后台接口(7个):
- POST /api/admin/exchanges(发起换货)
- GET /api/admin/exchanges(换货列表)
- GET /api/admin/exchanges/:id(换货详情)
- POST /api/admin/exchanges/:id/ship(发货)
- POST /api/admin/exchanges/:id/complete(确认完成+可选迁移)
- POST /api/admin/exchanges/:id/cancel(取消)
- POST /api/admin/exchanges/:id/renew(旧资产转新)

客户端接口(2个):
- GET /api/c/v1/exchange/pending(查询换货通知)
- POST /api/c/v1/exchange/:id/shipping-info(填写收货信息)

核心能力:
- ExchangeOrder 模型与状态机(1待填写→2待发货→3已发货→4已完成,1/2可取消→5)
- 全量迁移事务(11张表:钱包、套餐、标签、客户绑定等)
- 旧资产转新(generation+1、状态重置、新钱包、历史隔离)
- 旧 CardReplacementRecord 表改名为 legacy,is_replaced 过滤改为查新表
- 数据库迁移:000085 新建 tb_exchange_order,000086 旧表改名
This commit is contained in:
2026-03-19 13:26:54 +08:00
parent df76e33105
commit e78f5794b9
41 changed files with 5242 additions and 10 deletions

View File

@@ -92,6 +92,7 @@ type AssetRechargeRecord struct {
LinkedOrderType string `gorm:"column:linked_order_type;type:varchar(20);comment:关联订单类型" json:"linked_order_type,omitempty"`
LinkedCarrierType string `gorm:"column:linked_carrier_type;type:varchar(20);comment:关联载体类型" json:"linked_carrier_type,omitempty"`
LinkedCarrierID *uint `gorm:"column:linked_carrier_id;type:bigint;comment:关联载体ID" json:"linked_carrier_id,omitempty"`
AutoPurchaseStatus string `gorm:"column:auto_purchase_status;type:varchar(20);default:'';comment:强充自动代购状态(pending-待处理 success-成功 failed-失败)" json:"auto_purchase_status,omitempty"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
}

View File

@@ -0,0 +1,104 @@
package dto
import "time"
type CreateExchangeRequest struct {
OldAssetType string `json:"old_asset_type" validate:"required,oneof=iot_card device" required:"true" description:"旧资产类型 (iot_card:物联网卡, device:设备)"`
OldIdentifier string `json:"old_identifier" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"旧资产标识符(ICCID/虚拟号/IMEI/SN)"`
ExchangeReason string `json:"exchange_reason" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"换货原因"`
Remark *string `json:"remark" validate:"omitempty,max=500" maxLength:"500" description:"备注"`
}
type ExchangeListRequest struct {
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=5" minimum:"1" maximum:"5" description:"换货状态 (1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)"`
Identifier string `json:"identifier" query:"identifier" validate:"omitempty,max=100" maxLength:"100" description:"资产标识符搜索(旧资产/新资产标识符模糊匹配)"`
CreatedAtStart *time.Time `json:"created_at_start" query:"created_at_start" description:"创建时间起始"`
CreatedAtEnd *time.Time `json:"created_at_end" query:"created_at_end" description:"创建时间结束"`
}
type ExchangeShipRequest struct {
ExpressCompany string `json:"express_company" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"快递公司"`
ExpressNo string `json:"express_no" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"快递单号"`
NewIdentifier string `json:"new_identifier" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"新资产标识符(ICCID/虚拟号/IMEI/SN)"`
MigrateData bool `json:"migrate_data" required:"true" description:"是否执行全量迁移 (true:执行, false:不执行)"`
}
type ExchangeCancelRequest struct {
Remark *string `json:"remark" validate:"omitempty,max=500" maxLength:"500" description:"取消备注"`
}
type ClientShippingInfoRequest struct {
RecipientName string `json:"recipient_name" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"收件人姓名"`
RecipientPhone string `json:"recipient_phone" validate:"required,min=1,max=20" required:"true" minLength:"1" maxLength:"20" description:"收件人电话"`
RecipientAddress string `json:"recipient_address" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"收货地址"`
}
type ClientExchangePendingRequest struct {
Identifier string `json:"identifier" query:"identifier" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"资产标识符(ICCID/虚拟号/IMEI/SN)"`
}
type ExchangeIDRequest struct {
ID uint `path:"id" required:"true" description:"换货单ID"`
}
type ExchangeShipParams struct {
ID uint `path:"id" required:"true" description:"换货单ID"`
ExchangeShipRequest
}
type ExchangeCancelParams struct {
ID uint `path:"id" required:"true" description:"换货单ID"`
ExchangeCancelRequest
}
type ClientShippingInfoParams struct {
ID uint `path:"id" required:"true" description:"换货单ID"`
ClientShippingInfoRequest
}
type ExchangeOrderResponse struct {
ID uint `json:"id" description:"换货单ID"`
ExchangeNo string `json:"exchange_no" description:"换货单号"`
OldAssetType string `json:"old_asset_type" description:"旧资产类型 (iot_card:物联网卡, device:设备)"`
OldAssetID uint `json:"old_asset_id" description:"旧资产ID"`
OldAssetIdentifier string `json:"old_asset_identifier" description:"旧资产标识符"`
NewAssetType string `json:"new_asset_type" description:"新资产类型 (iot_card:物联网卡, device:设备)"`
NewAssetID *uint `json:"new_asset_id,omitempty" description:"新资产ID"`
NewAssetIdentifier string `json:"new_asset_identifier" description:"新资产标识符"`
RecipientName string `json:"recipient_name" description:"收件人姓名"`
RecipientPhone string `json:"recipient_phone" description:"收件人电话"`
RecipientAddress string `json:"recipient_address" description:"收货地址"`
ExpressCompany string `json:"express_company" description:"快递公司"`
ExpressNo string `json:"express_no" description:"快递单号"`
MigrateData bool `json:"migrate_data" description:"是否执行全量迁移"`
MigrationCompleted bool `json:"migration_completed" description:"迁移是否已完成"`
MigrationBalance int64 `json:"migration_balance" description:"迁移转移金额(分)"`
ExchangeReason string `json:"exchange_reason" description:"换货原因"`
Remark *string `json:"remark,omitempty" description:"备注"`
Status int `json:"status" description:"换货状态 (1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)"`
StatusText string `json:"status_text" description:"换货状态文本"`
ShopID *uint `json:"shop_id,omitempty" description:"所属店铺ID"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
DeletedAt *time.Time `json:"deleted_at,omitempty" description:"删除时间"`
Creator uint `json:"creator" description:"创建人ID"`
Updater uint `json:"updater" description:"更新人ID"`
}
type ExchangeListResponse struct {
List []*ExchangeOrderResponse `json:"list" description:"换货单列表"`
Total int64 `json:"total" description:"总数"`
Page int `json:"page" description:"当前页码"`
PageSize int `json:"page_size" description:"每页数量"`
}
type ClientExchangePendingResponse struct {
ID uint `json:"id" description:"换货单ID"`
ExchangeNo string `json:"exchange_no" description:"换货单号"`
Status int `json:"status" description:"换货状态 (1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)"`
StatusText string `json:"status_text" description:"换货状态文本"`
ExchangeReason string `json:"exchange_reason" description:"换货原因"`
CreatedAt time.Time `json:"created_at" description:"创建时间"`
}

View File

@@ -0,0 +1,65 @@
package model
import (
"fmt"
"math/rand"
"time"
"gorm.io/gorm"
)
// ExchangeOrder 换货单模型
// 承载客户端换货的完整生命周期:后台发起 → 客户端填写收货信息 → 后台发货 → 确认完成(含可选全量迁移) → 旧资产可转新
// 状态机1-待填写信息 → 2-待发货 → 3-已发货待确认 → 4-已完成1/2 时可取消 → 5-已取消
type ExchangeOrder struct {
gorm.Model
BaseModel `gorm:"embedded"`
// 单号
ExchangeNo string `gorm:"column:exchange_no;type:varchar(50);not null;uniqueIndex:idx_exchange_order_no,where:deleted_at IS NULL;comment:换货单号(EXC+日期+随机数)" json:"exchange_no"`
// 旧资产快照
OldAssetType string `gorm:"column:old_asset_type;type:varchar(20);not null;comment:旧资产类型(iot_card/device)" json:"old_asset_type"`
OldAssetID uint `gorm:"column:old_asset_id;not null;index:idx_exchange_order_old_asset;comment:旧资产ID" json:"old_asset_id"`
OldAssetIdentifier string `gorm:"column:old_asset_identifier;type:varchar(100);not null;comment:旧资产标识符(ICCID/虚拟号)" json:"old_asset_identifier"`
// 新资产快照(发货时填写)
NewAssetType string `gorm:"column:new_asset_type;type:varchar(20);comment:新资产类型(iot_card/device)" json:"new_asset_type"`
NewAssetID *uint `gorm:"column:new_asset_id;comment:新资产ID" json:"new_asset_id,omitempty"`
NewAssetIdentifier string `gorm:"column:new_asset_identifier;type:varchar(100);comment:新资产标识符(ICCID/虚拟号)" json:"new_asset_identifier"`
// 收货信息(客户端填写)
RecipientName string `gorm:"column:recipient_name;type:varchar(50);comment:收件人姓名" json:"recipient_name"`
RecipientPhone string `gorm:"column:recipient_phone;type:varchar(20);comment:收件人电话" json:"recipient_phone"`
RecipientAddress string `gorm:"column:recipient_address;type:text;comment:收货地址" json:"recipient_address"`
// 物流信息(后台发货时填写)
ExpressCompany string `gorm:"column:express_company;type:varchar(100);comment:快递公司" json:"express_company"`
ExpressNo string `gorm:"column:express_no;type:varchar(100);comment:快递单号" json:"express_no"`
// 迁移相关
MigrateData bool `gorm:"column:migrate_data;type:boolean;default:false;comment:是否执行全量迁移" json:"migrate_data"`
MigrationCompleted bool `gorm:"column:migration_completed;type:boolean;default:false;comment:迁移是否已完成" json:"migration_completed"`
MigrationBalance int64 `gorm:"column:migration_balance;type:bigint;default:0;comment:迁移转移金额(分)" json:"migration_balance"`
// 业务信息
ExchangeReason string `gorm:"column:exchange_reason;type:varchar(100);not null;comment:换货原因" json:"exchange_reason"`
Remark *string `gorm:"column:remark;type:text;comment:备注" json:"remark,omitempty"`
Status int `gorm:"column:status;type:int;not null;default:1;index:idx_exchange_order_status;comment:换货状态 1-待填写信息 2-待发货 3-已发货待确认 4-已完成 5-已取消" json:"status"`
// 多租户
ShopID *uint `gorm:"column:shop_id;index;comment:所属店铺ID" json:"shop_id,omitempty"`
}
// TableName 指定表名
func (ExchangeOrder) TableName() string {
return "tb_exchange_order"
}
// GenerateExchangeNo 生成换货单号
// 格式EXC + 年月日时分秒 + 6位随机数如 EXC20260319143052123456
func GenerateExchangeNo() string {
now := time.Now()
randomNum := rand.Intn(1000000)
return fmt.Sprintf("EXC%s%06d", now.Format("20060102150405"), randomNum)
}