feat: 实现账号与佣金管理模块
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m35s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m35s
新增功能: - 店铺佣金查询:店铺佣金统计、店铺佣金记录列表、店铺提现记录 - 佣金提现审批:提现申请列表、审批通过、审批拒绝 - 提现配置管理:配置列表、新增配置、获取当前生效配置 - 企业管理:企业列表、创建、更新、删除、获取详情 - 企业卡授权:授权列表、批量授权、批量取消授权、统计 - 客户账号管理:账号列表、创建、更新状态、重置密码 - 我的佣金:佣金统计、佣金记录、提现申请、提现记录 数据库变更: - 扩展 tb_commission_withdrawal_request 新增提现单号等字段 - 扩展 tb_account 新增 is_primary 字段 - 扩展 tb_commission_record 新增 shop_id、balance_after - 扩展 tb_commission_withdrawal_setting 新增每日提现次数限制 - 扩展 tb_iot_card、tb_device 新增 shop_id 冗余字段 - 新建 tb_enterprise_card_authorization 企业卡授权表 - 新建 tb_asset_allocation_record 资产分配记录表 - 数据迁移:owner_type 枚举值 agent 统一为 shop 测试: - 新增 7 个单元测试文件覆盖各服务 - 修复集成测试 Redis 依赖问题
This commit is contained in:
440
internal/service/enterprise_card/service.go
Normal file
440
internal/service/enterprise_card/service.go
Normal file
@@ -0,0 +1,440 @@
|
||||
package enterprise_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
enterpriseStore *postgres.EnterpriseStore
|
||||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore
|
||||
}
|
||||
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
enterpriseStore *postgres.EnterpriseStore,
|
||||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
enterpriseStore: enterpriseStore,
|
||||
enterpriseCardAuthStore: enterpriseCardAuthStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, req *model.AllocateCardsPreviewReq) (*model.AllocateCardsPreviewResp, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
var iotCards []model.IotCard
|
||||
if err := s.db.WithContext(ctx).Where("iccid IN ?", req.ICCIDs).Find(&iotCards).Error; err != nil {
|
||||
return nil, fmt.Errorf("查询卡信息失败: %w", err)
|
||||
}
|
||||
|
||||
cardMap := make(map[string]*model.IotCard)
|
||||
cardIDMap := make(map[uint]*model.IotCard)
|
||||
for i := range iotCards {
|
||||
cardMap[iotCards[i].ICCID] = &iotCards[i]
|
||||
cardIDMap[iotCards[i].ID] = &iotCards[i]
|
||||
}
|
||||
|
||||
cardIDs := make([]uint, 0, len(iotCards))
|
||||
for _, card := range iotCards {
|
||||
cardIDs = append(cardIDs, card.ID)
|
||||
}
|
||||
|
||||
var bindings []model.DeviceSimBinding
|
||||
if len(cardIDs) > 0 {
|
||||
s.db.WithContext(ctx).Where("iot_card_id IN ? AND bind_status = 1", cardIDs).Find(&bindings)
|
||||
}
|
||||
|
||||
cardToDevice := make(map[uint]uint)
|
||||
deviceCards := make(map[uint][]uint)
|
||||
for _, binding := range bindings {
|
||||
cardToDevice[binding.IotCardID] = binding.DeviceID
|
||||
deviceCards[binding.DeviceID] = append(deviceCards[binding.DeviceID], binding.IotCardID)
|
||||
}
|
||||
|
||||
deviceIDs := make([]uint, 0, len(deviceCards))
|
||||
for deviceID := range deviceCards {
|
||||
deviceIDs = append(deviceIDs, deviceID)
|
||||
}
|
||||
var devices []model.Device
|
||||
deviceMap := make(map[uint]*model.Device)
|
||||
if len(deviceIDs) > 0 {
|
||||
s.db.WithContext(ctx).Where("id IN ?", deviceIDs).Find(&devices)
|
||||
for i := range devices {
|
||||
deviceMap[devices[i].ID] = &devices[i]
|
||||
}
|
||||
}
|
||||
|
||||
resp := &model.AllocateCardsPreviewResp{
|
||||
StandaloneCards: make([]model.StandaloneCard, 0),
|
||||
DeviceBundles: make([]model.DeviceBundle, 0),
|
||||
FailedItems: make([]model.FailedItem, 0),
|
||||
}
|
||||
|
||||
processedDevices := make(map[uint]bool)
|
||||
|
||||
for _, iccid := range req.ICCIDs {
|
||||
card, exists := cardMap[iccid]
|
||||
if !exists {
|
||||
resp.FailedItems = append(resp.FailedItems, model.FailedItem{
|
||||
ICCID: iccid,
|
||||
Reason: "卡不存在",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
deviceID, hasDevice := cardToDevice[card.ID]
|
||||
if !hasDevice {
|
||||
resp.StandaloneCards = append(resp.StandaloneCards, model.StandaloneCard{
|
||||
ICCID: card.ICCID,
|
||||
IotCardID: card.ID,
|
||||
MSISDN: card.MSISDN,
|
||||
CarrierID: card.CarrierID,
|
||||
StatusName: getCardStatusName(card.Status),
|
||||
})
|
||||
} else {
|
||||
if processedDevices[deviceID] {
|
||||
continue
|
||||
}
|
||||
processedDevices[deviceID] = true
|
||||
|
||||
device := deviceMap[deviceID]
|
||||
if device == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bundleCardIDs := deviceCards[deviceID]
|
||||
bundle := model.DeviceBundle{
|
||||
DeviceID: deviceID,
|
||||
DeviceNo: device.DeviceNo,
|
||||
BundleCards: make([]model.DeviceBundleCard, 0),
|
||||
}
|
||||
|
||||
for _, bundleCardID := range bundleCardIDs {
|
||||
bundleCard := cardIDMap[bundleCardID]
|
||||
if bundleCard == nil {
|
||||
continue
|
||||
}
|
||||
if bundleCard.ID == card.ID {
|
||||
bundle.TriggerCard = model.DeviceBundleCard{
|
||||
ICCID: bundleCard.ICCID,
|
||||
IotCardID: bundleCard.ID,
|
||||
MSISDN: bundleCard.MSISDN,
|
||||
}
|
||||
} else {
|
||||
bundle.BundleCards = append(bundle.BundleCards, model.DeviceBundleCard{
|
||||
ICCID: bundleCard.ICCID,
|
||||
IotCardID: bundleCard.ID,
|
||||
MSISDN: bundleCard.MSISDN,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resp.DeviceBundles = append(resp.DeviceBundles, bundle)
|
||||
}
|
||||
}
|
||||
|
||||
deviceCardCount := 0
|
||||
for _, bundle := range resp.DeviceBundles {
|
||||
deviceCardCount += 1 + len(bundle.BundleCards)
|
||||
}
|
||||
|
||||
resp.Summary = model.AllocatePreviewSummary{
|
||||
StandaloneCardCount: len(resp.StandaloneCards),
|
||||
DeviceCount: len(resp.DeviceBundles),
|
||||
DeviceCardCount: deviceCardCount,
|
||||
TotalCardCount: len(resp.StandaloneCards) + deviceCardCount,
|
||||
FailedCount: len(resp.FailedItems),
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *model.AllocateCardsReq) (*model.AllocateCardsResp, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
currentShopID := middleware.GetShopIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
preview, err := s.AllocateCardsPreview(ctx, enterpriseID, &model.AllocateCardsPreviewReq{ICCIDs: req.ICCIDs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(preview.DeviceBundles) > 0 && !req.ConfirmDeviceBundles {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "存在设备包,请确认整体授权设备下所有卡")
|
||||
}
|
||||
|
||||
resp := &model.AllocateCardsResp{
|
||||
FailedItems: preview.FailedItems,
|
||||
FailCount: len(preview.FailedItems),
|
||||
AllocatedDevices: make([]model.AllocatedDevice, 0),
|
||||
}
|
||||
|
||||
cardIDsToAllocate := make([]uint, 0)
|
||||
for _, card := range preview.StandaloneCards {
|
||||
cardIDsToAllocate = append(cardIDsToAllocate, card.IotCardID)
|
||||
}
|
||||
for _, bundle := range preview.DeviceBundles {
|
||||
cardIDsToAllocate = append(cardIDsToAllocate, bundle.TriggerCard.IotCardID)
|
||||
for _, card := range bundle.BundleCards {
|
||||
cardIDsToAllocate = append(cardIDsToAllocate, card.IotCardID)
|
||||
}
|
||||
iccids := []string{bundle.TriggerCard.ICCID}
|
||||
for _, card := range bundle.BundleCards {
|
||||
iccids = append(iccids, card.ICCID)
|
||||
}
|
||||
resp.AllocatedDevices = append(resp.AllocatedDevices, model.AllocatedDevice{
|
||||
DeviceID: bundle.DeviceID,
|
||||
DeviceNo: bundle.DeviceNo,
|
||||
CardCount: 1 + len(bundle.BundleCards),
|
||||
ICCIDs: iccids,
|
||||
})
|
||||
}
|
||||
|
||||
existingAuths, err := s.enterpriseCardAuthStore.GetActiveAuthsByCardIDs(ctx, enterpriseID, cardIDsToAllocate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询已有授权失败: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
auths := make([]*model.EnterpriseCardAuthorization, 0)
|
||||
for _, cardID := range cardIDsToAllocate {
|
||||
if existingAuths[cardID] {
|
||||
continue
|
||||
}
|
||||
auths = append(auths, &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterpriseID,
|
||||
IotCardID: cardID,
|
||||
ShopID: currentShopID,
|
||||
AuthorizedBy: currentUserID,
|
||||
AuthorizedAt: &now,
|
||||
Status: 1,
|
||||
})
|
||||
}
|
||||
|
||||
if len(auths) > 0 {
|
||||
if err := s.enterpriseCardAuthStore.BatchCreate(ctx, auths); err != nil {
|
||||
return nil, fmt.Errorf("创建授权记录失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SuccessCount = len(cardIDsToAllocate)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) RecallCards(ctx context.Context, enterpriseID uint, req *model.RecallCardsReq) (*model.RecallCardsResp, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
var iotCards []model.IotCard
|
||||
if err := s.db.WithContext(ctx).Where("iccid IN ?", req.ICCIDs).Find(&iotCards).Error; err != nil {
|
||||
return nil, fmt.Errorf("查询卡信息失败: %w", err)
|
||||
}
|
||||
|
||||
cardMap := make(map[string]*model.IotCard)
|
||||
cardIDMap := make(map[uint]*model.IotCard)
|
||||
cardIDs := make([]uint, 0, len(iotCards))
|
||||
for i := range iotCards {
|
||||
cardMap[iotCards[i].ICCID] = &iotCards[i]
|
||||
cardIDMap[iotCards[i].ID] = &iotCards[i]
|
||||
cardIDs = append(cardIDs, iotCards[i].ID)
|
||||
}
|
||||
|
||||
existingAuths, err := s.enterpriseCardAuthStore.GetActiveAuthsByCardIDs(ctx, enterpriseID, cardIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询已有授权失败: %w", err)
|
||||
}
|
||||
|
||||
resp := &model.RecallCardsResp{
|
||||
FailedItems: make([]model.FailedItem, 0),
|
||||
RecalledDevices: make([]model.RecalledDevice, 0),
|
||||
}
|
||||
|
||||
cardIDsToRecall := make([]uint, 0)
|
||||
for _, iccid := range req.ICCIDs {
|
||||
card, exists := cardMap[iccid]
|
||||
if !exists {
|
||||
resp.FailedItems = append(resp.FailedItems, model.FailedItem{
|
||||
ICCID: iccid,
|
||||
Reason: "卡不存在",
|
||||
})
|
||||
continue
|
||||
}
|
||||
if !existingAuths[card.ID] {
|
||||
resp.FailedItems = append(resp.FailedItems, model.FailedItem{
|
||||
ICCID: iccid,
|
||||
Reason: "该卡未授权给此企业",
|
||||
})
|
||||
continue
|
||||
}
|
||||
cardIDsToRecall = append(cardIDsToRecall, card.ID)
|
||||
}
|
||||
|
||||
if len(cardIDsToRecall) > 0 {
|
||||
if err := s.enterpriseCardAuthStore.BatchUpdateStatus(ctx, enterpriseID, cardIDsToRecall, 0); err != nil {
|
||||
return nil, fmt.Errorf("回收授权失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SuccessCount = len(cardIDsToRecall)
|
||||
resp.FailCount = len(resp.FailedItems)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListCards(ctx context.Context, enterpriseID uint, req *model.EnterpriseCardListReq) (*model.EnterpriseCardPageResult, error) {
|
||||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
cardIDs, err := s.enterpriseCardAuthStore.ListCardIDsByEnterprise(ctx, enterpriseID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询授权卡ID失败: %w", err)
|
||||
}
|
||||
|
||||
if len(cardIDs) == 0 {
|
||||
return &model.EnterpriseCardPageResult{
|
||||
Items: make([]model.EnterpriseCardItem, 0),
|
||||
Total: 0,
|
||||
Page: req.Page,
|
||||
Size: req.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
page := req.Page
|
||||
pageSize := req.PageSize
|
||||
if page == 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize == 0 {
|
||||
pageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.IotCard{}).Where("id IN ?", cardIDs)
|
||||
|
||||
if req.Status != nil {
|
||||
query = query.Where("status = ?", *req.Status)
|
||||
}
|
||||
if req.CarrierID != nil {
|
||||
query = query.Where("carrier_id = ?", *req.CarrierID)
|
||||
}
|
||||
if req.ICCID != "" {
|
||||
query = query.Where("iccid LIKE ?", "%"+req.ICCID+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, fmt.Errorf("统计卡数量失败: %w", err)
|
||||
}
|
||||
|
||||
var cards []model.IotCard
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&cards).Error; err != nil {
|
||||
return nil, fmt.Errorf("查询卡列表失败: %w", err)
|
||||
}
|
||||
|
||||
items := make([]model.EnterpriseCardItem, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
items = append(items, model.EnterpriseCardItem{
|
||||
ID: card.ID,
|
||||
ICCID: card.ICCID,
|
||||
MSISDN: card.MSISDN,
|
||||
CarrierID: card.CarrierID,
|
||||
Status: card.Status,
|
||||
StatusName: getCardStatusName(card.Status),
|
||||
NetworkStatus: card.NetworkStatus,
|
||||
NetworkStatusName: getNetworkStatusName(card.NetworkStatus),
|
||||
})
|
||||
}
|
||||
|
||||
return &model.EnterpriseCardPageResult{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
Size: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) SuspendCard(ctx context.Context, enterpriseID, cardID uint) error {
|
||||
return s.updateCardNetworkStatus(ctx, enterpriseID, cardID, 0)
|
||||
}
|
||||
|
||||
func (s *Service) ResumeCard(ctx context.Context, enterpriseID, cardID uint) error {
|
||||
return s.updateCardNetworkStatus(ctx, enterpriseID, cardID, 1)
|
||||
}
|
||||
|
||||
func (s *Service) updateCardNetworkStatus(ctx context.Context, enterpriseID, cardID uint, networkStatus int) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
auth, err := s.enterpriseCardAuthStore.GetByEnterpriseAndCard(ctx, enterpriseID, cardID)
|
||||
if err != nil || auth.Status != 1 {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作此卡")
|
||||
}
|
||||
|
||||
return s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("id = ?", cardID).
|
||||
Update("network_status", networkStatus).Error
|
||||
}
|
||||
|
||||
func getCardStatusName(status int) string {
|
||||
switch status {
|
||||
case 1:
|
||||
return "在库"
|
||||
case 2:
|
||||
return "已分销"
|
||||
case 3:
|
||||
return "已激活"
|
||||
case 4:
|
||||
return "已停用"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func getNetworkStatusName(status int) string {
|
||||
if status == 1 {
|
||||
return "开机"
|
||||
}
|
||||
return "停机"
|
||||
}
|
||||
Reference in New Issue
Block a user