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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user