feat: 订单创建时快照买家手机号/昵称和套餐类型
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:
2026-04-10 17:12:56 +08:00
parent 3cb16804a4
commit 5496cb58aa
20 changed files with 489 additions and 49 deletions

View File

@@ -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,

View File

@@ -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),

View File

@@ -101,6 +101,8 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q
nil, // queueClient: 超时取消不触发分佣
deps.Logger,
stores.AssetIdentifier,
stores.PersonalCustomer,
stores.PersonalCustomerPhone,
)
// 创建停复机服务并注入回调:流量耗尽自动停机、套餐激活/重置/支付后自动复机

View File

@@ -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,
}
}

View File

@@ -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:"订单总金额(分)"`

View File

@@ -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 指定表名

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {