From 5496cb58aa569fb998d1973e528007b3f4e6f908 Mon Sep 17 00:00:00 2001 From: huang Date: Fri, 10 Apr 2026 17:12:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=A2=E5=8D=95=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E6=97=B6=E5=BF=AB=E7=85=A7=E4=B9=B0=E5=AE=B6=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E5=8F=B7/=E6=98=B5=E7=A7=B0=E5=92=8C=E5=A5=97=E9=A4=90?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增字段: - tb_order: buyer_phone, buyer_nickname(个人客户下单时快照) - tb_order_item: package_type(套餐类型快照) 后台订单列表支持按 buyer_phone 精确过滤查询。 OpenSpec: order-buyer-snapshot --- docs/admin-openapi.yaml | 31 ++++++ internal/bootstrap/handlers.go | 4 + internal/bootstrap/services.go | 10 +- internal/bootstrap/worker_services.go | 2 + internal/bootstrap/worker_stores.go | 6 ++ internal/model/dto/order_dto.go | 16 +-- internal/model/order.go | 19 ++-- internal/service/client_order/service.go | 97 ++++++++++++------- internal/service/order/service.go | 37 +++++++ internal/store/postgres/order_store.go | 3 + ...00112_add_buyer_snapshot_to_order.down.sql | 8 ++ .../000112_add_buyer_snapshot_to_order.up.sql | 16 +++ .../.openspec.yaml | 2 + .../2026-04-10-order-buyer-snapshot/design.md | 95 ++++++++++++++++++ .../proposal.md | 34 +++++++ .../specs/client-order-purchase/spec.md | 21 ++++ .../specs/iot-order/spec.md | 28 ++++++ .../specs/order-management/spec.md | 52 ++++++++++ .../2026-04-10-order-buyer-snapshot/tasks.md | 55 +++++++++++ pkg/queue/types.go | 2 + 20 files changed, 489 insertions(+), 49 deletions(-) create mode 100644 migrations/000112_add_buyer_snapshot_to_order.down.sql create mode 100644 migrations/000112_add_buyer_snapshot_to_order.up.sql create mode 100644 openspec/changes/archive/2026-04-10-order-buyer-snapshot/.openspec.yaml create mode 100644 openspec/changes/archive/2026-04-10-order-buyer-snapshot/design.md create mode 100644 openspec/changes/archive/2026-04-10-order-buyer-snapshot/proposal.md create mode 100644 openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/client-order-purchase/spec.md create mode 100644 openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/iot-order/spec.md create mode 100644 openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/order-management/spec.md create mode 100644 openspec/changes/archive/2026-04-10-order-buyer-snapshot/tasks.md diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index bb27875..468bebc 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -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: diff --git a/internal/bootstrap/handlers.go b/internal/bootstrap/handlers.go index 8f1c523..8e786de 100644 --- a/internal/bootstrap/handlers.go +++ b/internal/bootstrap/handlers.go @@ -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, diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index 1e1f405..3bf29d6 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -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), diff --git a/internal/bootstrap/worker_services.go b/internal/bootstrap/worker_services.go index 357959e..b697a8a 100644 --- a/internal/bootstrap/worker_services.go +++ b/internal/bootstrap/worker_services.go @@ -101,6 +101,8 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q nil, // queueClient: 超时取消不触发分佣 deps.Logger, stores.AssetIdentifier, + stores.PersonalCustomer, + stores.PersonalCustomerPhone, ) // 创建停复机服务并注入回调:流量耗尽自动停机、套餐激活/重置/支付后自动复机 diff --git a/internal/bootstrap/worker_stores.go b/internal/bootstrap/worker_stores.go index b7a2399..9da90a4 100644 --- a/internal/bootstrap/worker_stores.go +++ b/internal/bootstrap/worker_stores.go @@ -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, } } diff --git a/internal/model/dto/order_dto.go b/internal/model/dto/order_dto.go index 3efd337..b44f419 100644 --- a/internal/model/dto/order_dto.go +++ b/internal/model/dto/order_dto.go @@ -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:"订单总金额(分)"` diff --git a/internal/model/order.go b/internal/model/order.go index c496635..1498f85 100644 --- a/internal/model/order.go +++ b/internal/model/order.go @@ -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 指定表名 diff --git a/internal/service/client_order/service.go b/internal/service/client_order/service.go index cc1974d..ed090b5 100644 --- a/internal/service/client_order/service.go +++ b/internal/service/client_order/service.go @@ -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, diff --git a/internal/service/order/service.go b/internal/service/order/service.go index f8d1980..81077ea 100644 --- a/internal/service/order/service.go +++ b/internal/service/order/service.go @@ -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, diff --git a/internal/store/postgres/order_store.go b/internal/store/postgres/order_store.go index d434914..44f8482 100644 --- a/internal/store/postgres/order_store.go +++ b/internal/store/postgres/order_store.go @@ -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 { diff --git a/migrations/000112_add_buyer_snapshot_to_order.down.sql b/migrations/000112_add_buyer_snapshot_to_order.down.sql new file mode 100644 index 0000000..8ea0f1a --- /dev/null +++ b/migrations/000112_add_buyer_snapshot_to_order.down.sql @@ -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; diff --git a/migrations/000112_add_buyer_snapshot_to_order.up.sql b/migrations/000112_add_buyer_snapshot_to_order.up.sql new file mode 100644 index 0000000..2053fc1 --- /dev/null +++ b/migrations/000112_add_buyer_snapshot_to_order.up.sql @@ -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:加油包)'; diff --git a/openspec/changes/archive/2026-04-10-order-buyer-snapshot/.openspec.yaml b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/.openspec.yaml new file mode 100644 index 0000000..e49efd1 --- /dev/null +++ b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-10 diff --git a/openspec/changes/archive/2026-04-10-order-buyer-snapshot/design.md b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/design.md new file mode 100644 index 0000000..422960a --- /dev/null +++ b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/design.md @@ -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 + +- 无 diff --git a/openspec/changes/archive/2026-04-10-order-buyer-snapshot/proposal.md b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/proposal.md new file mode 100644 index 0000000..01e7246 --- /dev/null +++ b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/proposal.md @@ -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 过滤 +- **无破坏性变更**:新字段均可为空,现有接口响应向下兼容(仅追加字段) diff --git a/openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/client-order-purchase/spec.md b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/client-order-purchase/spec.md new file mode 100644 index 0000000..acd574c --- /dev/null +++ b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/client-order-purchase/spec.md @@ -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 字段值 diff --git a/openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/iot-order/spec.md b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/iot-order/spec.md new file mode 100644 index 0000000..d8217fc --- /dev/null +++ b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/iot-order/spec.md @@ -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 仍为下单时的快照值,不因套餐删除而变为空 diff --git a/openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/order-management/spec.md b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/order-management/spec.md new file mode 100644 index 0000000..7ba0f9a --- /dev/null +++ b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/specs/order-management/spec.md @@ -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 的历史订单(精确匹配语义) diff --git a/openspec/changes/archive/2026-04-10-order-buyer-snapshot/tasks.md b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/tasks.md new file mode 100644 index 0000000..8d45d8f --- /dev/null +++ b/openspec/changes/archive/2026-04-10-order-buyer-snapshot/tasks.md @@ -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 参数,验证返回结果正确过滤 diff --git a/pkg/queue/types.go b/pkg/queue/types.go index 5ad05aa..22e94c3 100644 --- a/pkg/queue/types.go +++ b/pkg/queue/types.go @@ -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 的集合