feat: 技术债务清理(支付配置动态化、API文档补全、轮询常量提取、废弃代码清理)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m13s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m13s
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
commissionWithdrawalSettingSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal_setting"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/payment"
|
||||
|
||||
assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
assetWalletSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_wallet"
|
||||
@@ -125,6 +126,9 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
// 创建支付配置服务(Order 和 Recharge 依赖)
|
||||
wechatConfig := wechatConfigSvc.New(s.WechatConfig, s.Order, s.AssetRecharge, s.AgentRecharge, accountAudit, deps.Redis, deps.Logger)
|
||||
|
||||
// 创建支付配置动态加载器(Order 和 Recharge 依赖)
|
||||
paymentLoader := payment.NewPaymentConfigLoader(s.WechatConfig, deps.Redis, deps.Logger)
|
||||
|
||||
packageActivation := packageSvc.NewActivationService(
|
||||
deps.DB,
|
||||
deps.Redis,
|
||||
@@ -199,9 +203,9 @@ 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, s.PersonalCustomer, s.PersonalCustomerPhone),
|
||||
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, paymentLoader, 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),
|
||||
Recharge: rechargeSvc.New(deps.DB, s.AssetRecharge, s.AssetWallet, s.AssetWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, wechatConfig, paymentLoader, deps.Logger, deps.QueueClient),
|
||||
PollingConfig: pollingSvc.NewConfigService(s.PollingConfig),
|
||||
PollingConcurrency: pollingSvc.NewConcurrencyService(s.PollingConcurrencyConfig, deps.Redis),
|
||||
PollingMonitoring: pollingSvc.NewMonitoringService(deps.Redis),
|
||||
|
||||
@@ -98,6 +98,7 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q
|
||||
stores.Package,
|
||||
nil, // wechatConfigService: 超时取消不需要
|
||||
nil, // wechatPayment: 超时取消不需要
|
||||
nil, // paymentLoader: 超时取消不需要
|
||||
nil, // queueClient: 超时取消不触发分佣
|
||||
deps.Logger,
|
||||
stores.AssetIdentifier,
|
||||
|
||||
@@ -293,13 +293,13 @@ func (h *PollingManualTriggerHandler) getTriggerTypeName(triggerType string) str
|
||||
|
||||
func (h *PollingManualTriggerHandler) getStatusName(status string) string {
|
||||
switch status {
|
||||
case "pending":
|
||||
case constants.PollingManualTriggerStatusPending:
|
||||
return "待处理"
|
||||
case "processing":
|
||||
case constants.PollingManualTriggerStatusProcessing:
|
||||
return "处理中"
|
||||
case "completed":
|
||||
case constants.PollingManualTriggerStatusCompleted:
|
||||
return "已完成"
|
||||
case "cancelled":
|
||||
case constants.PollingManualTriggerStatusCancelled:
|
||||
return "已取消"
|
||||
default:
|
||||
return status
|
||||
|
||||
@@ -31,14 +31,12 @@ type Device struct {
|
||||
DevicePasswordEncrypted string `gorm:"column:device_password_encrypted;type:varchar(255);comment:设备登录密码(加密)" json:"device_password_encrypted"`
|
||||
DeviceAPIEndpoint string `gorm:"column:device_api_endpoint;type:varchar(500);comment:设备API端点" json:"device_api_endpoint"`
|
||||
SeriesID *uint `gorm:"column:series_id;index;comment:套餐系列ID(关联PackageSeries)" json:"series_id,omitempty"`
|
||||
FirstCommissionPaid bool `gorm:"column:first_commission_paid;type:boolean;default:false;comment:一次性佣金是否已发放(废弃,使用按系列追踪)" json:"first_commission_paid"`
|
||||
AccumulatedRecharge int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分,废弃,使用按系列追踪)" json:"accumulated_recharge"`
|
||||
AccumulatedRechargeBySeriesJSON string `gorm:"column:accumulated_recharge_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的累计充值金额" json:"-"`
|
||||
FirstRechargeTriggeredBySeriesJSON string `gorm:"column:first_recharge_triggered_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的首充触发状态" json:"-"`
|
||||
AssetStatus int `gorm:"column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用" json:"asset_status"`
|
||||
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
||||
// EnablePolling 是否参与轮询,false 时设备下所有卡不加入轮询队列
|
||||
EnablePolling bool `gorm:"column:enable_polling;not null;default:true" json:"enable_polling"`
|
||||
EnablePolling bool `gorm:"column:enable_polling;not null;default:true" json:"enable_polling"`
|
||||
// 以下字段由 Gateway sync-info 接口同步,有离线存储意义
|
||||
OnlineStatus int `gorm:"column:online_status;type:int;not null;default:0;comment:在线状态:0未知 1在线 2离线" json:"online_status"`
|
||||
LastOnlineTime *time.Time `gorm:"column:last_online_time;comment:最后在线时间(来自 Gateway sync-info)" json:"last_online_time,omitempty"`
|
||||
|
||||
@@ -41,6 +41,7 @@ type AccountResponse struct {
|
||||
EnterpriseID *uint `json:"enterprise_id,omitempty" description:"关联企业ID"`
|
||||
EnterpriseName string `json:"enterprise_name,omitempty" description:"企业名称"`
|
||||
Status int `json:"status" description:"状态 (0:禁用, 1:启用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
Creator uint `json:"creator" description:"创建人ID"`
|
||||
Updater uint `json:"updater" description:"更新人ID"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
|
||||
@@ -4,21 +4,20 @@ import "time"
|
||||
|
||||
// AssetResolveResponse 统一资产解析响应
|
||||
type AssetResolveResponse struct {
|
||||
AssetType string `json:"asset_type" description:"资产类型:card 或 device"`
|
||||
AssetID uint `json:"asset_id" description:"资产数据库ID"`
|
||||
Identifier string `json:"identifier,omitempty" description:"解析时使用的标识符"`
|
||||
VirtualNo string `json:"virtual_no" description:"虚拟号"`
|
||||
Status int `json:"status" description:"资产状态"`
|
||||
BatchNo string `json:"batch_no" description:"批次号"`
|
||||
ShopID *uint `json:"shop_id,omitempty" description:"所属店铺ID"`
|
||||
ShopName string `json:"shop_name,omitempty" description:"所属店铺名称"`
|
||||
SeriesID *uint `json:"series_id,omitempty" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name,omitempty" description:"套餐系列名称"`
|
||||
FirstCommissionPaid bool `json:"first_commission_paid" description:"一次性佣金是否已发放"`
|
||||
AccumulatedRecharge int64 `json:"accumulated_recharge" description:"累计充值金额(分)"`
|
||||
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
AssetType string `json:"asset_type" description:"资产类型:card 或 device"`
|
||||
AssetID uint `json:"asset_id" description:"资产数据库ID"`
|
||||
Identifier string `json:"identifier,omitempty" description:"解析时使用的标识符"`
|
||||
VirtualNo string `json:"virtual_no" description:"虚拟号"`
|
||||
Status int `json:"status" description:"资产状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
StatusName string `json:"status_name" description:"资产状态名称(中文)"`
|
||||
BatchNo string `json:"batch_no" description:"批次号"`
|
||||
ShopID *uint `json:"shop_id,omitempty" description:"所属店铺ID"`
|
||||
ShopName string `json:"shop_name,omitempty" description:"所属店铺名称"`
|
||||
SeriesID *uint `json:"series_id,omitempty" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name,omitempty" description:"套餐系列名称"`
|
||||
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
// 状态聚合字段
|
||||
RealNameStatus int `json:"real_name_status" description:"实名状态:0未实名 1已实名"`
|
||||
RealNameAt *time.Time `json:"real_name_at" description:"最近一次完成实名的时间,未实名时为 null"`
|
||||
|
||||
@@ -18,16 +18,18 @@ type AssetInfoResponse struct {
|
||||
BoundPhone string `json:"bound_phone" description:"当前登录用户绑定的手机号"`
|
||||
|
||||
// === 基础信息(通用) ===
|
||||
AssetType string `json:"asset_type" description:"资产类型(card:卡, device:设备)"`
|
||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||
Identifier string `json:"identifier" description:"资产标识符"`
|
||||
VirtualNo string `json:"virtual_no" description:"虚拟号"`
|
||||
Status int `json:"status" description:"状态(1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
RealNameStatus int `json:"real_name_status" description:"实名状态(0:未实名, 1:已实名)"`
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
Generation string `json:"generation" description:"世代"`
|
||||
WalletBalance int64 `json:"wallet_balance" description:"钱包余额(分)"`
|
||||
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
|
||||
AssetType string `json:"asset_type" description:"资产类型(card:卡, device:设备)"`
|
||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||
Identifier string `json:"identifier" description:"资产标识符"`
|
||||
VirtualNo string `json:"virtual_no" description:"虚拟号"`
|
||||
Status int `json:"status" description:"状态(1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
RealNameStatus int `json:"real_name_status" description:"实名状态(0:未实名, 1:已实名)"`
|
||||
RealNameStatusName string `json:"real_name_status_name" description:"实名状态名称(中文)"`
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
Generation string `json:"generation" description:"世代"`
|
||||
WalletBalance int64 `json:"wallet_balance" description:"钱包余额(分)"`
|
||||
ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"`
|
||||
|
||||
// === 套餐信息(通用) ===
|
||||
CurrentPackage string `json:"current_package" description:"当前套餐名称(无套餐时为空)"`
|
||||
@@ -48,13 +50,15 @@ type AssetInfoResponse struct {
|
||||
DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"设备保护期状态(none:无, stop:停机保护, start:开机保护)"`
|
||||
|
||||
// === 卡专属字段(asset_type=card 时有效) ===
|
||||
ICCID string `json:"iccid,omitempty" description:"卡ICCID"`
|
||||
MSISDN string `json:"msisdn,omitempty" description:"手机号"`
|
||||
CarrierID uint `json:"carrier_id,omitempty" description:"运营商ID"`
|
||||
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型(CMCC/CUCC/CTCC/CBN)"`
|
||||
NetworkStatus int `json:"network_status,omitempty" description:"网络状态(0:停机, 1:开机)"`
|
||||
ActivationStatus int `json:"activation_status,omitempty" description:"激活状态(0:未激活, 1:已激活)"`
|
||||
CardCategory string `json:"card_category,omitempty" description:"卡业务类型(normal:普通卡, industry:行业卡)"`
|
||||
ICCID string `json:"iccid,omitempty" description:"卡ICCID"`
|
||||
MSISDN string `json:"msisdn,omitempty" description:"手机号"`
|
||||
CarrierID uint `json:"carrier_id,omitempty" description:"运营商ID"`
|
||||
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型(CMCC/CUCC/CTCC/CBN)"`
|
||||
NetworkStatus int `json:"network_status,omitempty" description:"网络状态(0:停机, 1:开机)"`
|
||||
NetworkStatusName string `json:"network_status_name,omitempty" description:"网络状态名称(中文)"`
|
||||
ActivationStatus int `json:"activation_status,omitempty" description:"激活状态(0:未激活, 1:已激活)"`
|
||||
ActivationStatusName string `json:"activation_status_name,omitempty" description:"激活状态名称(中文)"`
|
||||
CardCategory string `json:"card_category,omitempty" description:"卡业务类型(normal:普通卡, industry:行业卡)"`
|
||||
|
||||
// === 卡绑定设备信息(asset_type=card 且绑定了设备时有效) ===
|
||||
BoundDeviceID *uint `json:"bound_device_id,omitempty" description:"绑定的设备ID"`
|
||||
|
||||
@@ -14,7 +14,8 @@ type CommissionRecordResponse struct {
|
||||
CommissionSource string `json:"commission_source" description:"佣金来源 (cost_diff:成本价差, one_time:一次性佣金)"`
|
||||
Amount int64 `json:"amount" description:"佣金金额(分)"`
|
||||
BalanceAfter int64 `json:"balance_after" description:"入账后钱包余额(分)"`
|
||||
Status int `json:"status" description:"状态 (1:已入账, 2:已失效, 99:待人工修正)"`
|
||||
Status int `json:"status" description:"状态 (1:已冻结, 2:解冻中, 3:已发放, 4:已失效, 99:待人工修正)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
ReleasedAt string `json:"released_at" description:"入账时间"`
|
||||
Remark string `json:"remark" description:"备注"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
|
||||
@@ -18,28 +18,26 @@ type ListDeviceRequest struct {
|
||||
}
|
||||
|
||||
type DeviceResponse struct {
|
||||
ID uint `json:"id" description:"设备ID"`
|
||||
VirtualNo string `json:"virtual_no" description:"设备虚拟号/别名"`
|
||||
IMEI string `json:"imei" description:"设备IMEI"`
|
||||
SN string `json:"sn" description:"设备序列号"`
|
||||
DeviceName string `json:"device_name" description:"设备名称"`
|
||||
DeviceModel string `json:"device_model" description:"设备型号"`
|
||||
DeviceType string `json:"device_type" description:"设备类型"`
|
||||
MaxSimSlots int `json:"max_sim_slots" description:"最大插槽数"`
|
||||
Manufacturer string `json:"manufacturer" description:"制造商"`
|
||||
BatchNo string `json:"batch_no" description:"批次号"`
|
||||
ShopID *uint `json:"shop_id" description:"店铺ID"`
|
||||
ShopName string `json:"shop_name" description:"店铺名称"`
|
||||
Status int `json:"status" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称"`
|
||||
BoundCardCount int `json:"bound_card_count" description:"已绑定卡数量"`
|
||||
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
FirstCommissionPaid bool `json:"first_commission_paid" description:"一次性佣金是否已发放"`
|
||||
AccumulatedRecharge int64 `json:"accumulated_recharge" description:"累计充值金额(分)"`
|
||||
ActivatedAt *time.Time `json:"activated_at" description:"激活时间"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
ID uint `json:"id" description:"设备ID"`
|
||||
VirtualNo string `json:"virtual_no" description:"设备虚拟号/别名"`
|
||||
IMEI string `json:"imei" description:"设备IMEI"`
|
||||
SN string `json:"sn" description:"设备序列号"`
|
||||
DeviceName string `json:"device_name" description:"设备名称"`
|
||||
DeviceModel string `json:"device_model" description:"设备型号"`
|
||||
DeviceType string `json:"device_type" description:"设备类型"`
|
||||
MaxSimSlots int `json:"max_sim_slots" description:"最大插槽数"`
|
||||
Manufacturer string `json:"manufacturer" description:"制造商"`
|
||||
BatchNo string `json:"batch_no" description:"批次号"`
|
||||
ShopID *uint `json:"shop_id" description:"店铺ID"`
|
||||
ShopName string `json:"shop_name" description:"店铺名称"`
|
||||
Status int `json:"status" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称"`
|
||||
BoundCardCount int `json:"bound_card_count" description:"已绑定卡数量"`
|
||||
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
ActivatedAt *time.Time `json:"activated_at" description:"激活时间"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
// 设备实时同步字段(由 sync-info 接口写入)
|
||||
OnlineStatus int `json:"online_status" description:"在线状态:0未知 1在线 2离线"`
|
||||
LastOnlineTime *time.Time `json:"last_online_time" description:"最后在线时间"`
|
||||
@@ -80,6 +78,7 @@ type DeviceCardBindingResponse struct {
|
||||
MSISDN string `json:"msisdn" description:"接入号"`
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
Status int `json:"status" description:"卡状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
StatusName string `json:"status_name" description:"卡状态名称(中文)"`
|
||||
BindTime *time.Time `json:"bind_time" description:"绑定时间"`
|
||||
IsCurrent bool `json:"is_current" description:"是否为当前使用的卡"`
|
||||
}
|
||||
|
||||
@@ -13,29 +13,6 @@ type StandaloneCard struct {
|
||||
StatusName string `json:"status_name" description:"状态名称"`
|
||||
}
|
||||
|
||||
// Deprecated: 已废弃,不再支持通过单卡授权接口授权设备卡,请使用设备授权接口
|
||||
type DeviceBundle struct {
|
||||
DeviceID uint `json:"device_id" description:"设备ID"`
|
||||
VirtualNo string `json:"virtual_no" description:"设备虚拟号"`
|
||||
TriggerCard DeviceBundleCard `json:"trigger_card" description:"触发卡(用户选择的卡)"`
|
||||
BundleCards []DeviceBundleCard `json:"bundle_cards" description:"连带卡(同设备的其他卡)"`
|
||||
}
|
||||
|
||||
// Deprecated: 已废弃,不再支持通过单卡授权接口授权设备卡,请使用设备授权接口
|
||||
type DeviceBundleCard struct {
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
IotCardID uint `json:"iot_card_id" description:"卡ID"`
|
||||
MSISDN string `json:"msisdn" description:"手机号"`
|
||||
}
|
||||
|
||||
// Deprecated: 已废弃,不再支持通过单卡授权接口授权设备卡,请使用设备授权接口
|
||||
type AllocatedDevice struct {
|
||||
DeviceID uint `json:"device_id" description:"设备ID"`
|
||||
VirtualNo string `json:"virtual_no" description:"设备虚拟号"`
|
||||
CardCount int `json:"card_count" description:"卡数量"`
|
||||
ICCIDs []string `json:"iccids" description:"卡ICCID列表"`
|
||||
}
|
||||
|
||||
type FailedItem struct {
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
Reason string `json:"reason" description:"失败原因"`
|
||||
|
||||
@@ -33,12 +33,16 @@ type StandaloneIotCardResponse struct {
|
||||
BatchNo string `json:"batch_no" description:"批次号"`
|
||||
Supplier string `json:"supplier" description:"供应商"`
|
||||
Status int `json:"status" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
ShopID *uint `json:"shop_id" description:"店铺ID"`
|
||||
ShopName string `json:"shop_name" description:"店铺名称"`
|
||||
ActivatedAt *time.Time `json:"activated_at" description:"激活时间"`
|
||||
ActivationStatus int `json:"activation_status" description:"激活状态 (0:未激活, 1:已激活)"`
|
||||
ActivationStatusName string `json:"activation_status_name" description:"激活状态名称(中文)"`
|
||||
RealNameStatus int `json:"real_name_status" description:"实名状态 (0:未实名, 1:已实名)"`
|
||||
RealNameStatusName string `json:"real_name_status_name" description:"实名状态名称(中文)"`
|
||||
NetworkStatus int `json:"network_status" description:"网络状态 (0:停机, 1:开机)"`
|
||||
NetworkStatusName string `json:"network_status_name" description:"网络状态名称(中文)"`
|
||||
DataUsageMB int64 `json:"data_usage_mb" description:"累计流量使用(MB)"`
|
||||
CurrentMonthUsageMB float64 `json:"current_month_usage_mb" description:"本月已用流量(MB)"`
|
||||
CurrentMonthStartDate *time.Time `json:"current_month_start_date" description:"本月开始日期"`
|
||||
@@ -48,8 +52,6 @@ type StandaloneIotCardResponse struct {
|
||||
EnablePolling bool `json:"enable_polling" description:"是否参与轮询"`
|
||||
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
FirstCommissionPaid bool `json:"first_commission_paid" description:"一次性佣金是否已发放"`
|
||||
AccumulatedRecharge int64 `json:"accumulated_recharge" description:"累计充值金额(分)"`
|
||||
DeviceVirtualNo string `json:"device_virtual_no" description:"绑定设备的虚拟号(未绑定时为空字符串)"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
|
||||
@@ -21,7 +21,9 @@ type MyPackageResponse struct {
|
||||
ProfitMargin int64 `json:"profit_margin" description:"利润空间(分)= 建议售价 - 成本价"`
|
||||
PriceSource string `json:"price_source" description:"价格来源 (series_pricing:系列加价, package_override:单套餐覆盖)"`
|
||||
Status int `json:"status" description:"套餐状态 (0:禁用, 1:启用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
ShelfStatus int `json:"shelf_status" description:"上架状态 (1:上架, 2:下架)"`
|
||||
ShelfStatusName string `json:"shelf_status_name" description:"上架状态名称(中文)"`
|
||||
}
|
||||
|
||||
// MyPackagePageResult 我的可售套餐分页结果
|
||||
@@ -46,7 +48,9 @@ type MyPackageDetailResponse struct {
|
||||
ProfitMargin int64 `json:"profit_margin" description:"利润空间(分)"`
|
||||
PriceSource string `json:"price_source" description:"价格来源 (series_pricing:系列加价, package_override:单套餐覆盖)"`
|
||||
Status int `json:"status" description:"套餐状态 (0:禁用, 1:启用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
ShelfStatus int `json:"shelf_status" description:"上架状态 (1:上架, 2:下架)"`
|
||||
ShelfStatusName string `json:"shelf_status_name" description:"上架状态名称(中文)"`
|
||||
}
|
||||
|
||||
// MySeriesAllocationListRequest 我的套餐系列分配列表请求
|
||||
@@ -66,6 +70,7 @@ type MySeriesAllocationResponse struct {
|
||||
AvailablePackageCount int `json:"available_package_count" description:"可售套餐数量"`
|
||||
AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"`
|
||||
Status int `json:"status" description:"状态 (0:禁用, 1:启用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
}
|
||||
|
||||
// MySeriesAllocationPageResult 我的套餐系列分配分页结果
|
||||
|
||||
@@ -64,6 +64,7 @@ type OrderResponse struct {
|
||||
PaymentVoucherKey string `json:"payment_voucher_key,omitempty" description:"线下支付凭证对象存储file_key(仅线下支付订单有值)"`
|
||||
IsPurchaseOnBehalf bool `json:"is_purchase_on_behalf" description:"是否为代购订单"`
|
||||
CommissionStatus int `json:"commission_status" description:"佣金状态 (1:待计算, 2:已计算)"`
|
||||
CommissionStatusName string `json:"commission_status_name" description:"佣金状态名称(中文)"`
|
||||
CommissionConfigVersion int `json:"commission_config_version" description:"佣金配置版本"`
|
||||
|
||||
// 操作者信息
|
||||
|
||||
@@ -85,7 +85,9 @@ type PackageResponse struct {
|
||||
CostPrice int64 `json:"cost_price" description:"成本价(分)"`
|
||||
OneTimeCommissionAmount *int64 `json:"one_time_commission_amount,omitempty" description:"一次性佣金金额(分,代理视角)"`
|
||||
Status int `json:"status" description:"状态 (0:禁用, 1:启用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
ShelfStatus int `json:"shelf_status" description:"上架状态 (1:上架, 2:下架)"`
|
||||
ShelfStatusName string `json:"shelf_status_name" description:"上架状态名称(中文)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
RetailPrice *int64 `json:"retail_price,omitempty" description:"代理零售价(分),仅代理用户可见"`
|
||||
|
||||
@@ -96,8 +96,6 @@ type RechargeCheckResponse struct {
|
||||
Threshold int64 `json:"threshold" description:"佣金触发阈值(分)"`
|
||||
// 提示信息
|
||||
Message string `json:"message" description:"提示信息"`
|
||||
// 一次性佣金是否已发放
|
||||
FirstCommissionPaid bool `json:"first_commission_paid" description:"一次性佣金是否已发放"`
|
||||
}
|
||||
|
||||
// GetRechargeRequest 获取充值订单详情请求
|
||||
|
||||
@@ -63,6 +63,7 @@ type RefundResponse struct {
|
||||
ApprovedRefundAmount *int64 `json:"approved_refund_amount,omitempty" description:"审批实际退款金额(分)"`
|
||||
RefundReason string `json:"refund_reason" description:"退款原因"`
|
||||
Status int `json:"status" description:"状态 (1:待审批, 2:已通过, 3:已拒绝, 4:已退回)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
ProcessorID *uint `json:"processor_id,omitempty" description:"审批人ID"`
|
||||
ProcessedAt string `json:"processed_at,omitempty" description:"审批时间"`
|
||||
RejectReason string `json:"reject_reason,omitempty" description:"拒绝原因"`
|
||||
|
||||
@@ -51,6 +51,7 @@ type ShopResponse struct {
|
||||
District string `json:"district" description:"区县"`
|
||||
Address string `json:"address" description:"详细地址"`
|
||||
Status int `json:"status" description:"状态 (0:禁用, 1:启用)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ type IotCard struct {
|
||||
LastProtectCheckAt *time.Time `gorm:"column:last_protect_check_at;comment:上次保护期一致性检查时间" json:"last_protect_check_at"`
|
||||
LastSyncTime *time.Time `gorm:"column:last_sync_time;comment:最后一次与Gateway同步时间" json:"last_sync_time"`
|
||||
SeriesID *uint `gorm:"column:series_id;index;comment:套餐系列ID(关联PackageSeries)" json:"series_id,omitempty"`
|
||||
FirstCommissionPaid bool `gorm:"column:first_commission_paid;type:boolean;default:false;comment:一次性佣金是否已发放(废弃,使用按系列追踪)" json:"first_commission_paid"`
|
||||
AccumulatedRecharge int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分,废弃,使用按系列追踪)" json:"accumulated_recharge"`
|
||||
AccumulatedRechargeBySeriesJSON string `gorm:"column:accumulated_recharge_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的累计充值金额" json:"-"`
|
||||
FirstRechargeTriggeredBySeriesJSON string `gorm:"column:first_recharge_triggered_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的首充触发状态" json:"-"`
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package routes
|
||||
@@ -797,6 +797,7 @@ func (s *Service) toAccountResponse(acc *model.Account, shopMap map[uint]string,
|
||||
ShopID: acc.ShopID,
|
||||
EnterpriseID: acc.EnterpriseID,
|
||||
Status: acc.Status,
|
||||
StatusName: constants.GetStatusName(acc.Status),
|
||||
Creator: acc.Creator,
|
||||
Updater: acc.Updater,
|
||||
CreatedAt: acc.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
|
||||
@@ -140,31 +140,30 @@ func (s *Service) Resolve(ctx context.Context, identifier string) (*dto.AssetRes
|
||||
// buildDeviceResolveResponse 构建设备类型的资产解析响应
|
||||
func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model.Device) (*dto.AssetResolveResponse, error) {
|
||||
resp := &dto.AssetResolveResponse{
|
||||
AssetType: "device",
|
||||
AssetID: device.ID,
|
||||
VirtualNo: device.VirtualNo,
|
||||
Status: device.Status,
|
||||
BatchNo: device.BatchNo,
|
||||
ShopID: device.ShopID,
|
||||
SeriesID: device.SeriesID,
|
||||
FirstCommissionPaid: device.FirstCommissionPaid,
|
||||
AccumulatedRecharge: device.AccumulatedRecharge,
|
||||
ActivatedAt: device.ActivatedAt,
|
||||
CreatedAt: device.CreatedAt,
|
||||
UpdatedAt: device.UpdatedAt,
|
||||
DeviceName: device.DeviceName,
|
||||
IMEI: device.IMEI,
|
||||
SN: device.SN,
|
||||
DeviceModel: device.DeviceModel,
|
||||
DeviceType: device.DeviceType,
|
||||
MaxSimSlots: device.MaxSimSlots,
|
||||
Manufacturer: device.Manufacturer,
|
||||
OnlineStatus: device.OnlineStatus,
|
||||
LastOnlineTime: device.LastOnlineTime,
|
||||
SoftwareVersion: device.SoftwareVersion,
|
||||
SwitchMode: device.SwitchMode,
|
||||
LastGatewaySyncAt: device.LastGatewaySyncAt,
|
||||
EnablePolling: device.EnablePolling,
|
||||
AssetType: "device",
|
||||
AssetID: device.ID,
|
||||
VirtualNo: device.VirtualNo,
|
||||
Status: device.Status,
|
||||
StatusName: constants.GetIotCardStatusName(device.Status),
|
||||
BatchNo: device.BatchNo,
|
||||
ShopID: device.ShopID,
|
||||
SeriesID: device.SeriesID,
|
||||
ActivatedAt: device.ActivatedAt,
|
||||
CreatedAt: device.CreatedAt,
|
||||
UpdatedAt: device.UpdatedAt,
|
||||
DeviceName: device.DeviceName,
|
||||
IMEI: device.IMEI,
|
||||
SN: device.SN,
|
||||
DeviceModel: device.DeviceModel,
|
||||
DeviceType: device.DeviceType,
|
||||
MaxSimSlots: device.MaxSimSlots,
|
||||
Manufacturer: device.Manufacturer,
|
||||
OnlineStatus: device.OnlineStatus,
|
||||
LastOnlineTime: device.LastOnlineTime,
|
||||
SoftwareVersion: device.SoftwareVersion,
|
||||
SwitchMode: device.SwitchMode,
|
||||
LastGatewaySyncAt: device.LastGatewaySyncAt,
|
||||
EnablePolling: device.EnablePolling,
|
||||
}
|
||||
|
||||
// 查绑定卡
|
||||
@@ -235,31 +234,30 @@ func (s *Service) buildDeviceResolveResponse(ctx context.Context, device *model.
|
||||
// buildCardResolveResponse 构建卡类型的资产解析响应
|
||||
func (s *Service) buildCardResolveResponse(ctx context.Context, card *model.IotCard) (*dto.AssetResolveResponse, error) {
|
||||
resp := &dto.AssetResolveResponse{
|
||||
AssetType: "card",
|
||||
AssetID: card.ID,
|
||||
VirtualNo: card.VirtualNo,
|
||||
Status: card.Status,
|
||||
BatchNo: card.BatchNo,
|
||||
ShopID: card.ShopID,
|
||||
SeriesID: card.SeriesID,
|
||||
FirstCommissionPaid: card.FirstCommissionPaid,
|
||||
AccumulatedRecharge: card.AccumulatedRecharge,
|
||||
ActivatedAt: card.ActivatedAt,
|
||||
CreatedAt: card.CreatedAt,
|
||||
UpdatedAt: card.UpdatedAt,
|
||||
RealNameStatus: card.RealNameStatus,
|
||||
RealNameAt: card.FirstRealnameAt,
|
||||
NetworkStatus: card.NetworkStatus,
|
||||
ICCID: card.ICCID,
|
||||
CarrierID: card.CarrierID,
|
||||
CarrierType: card.CarrierType,
|
||||
CarrierName: card.CarrierName,
|
||||
MSISDN: card.MSISDN,
|
||||
IMSI: card.IMSI,
|
||||
CardCategory: card.CardCategory,
|
||||
Supplier: card.Supplier,
|
||||
ActivationStatus: card.ActivationStatus,
|
||||
EnablePolling: card.EnablePolling,
|
||||
AssetType: "card",
|
||||
AssetID: card.ID,
|
||||
VirtualNo: card.VirtualNo,
|
||||
Status: card.Status,
|
||||
StatusName: constants.GetIotCardStatusName(card.Status),
|
||||
BatchNo: card.BatchNo,
|
||||
ShopID: card.ShopID,
|
||||
SeriesID: card.SeriesID,
|
||||
ActivatedAt: card.ActivatedAt,
|
||||
CreatedAt: card.CreatedAt,
|
||||
UpdatedAt: card.UpdatedAt,
|
||||
RealNameStatus: card.RealNameStatus,
|
||||
RealNameAt: card.FirstRealnameAt,
|
||||
NetworkStatus: card.NetworkStatus,
|
||||
ICCID: card.ICCID,
|
||||
CarrierID: card.CarrierID,
|
||||
CarrierType: card.CarrierType,
|
||||
CarrierName: card.CarrierName,
|
||||
MSISDN: card.MSISDN,
|
||||
IMSI: card.IMSI,
|
||||
CardCategory: card.CardCategory,
|
||||
Supplier: card.Supplier,
|
||||
ActivationStatus: card.ActivationStatus,
|
||||
EnablePolling: card.EnablePolling,
|
||||
}
|
||||
|
||||
// 查绑定设备
|
||||
|
||||
@@ -137,6 +137,7 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// [待确认] 业务是否需要实名认证检查?参见 tech-debt-cleanup 提案
|
||||
// REALNAME-03: 按卡类型判断实名需求,普通卡需要实名,行业卡不需要
|
||||
// if assetInfo.CardCategory == "normal" && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
|
||||
// return nil, errors.New(errors.CodeNeedRealname)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"go.uber.org/zap"
|
||||
@@ -56,8 +57,9 @@ func (s *Service) ListBindings(ctx context.Context, deviceID uint) (*dto.ListDev
|
||||
IotCardID: binding.IotCardID,
|
||||
ICCID: card.ICCID,
|
||||
MSISDN: card.MSISDN,
|
||||
CarrierName: card.CarrierName, // 直接使用 IotCard 的冗余字段
|
||||
CarrierName: card.CarrierName,
|
||||
Status: card.Status,
|
||||
StatusName: constants.GetIotCardStatusName(card.Status),
|
||||
BindTime: binding.BindTime,
|
||||
IsCurrent: binding.IsCurrent,
|
||||
}
|
||||
|
||||
@@ -558,31 +558,29 @@ func (s *Service) extractDeviceIDs(devices []*model.Device) []uint {
|
||||
|
||||
func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string, seriesMap map[uint]string, bindingCounts map[uint]int64) *dto.DeviceResponse {
|
||||
resp := &dto.DeviceResponse{
|
||||
ID: device.ID,
|
||||
VirtualNo: device.VirtualNo,
|
||||
IMEI: device.IMEI,
|
||||
SN: device.SN,
|
||||
DeviceName: device.DeviceName,
|
||||
DeviceModel: device.DeviceModel,
|
||||
DeviceType: device.DeviceType,
|
||||
MaxSimSlots: device.MaxSimSlots,
|
||||
Manufacturer: device.Manufacturer,
|
||||
BatchNo: device.BatchNo,
|
||||
ShopID: device.ShopID,
|
||||
Status: device.Status,
|
||||
StatusName: s.getDeviceStatusName(device.Status),
|
||||
BoundCardCount: int(bindingCounts[device.ID]),
|
||||
SeriesID: device.SeriesID,
|
||||
FirstCommissionPaid: device.FirstCommissionPaid,
|
||||
AccumulatedRecharge: device.AccumulatedRecharge,
|
||||
ActivatedAt: device.ActivatedAt,
|
||||
CreatedAt: device.CreatedAt,
|
||||
UpdatedAt: device.UpdatedAt,
|
||||
OnlineStatus: device.OnlineStatus,
|
||||
LastOnlineTime: device.LastOnlineTime,
|
||||
SoftwareVersion: device.SoftwareVersion,
|
||||
SwitchMode: device.SwitchMode,
|
||||
LastGatewaySyncAt: device.LastGatewaySyncAt,
|
||||
ID: device.ID,
|
||||
VirtualNo: device.VirtualNo,
|
||||
IMEI: device.IMEI,
|
||||
SN: device.SN,
|
||||
DeviceName: device.DeviceName,
|
||||
DeviceModel: device.DeviceModel,
|
||||
DeviceType: device.DeviceType,
|
||||
MaxSimSlots: device.MaxSimSlots,
|
||||
Manufacturer: device.Manufacturer,
|
||||
BatchNo: device.BatchNo,
|
||||
ShopID: device.ShopID,
|
||||
Status: device.Status,
|
||||
StatusName: s.getDeviceStatusName(device.Status),
|
||||
BoundCardCount: int(bindingCounts[device.ID]),
|
||||
SeriesID: device.SeriesID,
|
||||
ActivatedAt: device.ActivatedAt,
|
||||
CreatedAt: device.CreatedAt,
|
||||
UpdatedAt: device.UpdatedAt,
|
||||
OnlineStatus: device.OnlineStatus,
|
||||
LastOnlineTime: device.LastOnlineTime,
|
||||
SoftwareVersion: device.SoftwareVersion,
|
||||
SwitchMode: device.SwitchMode,
|
||||
LastGatewaySyncAt: device.LastGatewaySyncAt,
|
||||
}
|
||||
|
||||
if device.ShopID != nil && *device.ShopID > 0 {
|
||||
|
||||
@@ -178,8 +178,6 @@ func (s *Service) copyAccumulatedFieldsWithTx(tx *gorm.DB, oldAsset, newAsset *r
|
||||
return errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", newAsset.AssetID).Updates(map[string]any{
|
||||
"accumulated_recharge": oldAsset.Card.AccumulatedRecharge,
|
||||
"first_commission_paid": oldAsset.Card.FirstCommissionPaid,
|
||||
"accumulated_recharge_by_series": oldAsset.Card.AccumulatedRechargeBySeriesJSON,
|
||||
"first_recharge_triggered_by_series": oldAsset.Card.FirstRechargeTriggeredBySeriesJSON,
|
||||
"updated_at": time.Now(),
|
||||
@@ -193,8 +191,6 @@ func (s *Service) copyAccumulatedFieldsWithTx(tx *gorm.DB, oldAsset, newAsset *r
|
||||
return errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
if err := tx.Model(&model.Device{}).Where("id = ?", newAsset.AssetID).Updates(map[string]any{
|
||||
"accumulated_recharge": oldAsset.Device.AccumulatedRecharge,
|
||||
"first_commission_paid": oldAsset.Device.FirstCommissionPaid,
|
||||
"accumulated_recharge_by_series": oldAsset.Device.AccumulatedRechargeBySeriesJSON,
|
||||
"first_recharge_triggered_by_series": oldAsset.Device.FirstRechargeTriggeredBySeriesJSON,
|
||||
"updated_at": time.Now(),
|
||||
|
||||
@@ -264,11 +264,15 @@ func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]str
|
||||
BatchNo: card.BatchNo,
|
||||
Supplier: card.Supplier,
|
||||
Status: card.Status,
|
||||
StatusName: constants.GetIotCardStatusName(card.Status),
|
||||
ShopID: card.ShopID,
|
||||
ActivatedAt: card.ActivatedAt,
|
||||
ActivationStatus: card.ActivationStatus,
|
||||
ActivationStatusName: constants.GetActivationStatusName(card.ActivationStatus),
|
||||
RealNameStatus: card.RealNameStatus,
|
||||
RealNameStatusName: constants.GetRealNameStatusName(card.RealNameStatus),
|
||||
NetworkStatus: card.NetworkStatus,
|
||||
NetworkStatusName: constants.GetNetworkStatusName(card.NetworkStatus),
|
||||
DataUsageMB: card.DataUsageMB,
|
||||
CurrentMonthUsageMB: card.CurrentMonthUsageMB,
|
||||
CurrentMonthStartDate: card.CurrentMonthStartDate,
|
||||
@@ -277,8 +281,6 @@ func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]str
|
||||
LastRealNameCheckAt: card.LastRealNameCheckAt,
|
||||
EnablePolling: card.EnablePolling,
|
||||
SeriesID: card.SeriesID,
|
||||
FirstCommissionPaid: card.FirstCommissionPaid,
|
||||
AccumulatedRecharge: card.AccumulatedRecharge,
|
||||
DeviceVirtualNo: card.DeviceVirtualNo,
|
||||
CreatedAt: card.CreatedAt,
|
||||
UpdatedAt: card.UpdatedAt,
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/payment"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -48,6 +49,7 @@ type Service struct {
|
||||
packageStore *postgres.PackageStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
wechatPayment wechat.PaymentServiceInterface
|
||||
paymentLoader payment.PaymentConfigLoader
|
||||
queueClient *queue.Client
|
||||
logger *zap.Logger
|
||||
resumeCallback packagepkg.ResumeCallback
|
||||
@@ -73,6 +75,7 @@ func New(
|
||||
packageStore *postgres.PackageStore,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
wechatPayment wechat.PaymentServiceInterface,
|
||||
paymentLoader payment.PaymentConfigLoader,
|
||||
queueClient *queue.Client,
|
||||
logger *zap.Logger,
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore,
|
||||
@@ -96,6 +99,7 @@ func New(
|
||||
packageStore: packageStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
wechatPayment: wechatPayment,
|
||||
paymentLoader: paymentLoader,
|
||||
queueClient: queueClient,
|
||||
logger: logger,
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
@@ -109,250 +113,6 @@ func (s *Service) SetResumeCallback(callback packagepkg.ResumeCallback) {
|
||||
s.resumeCallback = callback
|
||||
}
|
||||
|
||||
// CreateLegacy 创建订单(已废弃)
|
||||
// Deprecated: 使用 CreateAdminOrder 或 CreateH5Order 替代。保留用于回滚。
|
||||
func (s *Service) CreateLegacy(ctx context.Context, req *dto.CreateOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
|
||||
var validationResult *purchase_validation.PurchaseValidationResult
|
||||
var err error
|
||||
|
||||
if req.OrderType == model.OrderTypeSingleCard {
|
||||
if req.IotCardID == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID")
|
||||
}
|
||||
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, *req.IotCardID, req.PackageIDs)
|
||||
} else if req.OrderType == model.OrderTypeDevice {
|
||||
if req.DeviceID == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID")
|
||||
}
|
||||
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, *req.DeviceID, req.PackageIDs)
|
||||
} else {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 下单阶段校验混买限制:禁止同一订单同时包含正式套餐和加油包
|
||||
if err := validatePackageTypeMixFromPackages(validationResult.Packages); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 幂等性检查:防止同一买家对同一载体短时间内重复下单
|
||||
carrierType, carrierID := resolveCarrierInfo(req)
|
||||
existingOrderID, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingOrderID > 0 {
|
||||
return s.Get(ctx, existingOrderID)
|
||||
}
|
||||
// 获取到分布式锁后,确保无论成功还是失败都释放
|
||||
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult)
|
||||
if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
|
||||
return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求")
|
||||
}
|
||||
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
|
||||
// 提取资源所属店铺ID
|
||||
var resourceShopID *uint
|
||||
var seriesID *uint
|
||||
if validationResult.Card != nil {
|
||||
resourceShopID = validationResult.Card.ShopID
|
||||
seriesID = validationResult.Card.SeriesID
|
||||
} else if validationResult.Device != nil {
|
||||
resourceShopID = validationResult.Device.ShopID
|
||||
seriesID = validationResult.Device.SeriesID
|
||||
}
|
||||
|
||||
// 初始化订单字段
|
||||
orderBuyerType := buyerType
|
||||
orderBuyerID := buyerID
|
||||
totalAmount := validationResult.TotalPrice
|
||||
paymentMethod := req.PaymentMethod
|
||||
paymentStatus := model.PaymentStatusPending
|
||||
var paidAt *time.Time
|
||||
now := time.Now()
|
||||
isPurchaseOnBehalf := false
|
||||
var operatorID *uint
|
||||
operatorType := ""
|
||||
var actualPaidAmount *int64
|
||||
purchaseRole := ""
|
||||
var sellerShopID *uint = resourceShopID
|
||||
var sellerCostPrice int64
|
||||
|
||||
// 场景判断:offline(平台代购)、wallet(代理钱包支付)、其他(待支付)
|
||||
if req.PaymentMethod == model.PaymentMethodOffline {
|
||||
// ==== 场景 1:平台代购(offline)====
|
||||
purchaseBuyerID, buyerCostPrice, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orderBuyerType = model.BuyerTypeAgent
|
||||
orderBuyerID = purchaseBuyerID
|
||||
totalAmount = buyerCostPrice
|
||||
paymentMethod = model.PaymentMethodOffline
|
||||
paymentStatus = model.PaymentStatusPaid
|
||||
paidAt = purchasePaidAt
|
||||
isPurchaseOnBehalf = true
|
||||
sellerCostPrice = buyerCostPrice
|
||||
|
||||
// 设置操作者信息(平台代购)
|
||||
operatorID = nil
|
||||
operatorType = constants.OwnerTypePlatform
|
||||
purchaseRole = model.PurchaseRolePurchasedByPlatform
|
||||
actualPaidAmount = nil
|
||||
|
||||
} else if req.PaymentMethod == model.PaymentMethodWallet {
|
||||
// ==== 场景 2:代理钱包支付(wallet)====
|
||||
if buyerType == "" {
|
||||
if resourceShopID == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "不支持的钱包支付场景")
|
||||
}
|
||||
buyerType = model.BuyerTypeAgent
|
||||
buyerID = *resourceShopID
|
||||
}
|
||||
|
||||
// 只有代理账号可以使用钱包支付
|
||||
if buyerType != model.BuyerTypeAgent {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "只有代理账号可以使用钱包支付")
|
||||
}
|
||||
operatorShopID := buyerID
|
||||
|
||||
// 判断资源是否属于操作者
|
||||
if resourceShopID == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "资源店铺ID为空")
|
||||
}
|
||||
|
||||
// 获取第一个套餐ID用于查询成本价
|
||||
if len(validationResult.Packages) == 0 {
|
||||
return nil, errors.New(errors.CodeInternalError, "套餐列表为空")
|
||||
}
|
||||
firstPackageID := validationResult.Packages[0].ID
|
||||
|
||||
if *resourceShopID == operatorShopID {
|
||||
// ==== 子场景 2.1:代理自购 ====
|
||||
costPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderBuyerType = model.BuyerTypeAgent
|
||||
orderBuyerID = operatorShopID
|
||||
totalAmount = costPrice
|
||||
paymentMethod = model.PaymentMethodWallet
|
||||
paymentStatus = model.PaymentStatusPaid
|
||||
paidAt = &now
|
||||
isPurchaseOnBehalf = false
|
||||
|
||||
operatorID = &operatorShopID
|
||||
operatorType = "agent"
|
||||
actualPaidAmountVal := costPrice
|
||||
actualPaidAmount = &actualPaidAmountVal
|
||||
purchaseRole = model.PurchaseRoleSelfPurchase
|
||||
sellerCostPrice = costPrice
|
||||
|
||||
} else {
|
||||
// ==== 子场景 2.2:代理代购(给下级购买)====
|
||||
// 获取买家成本价
|
||||
buyerCostPrice, err := s.getCostPrice(ctx, *resourceShopID, firstPackageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取操作者成本价
|
||||
operatorCostPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderBuyerType = model.BuyerTypeAgent
|
||||
orderBuyerID = *resourceShopID
|
||||
totalAmount = buyerCostPrice
|
||||
paymentMethod = model.PaymentMethodWallet
|
||||
paymentStatus = model.PaymentStatusPaid
|
||||
paidAt = &now
|
||||
isPurchaseOnBehalf = true
|
||||
|
||||
operatorID = &operatorShopID
|
||||
operatorType = "agent"
|
||||
actualPaidAmount = &operatorCostPrice
|
||||
purchaseRole = model.PurchaseRolePurchaseForSubordinate
|
||||
// 代理代购下级时,佣金差额基准应是操作方(代理自己)的成本价,而非下级的成本价
|
||||
// 使用 buyerCostPrice 会导致差额为 0,上级代理无法获得应有佣金
|
||||
sellerCostPrice = operatorCostPrice
|
||||
}
|
||||
}
|
||||
|
||||
order := &model.Order{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: userID,
|
||||
Updater: userID,
|
||||
},
|
||||
OrderNo: s.orderStore.GenerateOrderNo(),
|
||||
OrderType: req.OrderType,
|
||||
BuyerType: orderBuyerType,
|
||||
BuyerID: orderBuyerID,
|
||||
IotCardID: req.IotCardID,
|
||||
DeviceID: req.DeviceID,
|
||||
TotalAmount: totalAmount,
|
||||
PaymentMethod: paymentMethod,
|
||||
PaymentStatus: paymentStatus,
|
||||
PaidAt: paidAt,
|
||||
CommissionStatus: model.CommissionStatusPending,
|
||||
CommissionConfigVersion: 0,
|
||||
SeriesID: seriesID,
|
||||
SellerShopID: sellerShopID,
|
||||
SellerCostPrice: sellerCostPrice,
|
||||
IsPurchaseOnBehalf: isPurchaseOnBehalf,
|
||||
OperatorID: operatorID,
|
||||
OperatorType: operatorType,
|
||||
ActualPaidAmount: actualPaidAmount,
|
||||
PurchaseRole: purchaseRole,
|
||||
}
|
||||
|
||||
items := s.buildOrderItems(userID, validationResult.Packages)
|
||||
|
||||
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
|
||||
|
||||
// 根据支付方式选择创建订单的方式
|
||||
if req.PaymentMethod == model.PaymentMethodOffline {
|
||||
// 平台代购:创建订单并立即激活套餐
|
||||
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.enqueueCommissionCalculation(ctx, order.ID)
|
||||
s.markOrderCreated(ctx, idempotencyKey, order.ID)
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
|
||||
} else if req.PaymentMethod == model.PaymentMethodWallet {
|
||||
// 钱包支付:创建订单、扣款、激活套餐(在事务中完成)
|
||||
if operatorID == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "钱包支付场景下 operatorID 不能为空")
|
||||
}
|
||||
operatorShopID := *operatorID
|
||||
buyerShopID := orderBuyerID
|
||||
|
||||
if err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.markOrderCreated(ctx, idempotencyKey, order.ID)
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
|
||||
} else {
|
||||
// 其他支付方式:创建待支付订单
|
||||
if err := s.orderStore.Create(ctx, order, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.markOrderCreated(ctx, idempotencyKey, order.ID)
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAdminOrder 后台订单创建(仅支持 wallet/offline,立即扣款或激活)
|
||||
// 与 CreateH5Order 的核心区别:后台订单不创建待支付状态,wallet 立即扣款,offline 立即激活
|
||||
// POST /api/admin/orders
|
||||
@@ -2227,6 +1987,7 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
|
||||
PaymentVoucherKey: order.PaymentVoucherKey,
|
||||
IsPurchaseOnBehalf: order.IsPurchaseOnBehalf,
|
||||
CommissionStatus: order.CommissionStatus,
|
||||
CommissionStatusName: constants.GetOrderCommissionStatusName(order.CommissionStatus),
|
||||
CommissionConfigVersion: order.CommissionConfigVersion,
|
||||
|
||||
OperatorID: order.OperatorID,
|
||||
@@ -2251,13 +2012,8 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
|
||||
}
|
||||
|
||||
// WechatPayJSAPI 发起微信 JSAPI 支付
|
||||
// TODO: 从 payment_config_id 加载配置动态创建 Payment 实例,替代 s.wechatPayment 单例
|
||||
// 通过 order.PaymentConfigID 动态加载支付配置,支持多商户场景
|
||||
func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.WechatPayJSAPIResponse, error) {
|
||||
if s.wechatPayment == nil {
|
||||
s.logger.Error("微信支付服务未配置")
|
||||
return nil, errors.New(errors.CodeWechatPayFailed, "微信支付服务未配置")
|
||||
}
|
||||
|
||||
order, err := s.orderStore.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -2274,6 +2030,16 @@ func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID strin
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
// 按 payment_config_id 动态加载支付配置
|
||||
if order.PaymentConfigID == nil {
|
||||
s.logger.Error("订单缺少支付配置", zap.Uint("order_id", orderID))
|
||||
return nil, errors.New(errors.CodeWechatConfigNotFound, "订单缺少支付配置")
|
||||
}
|
||||
paymentSvc, err := s.paymentLoader.LoadConfig(ctx, *order.PaymentConfigID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
|
||||
@@ -2283,7 +2049,7 @@ func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID strin
|
||||
description = items[0].PackageName
|
||||
}
|
||||
|
||||
result, err := s.wechatPayment.CreateJSAPIOrder(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
|
||||
result, err := paymentSvc.CreateJSAPIOrder(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
|
||||
if err != nil {
|
||||
s.logger.Error("创建 JSAPI 支付失败",
|
||||
zap.Uint("order_id", orderID),
|
||||
@@ -2307,13 +2073,8 @@ func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID strin
|
||||
}
|
||||
|
||||
// WechatPayH5 发起微信 H5 支付
|
||||
// TODO: 从 payment_config_id 加载配置动态创建 Payment 实例,替代 s.wechatPayment 单例
|
||||
// 通过 order.PaymentConfigID 动态加载支付配置,支持多商户场景
|
||||
func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.WechatH5SceneInfo, buyerType string, buyerID uint) (*dto.WechatPayH5Response, error) {
|
||||
if s.wechatPayment == nil {
|
||||
s.logger.Error("微信支付服务未配置")
|
||||
return nil, errors.New(errors.CodeWechatPayFailed, "微信支付服务未配置")
|
||||
}
|
||||
|
||||
order, err := s.orderStore.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -2330,6 +2091,16 @@ func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
// 按 payment_config_id 动态加载支付配置
|
||||
if order.PaymentConfigID == nil {
|
||||
s.logger.Error("订单缺少支付配置", zap.Uint("order_id", orderID))
|
||||
return nil, errors.New(errors.CodeWechatConfigNotFound, "订单缺少支付配置")
|
||||
}
|
||||
paymentSvc, err := s.paymentLoader.LoadConfig(ctx, *order.PaymentConfigID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
|
||||
@@ -2344,7 +2115,7 @@ func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.
|
||||
H5Type: sceneInfo.H5Info.Type,
|
||||
}
|
||||
|
||||
result, err := s.wechatPayment.CreateH5Order(ctx, order.OrderNo, description, int(order.TotalAmount), h5SceneInfo)
|
||||
result, err := paymentSvc.CreateH5Order(ctx, order.OrderNo, description, int(order.TotalAmount), h5SceneInfo)
|
||||
if err != nil {
|
||||
s.logger.Error("创建 H5 支付失败",
|
||||
zap.Uint("order_id", orderID),
|
||||
|
||||
@@ -598,6 +598,8 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
|
||||
}
|
||||
}
|
||||
|
||||
resp.StatusName = constants.GetStatusName(resp.Status)
|
||||
resp.ShelfStatusName = constants.GetShelfStatusName(resp.ShelfStatus)
|
||||
return resp
|
||||
}
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ func (s *AlertService) triggerAlert(ctx context.Context, rule *model.PollingAler
|
||||
CurrentValue: currentValue,
|
||||
AlertMessage: alertMessage,
|
||||
NotificationChannels: rule.NotificationChannels,
|
||||
NotificationStatus: "pending",
|
||||
NotificationStatus: constants.PollingManualTriggerStatusPending,
|
||||
}
|
||||
|
||||
if err := s.historyStore.Create(ctx, history); err != nil {
|
||||
|
||||
@@ -86,7 +86,7 @@ func (s *ManualTriggerService) TriggerSingle(ctx context.Context, cardID uint, t
|
||||
TriggerType: "single",
|
||||
CardIDs: string(cardIDsJSON),
|
||||
TotalCount: 1,
|
||||
Status: "processing",
|
||||
Status: constants.PollingManualTriggerStatusProcessing,
|
||||
TriggeredBy: triggeredBy,
|
||||
TriggeredAt: time.Now(),
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func (s *ManualTriggerService) TriggerSingle(ctx context.Context, cardID uint, t
|
||||
|
||||
// 更新日志状态
|
||||
_ = s.logStore.UpdateProgress(ctx, triggerLog.ID, 1, 1, 0)
|
||||
_ = s.logStore.UpdateStatus(ctx, triggerLog.ID, "completed")
|
||||
_ = s.logStore.UpdateStatus(ctx, triggerLog.ID, constants.PollingManualTriggerStatusCompleted)
|
||||
|
||||
s.logger.Info("单卡手动触发成功",
|
||||
zap.Uint("card_id", cardID),
|
||||
@@ -153,7 +153,7 @@ func (s *ManualTriggerService) TriggerBatch(ctx context.Context, cardIDs []uint,
|
||||
TriggerType: "batch",
|
||||
CardIDs: string(cardIDsJSON),
|
||||
TotalCount: len(cardIDs),
|
||||
Status: "processing",
|
||||
Status: constants.PollingManualTriggerStatusProcessing,
|
||||
TriggeredBy: triggeredBy,
|
||||
TriggeredAt: time.Now(),
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func (s *ManualTriggerService) processBatchTrigger(ctx context.Context, logID ui
|
||||
|
||||
// 更新最终状态
|
||||
_ = s.logStore.UpdateProgress(ctx, logID, processedCount, successCount, failedCount)
|
||||
_ = s.logStore.UpdateStatus(ctx, logID, "completed")
|
||||
_ = s.logStore.UpdateStatus(ctx, logID, constants.PollingManualTriggerStatusCompleted)
|
||||
|
||||
s.logger.Info("批量手动触发完成",
|
||||
zap.Uint("log_id", logID),
|
||||
@@ -274,7 +274,7 @@ func (s *ManualTriggerService) TriggerByCondition(ctx context.Context, filter *C
|
||||
CardIDs: string(cardIDsJSON),
|
||||
ConditionFilter: string(filterJSON),
|
||||
TotalCount: len(cardIDs),
|
||||
Status: "processing",
|
||||
Status: constants.PollingManualTriggerStatusProcessing,
|
||||
TriggeredBy: triggeredBy,
|
||||
TriggeredAt: time.Now(),
|
||||
}
|
||||
@@ -344,11 +344,11 @@ func (s *ManualTriggerService) CancelTrigger(ctx context.Context, logID uint, tr
|
||||
return errors.New(errors.CodeForbidden, "无权限取消该任务")
|
||||
}
|
||||
|
||||
if log.Status != "pending" && log.Status != "processing" {
|
||||
if log.Status != constants.PollingManualTriggerStatusPending && log.Status != constants.PollingManualTriggerStatusProcessing {
|
||||
return errors.New(errors.CodeInvalidParam, "任务已完成或已取消")
|
||||
}
|
||||
|
||||
return s.logStore.UpdateStatus(ctx, logID, "cancelled")
|
||||
return s.logStore.UpdateStatus(ctx, logID, constants.PollingManualTriggerStatusCancelled)
|
||||
}
|
||||
|
||||
// GetRunningTasks 获取正在运行的任务
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/task"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/payment"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
@@ -28,7 +29,6 @@ type ForceRechargeRequirement struct {
|
||||
CurrentAccumulated int64 `json:"current_accumulated"` // 当前累计充值
|
||||
Threshold int64 `json:"threshold"` // 佣金触发阈值
|
||||
Message string `json:"message"` // 提示信息
|
||||
FirstCommissionPaid bool `json:"first_commission_paid"` // 一次性佣金是否已发放
|
||||
}
|
||||
|
||||
// WechatConfigServiceInterface 支付配置服务接口
|
||||
@@ -49,6 +49,7 @@ type Service struct {
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
commissionRecordStore *postgres.CommissionRecordStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
paymentLoader payment.PaymentConfigLoader
|
||||
queueClient *queue.Client // 任务队列客户端,用于触发自动购包任务
|
||||
logger *zap.Logger
|
||||
}
|
||||
@@ -65,6 +66,7 @@ func New(
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
commissionRecordStore *postgres.CommissionRecordStore,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
paymentLoader payment.PaymentConfigLoader,
|
||||
logger *zap.Logger,
|
||||
queueClient *queue.Client,
|
||||
) *Service {
|
||||
@@ -79,6 +81,7 @@ func New(
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
commissionRecordStore: commissionRecordStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
paymentLoader: paymentLoader,
|
||||
queueClient: queueClient,
|
||||
logger: logger,
|
||||
}
|
||||
@@ -88,10 +91,10 @@ func New(
|
||||
// 验证资源、金额范围、强充要求,生成订单号
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateRechargeRequest, userID uint) (*dto.RechargeResponse, error) {
|
||||
// 1. 验证金额范围
|
||||
if req.Amount < constants.RechargeMinAmount {
|
||||
if req.Amount < constants.AssetRechargeMinAmount {
|
||||
return nil, errors.New(errors.CodeRechargeAmountInvalid, "充值金额不能低于1元")
|
||||
}
|
||||
if req.Amount > constants.RechargeMaxAmount {
|
||||
if req.Amount > constants.AssetRechargeMaxAmount {
|
||||
return nil, errors.New(errors.CodeRechargeAmountInvalid, "充值金额不能超过100000元")
|
||||
}
|
||||
|
||||
@@ -268,7 +271,7 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID
|
||||
|
||||
// HandlePaymentCallback 支付回调处理
|
||||
// 支持幂等性检查、事务处理、更新余额、触发佣金
|
||||
// TODO: 按 payment_config_id 加载配置验签(当前留桩,验签由外层处理)
|
||||
// 验签由外层 Handler 负责;如需按 payment_config_id 加载配置,可通过 s.paymentLoader 获取
|
||||
func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error {
|
||||
// 1. 查询充值订单
|
||||
recharge, err := s.assetRechargeStore.GetByRechargeNo(ctx, rechargeNo)
|
||||
@@ -414,8 +417,8 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
|
||||
func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceType string, resourceID uint) (*ForceRechargeRequirement, error) {
|
||||
result := &ForceRechargeRequirement{
|
||||
NeedForceRecharge: false,
|
||||
MinAmount: constants.RechargeMinAmount,
|
||||
MaxAmount: constants.RechargeMaxAmount,
|
||||
MinAmount: constants.AssetRechargeMinAmount,
|
||||
MaxAmount: constants.AssetRechargeMaxAmount,
|
||||
Message: "无强充要求,可自由充值",
|
||||
}
|
||||
|
||||
@@ -455,7 +458,6 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp
|
||||
}
|
||||
|
||||
result.CurrentAccumulated = accumulatedRecharge
|
||||
result.FirstCommissionPaid = firstCommissionPaid
|
||||
|
||||
if seriesID == nil || shopID == nil {
|
||||
return result, nil
|
||||
|
||||
@@ -651,6 +651,7 @@ func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse {
|
||||
ApprovedRefundAmount: r.ApprovedRefundAmount,
|
||||
RefundReason: r.RefundReason,
|
||||
Status: r.Status,
|
||||
StatusName: constants.GetRefundStatusName(r.Status),
|
||||
ProcessorID: r.ProcessorID,
|
||||
RejectReason: r.RejectReason,
|
||||
Remark: r.Remark,
|
||||
|
||||
@@ -189,6 +189,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopRequest) (*dto.
|
||||
District: shop.District,
|
||||
Address: shop.Address,
|
||||
Status: shop.Status,
|
||||
StatusName: constants.GetStatusName(shop.Status),
|
||||
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}, nil
|
||||
@@ -232,6 +233,7 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopReques
|
||||
District: shop.District,
|
||||
Address: shop.Address,
|
||||
Status: shop.Status,
|
||||
StatusName: constants.GetStatusName(shop.Status),
|
||||
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}, nil
|
||||
@@ -338,6 +340,7 @@ func (s *Service) ListShopResponses(ctx context.Context, req *dto.ShopListReques
|
||||
District: shop.District,
|
||||
Address: shop.Address,
|
||||
Status: shop.Status,
|
||||
StatusName: constants.GetStatusName(shop.Status),
|
||||
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
|
||||
@@ -642,7 +642,7 @@ func (s *Service) CreateWithdrawalRequest(ctx context.Context, shopID uint, req
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: shopID,
|
||||
UserID: currentUserID,
|
||||
TransactionType: constants.TransactionTypeWithdrawal,
|
||||
TransactionType: constants.AgentTransactionTypeWithdrawal,
|
||||
Amount: -req.Amount,
|
||||
BalanceBefore: wallet.Balance,
|
||||
BalanceAfter: wallet.Balance - req.Amount,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// PollingManualTriggerLogStore 手动触发日志存储
|
||||
@@ -66,7 +67,7 @@ func (s *PollingManualTriggerLogStore) List(ctx context.Context, page, pageSize
|
||||
func (s *PollingManualTriggerLogStore) GetRunning(ctx context.Context, triggeredBy uint) ([]*model.PollingManualTriggerLog, error) {
|
||||
var logs []*model.PollingManualTriggerLog
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("triggered_by = ? AND status IN ('pending', 'processing')", triggeredBy).
|
||||
Where("triggered_by = ? AND status IN (?, ?)", triggeredBy, constants.PollingManualTriggerStatusPending, constants.PollingManualTriggerStatusProcessing).
|
||||
Order("triggered_at DESC").
|
||||
Find(&logs).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -88,7 +89,7 @@ func (s *PollingManualTriggerLogStore) UpdateProgress(ctx context.Context, id ui
|
||||
// UpdateStatus 更新触发状态
|
||||
func (s *PollingManualTriggerLogStore) UpdateStatus(ctx context.Context, id uint, status string) error {
|
||||
updates := map[string]any{"status": status}
|
||||
if status == "completed" || status == "cancelled" {
|
||||
if status == constants.PollingManualTriggerStatusCompleted || status == constants.PollingManualTriggerStatusCancelled {
|
||||
updates["completed_at"] = gorm.Expr("CURRENT_TIMESTAMP")
|
||||
}
|
||||
return s.db.WithContext(ctx).Model(&model.PollingManualTriggerLog{}).
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DataSyncPayload 数据同步任务载荷
|
||||
type DataSyncPayload struct {
|
||||
RequestID string `json:"request_id"`
|
||||
SyncType string `json:"sync_type"` // sim_status, flow_usage, real_name
|
||||
StartDate string `json:"start_date"` // YYYY-MM-DD
|
||||
EndDate string `json:"end_date"` // YYYY-MM-DD
|
||||
BatchSize int `json:"batch_size"` // 批量大小
|
||||
}
|
||||
|
||||
// SyncHandler 数据同步任务处理器
|
||||
type SyncHandler struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSyncHandler 创建数据同步任务处理器
|
||||
func NewSyncHandler(db *gorm.DB, logger *zap.Logger) *SyncHandler {
|
||||
return &SyncHandler{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleDataSync 处理数据同步任务
|
||||
func (h *SyncHandler) HandleDataSync(ctx context.Context, task *asynq.Task) error {
|
||||
// 解析任务载荷
|
||||
var payload DataSyncPayload
|
||||
if err := sonic.Unmarshal(task.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析数据同步任务载荷失败",
|
||||
zap.Error(err),
|
||||
zap.String("task_id", task.ResultWriter().TaskID()),
|
||||
)
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
|
||||
// 验证载荷
|
||||
if err := h.validatePayload(&payload); err != nil {
|
||||
h.logger.Error("数据同步任务载荷验证失败",
|
||||
zap.Error(err),
|
||||
zap.String("request_id", payload.RequestID),
|
||||
)
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
|
||||
// 设置默认批量大小
|
||||
if payload.BatchSize <= 0 {
|
||||
payload.BatchSize = 100
|
||||
}
|
||||
|
||||
// 记录任务开始
|
||||
h.logger.Info("开始处理数据同步任务",
|
||||
zap.String("request_id", payload.RequestID),
|
||||
zap.String("sync_type", payload.SyncType),
|
||||
zap.String("start_date", payload.StartDate),
|
||||
zap.String("end_date", payload.EndDate),
|
||||
zap.Int("batch_size", payload.BatchSize),
|
||||
)
|
||||
|
||||
// 执行数据同步
|
||||
if err := h.syncData(ctx, &payload); err != nil {
|
||||
h.logger.Error("数据同步失败",
|
||||
zap.Error(err),
|
||||
zap.String("request_id", payload.RequestID),
|
||||
zap.String("sync_type", payload.SyncType),
|
||||
)
|
||||
return err // 同步失败,可以重试
|
||||
}
|
||||
|
||||
// 记录任务完成
|
||||
h.logger.Info("数据同步成功",
|
||||
zap.String("request_id", payload.RequestID),
|
||||
zap.String("sync_type", payload.SyncType),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePayload 验证数据同步载荷
|
||||
func (h *SyncHandler) validatePayload(payload *DataSyncPayload) error {
|
||||
if payload.RequestID == "" {
|
||||
return fmt.Errorf("request_id 不能为空")
|
||||
}
|
||||
if payload.SyncType == "" {
|
||||
return fmt.Errorf("sync_type 不能为空")
|
||||
}
|
||||
validTypes := []string{"sim_status", "flow_usage", "real_name"}
|
||||
valid := false
|
||||
for _, t := range validTypes {
|
||||
if payload.SyncType == t {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("sync_type 无效,必须为 sim_status, flow_usage, real_name 之一")
|
||||
}
|
||||
if payload.StartDate == "" {
|
||||
return fmt.Errorf("start_date 不能为空")
|
||||
}
|
||||
if payload.EndDate == "" {
|
||||
return fmt.Errorf("end_date 不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncData 执行数据同步
|
||||
func (h *SyncHandler) syncData(ctx context.Context, payload *DataSyncPayload) error {
|
||||
// TODO: 实际实现中需要调用外部 API 或数据源进行同步
|
||||
|
||||
// 模拟批量同步
|
||||
totalRecords := 500 // 假设有 500 条记录需要同步
|
||||
batches := (totalRecords + payload.BatchSize - 1) / payload.BatchSize
|
||||
|
||||
for i := 0; i < batches; i++ {
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 模拟批量处理
|
||||
offset := i * payload.BatchSize
|
||||
limit := payload.BatchSize
|
||||
if offset+limit > totalRecords {
|
||||
limit = totalRecords - offset
|
||||
}
|
||||
|
||||
h.logger.Debug("同步批次",
|
||||
zap.String("sync_type", payload.SyncType),
|
||||
zap.Int("batch", i+1),
|
||||
zap.Int("total_batches", batches),
|
||||
zap.Int("offset", offset),
|
||||
zap.Int("limit", limit),
|
||||
)
|
||||
|
||||
// 模拟处理延迟
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// TODO: 实际实现中需要:
|
||||
// 1. 从外部 API 获取数据
|
||||
// 2. 使用事务批量更新数据库
|
||||
// 3. 记录同步状态
|
||||
}
|
||||
|
||||
h.logger.Info("批量同步完成",
|
||||
zap.String("sync_type", payload.SyncType),
|
||||
zap.Int("total_records", totalRecords),
|
||||
zap.Int("batches", batches),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user