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 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
MiFaZhan
2026-10-26 14:01:25 +08:00
commit 2ed5dfa6a1
1013 changed files with 138466 additions and 0 deletions
@@ -0,0 +1,941 @@
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
}