Files
iotlcm/server/internal/logic/app/app_card_allocate.go
T
MiFaZhan 2ed5dfa6a1 chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。

- app/:uni-app 小程序前端(基于 unibest,MIT)
  - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID
  - 内网后端地址 → admin.iotlcm.example.com
  - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明)
- server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0)
  - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件)
  - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化
  - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位)
  - 依赖号码尾部的断言同步更新
  - 补 server/LICENSE(Apache-2.0)
- 移除内部资产:CI 触发器
- 补根 README,LF 行尾 + .gitattributes

已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
2026-10-26 14:01:25 +08:00

942 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"context"
"fmt"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type appAllocateBatchContext struct {
Target *entity.SysDept
Cards []*appAllocateCardContext
TotalCount int
ValidCount int
FailCount int
FailReason string
}
type appAllocateCardContext struct {
Card *appCardQueryRow
ItemId string
RowNo int
SourceDeptId int64
ErrorReason string
}
// AllocateTargets 查询当前企业用户可划拨的目标客户。
func (s *sAppCard) AllocateTargets(ctx context.Context, param *model.AppCardAllocateTargetListParam) (items []*model.AppCardAllocateTargetModel, total int, err error) {
if param == nil {
param = &model.AppCardAllocateTargetListParam{}
}
user, err := appCurrentUser(ctx)
if err != nil {
return nil, 0, err
}
if s.isTerminalUser(ctx, user) {
return nil, 0, gerror.New("终端用户不能使用卡划拨功能")
}
if user.DeptId <= 0 {
return nil, 0, gerror.New("当前账号未绑定企业,不能使用卡划拨功能")
}
if param.Page <= 0 {
param.Page = 1
}
if param.PageSize <= 0 {
param.PageSize = 50
}
if param.PageSize > 1000 {
param.PageSize = 1000
}
var rows []*entity.SysDept
rows, total, err = s.loadAllocateTargetDepts(ctx, user, strings.TrimSpace(param.Keyword), param.Page, param.PageSize)
if err != nil {
return nil, 0, gerror.Wrap(err, "查询可划拨目标客户失败")
}
items = make([]*model.AppCardAllocateTargetModel, 0, len(rows))
for _, row := range rows {
items = append(items, appAllocateTargetModel(row, user.DeptId))
}
return items, total, nil
}
func (s *sAppCard) loadAllocateTargetDepts(ctx context.Context, user *entity.SysUser, keyword string, page int, pageSize int) ([]*entity.SysDept, int, error) {
if user == nil || user.DeptId <= 0 {
return []*entity.SysDept{}, 0, nil
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 50
}
var children []*entity.SysDept
err := dao.SysDept.Ctx(ctx).
Where(dao.SysDept.Columns().TenantId, user.TenantId).
Where(dao.SysDept.Columns().ParentId, user.DeptId).
WhereNull(dao.SysDept.Columns().DeletedAt).
OrderAsc(dao.SysDept.Columns().OrderNum).
OrderAsc(dao.SysDept.Columns().DeptId).
Scan(&children)
if err != nil {
return nil, 0, err
}
rows := make([]*entity.SysDept, 0, len(children))
for _, child := range children {
if child == nil || child.DeptId <= 0 || !appAllocateDeptMatchesKeyword(child, keyword) {
continue
}
rows = append(rows, child)
}
rows, err = appFilterDeptsByEnterpriseDataScope(ctx, user, rows)
if err != nil {
return nil, 0, err
}
total := len(rows)
start := (page - 1) * pageSize
if start >= total {
return []*entity.SysDept{}, total, nil
}
end := start + pageSize
if end > total {
end = total
}
return rows[start:end], total, nil
}
func appAllocateDeptMatchesKeyword(dept *entity.SysDept, keyword string) bool {
keyword = strings.TrimSpace(keyword)
if keyword == "" {
return true
}
if dept == nil {
return false
}
return strings.Contains(strings.TrimSpace(dept.DeptName), keyword) ||
strings.Contains(strings.TrimSpace(dept.DeptKey), keyword) ||
strings.Contains(fmt.Sprintf("%d", dept.DeptId), keyword)
}
// AllocateBatchPrepare 校验待划拨卡片,并写入划拨预处理批次。
func (s *sAppCard) AllocateBatchPrepare(ctx context.Context, param *model.AppCardAllocateBatchPrepareParam) (res *model.AppCardAllocateBatchModel, err error) {
if param == nil {
param = &model.AppCardAllocateBatchPrepareParam{}
}
user, err := appCurrentUser(ctx)
if err != nil {
return nil, err
}
batch, err := s.buildAllocateBatchContext(ctx, user, param.CardValues, param.TargetDeptId)
if err != nil {
return nil, err
}
ctx = appContextWithUser(ctx, user)
processor := newAppAllocateBatchProcessor(batch)
rows := appAllocateBatchRows(batch)
req := batchimport.SubmitRequest{
BizParams: param,
SourceType: consts.BatchSourceTypeBatchSelect,
BatchType: consts.BatchTypePrepare,
FailureBatchType: consts.BatchTypeAllocate,
SkipExecute: true,
TargetDeptId: batch.Target.DeptId,
Operator: batchimport.OperatorContext{
TenantId: user.TenantId,
DeptId: user.DeptId,
UserId: user.UserId,
},
}
result, err := batchimport.RunRows(ctx, req, processor, rows, "miniapp-allocate-prepare")
if err != nil {
return nil, gerror.Wrap(err, "写入划拨预处理批次失败")
}
status := "prepared"
batchNo := ""
if result != nil {
batchNo = result.BatchNo
}
if batch.FailCount > 0 || result == nil || result.ImportStatus != consts.ImportStatusSuccess {
status = "failed"
}
return s.buildAllocateBatchModel(ctx, user, batch, batchNo, "", consts.BatchTypePrepare, status), nil
}
// AllocateBatchSubmit 确认划拨预处理批次,并更新卡片归属;划拨后原企业的可见和办理权限仍保留。
func (s *sAppCard) AllocateBatchSubmit(ctx context.Context, param *model.AppCardAllocateBatchSubmitParam) (res *model.AppCardAllocateBatchModel, err error) {
if param == nil {
param = &model.AppCardAllocateBatchSubmitParam{}
}
user, err := appCurrentUser(ctx)
if err != nil {
return nil, err
}
if s.isTerminalUser(ctx, user) {
return nil, gerror.New("终端用户不能使用卡划拨功能")
}
now := gtime.Now()
var batch *appAllocateBatchContext
var sourceBatchNo string
batchNo := strings.TrimSpace(param.BatchNo)
err = dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
prepareBatch, err := s.lockAppAllocatePrepareBatch(ctx, tx, user, batchNo)
if err != nil {
return err
}
sourceBatchNo = appAllocateSourceBatchNoFromBatch(prepareBatch)
targetDeptId := prepareBatch.TargetDeptId
if param.TargetDeptId > 0 && param.TargetDeptId != targetDeptId {
return gerror.New("提交目标客户与预处理批次不一致,请重新校验")
}
target, err := s.loadAllocateTarget(ctx, user, targetDeptId)
if err != nil {
return err
}
details, err := s.loadAppAllocatePrepareBatchDetails(ctx, tx, prepareBatch.BatchNo)
if err != nil {
return err
}
if len(details) == 0 {
return gerror.New("划拨预处理批次没有可确认的卡")
}
cardValues := make([]string, 0, len(details))
for _, detail := range details {
cardValues = append(cardValues, strings.TrimSpace(detail.ItemId))
}
if len(param.CardValues) > 0 && !appSameStringSet(appNormalizeAllocateCardValues(param.CardValues), cardValues) {
return gerror.New("提交卡片与预处理批次不一致,请重新校验")
}
batch = &appAllocateBatchContext{
Target: target,
Cards: make([]*appAllocateCardContext, 0, len(details)),
}
for _, detail := range details {
card, err := s.lockAllocateCardForSubmit(ctx, tx, user, detail, target.DeptId)
if err != nil {
return err
}
sourceDeptId := card.DeptId
if detail.Snapshot != nil && detail.Snapshot.Get("sourceDeptId").Int64() > 0 {
sourceDeptId = detail.Snapshot.Get("sourceDeptId").Int64()
}
batch.Cards = append(batch.Cards, &appAllocateCardContext{
Card: card,
ItemId: card.Iccid,
RowNo: detail.RowNo,
SourceDeptId: sourceDeptId,
})
}
batch.refreshCounts()
if err = s.updateAllocateCards(ctx, tx, user, batch, prepareBatch.BatchNo, now); err != nil {
return err
}
if err = s.markAppAllocatePrepareBatchConfirmed(ctx, tx, user, prepareBatch, batch, sourceBatchNo, strings.TrimSpace(param.Remark), now); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
if batch == nil {
return nil, gerror.New("划拨提交结果不完整")
}
res = s.buildAllocateBatchModel(ctx, user, batch, batchNo, sourceBatchNo, consts.BatchTypeAllocate, "success")
_ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{
EventType: appAsyncEventAllocateCompleted,
BatchNo: batchNo,
BatchType: consts.BatchTypeAllocate,
TenantId: user.TenantId,
UserId: user.UserId,
DeptId: user.DeptId,
Trigger: "allocate_submit",
})
return res, nil
}
func (s *sAppCard) buildAllocateBatchContext(ctx context.Context, user *entity.SysUser, cardValues []string, targetDeptId int64) (*appAllocateBatchContext, error) {
if s.isTerminalUser(ctx, user) {
return nil, gerror.New("终端用户不能使用卡划拨功能")
}
if user.DeptId <= 0 {
return nil, gerror.New("当前账号未绑定企业,不能使用卡划拨功能")
}
values := appNormalizeAllocateCardValues(cardValues)
if len(values) == 0 {
return nil, gerror.New("请选择需要划拨的卡")
}
if len(values) > 100 {
return nil, gerror.New("单次批量划拨最多支持100张卡")
}
target, err := s.loadAllocateTarget(ctx, user, targetDeptId)
if err != nil {
return nil, err
}
batch := &appAllocateBatchContext{
Target: target,
Cards: make([]*appAllocateCardContext, 0, len(values)),
TotalCount: len(values),
}
seenIccid := make(map[string]struct{}, len(values))
for index, value := range values {
item := &appAllocateCardContext{
ItemId: strings.TrimSpace(value),
RowNo: index + 2,
}
card, err := s.findVisibleCard(ctx, user, value)
if err != nil {
return nil, err
}
if card == nil {
item.ErrorReason = fmt.Sprintf("第%d张卡不存在或无权划拨:%s", index+1, value)
batch.Cards = append(batch.Cards, item)
continue
}
item.Card = card
item.ItemId = strings.TrimSpace(card.Iccid)
item.SourceDeptId = card.DeptId
iccid := strings.TrimSpace(card.Iccid)
if _, exists := seenIccid[iccid]; exists {
item.ErrorReason = fmt.Sprintf("批量划拨卡重复:%s", iccid)
batch.Cards = append(batch.Cards, item)
continue
}
seenIccid[iccid] = struct{}{}
if err = s.ensureAllocateCardOwnedByOperator(ctx, nil, user, card); err != nil {
item.ErrorReason = err.Error()
} else if err = s.validateAllocateCardBusinessRules(ctx, nil, card); err != nil {
item.ErrorReason = err.Error()
} else if card.DeptId == target.DeptId {
item.ErrorReason = fmt.Sprintf("卡%s已属于目标客户,无需重复划拨", appFirstNotEmpty(card.Iccid, value))
}
batch.Cards = append(batch.Cards, item)
}
batch.refreshCounts()
return batch, nil
}
func (batch *appAllocateBatchContext) refreshCounts() {
if batch == nil {
return
}
batch.TotalCount = len(batch.Cards)
batch.ValidCount = 0
batch.FailCount = 0
batch.FailReason = ""
for _, item := range batch.Cards {
if item == nil {
batch.FailCount++
continue
}
if strings.TrimSpace(item.ErrorReason) != "" {
batch.FailCount++
continue
}
batch.ValidCount++
}
if batch.FailCount > 0 {
batch.FailReason = "存在校验失败数据,本次未写入"
}
}
func (item *appAllocateCardContext) allocateItemId() string {
if item == nil {
return ""
}
if item.Card != nil && strings.TrimSpace(item.Card.Iccid) != "" {
return strings.TrimSpace(item.Card.Iccid)
}
return strings.TrimSpace(item.ItemId)
}
func (s *sAppCard) insertAppAllocatePrepareBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appAllocateBatchContext, batchNo string, now *gtime.Time) error {
batch.refreshCounts()
batchStatus := consts.BatchAsyncStatusSuccess
runResult := consts.BatchRunResultSuccess
successCount := batch.ValidCount
if batch.FailCount > 0 {
batchStatus = consts.BatchAsyncStatusFailed
runResult = consts.BatchRunResultFailed
successCount = 0
}
_, err := dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{
BatchNo: batchNo,
BatchType: consts.BatchTypePrepare,
BatchStatus: batchStatus,
BatchAsyncStatus: batchStatus,
SourceType: consts.BatchSourceTypeBatchSelect,
SourceBatchNo: batchNo,
IsCrossDept: 0,
TargetDeptId: batch.Target.DeptId,
DeptId: user.DeptId,
TotalCount: batch.TotalCount,
ValidCount: batch.ValidCount,
SuccessCount: successCount,
FailCount: batch.FailCount,
RunResult: runResult,
FailReason: batch.FailReason,
BatchInfo: appAllocatePrepareBatchInfo(batch, batchNo),
StartTime: now,
EndTime: now,
TenantId: user.TenantId,
CreatedDept: user.DeptId,
CreatedBy: user.UserId,
CreatedAt: now,
UpdatedBy: user.UserId,
UpdatedAt: now,
}).Insert()
if err != nil {
return gerror.Wrap(err, "写入划拨预处理主表失败")
}
details := make([]do.BatchDetail, 0, len(batch.Cards))
for _, item := range batch.Cards {
detailStatus := consts.BatchDetailStatusSuccess
if strings.TrimSpace(item.ErrorReason) != "" {
detailStatus = consts.BatchDetailStatusFailed
}
details = append(details, do.BatchDetail{
BatchNo: batchNo,
BatchType: consts.BatchTypePrepare,
DetailStatus: detailStatus,
BlankOutStatus: consts.BlankOutStatusNo,
RowNo: item.RowNo,
ItemId: item.allocateItemId(),
ErrorReason: strings.TrimSpace(item.ErrorReason),
Snapshot: appAllocatePrepareDetailSnapshot(batch, item),
TenantId: user.TenantId,
CreatedDept: user.DeptId,
CreatedBy: user.UserId,
CreatedAt: now,
UpdatedBy: user.UserId,
UpdatedAt: now,
})
}
if _, err = dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert(); err != nil {
return gerror.Wrap(err, "写入划拨预处理明细失败")
}
return nil
}
func (s *sAppCard) lockAppAllocatePrepareBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batchNo string) (*entity.BatchMain, error) {
batchNo = strings.TrimSpace(batchNo)
if batchNo == "" {
return nil, gerror.New("预处理批次号不能为空")
}
var batch *entity.BatchMain
columns := dao.BatchMain.Columns()
err := dao.BatchMain.Ctx(ctx).TX(tx).
Where(columns.BatchNo, batchNo).
Where(columns.BatchType, consts.BatchTypePrepare).
Where(columns.TenantId, user.TenantId).
Where(columns.CreatedBy, user.UserId).
LockUpdate().
Limit(1).
Scan(&batch)
if err != nil {
return nil, gerror.Wrap(err, "查询划拨预处理批次失败")
}
if batch == nil || batch.Id == 0 {
return nil, gerror.New("划拨预处理批次不存在或无权确认")
}
if batch.BatchInfo == nil || batch.BatchInfo.Get("targetBatchType").Int() != consts.BatchTypeAllocate {
return nil, gerror.New("批次不是卡划拨预处理批次")
}
if batch.RunResult != consts.BatchRunResultSuccess || batch.FailCount > 0 {
return nil, gerror.New("划拨预处理批次存在校验失败数据,请重新校验后再提交")
}
return batch, nil
}
func (s *sAppCard) loadAppAllocatePrepareBatchDetails(ctx context.Context, tx gdb.TX, batchNo string) ([]*entity.BatchDetail, error) {
var details []*entity.BatchDetail
columns := dao.BatchDetail.Columns()
err := dao.BatchDetail.Ctx(ctx).TX(tx).
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
Where(columns.BatchType, consts.BatchTypePrepare).
Where(columns.DetailStatus, consts.BatchDetailStatusSuccess).
Where(columns.BlankOutStatus, consts.BlankOutStatusNo).
OrderAsc(columns.RowNo).
Scan(&details)
if err != nil {
return nil, gerror.Wrap(err, "查询划拨预处理明细失败")
}
return details, nil
}
func (s *sAppCard) lockAllocateCardForSubmit(ctx context.Context, tx gdb.TX, user *entity.SysUser, detail *entity.BatchDetail, targetDeptId int64) (*appCardQueryRow, error) {
if detail == nil || strings.TrimSpace(detail.ItemId) == "" {
return nil, gerror.New("划拨批次明细缺少卡号")
}
expectedSourceDeptId := int64(0)
if detail.Snapshot != nil {
expectedSourceDeptId = detail.Snapshot.Get("sourceDeptId").Int64()
}
var card *appCardQueryRow
err := s.cardQueryBase(ctx, false).TX(tx).
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().TenantId, user.TenantId).
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Iccid, strings.TrimSpace(detail.ItemId)).
LockUpdate().
Limit(1).
Scan(&card)
if err != nil {
return nil, gerror.Wrapf(err, "锁定划拨卡%s失败", detail.ItemId)
}
if card == nil || card.Id == 0 {
return nil, gerror.Newf("卡%s不存在,不能划拨", detail.ItemId)
}
if err = s.attachCardChannelInfo(ctx, card); err != nil {
return nil, err
}
if expectedSourceDeptId > 0 && card.DeptId != expectedSourceDeptId {
return nil, gerror.Newf("卡%s归属已变化,请重新校验", card.Iccid)
}
if err = s.ensureAllocateCardVisible(ctx, tx, user, card); err != nil {
return nil, err
}
if err = s.ensureAllocateCardOwnedByOperator(ctx, tx, user, card); err != nil {
return nil, err
}
if err = s.validateAllocateCardBusinessRules(ctx, tx, card); err != nil {
return nil, err
}
if card.DeptId == targetDeptId {
return nil, gerror.Newf("卡%s已属于目标客户,无需重复划拨", card.Iccid)
}
return card, nil
}
func (s *sAppCard) validateAllocateCardBusinessRules(ctx context.Context, tx gdb.TX, card *appCardQueryRow) error {
if card == nil || strings.TrimSpace(card.Iccid) == "" {
return gerror.New("划拨卡信息不完整")
}
cardLabel := appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei)
if card.BossCardStatus == 4 {
return gerror.Newf("卡%s当前为停机状态,不能划拨", cardLabel)
}
if card.PackageStatus == consts.PackageStatusExpired ||
(card.FlowPkgEndAt != nil && !card.FlowPkgEndAt.IsZero() && card.FlowPkgEndAt.Before(gtime.Now())) {
return gerror.Newf("卡%s套餐已过期,不能划拨", cardLabel)
}
return nil
}
func (s *sAppCard) updateAllocateCards(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appAllocateBatchContext, batchNo string, now *gtime.Time) error {
for _, item := range batch.Cards {
result, err := dao.TSimCardInfo.Ctx(ctx).TX(tx).
Where(dao.TSimCardInfo.Columns().Id, item.Card.Id).
Where(dao.TSimCardInfo.Columns().DeptId, item.Card.DeptId).
Data(do.TSimCardInfo{
DeptId: batch.Target.DeptId,
CreatedDept: user.DeptId,
OutboundBatchNo: batchNo,
AllocationBatchNo: batchNo,
UpdatedBy: user.UserId,
UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrapf(err, "更新卡%s归属失败", item.Card.Iccid)
}
if affected, err := result.RowsAffected(); err != nil {
return gerror.Wrap(err, "读取划拨更新数量失败")
} else if affected != 1 {
return gerror.Newf("卡%s归属已变化,请重新校验", item.Card.Iccid)
}
if item.SourceDeptId <= 0 {
item.SourceDeptId = item.Card.DeptId
}
item.Card.DeptId = batch.Target.DeptId
item.Card.CreatedDept = user.DeptId
item.Card.OutboundBatchNo = batchNo
item.Card.AllocationBatchNo = batchNo
}
return nil
}
func (s *sAppCard) markAppAllocatePrepareBatchConfirmed(ctx context.Context, tx gdb.TX, user *entity.SysUser, prepareBatch *entity.BatchMain, batch *appAllocateBatchContext, sourceBatchNo string, remark string, now *gtime.Time) error {
batchNo := strings.TrimSpace(prepareBatch.BatchNo)
for _, item := range batch.Cards {
_, err := dao.BatchDetail.Ctx(ctx).TX(tx).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare).
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusSuccess).
Where(dao.BatchDetail.Columns().ItemId, item.Card.Iccid).
Data(do.BatchDetail{
BatchType: consts.BatchTypeAllocate,
Snapshot: appAllocateSubmitDetailSnapshot(batch, item, batchNo, sourceBatchNo),
UpdatedBy: user.UserId,
UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrap(err, "更新划拨批次明细失败")
}
}
if _, err := dao.BatchDetail.Ctx(ctx).TX(tx).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare).
Data(do.BatchDetail{
BatchType: consts.BatchTypeAllocate,
UpdatedBy: user.UserId,
UpdatedAt: now,
}).
Update(); err != nil {
return gerror.Wrap(err, "更新划拨批次明细类型失败")
}
logFiles, err := s.ensureAppAllocateBatchLogFiles(ctx, tx, user, batch, prepareBatch.BatchInfo, batchNo, sourceBatchNo, remark, now)
if err != nil {
return err
}
batchInfo := appAllocateBatchInfoWithLogFiles(
appAllocateSubmitBatchInfo(batch, batchNo, sourceBatchNo, remark, now),
logFiles,
)
_, err = dao.BatchMain.Ctx(ctx).TX(tx).
Where(dao.BatchMain.Columns().Id, prepareBatch.Id).
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypePrepare).
Data(do.BatchMain{
BatchType: consts.BatchTypeAllocate,
BatchStatus: consts.BatchStatusSuccess,
BatchAsyncStatus: consts.BatchAsyncStatusSuccess,
TargetDeptId: batch.Target.DeptId,
TotalCount: len(batch.Cards),
ValidCount: len(batch.Cards),
SuccessCount: len(batch.Cards),
FailCount: 0,
RunResult: consts.BatchRunResultSuccess,
BatchInfo: batchInfo,
EndTime: now,
UpdatedBy: user.UserId,
UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrap(err, "更新划拨批次主表失败")
}
return nil
}
func (s *sAppCard) loadAllocateTarget(ctx context.Context, user *entity.SysUser, targetDeptId int64) (*entity.SysDept, error) {
if targetDeptId <= 0 {
return nil, gerror.New("请选择划拨目标客户")
}
target, err := s.loadDeptById(ctx, user.TenantId, targetDeptId)
if err != nil {
return nil, err
}
if target == nil || target.DeptId == 0 {
return nil, gerror.New("划拨目标客户不存在")
}
if target.DeptId == user.DeptId {
return nil, gerror.New("划拨目标不能选择当前企业")
}
if target.ParentId != user.DeptId {
return nil, gerror.New("划拨目标客户不在当前企业直属下级范围内")
}
visible, err := appEnterpriseDeptVisibleByDataScope(ctx, user, target.DeptId)
if err != nil {
return nil, err
}
if !visible {
return nil, gerror.New("划拨目标客户不在当前角色可操作范围内")
}
return target, nil
}
func (s *sAppCard) ensureAllocateCardVisible(ctx context.Context, tx gdb.TX, user *entity.SysUser, card *appCardQueryRow) error {
if card == nil || card.Id <= 0 || strings.TrimSpace(card.Iccid) == "" {
return gerror.New("划拨卡信息不完整")
}
var visible *appCardQueryRow
if err := s.visibleCardModel(ctx, user).TX(tx).
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Id, card.Id).
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Iccid, card.Iccid).
Limit(1).
Scan(&visible); err != nil {
return gerror.Wrapf(err, "校验卡%s可操作范围失败", card.Iccid)
}
if visible == nil || visible.Id == 0 {
return gerror.Newf("卡%s不在当前企业可操作范围内", card.Iccid)
}
return nil
}
func (s *sAppCard) ensureAllocateCardOwnedByOperator(ctx context.Context, tx gdb.TX, user *entity.SysUser, card *appCardQueryRow) error {
if user == nil || user.DeptId <= 0 {
return gerror.New("当前用户未绑定机构,不能划拨卡片")
}
if card == nil || strings.TrimSpace(card.Iccid) == "" {
return gerror.New("划拨卡信息不完整")
}
if card.DeptId != user.DeptId {
cardLabel := appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei)
return gerror.Newf("卡%s当前归属不在本企业名下,划拨权已归当前归属客户", cardLabel)
}
return nil
}
func (s *sAppCard) loadDeptById(ctx context.Context, tenantId string, deptId int64) (*entity.SysDept, error) {
var dept *entity.SysDept
err := dao.SysDept.Ctx(ctx).
Where(dao.SysDept.Columns().TenantId, tenantId).
Where(dao.SysDept.Columns().DeptId, deptId).
WhereNull(dao.SysDept.Columns().DeletedAt).
Limit(1).
Scan(&dept)
if err != nil {
return nil, gerror.Wrap(err, "查询客户信息失败")
}
return dept, nil
}
func (s *sAppCard) buildAllocateBatchModel(ctx context.Context, user *entity.SysUser, batch *appAllocateBatchContext, batchNo string, sourceBatchNo string, batchType int, status string) *model.AppCardAllocateBatchModel {
batch.refreshCounts()
cards := make([]*model.AppCardModel, 0, len(batch.Cards))
details := make([]*model.AppCardAllocateBatchDetailModel, 0, len(batch.Cards))
for _, item := range batch.Cards {
if item == nil {
continue
}
if item.Card != nil && strings.TrimSpace(item.ErrorReason) == "" {
cards = append(cards, s.cardRowToModel(ctx, item.Card, user, appCardOwnerAgent, true))
}
details = append(details, &model.AppCardAllocateBatchDetailModel{
RowNo: item.RowNo,
ItemId: item.allocateItemId(),
Status: appAllocateDetailStatusText(item.ErrorReason),
ErrorReason: strings.TrimSpace(item.ErrorReason),
})
}
return &model.AppCardAllocateBatchModel{
BatchNo: strings.TrimSpace(batchNo),
SourceBatchNo: strings.TrimSpace(sourceBatchNo),
BatchType: batchType,
Quantity: batch.ValidCount,
TotalCount: batch.TotalCount,
ValidCount: batch.ValidCount,
FailCount: batch.FailCount,
FailReason: batch.FailReason,
Target: appAllocateTargetModel(batch.Target, user.DeptId),
Cards: cards,
Details: details,
Status: status,
}
}
func appAllocateTargetModel(dept *entity.SysDept, currentDeptId int64) *model.AppCardAllocateTargetModel {
if dept == nil {
return nil
}
label := strings.TrimSpace(dept.DeptName)
if strings.TrimSpace(dept.DeptKey) != "" {
label = fmt.Sprintf("%s(%s)", label, strings.TrimSpace(dept.DeptKey))
}
scope := "self"
scopeName := "当前机构"
if currentDeptId > 0 && dept.DeptId != currentDeptId && dept.ParentId == currentDeptId {
scope = "direct_child"
scopeName = "直属下级"
}
return &model.AppCardAllocateTargetModel{
DeptId: dept.DeptId,
ParentId: dept.ParentId,
DeptKey: strings.TrimSpace(dept.DeptKey),
DeptName: strings.TrimSpace(dept.DeptName),
DeptCategory: strings.TrimSpace(dept.DeptCategory),
TargetScope: scope,
TargetScopeName: scopeName,
Label: label,
}
}
func appAllocatePrepareBatchInfo(batch *appAllocateBatchContext, batchNo string) *gjson.Json {
return gjson.New(map[string]any{
"source": "app",
"batchNo": strings.TrimSpace(batchNo),
"sourceBatchNo": strings.TrimSpace(batchNo),
"targetBatchType": consts.BatchTypeAllocate,
"targetBatchName": "卡划拨",
"targetDeptId": batch.Target.DeptId,
"targetDeptName": batch.Target.DeptName,
"quantity": batch.ValidCount,
"totalCount": batch.TotalCount,
"validCount": batch.ValidCount,
"failCount": batch.FailCount,
"failReason": batch.FailReason,
"cards": appAllocateCardSnapshotList(batch),
})
}
func appAllocateSubmitBatchInfo(batch *appAllocateBatchContext, batchNo string, sourceBatchNo string, remark string, now *gtime.Time) *gjson.Json {
return gjson.New(map[string]any{
"source": "app",
"batchNo": strings.TrimSpace(batchNo),
"sourceBatchNo": strings.TrimSpace(sourceBatchNo),
"targetBatchType": consts.BatchTypeAllocate,
"targetBatchName": "卡划拨",
"targetDeptId": batch.Target.DeptId,
"targetDeptName": batch.Target.DeptName,
"quantity": len(batch.Cards),
"remark": strings.TrimSpace(remark),
"allocateTime": now,
"cards": appAllocateCardSnapshotList(batch),
})
}
func appAllocatePrepareDetailSnapshot(batch *appAllocateBatchContext, item *appAllocateCardContext) *gjson.Json {
if item == nil {
return gjson.New(map[string]any{})
}
card := item.Card
cardId := int64(0)
if card != nil {
cardId = card.Id
}
return gjson.New(map[string]any{
"ICCID": appAllocateSnapshotICCID(item),
"MSISDN": appAllocateSnapshotField(card, func(c *appCardQueryRow) string { return c.Msisdn }),
"IMSI": appAllocateSnapshotField(card, func(c *appCardQueryRow) string { return c.Imsi }),
"IMEI": appAllocateSnapshotField(card, func(c *appCardQueryRow) string { return c.Imei }),
"rowNo": item.RowNo,
"cardId": cardId,
"sourceDeptId": item.SourceDeptId,
"targetDeptId": batch.Target.DeptId,
"targetDeptName": batch.Target.DeptName,
"currentPackageCode": appCardCurrentPackageCode(card),
"packageName": appCardPackageName(card),
"errorReason": strings.TrimSpace(item.ErrorReason),
})
}
func appAllocateSnapshotICCID(item *appAllocateCardContext) string {
if item == nil {
return ""
}
if item.Card != nil && strings.TrimSpace(item.Card.Iccid) != "" {
return strings.TrimSpace(item.Card.Iccid)
}
return strings.TrimSpace(item.ItemId)
}
func appAllocateSnapshotField(card *appCardQueryRow, getter func(*appCardQueryRow) string) string {
if card == nil || getter == nil {
return ""
}
return strings.TrimSpace(getter(card))
}
func appAllocateDetailStatusText(reason string) string {
if strings.TrimSpace(reason) != "" {
return "failed"
}
return "success"
}
func appAllocateSubmitDetailSnapshot(batch *appAllocateBatchContext, item *appAllocateCardContext, batchNo string, sourceBatchNo string) *gjson.Json {
card := item.Card
return gjson.New(map[string]any{
"ICCID": card.Iccid,
"MSISDN": card.Msisdn,
"IMSI": card.Imsi,
"IMEI": card.Imei,
"rowNo": item.RowNo,
"cardId": card.Id,
"allocateBatchNo": strings.TrimSpace(batchNo),
"sourceBatchNo": strings.TrimSpace(sourceBatchNo),
"sourceDeptId": item.SourceDeptId,
"targetDeptId": batch.Target.DeptId,
"targetDeptName": batch.Target.DeptName,
"currentPackageCode": appCardCurrentPackageCode(card),
"packageName": appCardPackageName(card),
})
}
func appAllocateCardSnapshotList(batch *appAllocateBatchContext) []map[string]any {
cards := make([]map[string]any, 0, len(batch.Cards))
for _, item := range batch.Cards {
if item == nil {
continue
}
cardId := int64(0)
iccid := item.allocateItemId()
msisdn := ""
currentPackageCode := ""
if item.Card != nil {
cardId = item.Card.Id
msisdn = item.Card.Msisdn
currentPackageCode = appCardCurrentPackageCode(item.Card)
}
cards = append(cards, map[string]any{
"cardId": cardId,
"iccid": iccid,
"msisdn": msisdn,
"sourceDeptId": item.SourceDeptId,
"targetDeptId": batch.Target.DeptId,
"currentPackageCode": currentPackageCode,
"errorReason": strings.TrimSpace(item.ErrorReason),
})
}
return cards
}
func appAllocateSourceBatchNoFromBatch(batch *entity.BatchMain) string {
if batch == nil {
return ""
}
if batch.BatchInfo != nil {
if sourceBatchNo := strings.TrimSpace(batch.BatchInfo.Get("sourceBatchNo").String()); sourceBatchNo != "" {
return sourceBatchNo
}
}
return strings.TrimSpace(batch.SourceBatchNo)
}
func appNormalizeAllocateCardValues(values []string) []string {
seen := map[string]struct{}{}
result := make([]string, 0, len(values))
for _, raw := range values {
parts := strings.FieldsFunc(raw, func(r rune) bool {
switch r {
case ',', ',', ';', ';', '\n', '\r', '\t', ' ':
return true
default:
return false
}
})
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
}
return result
}