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/logger"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/sms"
|
||||
"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
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if cfg.Storage.Provider == "" || cfg.Storage.S3.Endpoint == "" {
|
||||
appLogger.Info("对象存储未配置,跳过初始化")
|
||||
|
||||
@@ -67,6 +67,11 @@ services:
|
||||
- JUNHONG_GATEWAY_APP_ID=LfjL0WjUqpwkItQ0
|
||||
- JUNHONG_GATEWAY_APP_SECRET=K0DYuWzbRE6zg5bX
|
||||
- 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:
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
|
||||
@@ -110,7 +110,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
AccountAudit: accountAudit,
|
||||
Role: roleSvc.New(s.Role, s.Permission, s.RolePermission),
|
||||
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(
|
||||
deps.DB,
|
||||
s.PersonalCustomerOpenID,
|
||||
|
||||
@@ -158,6 +158,36 @@ func (h *ClientAssetHandler) GetAssetInfo(c *fiber.Ctx) error {
|
||||
CarrierName: resolved.Asset.CarrierName,
|
||||
Generation: strconv.Itoa(resolved.Generation),
|
||||
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)
|
||||
@@ -270,6 +300,13 @@ func (h *ClientAssetHandler) GetPackageHistory(c *fiber.Ctx) error {
|
||||
} else {
|
||||
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
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
@@ -554,3 +591,45 @@ func packageStatusName(status int) string {
|
||||
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
|
||||
|
||||
import "time"
|
||||
|
||||
// ========================================
|
||||
// B1 资产信息
|
||||
// ========================================
|
||||
@@ -10,16 +12,107 @@ type AssetInfoRequest struct {
|
||||
}
|
||||
|
||||
// AssetInfoResponse B1 资产信息响应
|
||||
// 根据 asset_type 不同,设备专属字段或卡专属字段会分别填充(另一侧为零值/omit)
|
||||
type AssetInfoResponse struct {
|
||||
AssetType string `json:"asset_type" description:"资产类型 (card:卡, device:设备)"`
|
||||
// === 基础信息(通用) ===
|
||||
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:"状态 (0:禁用, 1:启用)"`
|
||||
RealNameStatus int `json:"real_name_status" description:"实名状态 (0:未实名, 1:已实名)"`
|
||||
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:"制式"`
|
||||
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时间戳)"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -57,6 +150,8 @@ type AssetPackageListResponse struct {
|
||||
// AssetPackageHistoryRequest B3 资产套餐历史请求
|
||||
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)"`
|
||||
PackageType *string `json:"package_type" query:"package_type" validate:"omitempty,oneof=formal addon" description:"套餐类型 (formal:正式套餐, addon:加油包)"`
|
||||
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:"每页数量"`
|
||||
}
|
||||
|
||||
@@ -12,13 +12,16 @@ import (
|
||||
|
||||
// RegisterPersonalCustomerRoutes 注册个人客户路由
|
||||
// 路由挂载在 /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) {
|
||||
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: "资产验证",
|
||||
Tags: []string{"个人客户 - 认证"},
|
||||
Auth: false,
|
||||
@@ -26,7 +29,7 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
||||
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: "公众号登录",
|
||||
Tags: []string{"个人客户 - 认证"},
|
||||
Auth: false,
|
||||
@@ -34,7 +37,7 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
||||
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: "小程序登录",
|
||||
Tags: []string{"个人客户 - 认证"},
|
||||
Auth: false,
|
||||
@@ -42,7 +45,7 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
||||
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: "发送验证码",
|
||||
Tags: []string{"个人客户 - 认证"},
|
||||
Auth: false,
|
||||
@@ -50,6 +53,10 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
||||
Output: &dto.ClientSendCodeResponse{},
|
||||
})
|
||||
|
||||
// === 需要认证的 auth 路由 ===
|
||||
authProtectedGroup := router.Group(authBasePath)
|
||||
authProtectedGroup.Use(personalAuthMiddleware.Authenticate())
|
||||
|
||||
Register(authProtectedGroup, doc, basePath+authBasePath, "POST", "/bind-phone", handlers.ClientAuth.BindPhone, RouteSpec{
|
||||
Summary: "绑定手机号",
|
||||
Tags: []string{"个人客户 - 认证"},
|
||||
|
||||
@@ -640,7 +640,7 @@ func (s *Service) resolveAssetBindingKey(ctx context.Context, tx *gorm.DB, asset
|
||||
}
|
||||
return "", errors.Wrap(errors.CodeInternalError, err, "查询卡资产失败")
|
||||
}
|
||||
return card.ICCID, nil
|
||||
return card.VirtualNo, nil
|
||||
}
|
||||
|
||||
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
|
||||
if successCount == 0 && lastErr != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, lastErr, "设备复机失败")
|
||||
return errors.Wrap(errors.CodeGatewayError, lastErr, "设备复机失败,所有卡均复机失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -264,15 +264,18 @@ func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) er
|
||||
}
|
||||
|
||||
if err := s.stopCardWithRetry(ctx, card); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "调网关停机失败")
|
||||
return errors.Wrap(errors.CodeGatewayError, err, "调用运营商停机失败,请稍后重试")
|
||||
}
|
||||
|
||||
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,
|
||||
"stopped_at": now,
|
||||
"stop_reason": constants.StopReasonManual,
|
||||
}).Error
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ManualStartCard 手动复机单张卡(通过ICCID)
|
||||
@@ -300,13 +303,16 @@ func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) e
|
||||
}
|
||||
|
||||
if err := s.resumeCardWithRetry(ctx, card); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "调网关复机失败")
|
||||
return errors.Wrap(errors.CodeGatewayError, err, "调用运营商复机失败,请稍后重试")
|
||||
}
|
||||
|
||||
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,
|
||||
"resumed_at": now,
|
||||
"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
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/pkg/auth"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
@@ -18,8 +15,6 @@ import (
|
||||
type Service struct {
|
||||
store *postgres.PersonalCustomerStore
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore
|
||||
verificationService *verification.Service
|
||||
jwtManager *auth.JWTManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -27,29 +22,15 @@ type Service struct {
|
||||
func NewService(
|
||||
store *postgres.PersonalCustomerStore,
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore,
|
||||
verificationService *verification.Service,
|
||||
jwtManager *auth.JWTManager,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
phoneStore: phoneStore,
|
||||
verificationService: verificationService,
|
||||
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 更新个人资料
|
||||
func (s *Service) UpdateProfile(ctx context.Context, customerID uint, nickname, avatarURL string) error {
|
||||
customer, err := s.store.GetByID(ctx, customerID)
|
||||
@@ -84,20 +65,6 @@ func (s *Service) UpdateProfile(ctx context.Context, customerID uint, nickname,
|
||||
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 获取个人资料(包含主手机号)
|
||||
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) {
|
||||
var records []*model.PersonalCustomerDevice
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("device_no = ?", deviceNo).
|
||||
Where("virtual_no = ?", deviceNo).
|
||||
Order("last_used_at DESC").
|
||||
Find(&records).Error; err != nil {
|
||||
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) {
|
||||
var record model.PersonalCustomerDevice
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func (s *PersonalCustomerDeviceStore) ExistsByCustomerAndDevice(ctx context.Cont
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).
|
||||
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 {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user