Compare commits
10 Commits
b9733c4913
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c10b70757f | |||
| 4d1e714366 | |||
| d2b765327c | |||
| 7dfcf41b41 | |||
| ed334b946b | |||
| 95b2334658 | |||
| da66e673fe | |||
| 284f6c15c7 | |||
| 55918a0b88 | |||
| d2494798aa |
@@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/pkg/database"
|
"github.com/break/junhong_cmp_fiber/pkg/database"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||||
|
"github.com/break/junhong_cmp_fiber/pkg/sms"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/storage"
|
"github.com/break/junhong_cmp_fiber/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -306,11 +307,42 @@ func initAuthComponents(cfg *config.Config, redisClient *redis.Client, appLogger
|
|||||||
refreshTTL := time.Duration(cfg.JWT.RefreshTokenTTL) * time.Second
|
refreshTTL := time.Duration(cfg.JWT.RefreshTokenTTL) * time.Second
|
||||||
tokenManager := auth.NewTokenManager(redisClient, accessTTL, refreshTTL)
|
tokenManager := auth.NewTokenManager(redisClient, accessTTL, refreshTTL)
|
||||||
|
|
||||||
verificationSvc := verification.NewService(redisClient, nil, appLogger)
|
smsClient := initSMS(cfg, appLogger)
|
||||||
|
verificationSvc := verification.NewService(redisClient, smsClient, appLogger)
|
||||||
|
|
||||||
return jwtManager, tokenManager, verificationSvc
|
return jwtManager, tokenManager, verificationSvc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initSMS(cfg *config.Config, appLogger *zap.Logger) *sms.Client {
|
||||||
|
if cfg.SMS.GatewayURL == "" {
|
||||||
|
appLogger.Info("短信服务未配置,跳过初始化")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := cfg.SMS.Timeout
|
||||||
|
if timeout == 0 {
|
||||||
|
timeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := sms.NewStandardHTTPClient(0)
|
||||||
|
client := sms.NewClient(
|
||||||
|
cfg.SMS.GatewayURL,
|
||||||
|
cfg.SMS.Username,
|
||||||
|
cfg.SMS.Password,
|
||||||
|
cfg.SMS.Signature,
|
||||||
|
timeout,
|
||||||
|
appLogger,
|
||||||
|
httpClient,
|
||||||
|
)
|
||||||
|
|
||||||
|
appLogger.Info("短信服务已初始化",
|
||||||
|
zap.String("gateway_url", cfg.SMS.GatewayURL),
|
||||||
|
zap.String("signature", cfg.SMS.Signature),
|
||||||
|
)
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
func initStorage(cfg *config.Config, appLogger *zap.Logger) *storage.Service {
|
func initStorage(cfg *config.Config, appLogger *zap.Logger) *storage.Service {
|
||||||
if cfg.Storage.Provider == "" || cfg.Storage.S3.Endpoint == "" {
|
if cfg.Storage.Provider == "" || cfg.Storage.S3.Endpoint == "" {
|
||||||
appLogger.Info("对象存储未配置,跳过初始化")
|
appLogger.Info("对象存储未配置,跳过初始化")
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ services:
|
|||||||
- JUNHONG_GATEWAY_APP_ID=LfjL0WjUqpwkItQ0
|
- JUNHONG_GATEWAY_APP_ID=LfjL0WjUqpwkItQ0
|
||||||
- JUNHONG_GATEWAY_APP_SECRET=K0DYuWzbRE6zg5bX
|
- JUNHONG_GATEWAY_APP_SECRET=K0DYuWzbRE6zg5bX
|
||||||
- JUNHONG_GATEWAY_TIMEOUT=30
|
- JUNHONG_GATEWAY_TIMEOUT=30
|
||||||
|
# 短信服务配置
|
||||||
|
- JUNHONG_SMS_GATEWAY_URL=https://gateway.sms.whjhft.com:8443
|
||||||
|
- JUNHONG_SMS_USERNAME=JH0001
|
||||||
|
- JUNHONG_SMS_PASSWORD=wwR8E4qnL6F0
|
||||||
|
- JUNHONG_SMS_SIGNATURE=【JHFTIOT】
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
AccountAudit: accountAudit,
|
AccountAudit: accountAudit,
|
||||||
Role: roleSvc.New(s.Role, s.Permission, s.RolePermission),
|
Role: roleSvc.New(s.Role, s.Permission, s.RolePermission),
|
||||||
Permission: permissionSvc.New(s.Permission, s.AccountRole, s.RolePermission, account, deps.Redis),
|
Permission: permissionSvc.New(s.Permission, s.AccountRole, s.RolePermission, account, deps.Redis),
|
||||||
PersonalCustomer: personalCustomerSvc.NewService(s.PersonalCustomer, s.PersonalCustomerPhone, deps.VerificationService, deps.JWTManager, deps.Logger),
|
PersonalCustomer: personalCustomerSvc.NewService(s.PersonalCustomer, s.PersonalCustomerPhone, deps.Logger),
|
||||||
ClientAuth: clientAuthSvc.New(
|
ClientAuth: clientAuthSvc.New(
|
||||||
deps.DB,
|
deps.DB,
|
||||||
s.PersonalCustomerOpenID,
|
s.PersonalCustomerOpenID,
|
||||||
|
|||||||
@@ -149,15 +149,45 @@ func (h *ClientAssetHandler) GetAssetInfo(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp := &dto.AssetInfoResponse{
|
resp := &dto.AssetInfoResponse{
|
||||||
AssetType: resolved.Asset.AssetType,
|
AssetType: resolved.Asset.AssetType,
|
||||||
AssetID: resolved.Asset.AssetID,
|
AssetID: resolved.Asset.AssetID,
|
||||||
Identifier: resolved.Identifier,
|
Identifier: resolved.Identifier,
|
||||||
VirtualNo: resolved.Asset.VirtualNo,
|
VirtualNo: resolved.Asset.VirtualNo,
|
||||||
Status: resolved.Asset.Status,
|
Status: resolved.Asset.Status,
|
||||||
RealNameStatus: resolved.Asset.RealNameStatus,
|
RealNameStatus: resolved.Asset.RealNameStatus,
|
||||||
CarrierName: resolved.Asset.CarrierName,
|
CarrierName: resolved.Asset.CarrierName,
|
||||||
Generation: strconv.Itoa(resolved.Generation),
|
Generation: strconv.Itoa(resolved.Generation),
|
||||||
WalletBalance: resolved.WalletBalance,
|
WalletBalance: resolved.WalletBalance,
|
||||||
|
ActivatedAt: resolved.Asset.ActivatedAt,
|
||||||
|
CurrentPackage: resolved.Asset.CurrentPackage,
|
||||||
|
PackageTotalMB: resolved.Asset.PackageTotalMB,
|
||||||
|
PackageUsedMB: resolved.Asset.PackageUsedMB,
|
||||||
|
PackageRemainMB: resolved.Asset.PackageRemainMB,
|
||||||
|
DeviceName: resolved.Asset.DeviceName,
|
||||||
|
IMEI: resolved.Asset.IMEI,
|
||||||
|
SN: resolved.Asset.SN,
|
||||||
|
DeviceModel: resolved.Asset.DeviceModel,
|
||||||
|
DeviceType: resolved.Asset.DeviceType,
|
||||||
|
Manufacturer: resolved.Asset.Manufacturer,
|
||||||
|
MaxSimSlots: resolved.Asset.MaxSimSlots,
|
||||||
|
BoundCardCount: resolved.Asset.BoundCardCount,
|
||||||
|
Cards: resolved.Asset.Cards,
|
||||||
|
DeviceProtectStatus: resolved.Asset.DeviceProtectStatus,
|
||||||
|
ICCID: resolved.Asset.ICCID,
|
||||||
|
MSISDN: resolved.Asset.MSISDN,
|
||||||
|
CarrierID: resolved.Asset.CarrierID,
|
||||||
|
CarrierType: resolved.Asset.CarrierType,
|
||||||
|
NetworkStatus: resolved.Asset.NetworkStatus,
|
||||||
|
ActivationStatus: resolved.Asset.ActivationStatus,
|
||||||
|
CardCategory: resolved.Asset.CardCategory,
|
||||||
|
BoundDeviceID: resolved.Asset.BoundDeviceID,
|
||||||
|
BoundDeviceNo: resolved.Asset.BoundDeviceNo,
|
||||||
|
BoundDeviceName: resolved.Asset.BoundDeviceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Gateway 同步接口对接后,替换为真实设备实时数据
|
||||||
|
if resp.AssetType == "device" {
|
||||||
|
resp.DeviceRealtime = buildMockDeviceRealtime()
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Success(c, resp)
|
return response.Success(c, resp)
|
||||||
@@ -270,6 +300,13 @@ func (h *ClientAssetHandler) GetPackageHistory(c *fiber.Ctx) error {
|
|||||||
} else {
|
} else {
|
||||||
query = query.Where("device_id = ?", resolved.Asset.AssetID)
|
query = query.Where("device_id = ?", resolved.Asset.AssetID)
|
||||||
}
|
}
|
||||||
|
if req.Status != nil {
|
||||||
|
query = query.Where("status = ?", *req.Status)
|
||||||
|
}
|
||||||
|
if req.PackageType != nil {
|
||||||
|
query = query.Where("package_id IN (?)",
|
||||||
|
h.db.Model(&model.Package{}).Select("id").Where("package_type = ?", *req.PackageType))
|
||||||
|
}
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
if err := query.Count(&total).Error; err != nil {
|
if err := query.Count(&total).Error; err != nil {
|
||||||
@@ -554,3 +591,45 @@ func packageStatusName(status int) string {
|
|||||||
return "未知"
|
return "未知"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildMockDeviceRealtime 构建设备实时状态假数据
|
||||||
|
// TODO: Gateway 同步接口对接后移除此函数,改为调用 Gateway 接口获取真实数据
|
||||||
|
func buildMockDeviceRealtime() *dto.DeviceRealtimeInfo {
|
||||||
|
onlineStatus := int64(1)
|
||||||
|
batteryLevel := int64(85)
|
||||||
|
deviceStatus := int64(1)
|
||||||
|
runTime := "3600"
|
||||||
|
connectTime := "3500"
|
||||||
|
rsrp := int64(-80)
|
||||||
|
rsrq := int64(-10)
|
||||||
|
rssi := "-65"
|
||||||
|
sinr := int64(15)
|
||||||
|
ssid := "JunHong-WiFi"
|
||||||
|
wifiEnabled := true
|
||||||
|
wifiPassword := "12345678"
|
||||||
|
ipAddress := "192.168.1.1"
|
||||||
|
lanIP := "192.168.1.1"
|
||||||
|
dailyUsage := "0"
|
||||||
|
maxClients := int64(32)
|
||||||
|
switchMode := 0
|
||||||
|
|
||||||
|
return &dto.DeviceRealtimeInfo{
|
||||||
|
OnlineStatus: &onlineStatus,
|
||||||
|
BatteryLevel: &batteryLevel,
|
||||||
|
Status: &deviceStatus,
|
||||||
|
RunTime: &runTime,
|
||||||
|
ConnectTime: &connectTime,
|
||||||
|
Rsrp: &rsrp,
|
||||||
|
Rsrq: &rsrq,
|
||||||
|
Rssi: &rssi,
|
||||||
|
Sinr: &sinr,
|
||||||
|
SSID: &ssid,
|
||||||
|
WifiEnabled: &wifiEnabled,
|
||||||
|
WifiPassword: &wifiPassword,
|
||||||
|
IPAddress: &ipAddress,
|
||||||
|
LANIP: &lanIP,
|
||||||
|
DailyUsage: &dailyUsage,
|
||||||
|
MaxClients: &maxClients,
|
||||||
|
SwitchMode: &switchMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// B1 资产信息
|
// B1 资产信息
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -10,16 +12,107 @@ type AssetInfoRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AssetInfoResponse B1 资产信息响应
|
// AssetInfoResponse B1 资产信息响应
|
||||||
|
// 根据 asset_type 不同,设备专属字段或卡专属字段会分别填充(另一侧为零值/omit)
|
||||||
type AssetInfoResponse struct {
|
type AssetInfoResponse struct {
|
||||||
AssetType string `json:"asset_type" description:"资产类型 (card:卡, device:设备)"`
|
// === 基础信息(通用) ===
|
||||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
AssetType string `json:"asset_type" description:"资产类型(card:卡, device:设备)"`
|
||||||
Identifier string `json:"identifier" description:"资产标识符"`
|
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||||
VirtualNo string `json:"virtual_no" description:"虚拟号"`
|
Identifier string `json:"identifier" description:"资产标识符"`
|
||||||
Status int `json:"status" description:"状态 (0:禁用, 1:启用)"`
|
VirtualNo string `json:"virtual_no" description:"虚拟号"`
|
||||||
RealNameStatus int `json:"real_name_status" description:"实名状态 (0:未实名, 1:已实名)"`
|
Status int `json:"status" description:"状态(1:在库, 2:已分销, 3:已激活, 4:已停用)"`
|
||||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
RealNameStatus int `json:"real_name_status" description:"实名状态(0:未实名, 1:已实名)"`
|
||||||
Generation string `json:"generation" description:"制式"`
|
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||||
WalletBalance int64 `json:"wallet_balance" 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:"当前套餐名称(无套餐时为空)"`
|
||||||
|
PackageTotalMB int64 `json:"package_total_mb" description:"当前套餐总虚流量(MB),已按虚流量比例换算"`
|
||||||
|
PackageUsedMB float64 `json:"package_used_mb" description:"当前已用虚流量(MB),已按虚流量比例换算"`
|
||||||
|
PackageRemainMB float64 `json:"package_remain_mb" description:"当前剩余虚流量(MB),已按虚流量比例换算"`
|
||||||
|
|
||||||
|
// === 设备专属字段(asset_type=device 时有效) ===
|
||||||
|
DeviceName string `json:"device_name,omitempty" description:"设备名称"`
|
||||||
|
IMEI string `json:"imei,omitempty" description:"设备IMEI"`
|
||||||
|
SN string `json:"sn,omitempty" description:"设备序列号"`
|
||||||
|
DeviceModel string `json:"device_model,omitempty" description:"设备型号"`
|
||||||
|
DeviceType string `json:"device_type,omitempty" description:"设备类型"`
|
||||||
|
Manufacturer string `json:"manufacturer,omitempty" description:"制造商"`
|
||||||
|
MaxSimSlots int `json:"max_sim_slots,omitempty" description:"最大插槽数"`
|
||||||
|
BoundCardCount int `json:"bound_card_count,omitempty" description:"绑定卡数量"`
|
||||||
|
Cards []BoundCardInfo `json:"cards,omitempty" description:"绑定卡列表(含每张卡的ICCID/MSISDN/网络状态/实名状态/插槽位置)"`
|
||||||
|
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:行业卡)"`
|
||||||
|
|
||||||
|
// === 卡绑定设备信息(asset_type=card 且绑定了设备时有效) ===
|
||||||
|
BoundDeviceID *uint `json:"bound_device_id,omitempty" description:"绑定的设备ID"`
|
||||||
|
BoundDeviceNo string `json:"bound_device_no,omitempty" description:"绑定的设备虚拟号"`
|
||||||
|
BoundDeviceName string `json:"bound_device_name,omitempty" description:"绑定的设备名称"`
|
||||||
|
|
||||||
|
// === 设备实时状态(来自 Gateway,同步接口对接后自动填充,当前返回 null) ===
|
||||||
|
DeviceRealtime *DeviceRealtimeInfo `json:"device_realtime,omitempty" description:"设备实时状态(Gateway 同步接口对接后填充,当前为 null)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceRealtimeInfo 设备实时状态信息
|
||||||
|
// 全量映射 Gateway DeviceInfoDetail 结构,所有字段均为可选
|
||||||
|
// 当前 Gateway 同步接口尚未对接,预留结构待后续填充
|
||||||
|
type DeviceRealtimeInfo struct {
|
||||||
|
// === 设备状态 ===
|
||||||
|
OnlineStatus *int64 `json:"online_status,omitempty" description:"在线状态(1:在线, 2:离线)"`
|
||||||
|
BatteryLevel *int64 `json:"battery_level,omitempty" description:"电池电量百分比"`
|
||||||
|
Status *int64 `json:"status,omitempty" description:"设备状态(1:正常, 0:禁用)"`
|
||||||
|
RunTime *string `json:"run_time,omitempty" description:"设备本次开机运行时间(秒)"`
|
||||||
|
ConnectTime *string `json:"connect_time,omitempty" description:"设备本次联网时间(秒)"`
|
||||||
|
LastOnlineTime *string `json:"last_online_time,omitempty" description:"设备最后在线时间"`
|
||||||
|
LastUpdateTime *string `json:"last_update_time,omitempty" description:"设备信息最后更新时间"`
|
||||||
|
|
||||||
|
// === 信号相关 ===
|
||||||
|
Rsrp *int64 `json:"rsrp,omitempty" description:"参考信号接收功率(dBm)"`
|
||||||
|
Rsrq *int64 `json:"rsrq,omitempty" description:"参考信号接收质量(dB)"`
|
||||||
|
Rssi *string `json:"rssi,omitempty" description:"接收信号强度"`
|
||||||
|
Sinr *int64 `json:"sinr,omitempty" description:"信噪比(dB)"`
|
||||||
|
|
||||||
|
// === WiFi 相关 ===
|
||||||
|
SSID *string `json:"ssid,omitempty" description:"WiFi热点名称"`
|
||||||
|
WifiEnabled *bool `json:"wifi_enabled,omitempty" description:"WiFi开关状态"`
|
||||||
|
WifiPassword *string `json:"wifi_password,omitempty" description:"WiFi密码"`
|
||||||
|
|
||||||
|
// === 网络相关 ===
|
||||||
|
IPAddress *string `json:"ip_address,omitempty" description:"IP地址"`
|
||||||
|
WANIP *string `json:"wan_ip,omitempty" description:"基站分配IPv4地址"`
|
||||||
|
LANIP *string `json:"lan_ip,omitempty" description:"局域网网关IP地址"`
|
||||||
|
MACAddress *string `json:"mac_address,omitempty" description:"MAC地址"`
|
||||||
|
|
||||||
|
// === 流量与速率 ===
|
||||||
|
DailyUsage *string `json:"daily_usage,omitempty" description:"日使用流量(字节)"`
|
||||||
|
DLStats *string `json:"dl_stats,omitempty" description:"本次开机下载流量(字节)"`
|
||||||
|
ULStats *string `json:"ul_stats,omitempty" description:"本次开机上传流量(字节)"`
|
||||||
|
LimitSpeed *int64 `json:"limit_speed,omitempty" description:"限速速率(KB/s)"`
|
||||||
|
|
||||||
|
// === 设备属性 ===
|
||||||
|
CurrentIccid *string `json:"current_iccid,omitempty" description:"当前使用的ICCID"`
|
||||||
|
MaxClients *int64 `json:"max_clients,omitempty" description:"最大连接客户端数"`
|
||||||
|
SoftwareVersion *string `json:"software_version,omitempty" description:"软件版本号"`
|
||||||
|
SwitchMode *int `json:"switch_mode,omitempty" description:"切卡模式(0:自动, 1:手动)"`
|
||||||
|
SyncInterval *int64 `json:"sync_interval,omitempty" description:"信息上报周期(秒)"`
|
||||||
|
|
||||||
|
// === Gateway 原始标识字段 ===
|
||||||
|
DeviceID *string `json:"device_id,omitempty" description:"Gateway设备ID(IMEI/SN)"`
|
||||||
|
DeviceName *string `json:"device_name,omitempty" description:"Gateway返回的设备名称"`
|
||||||
|
DeviceType *string `json:"device_type,omitempty" description:"Gateway返回的设备型号"`
|
||||||
|
Imei *string `json:"imei,omitempty" description:"Gateway返回的IMEI号"`
|
||||||
|
Imsi *string `json:"imsi,omitempty" description:"Gateway返回的IMSI"`
|
||||||
|
CreatedAt *int64 `json:"created_at,omitempty" description:"Gateway创建时间(Unix时间戳)"`
|
||||||
|
UpdatedAt *int64 `json:"updated_at,omitempty" description:"Gateway更新时间(Unix时间戳)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -56,9 +149,11 @@ type AssetPackageListResponse struct {
|
|||||||
|
|
||||||
// AssetPackageHistoryRequest B3 资产套餐历史请求
|
// AssetPackageHistoryRequest B3 资产套餐历史请求
|
||||||
type AssetPackageHistoryRequest struct {
|
type AssetPackageHistoryRequest struct {
|
||||||
Identifier string `json:"identifier" query:"identifier" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN)"`
|
Identifier string `json:"identifier" query:"identifier" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN)"`
|
||||||
Page int `json:"page" query:"page" validate:"required,min=1" required:"true" minimum:"1" description:"页码"`
|
PackageType *string `json:"package_type" query:"package_type" validate:"omitempty,oneof=formal addon" description:"套餐类型 (formal:正式套餐, addon:加油包)"`
|
||||||
PageSize int `json:"page_size" query:"page_size" validate:"required,min=1,max=100" required:"true" minimum:"1" maximum:"100" description:"每页数量"`
|
Status *int `json:"status" query:"status" validate:"omitempty,min=0,max=4" minimum:"0" maximum:"4" description:"套餐状态 (0:待生效, 1:生效中, 2:已用完, 3:已过期, 4:已失效)"`
|
||||||
|
Page int `json:"page" query:"page" validate:"required,min=1" required:"true" minimum:"1" description:"页码"`
|
||||||
|
PageSize int `json:"page_size" query:"page_size" validate:"required,min=1,max=100" required:"true" minimum:"1" maximum:"100" description:"每页数量"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssetPackageHistoryResponse B3 资产套餐历史响应
|
// AssetPackageHistoryResponse B3 资产套餐历史响应
|
||||||
|
|||||||
@@ -12,13 +12,16 @@ import (
|
|||||||
|
|
||||||
// RegisterPersonalCustomerRoutes 注册个人客户路由
|
// RegisterPersonalCustomerRoutes 注册个人客户路由
|
||||||
// 路由挂载在 /api/c/v1 下
|
// 路由挂载在 /api/c/v1 下
|
||||||
|
//
|
||||||
|
// 重要:Fiber 的 Group.Use() 会在路由表中注册全局 USE 处理器,
|
||||||
|
// 匹配该前缀下的所有请求(不区分 Group 对象)。
|
||||||
|
// 因此公开路由必须在任何 Use() 调用之前注册,利用 Fiber 按注册顺序匹配的机制,
|
||||||
|
// 确保公开路由优先命中并直接返回,不会被后续的认证中间件拦截。
|
||||||
func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator, basePath string, handlers *bootstrap.Handlers, personalAuthMiddleware *middleware.PersonalAuthMiddleware) {
|
func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator, basePath string, handlers *bootstrap.Handlers, personalAuthMiddleware *middleware.PersonalAuthMiddleware) {
|
||||||
authBasePath := "/auth"
|
authBasePath := "/auth"
|
||||||
authPublicGroup := router.Group(authBasePath)
|
|
||||||
authProtectedGroup := router.Group(authBasePath)
|
|
||||||
authProtectedGroup.Use(personalAuthMiddleware.Authenticate())
|
|
||||||
|
|
||||||
Register(authPublicGroup, doc, basePath+authBasePath, "POST", "/verify-asset", handlers.ClientAuth.VerifyAsset, RouteSpec{
|
// === 公开路由(无需认证)===
|
||||||
|
Register(router, doc, basePath, "POST", authBasePath+"/verify-asset", handlers.ClientAuth.VerifyAsset, RouteSpec{
|
||||||
Summary: "资产验证",
|
Summary: "资产验证",
|
||||||
Tags: []string{"个人客户 - 认证"},
|
Tags: []string{"个人客户 - 认证"},
|
||||||
Auth: false,
|
Auth: false,
|
||||||
@@ -26,7 +29,7 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
|||||||
Output: &dto.VerifyAssetResponse{},
|
Output: &dto.VerifyAssetResponse{},
|
||||||
})
|
})
|
||||||
|
|
||||||
Register(authPublicGroup, doc, basePath+authBasePath, "POST", "/wechat-login", handlers.ClientAuth.WechatLogin, RouteSpec{
|
Register(router, doc, basePath, "POST", authBasePath+"/wechat-login", handlers.ClientAuth.WechatLogin, RouteSpec{
|
||||||
Summary: "公众号登录",
|
Summary: "公众号登录",
|
||||||
Tags: []string{"个人客户 - 认证"},
|
Tags: []string{"个人客户 - 认证"},
|
||||||
Auth: false,
|
Auth: false,
|
||||||
@@ -34,7 +37,7 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
|||||||
Output: &dto.WechatLoginResponse{},
|
Output: &dto.WechatLoginResponse{},
|
||||||
})
|
})
|
||||||
|
|
||||||
Register(authPublicGroup, doc, basePath+authBasePath, "POST", "/miniapp-login", handlers.ClientAuth.MiniappLogin, RouteSpec{
|
Register(router, doc, basePath, "POST", authBasePath+"/miniapp-login", handlers.ClientAuth.MiniappLogin, RouteSpec{
|
||||||
Summary: "小程序登录",
|
Summary: "小程序登录",
|
||||||
Tags: []string{"个人客户 - 认证"},
|
Tags: []string{"个人客户 - 认证"},
|
||||||
Auth: false,
|
Auth: false,
|
||||||
@@ -42,7 +45,7 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
|||||||
Output: &dto.WechatLoginResponse{},
|
Output: &dto.WechatLoginResponse{},
|
||||||
})
|
})
|
||||||
|
|
||||||
Register(authPublicGroup, doc, basePath+authBasePath, "POST", "/send-code", handlers.ClientAuth.SendCode, RouteSpec{
|
Register(router, doc, basePath, "POST", authBasePath+"/send-code", handlers.ClientAuth.SendCode, RouteSpec{
|
||||||
Summary: "发送验证码",
|
Summary: "发送验证码",
|
||||||
Tags: []string{"个人客户 - 认证"},
|
Tags: []string{"个人客户 - 认证"},
|
||||||
Auth: false,
|
Auth: false,
|
||||||
@@ -50,6 +53,10 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
|||||||
Output: &dto.ClientSendCodeResponse{},
|
Output: &dto.ClientSendCodeResponse{},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// === 需要认证的 auth 路由 ===
|
||||||
|
authProtectedGroup := router.Group(authBasePath)
|
||||||
|
authProtectedGroup.Use(personalAuthMiddleware.Authenticate())
|
||||||
|
|
||||||
Register(authProtectedGroup, doc, basePath+authBasePath, "POST", "/bind-phone", handlers.ClientAuth.BindPhone, RouteSpec{
|
Register(authProtectedGroup, doc, basePath+authBasePath, "POST", "/bind-phone", handlers.ClientAuth.BindPhone, RouteSpec{
|
||||||
Summary: "绑定手机号",
|
Summary: "绑定手机号",
|
||||||
Tags: []string{"个人客户 - 认证"},
|
Tags: []string{"个人客户 - 认证"},
|
||||||
|
|||||||
@@ -640,7 +640,7 @@ func (s *Service) resolveAssetBindingKey(ctx context.Context, tx *gorm.DB, asset
|
|||||||
}
|
}
|
||||||
return "", errors.Wrap(errors.CodeInternalError, err, "查询卡资产失败")
|
return "", errors.Wrap(errors.CodeInternalError, err, "查询卡资产失败")
|
||||||
}
|
}
|
||||||
return card.ICCID, nil
|
return card.VirtualNo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if assetType == assetTypeDevice {
|
if assetType == assetTypeDevice {
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
// Package customer 提供客户管理的业务逻辑服务
|
|
||||||
// 包含客户信息管理、客户查询等功能
|
|
||||||
package customer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service 个人客户业务服务
|
|
||||||
type Service struct {
|
|
||||||
customerStore *postgres.PersonalCustomerStore
|
|
||||||
}
|
|
||||||
|
|
||||||
// New 创建个人客户服务
|
|
||||||
func New(customerStore *postgres.PersonalCustomerStore) *Service {
|
|
||||||
return &Service{
|
|
||||||
customerStore: customerStore,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create 创建个人客户
|
|
||||||
func (s *Service) Create(ctx context.Context, req *dto.CreatePersonalCustomerRequest) (*model.PersonalCustomer, error) {
|
|
||||||
// 检查手机号唯一性
|
|
||||||
if req.Phone != "" {
|
|
||||||
existing, err := s.customerStore.GetByPhone(ctx, req.Phone)
|
|
||||||
if err == nil && existing != nil {
|
|
||||||
return nil, errors.New(errors.CodeCustomerPhoneExists, "手机号已存在")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建个人客户
|
|
||||||
// 注意:根据新的数据模型,手机号应该存储在 PersonalCustomerPhone 表中
|
|
||||||
// 这里暂时先创建客户记录,手机号的存储后续通过 PersonalCustomerPhoneStore 实现
|
|
||||||
customer := &model.PersonalCustomer{
|
|
||||||
Nickname: req.Nickname,
|
|
||||||
AvatarURL: req.AvatarURL,
|
|
||||||
WxOpenID: req.WxOpenID,
|
|
||||||
WxUnionID: req.WxUnionID,
|
|
||||||
Status: constants.StatusEnabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.customerStore.Create(ctx, customer); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 创建 PersonalCustomerPhone 记录,需要通过 PersonalCustomerPhoneStore 创建手机号关联
|
|
||||||
|
|
||||||
return customer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update 更新个人客户信息
|
|
||||||
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePersonalCustomerRequest) (*model.PersonalCustomer, error) {
|
|
||||||
// 查询客户
|
|
||||||
customer, err := s.customerStore.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New(errors.CodeCustomerNotFound, "个人客户不存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 手机号的更新逻辑需要通过 PersonalCustomerPhoneStore 更新或创建手机号记录
|
|
||||||
|
|
||||||
// 更新字段
|
|
||||||
if req.Nickname != nil {
|
|
||||||
customer.Nickname = *req.Nickname
|
|
||||||
}
|
|
||||||
if req.AvatarURL != nil {
|
|
||||||
customer.AvatarURL = *req.AvatarURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.customerStore.Update(ctx, customer); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return customer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BindWeChat 绑定微信信息
|
|
||||||
func (s *Service) BindWeChat(ctx context.Context, id uint, wxOpenID, wxUnionID string) error {
|
|
||||||
customer, err := s.customerStore.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(errors.CodeCustomerNotFound, "个人客户不存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
customer.WxOpenID = wxOpenID
|
|
||||||
customer.WxUnionID = wxUnionID
|
|
||||||
|
|
||||||
return s.customerStore.Update(ctx, customer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByID 获取个人客户详情
|
|
||||||
func (s *Service) GetByID(ctx context.Context, id uint) (*model.PersonalCustomer, error) {
|
|
||||||
customer, err := s.customerStore.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New(errors.CodeCustomerNotFound, "个人客户不存在")
|
|
||||||
}
|
|
||||||
return customer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByPhone 根据手机号获取个人客户
|
|
||||||
func (s *Service) GetByPhone(ctx context.Context, phone string) (*model.PersonalCustomer, error) {
|
|
||||||
customer, err := s.customerStore.GetByPhone(ctx, phone)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New(errors.CodeCustomerNotFound, "个人客户不存在")
|
|
||||||
}
|
|
||||||
return customer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByWxOpenID 根据微信 OpenID 获取个人客户
|
|
||||||
func (s *Service) GetByWxOpenID(ctx context.Context, wxOpenID string) (*model.PersonalCustomer, error) {
|
|
||||||
customer, err := s.customerStore.GetByWxOpenID(ctx, wxOpenID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New(errors.CodeCustomerNotFound, "个人客户不存在")
|
|
||||||
}
|
|
||||||
return customer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List 查询个人客户列表
|
|
||||||
func (s *Service) List(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.PersonalCustomer, int64, error) {
|
|
||||||
return s.customerStore.List(ctx, opts, filters)
|
|
||||||
}
|
|
||||||
@@ -973,7 +973,7 @@ func (s *Service) StartDevice(ctx context.Context, deviceID uint) error {
|
|||||||
|
|
||||||
// 全部失败时返回 error
|
// 全部失败时返回 error
|
||||||
if successCount == 0 && lastErr != nil {
|
if successCount == 0 && lastErr != nil {
|
||||||
return errors.Wrap(errors.CodeInternalError, lastErr, "设备复机失败")
|
return errors.Wrap(errors.CodeGatewayError, lastErr, "设备复机失败,所有卡均复机失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -264,15 +264,18 @@ func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.stopCardWithRetry(ctx, card); err != nil {
|
if err := s.stopCardWithRetry(ctx, card); err != nil {
|
||||||
return errors.Wrap(errors.CodeInternalError, err, "调网关停机失败")
|
return errors.Wrap(errors.CodeGatewayError, err, "调用运营商停机失败,请稍后重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return s.db.WithContext(ctx).Model(card).Updates(map[string]any{
|
if err := s.db.WithContext(ctx).Model(card).Updates(map[string]any{
|
||||||
"network_status": constants.NetworkStatusOffline,
|
"network_status": constants.NetworkStatusOffline,
|
||||||
"stopped_at": now,
|
"stopped_at": now,
|
||||||
"stop_reason": constants.StopReasonManual,
|
"stop_reason": constants.StopReasonManual,
|
||||||
}).Error
|
}).Error; err != nil {
|
||||||
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManualStartCard 手动复机单张卡(通过ICCID)
|
// ManualStartCard 手动复机单张卡(通过ICCID)
|
||||||
@@ -300,13 +303,16 @@ func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.resumeCardWithRetry(ctx, card); err != nil {
|
if err := s.resumeCardWithRetry(ctx, card); err != nil {
|
||||||
return errors.Wrap(errors.CodeInternalError, err, "调网关复机失败")
|
return errors.Wrap(errors.CodeGatewayError, err, "调用运营商复机失败,请稍后重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return s.db.WithContext(ctx).Model(card).Updates(map[string]any{
|
if err := s.db.WithContext(ctx).Model(card).Updates(map[string]any{
|
||||||
"network_status": constants.NetworkStatusOnline,
|
"network_status": constants.NetworkStatusOnline,
|
||||||
"resumed_at": now,
|
"resumed_at": now,
|
||||||
"stop_reason": "",
|
"stop_reason": "",
|
||||||
}).Error
|
}).Error; err != nil {
|
||||||
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
// Package personal_customer 提供个人客户管理的业务逻辑服务
|
// Package personal_customer 提供个人客户资料管理的业务逻辑服务
|
||||||
// 包含个人客户注册、登录、微信绑定、短信验证等功能
|
|
||||||
package personal_customer
|
package personal_customer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/service/verification"
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/auth"
|
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -16,40 +13,24 @@ import (
|
|||||||
|
|
||||||
// Service 个人客户服务
|
// Service 个人客户服务
|
||||||
type Service struct {
|
type Service struct {
|
||||||
store *postgres.PersonalCustomerStore
|
store *postgres.PersonalCustomerStore
|
||||||
phoneStore *postgres.PersonalCustomerPhoneStore
|
phoneStore *postgres.PersonalCustomerPhoneStore
|
||||||
verificationService *verification.Service
|
logger *zap.Logger
|
||||||
jwtManager *auth.JWTManager
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService 创建个人客户服务实例
|
// NewService 创建个人客户服务实例
|
||||||
func NewService(
|
func NewService(
|
||||||
store *postgres.PersonalCustomerStore,
|
store *postgres.PersonalCustomerStore,
|
||||||
phoneStore *postgres.PersonalCustomerPhoneStore,
|
phoneStore *postgres.PersonalCustomerPhoneStore,
|
||||||
verificationService *verification.Service,
|
|
||||||
jwtManager *auth.JWTManager,
|
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
store: store,
|
store: store,
|
||||||
phoneStore: phoneStore,
|
phoneStore: phoneStore,
|
||||||
verificationService: verificationService,
|
logger: logger,
|
||||||
jwtManager: jwtManager,
|
|
||||||
logger: logger,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendVerificationCode 发送验证码
|
|
||||||
func (s *Service) SendVerificationCode(ctx context.Context, phone string) error {
|
|
||||||
return s.verificationService.SendCode(ctx, phone)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyCode 验证验证码
|
|
||||||
func (s *Service) VerifyCode(ctx context.Context, phone string, code string) error {
|
|
||||||
return s.verificationService.VerifyCode(ctx, phone, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateProfile 更新个人资料
|
// UpdateProfile 更新个人资料
|
||||||
func (s *Service) UpdateProfile(ctx context.Context, customerID uint, nickname, avatarURL string) error {
|
func (s *Service) UpdateProfile(ctx context.Context, customerID uint, nickname, avatarURL string) error {
|
||||||
customer, err := s.store.GetByID(ctx, customerID)
|
customer, err := s.store.GetByID(ctx, customerID)
|
||||||
@@ -84,20 +65,6 @@ func (s *Service) UpdateProfile(ctx context.Context, customerID uint, nickname,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProfile 获取个人资料
|
|
||||||
func (s *Service) GetProfile(ctx context.Context, customerID uint) (*model.PersonalCustomer, error) {
|
|
||||||
customer, err := s.store.GetByID(ctx, customerID)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("查询个人客户失败",
|
|
||||||
zap.Uint("customer_id", customerID),
|
|
||||||
zap.Error(err),
|
|
||||||
)
|
|
||||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
return customer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProfileWithPhone 获取个人资料(包含主手机号)
|
// GetProfileWithPhone 获取个人资料(包含主手机号)
|
||||||
func (s *Service) GetProfileWithPhone(ctx context.Context, customerID uint) (*model.PersonalCustomer, string, error) {
|
func (s *Service) GetProfileWithPhone(ctx context.Context, customerID uint) (*model.PersonalCustomer, string, error) {
|
||||||
// 获取客户信息
|
// 获取客户信息
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (s *PersonalCustomerDeviceStore) GetByCustomerID(ctx context.Context, custo
|
|||||||
func (s *PersonalCustomerDeviceStore) GetByDeviceNo(ctx context.Context, deviceNo string) ([]*model.PersonalCustomerDevice, error) {
|
func (s *PersonalCustomerDeviceStore) GetByDeviceNo(ctx context.Context, deviceNo string) ([]*model.PersonalCustomerDevice, error) {
|
||||||
var records []*model.PersonalCustomerDevice
|
var records []*model.PersonalCustomerDevice
|
||||||
if err := s.db.WithContext(ctx).
|
if err := s.db.WithContext(ctx).
|
||||||
Where("device_no = ?", deviceNo).
|
Where("virtual_no = ?", deviceNo).
|
||||||
Order("last_used_at DESC").
|
Order("last_used_at DESC").
|
||||||
Find(&records).Error; err != nil {
|
Find(&records).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -56,7 +56,7 @@ func (s *PersonalCustomerDeviceStore) GetByDeviceNo(ctx context.Context, deviceN
|
|||||||
func (s *PersonalCustomerDeviceStore) GetByCustomerAndDevice(ctx context.Context, customerID uint, deviceNo string) (*model.PersonalCustomerDevice, error) {
|
func (s *PersonalCustomerDeviceStore) GetByCustomerAndDevice(ctx context.Context, customerID uint, deviceNo string) (*model.PersonalCustomerDevice, error) {
|
||||||
var record model.PersonalCustomerDevice
|
var record model.PersonalCustomerDevice
|
||||||
if err := s.db.WithContext(ctx).
|
if err := s.db.WithContext(ctx).
|
||||||
Where("customer_id = ? AND device_no = ?", customerID, deviceNo).
|
Where("customer_id = ? AND virtual_no = ?", customerID, deviceNo).
|
||||||
First(&record).Error; err != nil {
|
First(&record).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ func (s *PersonalCustomerDeviceStore) ExistsByCustomerAndDevice(ctx context.Cont
|
|||||||
var count int64
|
var count int64
|
||||||
if err := s.db.WithContext(ctx).
|
if err := s.db.WithContext(ctx).
|
||||||
Model(&model.PersonalCustomerDevice{}).
|
Model(&model.PersonalCustomerDevice{}).
|
||||||
Where("customer_id = ? AND device_no = ? AND status = ?", customerID, deviceNo, 1).
|
Where("customer_id = ? AND virtual_no = ? AND status = ?", customerID, deviceNo, 1).
|
||||||
Count(&count).Error; err != nil {
|
Count(&count).Error; err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- 回滚:将已修复的 VirtualNo 还原为 ICCID
|
||||||
|
-- 注意:只还原那些 virtual_no 能匹配到 iot_card.virtual_no 的记录
|
||||||
|
UPDATE tb_personal_customer_device pcd
|
||||||
|
SET virtual_no = ic.iccid
|
||||||
|
FROM tb_iot_card ic
|
||||||
|
WHERE pcd.virtual_no = ic.virtual_no
|
||||||
|
AND pcd.deleted_at IS NULL
|
||||||
|
AND ic.virtual_no != ic.iccid;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- 修复个人客户设备绑定表中卡类型资产的绑定键
|
||||||
|
-- 问题:resolveAssetBindingKey 对卡类型错误地使用了 card.ICCID,应使用 card.VirtualNo
|
||||||
|
-- 影响:所有通过卡 ICCID 登录的个人客户绑定记录的 virtual_no 字段存的是 ICCID 而非资产虚拟号
|
||||||
|
-- 导致:归属校验 isCustomerOwnAsset 比对 VirtualNo 时永远不匹配,返回 403
|
||||||
|
UPDATE tb_personal_customer_device pcd
|
||||||
|
SET virtual_no = ic.virtual_no
|
||||||
|
FROM tb_iot_card ic
|
||||||
|
WHERE pcd.virtual_no = ic.iccid
|
||||||
|
AND pcd.deleted_at IS NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE tb_asset_recharge_record RENAME COLUMN asset_wallet_id TO card_wallet_id;
|
||||||
|
ALTER TABLE tb_asset_wallet_transaction RENAME COLUMN asset_wallet_id TO card_wallet_id;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- 修复迁移 000076 遗漏:表名已从 card_wallet 改为 asset_wallet,但列名 card_wallet_id 未同步更名
|
||||||
|
-- 导致 Model 中 column:asset_wallet_id 与数据库实际列名 card_wallet_id 不匹配,INSERT/SELECT 均失败
|
||||||
|
|
||||||
|
-- tb_asset_recharge_record: card_wallet_id → asset_wallet_id
|
||||||
|
ALTER TABLE tb_asset_recharge_record RENAME COLUMN card_wallet_id TO asset_wallet_id;
|
||||||
|
|
||||||
|
-- tb_asset_wallet_transaction: card_wallet_id → asset_wallet_id
|
||||||
|
ALTER TABLE tb_asset_wallet_transaction RENAME COLUMN card_wallet_id TO asset_wallet_id;
|
||||||
@@ -80,7 +80,7 @@ func (c *Client) SendMessage(ctx context.Context, content string, phones []strin
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
url := c.gatewayURL + "/api/sendMessageMass"
|
url := c.gatewayURL + "/sms/api/sendMessageMass"
|
||||||
|
|
||||||
// 创建带超时的上下文
|
// 创建带超时的上下文
|
||||||
reqCtx, cancel := context.WithTimeout(ctx, c.timeout)
|
reqCtx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||||
|
|||||||
Reference in New Issue
Block a user