feat: 订单创建时快照买家手机号/昵称和套餐类型
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
新增字段: - tb_order: buyer_phone, buyer_nickname(个人客户下单时快照) - tb_order_item: package_type(套餐类型快照) 后台订单列表支持按 buyer_phone 精确过滤查询。 OpenSpec: order-buyer-snapshot
This commit is contained in:
@@ -817,6 +817,9 @@ components:
|
||||
data_usage_mb:
|
||||
description: 已用真流量(MB)
|
||||
type: integer
|
||||
enable_virtual_data:
|
||||
description: 是否启用虚流量
|
||||
type: boolean
|
||||
expires_at:
|
||||
description: 到期时间(待生效套餐为空)
|
||||
format: date-time
|
||||
@@ -4274,6 +4277,9 @@ components:
|
||||
description: 批次号
|
||||
maxLength: 100
|
||||
type: string
|
||||
card_category:
|
||||
description: 卡业务类型 (normal:普通卡, industry:行业卡),默认 normal
|
||||
type: string
|
||||
carrier_id:
|
||||
description: 运营商ID
|
||||
minimum: 1
|
||||
@@ -4320,6 +4326,9 @@ components:
|
||||
batch_no:
|
||||
description: 批次号
|
||||
type: string
|
||||
card_category:
|
||||
description: 卡业务类型 (normal:普通卡, industry:行业卡)
|
||||
type: string
|
||||
carrier_id:
|
||||
description: 运营商ID
|
||||
minimum: 0
|
||||
@@ -4393,6 +4402,9 @@ components:
|
||||
batch_no:
|
||||
description: 批次号
|
||||
type: string
|
||||
card_category:
|
||||
description: 卡业务类型 (normal:普通卡, industry:行业卡)
|
||||
type: string
|
||||
carrier_id:
|
||||
description: 运营商ID
|
||||
minimum: 0
|
||||
@@ -4831,6 +4843,10 @@ components:
|
||||
package_name:
|
||||
description: 套餐名称
|
||||
type: string
|
||||
package_type:
|
||||
description: 套餐类型快照 (formal:正式套餐, addon:加油包)
|
||||
nullable: true
|
||||
type: string
|
||||
quantity:
|
||||
description: 数量
|
||||
type: integer
|
||||
@@ -4872,6 +4888,14 @@ components:
|
||||
description: 买家ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
buyer_nickname:
|
||||
description: 买家昵称快照(个人客户下单时记录)
|
||||
nullable: true
|
||||
type: string
|
||||
buyer_phone:
|
||||
description: 买家手机号快照(个人客户下单时记录)
|
||||
nullable: true
|
||||
type: string
|
||||
buyer_type:
|
||||
description: 买家类型 (personal:个人客户, agent:代理商)
|
||||
type: string
|
||||
@@ -14864,6 +14888,13 @@ paths:
|
||||
description: 资产标识符(ICCID 或 VirtualNo)精确查询
|
||||
maxLength: 100
|
||||
type: string
|
||||
- description: 买家手机号精确查询
|
||||
in: query
|
||||
name: buyer_phone
|
||||
schema:
|
||||
description: 买家手机号精确查询
|
||||
maxLength: 20
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
|
||||
@@ -24,6 +24,8 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
assetWalletTransactionStore := postgres.NewAssetWalletTransactionStore(deps.DB, deps.Redis)
|
||||
assetRechargeStore := postgres.NewAssetRechargeStore(deps.DB, deps.Redis)
|
||||
personalCustomerOpenIDStore := postgres.NewPersonalCustomerOpenIDStore(deps.DB)
|
||||
personalCustomerStore := postgres.NewPersonalCustomerStore(deps.DB, deps.Redis)
|
||||
personalCustomerPhoneStore := postgres.NewPersonalCustomerPhoneStore(deps.DB)
|
||||
orderStore := postgres.NewOrderStore(deps.DB, deps.Redis)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(deps.DB)
|
||||
shopSeriesAllocationStore := postgres.NewShopSeriesAllocationStore(deps.DB)
|
||||
@@ -37,6 +39,8 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
assetWalletStore,
|
||||
personalCustomerDeviceStore,
|
||||
personalCustomerOpenIDStore,
|
||||
personalCustomerStore,
|
||||
personalCustomerPhoneStore,
|
||||
svc.WechatConfig,
|
||||
svc.Order,
|
||||
packageSeriesStore,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/polling"
|
||||
accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account"
|
||||
accountAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/account_audit"
|
||||
@@ -108,6 +112,10 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
// 使用 PollingLifecycleService 替代 APICallback:通过分片队列准确操作(修复 api_callback.go 遗漏 protect 队列的 Bug3)
|
||||
pollingConfigStore := postgres.NewPollingConfigStore(deps.DB)
|
||||
pollingConfigMgr := polling.NewPollingConfigManager(pollingConfigStore, deps.Redis, deps.Logger)
|
||||
if err := pollingConfigMgr.Load(context.Background()); err != nil {
|
||||
deps.Logger.Warn("API 进程加载轮询配置失败", zap.Error(err))
|
||||
}
|
||||
pollingConfigMgr.Start(context.Background())
|
||||
pollingQueueMgr := polling.NewPollingQueueManager(deps.Redis, constants.PollingShardCount, deps.Logger)
|
||||
iotCard.SetPollingCallback(polling.NewPollingLifecycleService(pollingQueueMgr, pollingConfigMgr, s.IotCard, s.DeviceSimBinding, s.Device, deps.Logger))
|
||||
// 注入流量扣减回调,使手动刷新资产时能触发套餐流量扣减
|
||||
@@ -191,7 +199,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
ShopSeriesGrant: shopSeriesGrantSvc.New(deps.DB, s.ShopSeriesAllocation, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package, s.PackageSeries, deps.Logger),
|
||||
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||
PurchaseValidation: purchaseValidation,
|
||||
Order: orderSvc.New(deps.DB, deps.Redis, s.Order, s.OrderItem, s.AgentWallet, s.AssetWallet, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, s.PackageUsage, s.Package, wechatConfig, deps.WechatPayment, deps.QueueClient, deps.Logger, s.AssetIdentifier),
|
||||
Order: orderSvc.New(deps.DB, deps.Redis, s.Order, s.OrderItem, s.AgentWallet, s.AssetWallet, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, s.PackageUsage, s.Package, wechatConfig, deps.WechatPayment, deps.QueueClient, deps.Logger, s.AssetIdentifier, s.PersonalCustomer, s.PersonalCustomerPhone),
|
||||
Exchange: exchangeSvc.New(deps.DB, s.ExchangeOrder, s.IotCard, s.Device, s.AssetWallet, s.AssetWalletTransaction, s.PackageUsage, s.PackageUsageDailyRecord, s.ResourceTag, s.PersonalCustomerDevice, deps.Logger),
|
||||
Recharge: rechargeSvc.New(deps.DB, s.AssetRecharge, s.AssetWallet, s.AssetWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, wechatConfig, deps.Logger, deps.QueueClient),
|
||||
PollingConfig: pollingSvc.NewConfigService(s.PollingConfig),
|
||||
|
||||
@@ -101,6 +101,8 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q
|
||||
nil, // queueClient: 超时取消不触发分佣
|
||||
deps.Logger,
|
||||
stores.AssetIdentifier,
|
||||
stores.PersonalCustomer,
|
||||
stores.PersonalCustomerPhone,
|
||||
)
|
||||
|
||||
// 创建停复机服务并注入回调:流量耗尽自动停机、套餐激活/重置/支付后自动复机
|
||||
|
||||
@@ -30,6 +30,8 @@ type workerStores struct {
|
||||
AgentWalletTransaction *postgres.AgentWalletTransactionStore
|
||||
AssetWallet *postgres.AssetWalletStore
|
||||
AssetIdentifier *postgres.AssetIdentifierStore
|
||||
PersonalCustomer *postgres.PersonalCustomerStore
|
||||
PersonalCustomerPhone *postgres.PersonalCustomerPhoneStore
|
||||
}
|
||||
|
||||
func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
|
||||
@@ -58,6 +60,8 @@ func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
|
||||
AgentWalletTransaction: postgres.NewAgentWalletTransactionStore(deps.DB, deps.Redis),
|
||||
AssetWallet: postgres.NewAssetWalletStore(deps.DB, deps.Redis),
|
||||
AssetIdentifier: postgres.NewAssetIdentifierStore(deps.DB),
|
||||
PersonalCustomer: postgres.NewPersonalCustomerStore(deps.DB, deps.Redis),
|
||||
PersonalCustomerPhone: postgres.NewPersonalCustomerPhoneStore(deps.DB),
|
||||
}
|
||||
|
||||
return &queue.WorkerStores{
|
||||
@@ -85,5 +89,7 @@ func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
|
||||
AgentWalletTransaction: stores.AgentWalletTransaction,
|
||||
AssetWallet: stores.AssetWallet,
|
||||
AssetIdentifier: stores.AssetIdentifier,
|
||||
PersonalCustomer: stores.PersonalCustomer,
|
||||
PersonalCustomerPhone: stores.PersonalCustomerPhone,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ type OrderListRequest struct {
|
||||
EndTime *time.Time `json:"end_time" query:"end_time" description:"创建时间结束"`
|
||||
IsExpired *bool `json:"is_expired" query:"is_expired" description:"是否已过期 (true:已过期, false:未过期)"`
|
||||
Identifier string `json:"identifier" query:"identifier" validate:"omitempty,max=100" maxLength:"100" description:"资产标识符(ICCID 或 VirtualNo)精确查询"`
|
||||
BuyerPhone string `json:"buyer_phone" query:"buyer_phone" validate:"omitempty,max=20" maxLength:"20" description:"买家手机号精确查询"`
|
||||
}
|
||||
|
||||
type PayOrderRequest struct {
|
||||
@@ -36,12 +37,13 @@ type PayOrderRequest struct {
|
||||
}
|
||||
|
||||
type OrderItemResponse struct {
|
||||
ID uint `json:"id" description:"明细ID"`
|
||||
PackageID uint `json:"package_id" description:"套餐ID"`
|
||||
PackageName string `json:"package_name" description:"套餐名称"`
|
||||
Quantity int `json:"quantity" description:"数量"`
|
||||
UnitPrice int64 `json:"unit_price" description:"单价(分)"`
|
||||
Amount int64 `json:"amount" description:"小计金额(分)"`
|
||||
ID uint `json:"id" description:"明细ID"`
|
||||
PackageID uint `json:"package_id" description:"套餐ID"`
|
||||
PackageName string `json:"package_name" description:"套餐名称"`
|
||||
PackageType *string `json:"package_type" description:"套餐类型快照 (formal:正式套餐, addon:加油包)"`
|
||||
Quantity int `json:"quantity" description:"数量"`
|
||||
UnitPrice int64 `json:"unit_price" description:"单价(分)"`
|
||||
Amount int64 `json:"amount" description:"小计金额(分)"`
|
||||
}
|
||||
|
||||
type OrderResponse struct {
|
||||
@@ -50,6 +52,8 @@ type OrderResponse struct {
|
||||
OrderType string `json:"order_type" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
|
||||
BuyerType string `json:"buyer_type" description:"买家类型 (personal:个人客户, agent:代理商)"`
|
||||
BuyerID uint `json:"buyer_id" description:"买家ID"`
|
||||
BuyerPhone *string `json:"buyer_phone" description:"买家手机号快照(个人客户下单时记录)"`
|
||||
BuyerNickname *string `json:"buyer_nickname" description:"买家昵称快照(个人客户下单时记录)"`
|
||||
IotCardID *uint `json:"iot_card_id,omitempty" description:"IoT卡ID"`
|
||||
DeviceID *uint `json:"device_id,omitempty" description:"设备ID"`
|
||||
TotalAmount int64 `json:"total_amount" description:"订单总金额(分)"`
|
||||
|
||||
@@ -18,8 +18,10 @@ type Order struct {
|
||||
OrderType string `gorm:"column:order_type;type:varchar(20);not null;comment:订单类型 single_card-单卡购买 device-设备购买" json:"order_type"`
|
||||
|
||||
// 买家信息
|
||||
BuyerType string `gorm:"column:buyer_type;type:varchar(20);not null;comment:买家类型 personal-个人客户 agent-代理商" json:"buyer_type"`
|
||||
BuyerID uint `gorm:"column:buyer_id;index:idx_order_buyer;not null;comment:买家ID(个人客户ID或店铺ID)" json:"buyer_id"`
|
||||
BuyerType string `gorm:"column:buyer_type;type:varchar(20);not null;comment:买家类型 personal-个人客户 agent-代理商" json:"buyer_type"`
|
||||
BuyerID uint `gorm:"column:buyer_id;index:idx_order_buyer;not null;comment:买家ID(个人客户ID或店铺ID)" json:"buyer_id"`
|
||||
BuyerPhone *string `gorm:"column:buyer_phone;type:varchar(20);comment:下单时买家主手机号快照" json:"buyer_phone"`
|
||||
BuyerNickname *string `gorm:"column:buyer_nickname;type:varchar(100);comment:下单时买家昵称快照" json:"buyer_nickname"`
|
||||
|
||||
// 关联资源
|
||||
IotCardID *uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡购买时有值)" json:"iot_card_id,omitempty"`
|
||||
@@ -121,12 +123,13 @@ type OrderItem struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
|
||||
OrderID uint `gorm:"column:order_id;index:idx_order_item_order_id;not null;comment:订单ID" json:"order_id"`
|
||||
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
|
||||
PackageName string `gorm:"column:package_name;type:varchar(100);not null;comment:套餐名称(快照)" json:"package_name"`
|
||||
Quantity int `gorm:"column:quantity;type:int;default:1;not null;comment:数量" json:"quantity"`
|
||||
UnitPrice int64 `gorm:"column:unit_price;type:bigint;not null;comment:单价(分)" json:"unit_price"`
|
||||
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:小计金额(分)" json:"amount"`
|
||||
OrderID uint `gorm:"column:order_id;index:idx_order_item_order_id;not null;comment:订单ID" json:"order_id"`
|
||||
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
|
||||
PackageName string `gorm:"column:package_name;type:varchar(100);not null;comment:套餐名称(快照)" json:"package_name"`
|
||||
PackageType *string `gorm:"column:package_type;type:varchar(20);comment:套餐类型快照 formal-正式套餐 addon-加油包" json:"package_type"`
|
||||
Quantity int `gorm:"column:quantity;type:int;default:1;not null;comment:数量" json:"quantity"`
|
||||
UnitPrice int64 `gorm:"column:unit_price;type:bigint;not null;comment:单价(分)" json:"unit_price"`
|
||||
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:小计金额(分)" json:"amount"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -48,22 +48,24 @@ type ForceRechargeRequirement struct {
|
||||
|
||||
// Service 客户端订单服务。
|
||||
type Service struct {
|
||||
assetService *asset.Service
|
||||
purchaseValidationService *purchase_validation.Service
|
||||
orderStore *postgres.OrderStore
|
||||
rechargeRecordStore *postgres.AssetRechargeStore
|
||||
walletStore *postgres.AssetWalletStore
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
orderPaymentService OrderWalletPayServiceInterface
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
assetService *asset.Service
|
||||
purchaseValidationService *purchase_validation.Service
|
||||
orderStore *postgres.OrderStore
|
||||
rechargeRecordStore *postgres.AssetRechargeStore
|
||||
walletStore *postgres.AssetWalletStore
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore
|
||||
personalCustomerStore *postgres.PersonalCustomerStore
|
||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
orderPaymentService OrderWalletPayServiceInterface
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// New 创建客户端订单服务。
|
||||
@@ -75,6 +77,8 @@ func New(
|
||||
walletStore *postgres.AssetWalletStore,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore,
|
||||
personalCustomerStore *postgres.PersonalCustomerStore,
|
||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
orderPaymentService OrderWalletPayServiceInterface,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
@@ -86,22 +90,24 @@ func New(
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
assetService: assetService,
|
||||
purchaseValidationService: purchaseValidationService,
|
||||
orderStore: orderStore,
|
||||
rechargeRecordStore: rechargeRecordStore,
|
||||
walletStore: walletStore,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
openIDStore: openIDStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
orderPaymentService: orderPaymentService,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
db: db,
|
||||
redis: redisClient,
|
||||
logger: logger,
|
||||
assetService: assetService,
|
||||
purchaseValidationService: purchaseValidationService,
|
||||
orderStore: orderStore,
|
||||
rechargeRecordStore: rechargeRecordStore,
|
||||
walletStore: walletStore,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
openIDStore: openIDStore,
|
||||
personalCustomerStore: personalCustomerStore,
|
||||
personalCustomerPhoneStore: personalCustomerPhoneStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
orderPaymentService: orderPaymentService,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
db: db,
|
||||
redis: redisClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +302,7 @@ func (s *Service) createPackageOrder(
|
||||
sellerCostPrice = costPrice
|
||||
}
|
||||
|
||||
order, err := s.buildPendingOrder(customerID, validationResult, sellerCostPrice)
|
||||
order, err := s.buildPendingOrder(ctx, customerID, validationResult, sellerCostPrice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -401,7 +407,27 @@ func (s *Service) createForceRechargeOrder(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) buildPendingOrder(customerID uint, result *purchase_validation.PurchaseValidationResult, sellerCostPrice int64) (*model.Order, error) {
|
||||
func (s *Service) fetchBuyerSnapshot(ctx context.Context, customerID uint) (phone, nickname *string) {
|
||||
if customerID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
customer, err := s.personalCustomerStore.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Warn("fetchBuyerSnapshot: 查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err))
|
||||
} else if customer != nil && customer.Nickname != "" {
|
||||
nickname = &customer.Nickname
|
||||
}
|
||||
|
||||
phoneRecord, err := s.personalCustomerPhoneStore.GetPrimaryPhone(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Warn("fetchBuyerSnapshot: 查询买家手机号失败", zap.Uint("customer_id", customerID), zap.Error(err))
|
||||
} else if phoneRecord != nil && phoneRecord.Phone != "" {
|
||||
phone = &phoneRecord.Phone
|
||||
}
|
||||
return phone, nickname
|
||||
}
|
||||
|
||||
func (s *Service) buildPendingOrder(ctx context.Context, customerID uint, result *purchase_validation.PurchaseValidationResult, sellerCostPrice int64) (*model.Order, error) {
|
||||
orderType := resolveOrderType(result)
|
||||
if orderType == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
@@ -438,6 +464,8 @@ func (s *Service) buildPendingOrder(customerID uint, result *purchase_validation
|
||||
order.SellerShopID = result.Device.ShopID
|
||||
}
|
||||
|
||||
order.BuyerPhone, order.BuyerNickname = s.fetchBuyerSnapshot(ctx, customerID)
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
@@ -462,6 +490,7 @@ func (s *Service) buildOrderItems(ctx context.Context, customerID uint, result *
|
||||
},
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageType: &pkg.PackageType,
|
||||
Quantity: 1,
|
||||
UnitPrice: unitPrice,
|
||||
Amount: unitPrice,
|
||||
|
||||
@@ -52,6 +52,8 @@ type Service struct {
|
||||
logger *zap.Logger
|
||||
resumeCallback packagepkg.ResumeCallback
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
personalCustomerStore *postgres.PersonalCustomerStore
|
||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -74,6 +76,8 @@ func New(
|
||||
queueClient *queue.Client,
|
||||
logger *zap.Logger,
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore,
|
||||
personalCustomerStore *postgres.PersonalCustomerStore,
|
||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
@@ -95,6 +99,8 @@ func New(
|
||||
queueClient: queueClient,
|
||||
logger: logger,
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
personalCustomerStore: personalCustomerStore,
|
||||
personalCustomerPhoneStore: personalCustomerPhoneStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,6 +635,10 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
|
||||
order.PaymentVoucherKey = strings.TrimSpace(req.PaymentVoucherKey)
|
||||
}
|
||||
|
||||
if orderBuyerType == model.BuyerTypePersonal {
|
||||
order.BuyerPhone, order.BuyerNickname = s.fetchBuyerSnapshot(ctx, orderBuyerID)
|
||||
}
|
||||
|
||||
items := s.buildOrderItems(userID, validationResult.Packages)
|
||||
|
||||
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, orderType, carrierType, carrierID, req.PackageIDs)
|
||||
@@ -961,6 +971,7 @@ func (s *Service) buildOrderItems(operatorID uint, packages []*model.Package) []
|
||||
},
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageType: &pkg.PackageType,
|
||||
Quantity: 1,
|
||||
UnitPrice: pkg.SuggestedRetailPrice,
|
||||
Amount: pkg.SuggestedRetailPrice,
|
||||
@@ -971,6 +982,26 @@ func (s *Service) buildOrderItems(operatorID uint, packages []*model.Package) []
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *Service) fetchBuyerSnapshot(ctx context.Context, customerID uint) (phone, nickname *string) {
|
||||
if customerID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
customer, err := s.personalCustomerStore.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Warn("fetchBuyerSnapshot: 查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err))
|
||||
} else if customer != nil && customer.Nickname != "" {
|
||||
nickname = &customer.Nickname
|
||||
}
|
||||
|
||||
phoneRecord, err := s.personalCustomerPhoneStore.GetPrimaryPhone(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Warn("fetchBuyerSnapshot: 查询买家手机号失败", zap.Uint("customer_id", customerID), zap.Error(err))
|
||||
} else if phoneRecord != nil && phoneRecord.Phone != "" {
|
||||
phone = &phoneRecord.Phone
|
||||
}
|
||||
return phone, nickname
|
||||
}
|
||||
|
||||
// getCostPrice 查询店铺对套餐的成本价
|
||||
// shopID: 店铺ID
|
||||
// packageID: 套餐ID
|
||||
@@ -1222,6 +1253,9 @@ func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType
|
||||
if req.Identifier != "" {
|
||||
filters["identifier"] = req.Identifier
|
||||
}
|
||||
if req.BuyerPhone != "" {
|
||||
filters["buyer_phone"] = req.BuyerPhone
|
||||
}
|
||||
|
||||
orders, total, err := s.orderStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
@@ -2122,6 +2156,7 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
|
||||
ID: item.ID,
|
||||
PackageID: item.PackageID,
|
||||
PackageName: item.PackageName,
|
||||
PackageType: item.PackageType,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: item.UnitPrice,
|
||||
Amount: item.Amount,
|
||||
@@ -2180,6 +2215,8 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
|
||||
OrderType: order.OrderType,
|
||||
BuyerType: order.BuyerType,
|
||||
BuyerID: order.BuyerID,
|
||||
BuyerPhone: order.BuyerPhone,
|
||||
BuyerNickname: order.BuyerNickname,
|
||||
IotCardID: order.IotCardID,
|
||||
DeviceID: order.DeviceID,
|
||||
TotalAmount: order.TotalAmount,
|
||||
|
||||
@@ -136,6 +136,9 @@ func (s *OrderStore) List(ctx context.Context, opts *store.QueryOptions, filters
|
||||
if v, ok := filters["identifier"]; ok {
|
||||
query = query.Where("asset_identifier = ?", v)
|
||||
}
|
||||
if v, ok := filters["buyer_phone"]; ok {
|
||||
query = query.Where("buyer_phone = ?", v)
|
||||
}
|
||||
if v, ok := filters["is_expired"]; ok {
|
||||
isExpired, _ := v.(bool)
|
||||
if isExpired {
|
||||
|
||||
8
migrations/000112_add_buyer_snapshot_to_order.down.sql
Normal file
8
migrations/000112_add_buyer_snapshot_to_order.down.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
DROP INDEX IF EXISTS idx_order_buyer_phone;
|
||||
|
||||
ALTER TABLE tb_order
|
||||
DROP COLUMN IF EXISTS buyer_phone,
|
||||
DROP COLUMN IF EXISTS buyer_nickname;
|
||||
|
||||
ALTER TABLE tb_order_item
|
||||
DROP COLUMN IF EXISTS package_type;
|
||||
16
migrations/000112_add_buyer_snapshot_to_order.up.sql
Normal file
16
migrations/000112_add_buyer_snapshot_to_order.up.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- 订单买家快照字段:buyer_phone、buyer_nickname(下单时快照买家手机号和昵称)
|
||||
-- 套餐类型快照字段:package_type(下单时快照套餐类型,避免套餐删除后信息丢失)
|
||||
|
||||
ALTER TABLE tb_order
|
||||
ADD COLUMN IF NOT EXISTS buyer_phone VARCHAR(20) NULL,
|
||||
ADD COLUMN IF NOT EXISTS buyer_nickname VARCHAR(100) NULL;
|
||||
|
||||
COMMENT ON COLUMN tb_order.buyer_phone IS '下单时买家主手机号快照(个人客户有值,代理商留空)';
|
||||
COMMENT ON COLUMN tb_order.buyer_nickname IS '下单时买家昵称快照(个人客户有值,代理商留空)';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_buyer_phone ON tb_order (buyer_phone) WHERE buyer_phone IS NOT NULL;
|
||||
|
||||
ALTER TABLE tb_order_item
|
||||
ADD COLUMN IF NOT EXISTS package_type VARCHAR(20) NULL;
|
||||
|
||||
COMMENT ON COLUMN tb_order_item.package_type IS '套餐类型快照 (formal:正式套餐, addon:加油包)';
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-10
|
||||
@@ -0,0 +1,95 @@
|
||||
## Context
|
||||
|
||||
当前 `tb_order` 仅存储 `buyer_id`(个人客户 ID 或店铺 ID),售后需验证订单归属时必须跨表关联 `tb_personal_customer_phone` 查询手机号,增加了查询复杂度且在客户数据变更后可能出现不一致。
|
||||
|
||||
`tb_order_item` 已有 `package_name` 快照字段,但缺少 `package_type` 快照。套餐删除后 `tb_package.package_type` 不可查,导致历史订单的套餐类型信息永久丢失,影响报表统计和套餐组合校验。
|
||||
|
||||
相关 Store 方法已存在且可直接复用:
|
||||
- `PersonalCustomerStore.GetByID(ctx, id)` → 返回 `model.PersonalCustomer`,含 `Nickname` 字段
|
||||
- `PersonalCustomerPhoneStore.GetPrimaryPhone(ctx, customerID)` → 返回 `model.PersonalCustomerPhone`,含 `Phone` 字段
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- `tb_order` 新增 `buyer_phone`、`buyer_nickname` 字段,下单时快照买家信息
|
||||
- `tb_order_item` 新增 `package_type` 字段,下单时快照套餐类型
|
||||
- C 端下单(`client_order.Service.CreateOrder`)自动填充三个快照字段
|
||||
- 后台下单(`order.Service`)在个人客户代购场景填充 `buyer_phone`、`buyer_nickname`;所有场景填充 `package_type`
|
||||
- 后台订单列表支持按 `buyer_phone` 精确过滤
|
||||
- 后台订单 DTO(`OrderResponse`)新增 `buyer_phone`、`buyer_nickname` 字段
|
||||
- 历史数据:`package_type` 从 `tb_package` 回填;`buyer_phone`/`buyer_nickname` 留空
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改代理商订单的 `buyer_phone`/`buyer_nickname`(代理商无个人手机号概念,留空)
|
||||
- 不修改 C 端订单详情 DTO(C 端用户无需看到自己的手机号)
|
||||
- 不实现手机号模糊搜索(仅精确匹配,避免全表扫描)
|
||||
- 不同步历史订单的 `buyer_phone`/`buyer_nickname`(历史数据留空,不回填)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1:快照字段允许为空
|
||||
|
||||
`buyer_phone`、`buyer_nickname` 设为 `VARCHAR NOT NULL DEFAULT ''` 或 `VARCHAR NULL`。
|
||||
|
||||
**选择**:`VARCHAR NULL`,不设默认值。
|
||||
|
||||
**理由**:代理商订单无手机号,强制默认空字符串会导致按手机号过滤时干扰结果(空字符串会匹配到 `buyer_phone = ''` 的查询)。NULL 值语义更清晰,过滤时 `buyer_phone = ?` 自动跳过 NULL 行。
|
||||
|
||||
### 决策 2:买家信息的查询策略(不阻塞下单)
|
||||
|
||||
`GetByID` 和 `GetPrimaryPhone` 查询失败时仅记录日志,不返回错误,下单流程继续。
|
||||
|
||||
**理由**:买家快照是辅助信息,不影响核心业务(套餐激活、支付、佣金)。若因网络抖动或数据缺失导致查询失败而拒绝下单,损失远大于快照字段为空的代价。
|
||||
|
||||
### 决策 3:package_type 从已有套餐对象直接读取
|
||||
|
||||
`buildOrderItems` 中的 `pkg` 参数已是 `*model.Package` 对象,含 `PackageType` 字段,无需额外查询。
|
||||
|
||||
**理由**:零额外数据库查询,直接复用已加载的套餐数据。
|
||||
|
||||
### 决策 4:买家信息注入位置
|
||||
|
||||
在 `buildPendingOrder` 函数或其调用处注入,而非在 `buildOrderItems` 里。
|
||||
|
||||
**理由**:`buyer_phone`/`buyer_nickname` 属于 `Order` 表字段,与 `OrderItem` 无关,职责分离清晰。
|
||||
|
||||
### 决策 5:buyer_phone 普通索引
|
||||
|
||||
`buyer_phone` 建普通 B-tree 索引(允许 NULL 的列 PostgreSQL 默认跳过 NULL 值入索引,稀疏索引天然节省空间)。
|
||||
|
||||
**理由**:售后场景按手机号过滤是低频辅助查询,普通索引足够,无需唯一索引(同一手机号可有多个订单)。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[风险] 历史订单 buyer_phone/buyer_nickname 为空** → 缓解:明确文档说明历史数据为空,售后系统在展示时标注"下单时未记录"
|
||||
- **[风险] package_type 回填脚本执行期间锁表** → 缓解:使用 `UPDATE ... WHERE package_type IS NULL LIMIT 500` 分批回填,或在低峰期执行
|
||||
- **[权衡] C 端 Service 新增两个 Store 依赖** → 接受:Store 注入是项目标准模式,结构体字段注入不影响测试性
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 部署步骤
|
||||
|
||||
1. 执行数据库迁移(新增字段,不修改现有列)
|
||||
2. 部署新版本代码(包含 package_type 回填逻辑或手动执行 SQL)
|
||||
3. 验证:新订单的三个快照字段是否正确填充
|
||||
4. 可选:验证 buyer_phone 过滤接口是否返回正确结果
|
||||
|
||||
### 历史数据回填 SQL
|
||||
|
||||
```sql
|
||||
-- 回填 package_type(仅回填能匹配到套餐的记录)
|
||||
UPDATE tb_order_item oi
|
||||
SET package_type = p.package_type
|
||||
FROM tb_package p
|
||||
WHERE oi.package_id = p.id
|
||||
AND oi.package_type IS NULL
|
||||
AND oi.deleted_at IS NULL;
|
||||
```
|
||||
|
||||
### 回滚策略
|
||||
|
||||
新增字段均为 NULL,回滚代码后字段保留但不被读写,无副作用。迁移本身可通过 `ALTER TABLE DROP COLUMN` 回滚(数据丢失,但新字段为快照信息,回滚成本低)。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 无
|
||||
@@ -0,0 +1,34 @@
|
||||
## Why
|
||||
|
||||
订单创建后,售后场景无法通过手机号快速验证订单归属(需跨表关联),且 `OrderItem` 缺少套餐类型快照,套餐删除后类型信息永久丢失。将买家手机号、昵称、套餐类型在创建时快照到订单,彻底解决这两个问题。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Order 表**:新增 `buyer_phone`(下单时买家主手机号快照)、`buyer_nickname`(下单时买家昵称快照,允许为空)
|
||||
- **OrderItem 表**:新增 `package_type`(套餐类型快照,`formal` / `addon`)
|
||||
- **数据库迁移**:新增字段;历史订单 `buyer_phone` / `buyer_nickname` 留空,`package_type` 从套餐表回填
|
||||
- **下单逻辑**:`client_order` Service 和 `admin_order` Service 创建订单时自动查询并填充三个快照字段
|
||||
- **查询接口**:订单列表/详情 DTO 暴露新字段;后台订单列表支持按 `buyer_phone` 精确过滤
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
无新能力,均为现有能力的字段扩充。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `order-management`:订单模型新增买家快照字段(buyer_phone、buyer_nickname),后台查询支持按手机号过滤
|
||||
- `client-order-purchase`:C 端下单时自动填充买家快照(手机号、昵称)
|
||||
- `iot-order`:OrderItem 模型新增 package_type 快照,下单时填充
|
||||
|
||||
## Impact
|
||||
|
||||
- **数据层**:`tb_order`(新增 2 列)、`tb_order_item`(新增 1 列)、迁移脚本
|
||||
- **代码层**:
|
||||
- `internal/model/order.go` — Order、OrderItem struct 新增字段
|
||||
- `internal/service/client_order/service.go` — CreateOrder 填充快照
|
||||
- `internal/service/admin_order/service.go`(代购场景)— 同步填充
|
||||
- `internal/model/dto/order_dto.go`、`client_order_dto.go` — 响应 DTO 新增字段
|
||||
- `internal/store/postgres/order_store.go` — List 查询支持 buyer_phone 过滤
|
||||
- **无破坏性变更**:新字段均可为空,现有接口响应向下兼容(仅追加字段)
|
||||
@@ -0,0 +1,21 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: D1 创建套餐购买订单接口
|
||||
|
||||
系统 SHALL 允许个人客户发起套餐购买,创建待支付订单。下单时系统 MUST 自动查询买家手机号和昵称填入快照字段,查询失败时不阻塞下单流程。
|
||||
|
||||
#### Scenario: 创建套餐购买订单(正常流程)
|
||||
- **WHEN** 个人客户传入合法的套餐和资产标识符
|
||||
- **THEN** 系统创建待支付订单,响应包含 order_id、order_no、total_amount、payment_status
|
||||
|
||||
#### Scenario: 下单时买家快照被填充
|
||||
- **WHEN** 个人客户成功下单,系统能查询到该客户的主手机号
|
||||
- **THEN** 新创建订单的 buyer_phone 等于该客户的主手机号,buyer_nickname 等于该客户的昵称
|
||||
|
||||
#### Scenario: 买家信息查询失败时订单仍创建成功
|
||||
- **WHEN** 个人客户下单,查询手机号时数据库返回错误
|
||||
- **THEN** 系统创建订单成功,buyer_phone 为空,日志中记录警告信息
|
||||
|
||||
#### Scenario: 购买套餐时 OrderItem 含 package_type 快照
|
||||
- **WHEN** 个人客户购买套餐(套餐类型为 formal 或 addon)
|
||||
- **THEN** 新创建的 OrderItem 中 package_type 等于该套餐的 package_type 字段值
|
||||
@@ -0,0 +1,28 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 订单实体定义
|
||||
|
||||
系统 SHALL 定义订单(Order)实体,统一管理两种订单类型:套餐订单、号卡订单,并支持混合支付方式(钱包 + 在线支付)。
|
||||
|
||||
**修改说明(本次变更)**:
|
||||
- `Order` 新增 `buyer_phone`(varchar(20),NULL):下单时买家主手机号快照
|
||||
- `Order` 新增 `buyer_nickname`(varchar(100),NULL):下单时买家昵称快照
|
||||
- `OrderItem` 新增 `package_type`(varchar(20),NULL):套餐类型快照(formal / addon)
|
||||
|
||||
**支付规则(保持不变)**:
|
||||
- `wallet_payment_amount` + `online_payment_amount` = `amount`(订单总金额)
|
||||
- 当 `payment_method` 为 "wallet" 时,`wallet_payment_amount` = `amount`,`online_payment_amount` = 0
|
||||
- 当 `payment_method` 为 "online" 时,`online_payment_amount` = `amount`,`wallet_payment_amount` = 0
|
||||
- 混合支付时,`payment_method` 为 "mixed",两个字段都 > 0
|
||||
|
||||
#### Scenario: 全额钱包支付
|
||||
- **WHEN** 用户购买套餐,订单金额为 3000 分(30 元),选择钱包支付,钱包余额为 10000 分
|
||||
- **THEN** 系统创建订单,`amount` 为 3000,`payment_method` 为 "wallet",`wallet_payment_amount` 为 3000,`online_payment_amount` 为 0
|
||||
|
||||
#### Scenario: OrderItem 包含套餐类型快照
|
||||
- **WHEN** 下单时套餐的 package_type 为 "formal"
|
||||
- **THEN** 生成的 OrderItem 中 package_type 为 "formal"
|
||||
|
||||
#### Scenario: 套餐删除后 OrderItem 保留类型快照
|
||||
- **WHEN** 套餐被软删除后查询历史订单明细
|
||||
- **THEN** OrderItem 的 package_type 仍为下单时的快照值,不因套餐删除而变为空
|
||||
@@ -0,0 +1,52 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 订单买家快照字段
|
||||
系统 SHALL 在订单创建时将买家手机号和昵称快照到 `tb_order` 表,字段允许为空。
|
||||
|
||||
- `buyer_phone`:下单时买家主手机号快照(varchar(20),允许 NULL;代理商订单留空)
|
||||
- `buyer_nickname`:下单时买家昵称快照(varchar(100),允许 NULL)
|
||||
- `buyer_phone` 字段建普通索引以支持按手机号过滤
|
||||
|
||||
#### Scenario: 个人客户下单时快照手机号和昵称
|
||||
- **WHEN** 个人客户下单,系统能查询到该客户的主手机号和昵称
|
||||
- **THEN** 订单的 `buyer_phone` 和 `buyer_nickname` 被填充为查询结果
|
||||
|
||||
#### Scenario: 买家信息查询失败时不阻塞下单
|
||||
- **WHEN** 个人客户下单,但查询主手机号或昵称时发生错误(如数据库异常)
|
||||
- **THEN** 系统记录 warn 日志,订单正常创建,`buyer_phone`/`buyer_nickname` 为空
|
||||
|
||||
#### Scenario: 代理商订单不填充买家手机号
|
||||
- **WHEN** 代理商下单(buyer_type = agent)
|
||||
- **THEN** 订单的 `buyer_phone`、`buyer_nickname` 为 NULL
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 查询订单列表
|
||||
|
||||
系统 SHALL 提供订单列表查询,支持按支付状态、订单类型、是否代购、买家手机号筛选。
|
||||
|
||||
#### Scenario: 个人客户查询自己的订单
|
||||
- **WHEN** 个人客户查询订单列表
|
||||
- **THEN** 系统只返回该客户的订单
|
||||
|
||||
#### Scenario: 代理查询店铺订单
|
||||
- **WHEN** 代理查询订单列表
|
||||
- **THEN** 系统返回该店铺及下级店铺的订单(包含代购订单和普通订单)
|
||||
|
||||
#### Scenario: 按代购类型筛选
|
||||
- **WHEN** 指定 is_purchase_on_behalf = true 筛选
|
||||
- **THEN** 系统只返回代购订单
|
||||
|
||||
#### Scenario: 按支付状态筛选
|
||||
- **WHEN** 指定支付状态筛选
|
||||
- **THEN** 系统只返回匹配状态的订单
|
||||
|
||||
#### Scenario: 按买家手机号精确过滤
|
||||
- **WHEN** 后台请求中传入 buyer_phone 参数
|
||||
- **THEN** 系统只返回 buyer_phone 精确匹配的订单
|
||||
|
||||
#### Scenario: 买家手机号过滤不返回空字段订单
|
||||
- **WHEN** 后台按 buyer_phone 过滤,部分历史订单该字段为 NULL
|
||||
- **THEN** 系统不返回 buyer_phone 为 NULL 的历史订单(精确匹配语义)
|
||||
@@ -0,0 +1,55 @@
|
||||
## 1. 数据库迁移
|
||||
|
||||
- [x] 1.1 新建迁移文件,ALTER TABLE tb_order 新增 buyer_phone VARCHAR(20) NULL、buyer_nickname VARCHAR(100) NULL,为 buyer_phone 建普通索引
|
||||
- [x] 1.2 同一迁移文件中 ALTER TABLE tb_order_item 新增 package_type VARCHAR(20) NULL
|
||||
- [x] 1.3 执行迁移,运行 `migrate up` 确认迁移成功,验证三列已存在于数据库
|
||||
- [x] 1.4 执行历史数据回填 SQL:UPDATE tb_order_item SET package_type = p.package_type FROM tb_package p WHERE oi.package_id = p.id AND oi.package_type IS NULL
|
||||
|
||||
## 2. Model 层
|
||||
|
||||
- [x] 2.1 在 internal/model/order.go 的 Order struct 中新增 BuyerPhone、BuyerNickname 字段(gorm tag 含 column/type/comment)
|
||||
- [x] 2.2 在 internal/model/order.go 的 OrderItem struct 中新增 PackageType 字段(gorm tag 含 column/type/comment)
|
||||
- [x] 2.3 运行 `lsp_diagnostics` 确认 model 层无编译错误
|
||||
|
||||
## 3. DTO 层
|
||||
|
||||
- [x] 3.1 在 internal/model/dto/order_dto.go 的 OrderListRequest 中新增 BuyerPhone string 字段(query tag + validate omitempty + description)
|
||||
- [x] 3.2 在 internal/model/dto/order_dto.go 的 OrderResponse 中新增 BuyerPhone、BuyerNickname string 字段(json tag + description)
|
||||
- [x] 3.3 在 internal/model/dto/order_dto.go 的 OrderItemResponse 中新增 PackageType string 字段(json tag + description)
|
||||
- [x] 3.4 运行 `lsp_diagnostics` 确认 DTO 层无编译错误
|
||||
|
||||
## 4. Store 层
|
||||
|
||||
- [x] 4.1 在 internal/store/postgres/order_store.go 的 List 方法过滤逻辑中新增 buyer_phone 精确匹配条件(仿照 identifier 过滤写法)
|
||||
- [x] 4.2 运行 `lsp_diagnostics` 确认 Store 层无编译错误
|
||||
|
||||
## 5. C 端下单 Service 填充快照
|
||||
|
||||
- [x] 5.1 在 internal/service/client_order/service.go 的 Service struct 和 New() 函数中注入 personalCustomerStore *postgres.PersonalCustomerStore、personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore
|
||||
- [x] 5.2 在 internal/service/client_order/service.go 中新增私有方法 fetchBuyerSnapshot(ctx, customerID uint) (phone, nickname string),内部调用两个 Store,任一失败时记录 warn 日志并返回空字符串
|
||||
- [x] 5.3 在 buildPendingOrder 或其调用处调用 fetchBuyerSnapshot,将返回值赋给 Order.BuyerPhone、Order.BuyerNickname
|
||||
- [x] 5.4 在 buildOrderItems 中从 pkg.PackageType 读取值赋给 OrderItem.PackageType
|
||||
- [x] 5.5 更新 internal/bootstrap/services.go(或对应的初始化文件)中 client_order.New() 调用,传入新增的两个 Store 参数
|
||||
- [x] 5.6 运行 `lsp_diagnostics` 确认 C 端 Service 无编译错误
|
||||
|
||||
## 6. 后台下单 Service 填充快照
|
||||
|
||||
- [x] 6.1 在 internal/service/order/service.go 的 Service struct 中确认或注入 personalCustomerStore、personalCustomerPhoneStore(若已有则跳过注入步骤)
|
||||
- [x] 6.2 在 orderBuyerType == BuyerTypePersonal 的个人客户场景分支(约 line 465 附近)调用 fetchBuyerSnapshot 逻辑,填充 Order.BuyerPhone、Order.BuyerNickname
|
||||
- [x] 6.3 在 buildOrderItems 中从 pkg.PackageType 读取值赋给 OrderItem.PackageType
|
||||
- [x] 6.4 运行 `lsp_diagnostics` 确认后台 Service 无编译错误
|
||||
|
||||
## 7. Handler / Service 响应映射
|
||||
|
||||
- [x] 7.1 确认后台订单列表 Handler 已将 OrderListRequest.BuyerPhone 传入 filters map(key="buyer_phone")
|
||||
- [x] 7.2 确认订单 DTO 映射函数(toOrderResponse 或类似函数)已将 Order.BuyerPhone、Order.BuyerNickname 映射到 OrderResponse
|
||||
- [x] 7.3 确认 OrderItem 映射函数已将 OrderItem.PackageType 映射到 OrderItemResponse.PackageType
|
||||
- [x] 7.4 运行 `lsp_diagnostics` 确认 Handler 层无编译错误
|
||||
|
||||
## 8. 验证
|
||||
|
||||
- [x] 8.1 运行 `go build ./...` 确认整个项目编译通过
|
||||
- [x] 8.2 使用数据库工具查询 tb_order 确认新列存在,buyer_phone 索引已创建
|
||||
- [x] 8.3 使用数据库工具查询 tb_order_item 确认 package_type 列存在,历史数据回填记录数符合预期
|
||||
- [ ] 8.4 调用 C 端下单接口创建一个新订单,查询 tb_order 验证 buyer_phone/buyer_nickname 已填充
|
||||
- [ ] 8.5 调用后台订单列表接口,传入 buyer_phone 参数,验证返回结果正确过滤
|
||||
@@ -43,6 +43,8 @@ type WorkerStores struct {
|
||||
AgentWalletTransaction *postgres.AgentWalletTransactionStore
|
||||
AssetWallet *postgres.AssetWalletStore
|
||||
AssetIdentifier *postgres.AssetIdentifierStore
|
||||
PersonalCustomer *postgres.PersonalCustomerStore
|
||||
PersonalCustomerPhone *postgres.PersonalCustomerPhoneStore
|
||||
}
|
||||
|
||||
// WorkerServices Worker 侧所有 Service 的集合
|
||||
|
||||
Reference in New Issue
Block a user