sync: 同步 v1.0.4 并脱敏配置与镜像
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestBatchCardsFilterStatus(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
"ALTER TABLE batch_main ADD target_dept_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD dept_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD updated_by INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD batch_async_status INTEGER DEFAULT 3",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, batchType := range []int{consts.BatchTypeInbound, consts.BatchTypeOutbound, consts.BatchTypeAllocate} {
|
||||
batchNo := fmt.Sprintf("STATUS-%d", batchType)
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_main(batch_no,batch_type,batch_info) VALUES(?,?,'{}')", batchNo, batchType); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, status := range []int{0, 1, 2, 3, 4, 5} {
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(batch_no,batch_type,detail_status,row_no,item_id,error_reason,snapshot) VALUES(?,?,?,?,?,?,'{}')", batchNo, batchType, status, status+1, fmt.Sprintf("card-%d", status), fmt.Sprintf("reason-%d", status)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, status := range []int{0, 1, 2, 3, 4, 5} {
|
||||
rows, total, err := NewCommonBatchMain().ListCards(ctx, &model.BatchCardListParam{BatchNo: batchNo, DetailStatus: &status, PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil || total != 1 || len(rows) != 1 {
|
||||
t.Fatalf("type=%d status=%d total=%d err=%v", batchType, status, total, err)
|
||||
}
|
||||
if rows[0].DetailStatus != status || rows[0].ErrorReason != fmt.Sprintf("reason-%d", status) {
|
||||
t.Fatalf("incorrect detail: %+v", rows[0])
|
||||
}
|
||||
}
|
||||
for page, wantStatus := range []int{3, 0, 1, 2, 4, 5} {
|
||||
rows, total, err := NewCommonBatchMain().ListCards(ctx, &model.BatchCardListParam{BatchNo: batchNo, PageInfo: request.PageInfo{Page: page + 1, PageSize: 1}})
|
||||
if err != nil || total != 6 || len(rows) != 1 || rows[0].DetailStatus != wantStatus {
|
||||
t.Fatalf("type=%d page=%d status=%d total=%d err=%v", batchType, page+1, wantStatus, total, err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(batch_no,batch_type,detail_status,row_no,item_id,error_reason,snapshot) VALUES(?,?,?,?,?,?,'{}')", batchNo, batchType, consts.BatchDetailStatusFailed, 0, "cascade", "本批次存在失败明细,整批未执行"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, total, err := NewCommonBatchMain().ListCards(ctx, &model.BatchCardListParam{BatchNo: batchNo, PageInfo: request.PageInfo{Page: 1, PageSize: 1}})
|
||||
if err != nil || total != 7 || len(rows) != 1 || rows[0].Iccid != "card-3" {
|
||||
t.Fatalf("type=%d: real failure should precede legacy cascade: rows=%+v total=%d err=%v", batchType, rows, total, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/allocationunits"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
@@ -20,6 +23,7 @@ import (
|
||||
"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/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
@@ -94,6 +98,17 @@ func (s *sCommonBatchExecutionLog) ossModel(ctx context.Context, option ...*hand
|
||||
return handler.Model(dao.SysOss.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// exportTaskModel 导出任务表;导出批次的结果文件只登记在这里。
|
||||
func (s *sCommonBatchExecutionLog) exportTaskModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TExportTask.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) List(ctx context.Context, in *model.BatchExecutionLogListParam) (list []*model.BatchExecutionLogListModel, totalCount int, err error) {
|
||||
mod := s.batchModel(ctx).
|
||||
Fields(model.BatchExecutionLogListModel{})
|
||||
@@ -142,6 +157,8 @@ func (s *sCommonBatchExecutionLog) List(ctx context.Context, in *model.BatchExec
|
||||
}
|
||||
fields := []*allocationunits.Fields{}
|
||||
for _, item := range list {
|
||||
item.UnknownCount = batchExecutionLogUnknownCount(item.UnitInfo)
|
||||
item.RestartPhase, item.RestartResumeAt = batchRestartPhase(item.UnitInfo)
|
||||
if item.BatchType == consts.BatchTypeAllocate {
|
||||
item.Fields = allocationunits.Resolve(item.UnitInfo, item.CreatedDept, item.StoredTargetDeptId)
|
||||
fields = append(fields, &item.Fields)
|
||||
@@ -187,9 +204,11 @@ func (s *sCommonBatchExecutionLog) Summary(ctx context.Context, in *model.BatchE
|
||||
RunResult: batch.RunResult,
|
||||
FailReason: batch.FailReason,
|
||||
TotalCount: batch.TotalCount,
|
||||
UnknownCount: batchExecutionLogUnknownCount(batch.BatchInfo),
|
||||
StartTime: batch.StartTime,
|
||||
EndTime: batch.EndTime,
|
||||
}
|
||||
res.RestartPhase, res.RestartResumeAt = batchRestartPhase(batch.BatchInfo)
|
||||
|
||||
if batch.BatchType == consts.BatchTypeAllocate {
|
||||
res.Fields = allocationunits.Resolve(batch.BatchInfo, batch.CreatedDept, batch.TargetDeptId)
|
||||
@@ -204,11 +223,17 @@ func (s *sCommonBatchExecutionLog) Summary(ctx context.Context, in *model.BatchE
|
||||
res.SourceFile = sourceFile
|
||||
}
|
||||
|
||||
if resultFile, err := s.getResultFile(ctx, batch.BatchNo, batch.ResultOssId, batch.BatchInfo); err != nil {
|
||||
return nil, err
|
||||
// 导出任务的成品文件落在 t_export_task,不写 t_resource_batch_download,须分开取。
|
||||
var resultFile *model.BatchExecutionLogResultFileModel
|
||||
if batch.BatchType == consts.BatchTypeExport {
|
||||
resultFile, err = s.getExportResultFile(ctx, batch.BatchNo, batch.ResultOssId)
|
||||
} else {
|
||||
res.ResultFile = resultFile
|
||||
resultFile, err = s.getResultFile(ctx, batch.BatchNo, batch.ResultOssId, batch.BatchInfo)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.ResultFile = resultFile
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -263,7 +288,7 @@ func (s *sCommonBatchExecutionLog) DetailList(ctx context.Context, in *model.Bat
|
||||
}
|
||||
|
||||
var details []*entity.BatchDetail
|
||||
mod = mod.Page(in.Page, in.PageSize).OrderAsc(columns.RowNo)
|
||||
mod = orderBatchCards(mod.Page(in.Page, in.PageSize), batch.BatchType)
|
||||
if err = mod.ScanAndCount(&details, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取批次执行明细列表失败,请稍后重试")
|
||||
}
|
||||
@@ -328,6 +353,65 @@ func (s *sCommonBatchExecutionLog) DetailList(ctx context.Context, in *model.Bat
|
||||
return
|
||||
}
|
||||
|
||||
// dataReviewQueryMessage 通知 Worker 对既有 operationId 做一次只读补查。
|
||||
type dataReviewQueryMessage struct {
|
||||
BatchNo string `json:"batchNo"`
|
||||
RequestedBy string `json:"requestedBy,omitempty"`
|
||||
}
|
||||
|
||||
var publishDataReviewQuery = rabbitmq.Publish
|
||||
|
||||
// SyncReview 手动同步待核查批次的上游执行结果。
|
||||
// 只重查既有受理号(jobId),不重发办理请求、不清零自动补查计数;
|
||||
// 上游仍无结论的明细继续保持待核查。
|
||||
func (s *sCommonBatchExecutionLog) SyncReview(ctx context.Context, in *model.BatchExecutionLogSyncReviewParam) (res *model.BatchExecutionLogSyncReviewModel, err error) {
|
||||
if in == nil {
|
||||
in = &model.BatchExecutionLogSyncReviewParam{}
|
||||
}
|
||||
batchNo := strings.TrimSpace(in.BatchNo)
|
||||
reason := strings.TrimSpace(in.Reason)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("批次编号不能为空")
|
||||
}
|
||||
|
||||
columns := dao.BatchMain.Columns()
|
||||
batch := &entity.BatchMain{}
|
||||
// 沿用执行日志列表的可见性口径(部门权限过滤)。
|
||||
if err = s.batchModel(ctx).
|
||||
Fields(columns.Id, columns.BatchNo, columns.BatchType, columns.BatchAsyncStatus, columns.RunResult, columns.BatchInfo).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Scan(batch); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询批次信息失败")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, gerror.New("批次不存在或无权限")
|
||||
}
|
||||
if batch.BatchAsyncStatus != consts.BatchAsyncStatusReview {
|
||||
return nil, gerror.New("仅待核查批次支持手动同步上游结果")
|
||||
}
|
||||
unknownCount := batchExecutionLogUnknownCount(batch.BatchInfo)
|
||||
if unknownCount <= 0 {
|
||||
return nil, gerror.New("该批次没有待核查明细")
|
||||
}
|
||||
|
||||
message := dataReviewQueryMessage{BatchNo: batch.BatchNo, RequestedBy: contexts.GetUserName(ctx)}
|
||||
if err = publishDataReviewQuery(ctx, consts.QueueDataReviewQuery, message); err != nil {
|
||||
g.Log().Errorf(ctx, "手动同步上游结果消息发布失败, batchNo=%s, tenantId=%s, userId=%d, reason=%s, err=%+v",
|
||||
batch.BatchNo, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason, err)
|
||||
return nil, gerror.Wrap(err, "发布上游结果同步消息失败")
|
||||
}
|
||||
g.Log().Infof(ctx, "手动同步上游结果消息已发布, batchNo=%s, batchType=%d, unknownCount=%d, tenantId=%s, userId=%d, reason=%s",
|
||||
batch.BatchNo, batch.BatchType, unknownCount, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason)
|
||||
|
||||
return &model.BatchExecutionLogSyncReviewModel{
|
||||
BatchNo: batch.BatchNo,
|
||||
BatchType: batch.BatchType,
|
||||
BatchAsyncStatus: batch.BatchAsyncStatus,
|
||||
UnknownCount: unknownCount,
|
||||
Queued: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) getDetailBatch(ctx context.Context, batchNo string, batchType *int) (*entity.BatchMain, error) {
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("批次号不能为空")
|
||||
@@ -471,11 +555,13 @@ func (s *sCommonBatchExecutionLog) getSourceFile(ctx context.Context, sourceOssI
|
||||
|
||||
func (s *sCommonBatchExecutionLog) getResultFile(ctx context.Context, batchNo string, resultOssId int64, batchInfo *gjson.Json) (*model.BatchExecutionLogResultFileModel, error) {
|
||||
download := &entity.TResourceBatchDownload{}
|
||||
// 出库/划拨/导出等批次不写 t_resource_batch_download,查不到记录按「无结果文件」处理,
|
||||
// 不能当异常抛给页面(否则详情弹窗整体报 sql: no rows in result set)。
|
||||
if err := s.downloadModel(ctx).
|
||||
Where(dao.TResourceBatchDownload.Columns().BatchNo, batchNo).
|
||||
OrderDesc(dao.TResourceBatchDownload.Columns().Id).
|
||||
Limit(1).
|
||||
Scan(download); err != nil {
|
||||
Scan(download); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(err, "获取结果文件信息失败,请稍后重试!")
|
||||
}
|
||||
ossId := resultOssId
|
||||
@@ -490,14 +576,16 @@ func (s *sCommonBatchExecutionLog) getResultFile(ctx context.Context, batchNo st
|
||||
}
|
||||
|
||||
url := batchExecutionLogFileURL(ctx, download.FileUrl, download.FilePath)
|
||||
originalName := ""
|
||||
if ossId > 0 {
|
||||
oss := &entity.SysOss{}
|
||||
if err := s.ossModel(ctx).
|
||||
if scanErr := s.ossModel(ctx).
|
||||
Where(dao.SysOss.Columns().OssId, ossId).
|
||||
Scan(oss); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取结果文件OSS信息失败,请稍后重试!")
|
||||
Scan(oss); scanErr != nil && !errors.Is(scanErr, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(scanErr, "获取结果文件OSS信息失败,请稍后重试!")
|
||||
}
|
||||
if oss.OssId > 0 {
|
||||
originalName = oss.OriginalName
|
||||
if ossUrl := batchExecutionLogFileURL(ctx, oss.Url, oss.Path); ossUrl != "" {
|
||||
url = ossUrl
|
||||
}
|
||||
@@ -508,10 +596,49 @@ func (s *sCommonBatchExecutionLog) getResultFile(ctx context.Context, batchNo st
|
||||
DownloadId: download.Id,
|
||||
OssId: ossId,
|
||||
Url: url,
|
||||
OriginalName: originalName,
|
||||
DownloadStatus: resultDownloadStatus(download),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getExportResultFile 只暴露任务 ID;文件必须经下载中心重新校验本人、权限和有效期。
|
||||
func (s *sCommonBatchExecutionLog) getExportResultFile(ctx context.Context, taskNo string, _ int64) (*model.BatchExecutionLogResultFileModel, error) {
|
||||
task := &entity.TExportTask{}
|
||||
if err := s.exportTaskModel(ctx).
|
||||
Where(dao.TExportTask.Columns().TaskNo, taskNo).
|
||||
Where(dao.TExportTask.Columns().OwnerUserId, contexts.GetUserId(ctx)).
|
||||
Scan(task); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(err, "获取结果文件信息失败,请稍后重试!")
|
||||
}
|
||||
if task.Id <= 0 || task.DeletedTime != nil || task.Status != consts.DownloadStatusGenerated || task.OssId <= 0 ||
|
||||
(task.ExpireTime != nil && task.ExpireTime.Before(gtime.Now())) || !hasExportPermission(ctx, task.PermCode) {
|
||||
return nil, nil
|
||||
}
|
||||
return &model.BatchExecutionLogResultFileModel{
|
||||
ExportTaskId: task.Id,
|
||||
OriginalName: task.FileName,
|
||||
DownloadStatus: consts.DownloadStatusGenerated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func batchExecutionLogUnknownCount(batchInfo *gjson.Json) int {
|
||||
if batchInfo == nil {
|
||||
return 0
|
||||
}
|
||||
return batchInfo.Get("apnUnknownCount").Int()
|
||||
}
|
||||
|
||||
// batchRestartPhase 从 batch_info 取出重启阶段与计划复机时间。
|
||||
// 重启批次停机完成后要等待若干分钟再复机,这段时间主状态仍是处理中,
|
||||
// 明细却已落终态;前端据此显示「等待复机」,避免误以为卡住或已办结。
|
||||
func batchRestartPhase(batchInfo *gjson.Json) (phase, resumeAt string) {
|
||||
if batchInfo == nil {
|
||||
return "", ""
|
||||
}
|
||||
return strings.TrimSpace(batchInfo.Get("restart.phase").String()),
|
||||
strings.TrimSpace(batchInfo.Get("restart.resumeAt").String())
|
||||
}
|
||||
|
||||
func legacySourceOssId(batchInfo *gjson.Json) int64 {
|
||||
if batchInfo == nil {
|
||||
return 0
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestBatchExecutionLogDetailListPreservesUnknownStatusAndReason(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "DATA-REVIEW", consts.BatchTypeDataStart, consts.BatchSourceTypeBatchInput, 1, "test-review-card")
|
||||
const reason = "上游已受理,补查超时,执行结果待核查"
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE batch_detail SET detail_status = ?, error_reason = ? WHERE batch_no = ?", consts.BatchDetailStatusUnknown, reason, "DATA-REVIEW"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status := consts.BatchDetailStatusUnknown
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
BatchNo: "DATA-REVIEW", DetailStatus: &status,
|
||||
})
|
||||
if err != nil || total != 1 || len(list) != 1 {
|
||||
t.Fatalf("读取待核查明细失败:total=%d,明细数=%d,错误:%v", total, len(list), err)
|
||||
}
|
||||
if list[0].DetailStatus != consts.BatchDetailStatusUnknown || list[0].Error == nil || list[0].Error.Reason != reason {
|
||||
t.Fatalf("待核查状态或具体原因未保留:%+v", list[0])
|
||||
}
|
||||
status = consts.BatchDetailStatusFailed
|
||||
list, total, err = NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
BatchNo: "DATA-REVIEW", DetailStatus: &status,
|
||||
})
|
||||
if err != nil || total != 0 || len(list) != 0 {
|
||||
t.Fatalf("待核查明细被计入失败筛选:total=%d,明细数=%d,错误:%v", total, len(list), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogListPreservesReviewStatusAndResult(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE batch_main ADD batch_async_status INTEGER NOT NULL DEFAULT 0"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_main(batch_no, batch_type, batch_async_status, run_result) VALUES (?, ?, ?, ?), (?, ?, ?, ?)",
|
||||
"DATA-REVIEW", consts.BatchTypeDataStart, consts.BatchAsyncStatusReview, consts.BatchRunResultReview,
|
||||
"DATA-FAILED", consts.BatchTypeDataStop, consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status := consts.BatchAsyncStatusReview
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{
|
||||
BatchAsyncStatus: &status, PageInfo: request.PageInfo{Page: 1, PageSize: 20},
|
||||
})
|
||||
if err != nil || total != 1 || len(list) != 1 {
|
||||
t.Fatalf("读取待核查批次失败:total=%d,批次数=%d,错误:%v", total, len(list), err)
|
||||
}
|
||||
if list[0].BatchNo != "DATA-REVIEW" || list[0].BatchAsyncStatus != consts.BatchAsyncStatusReview || list[0].RunResult != consts.BatchRunResultReview {
|
||||
t.Fatalf("待核查父状态或运行结果未保留:%+v", list[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogUnknownCountFromBatchInfo(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
`ALTER TABLE batch_main ADD COLUMN target_dept_id INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE batch_main ADD COLUMN batch_async_status INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE batch_main ADD COLUMN fail_reason TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE batch_main ADD COLUMN source_oss_id INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE batch_main ADD COLUMN result_oss_id INTEGER NOT NULL DEFAULT 0`,
|
||||
`CREATE TABLE t_resource_batch_download (id INTEGER PRIMARY KEY, batch_no TEXT)`,
|
||||
`INSERT INTO t_resource_batch_download(id, batch_no) VALUES (1, 'DATA-REVIEW'), (2, 'DATA-LEGACY')`,
|
||||
`INSERT INTO batch_main(batch_no, batch_type, batch_info, total_count) VALUES ('DATA-REVIEW', 109, '{"apnUnknownCount":2}', 3), ('DATA-LEGACY', 109, '{}', 1)`,
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil || total != 2 || len(list) != 2 {
|
||||
t.Fatalf("读取未知计数列表失败: total=%d len=%d err=%v", total, len(list), err)
|
||||
}
|
||||
counts := map[string]int{}
|
||||
for _, item := range list {
|
||||
counts[item.BatchNo] = item.UnknownCount
|
||||
}
|
||||
if counts["DATA-REVIEW"] != 2 || counts["DATA-LEGACY"] != 0 {
|
||||
t.Fatalf("列表待核查数错误: %+v", counts)
|
||||
}
|
||||
for batchNo, want := range map[string]int{"DATA-REVIEW": 2, "DATA-LEGACY": 0} {
|
||||
summary, err := NewCommonBatchExecutionLog().Summary(ctx, &model.BatchExecutionLogDetailParam{BatchNo: batchNo})
|
||||
if err != nil || summary == nil || summary.UnknownCount != want {
|
||||
t.Fatalf("概要 %s 待核查数错误: summary=%+v err=%v", batchNo, summary, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogResultFilePreservesOriginalName(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
`CREATE TABLE t_resource_batch_download (id INTEGER PRIMARY KEY, batch_no TEXT, oss_id INTEGER, file_url TEXT, file_path TEXT)`,
|
||||
`CREATE TABLE sys_oss (oss_id INTEGER PRIMARY KEY, original_name TEXT, url TEXT, path TEXT)`,
|
||||
`INSERT INTO t_resource_batch_download(id, batch_no, oss_id, file_url, file_path) VALUES (1, 'DATA-REVIEW', 1, '/upload/DATA-REVIEW-result-nonce.xlsx', ''), (2, 'DATA-LEGACY', 2, '/upload/legacy.xlsx', '')`,
|
||||
`INSERT INTO sys_oss(oss_id, original_name, url, path) VALUES (1, 'DATA-REVIEW-result.xlsx', '/upload/DATA-REVIEW-result-nonce.xlsx', ''), (2, '', '/upload/legacy.xlsx', '')`,
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for batchNo, want := range map[string]string{"DATA-REVIEW": "DATA-REVIEW-result.xlsx", "DATA-LEGACY": ""} {
|
||||
result, err := NewCommonBatchExecutionLog().getResultFile(ctx, batchNo, 0, nil)
|
||||
if err != nil || result == nil || result.OriginalName != want {
|
||||
t.Fatalf("原始下载名未透传: batch=%s result=%+v err=%v", batchNo, result, err)
|
||||
}
|
||||
encoded, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var response map[string]any
|
||||
if err := json.Unmarshal(encoded, &response); err != nil || response["originalName"] != want {
|
||||
t.Fatalf("响应JSON丢失原始下载名: %s err=%v", encoded, err)
|
||||
}
|
||||
if batchNo == "DATA-REVIEW" && result.Url != "/upload/DATA-REVIEW-result-nonce.xlsx" {
|
||||
t.Fatalf("原始下载名影响了物理文件定位: %s", result.Url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogListReturnsTotalCount(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(
|
||||
batch_no, batch_type, source_type, total_count, run_result,
|
||||
created_dept, created_by, tenant_id
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, "OUT-1", 2, 0, 7, 2, 0, 0, ""); err != nil {
|
||||
t.Fatalf("insert batch: %v", err)
|
||||
}
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{
|
||||
PageInfo: request.PageInfo{Page: 1, PageSize: 20},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list execution logs: %v", err)
|
||||
}
|
||||
if total != 1 {
|
||||
t.Fatalf("total rows = %d, want 1", total)
|
||||
}
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("list length = %d, want 1", len(list))
|
||||
}
|
||||
if list[0].TotalCount != 7 {
|
||||
t.Fatalf("totalCount = %d, want 7", list[0].TotalCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocateDetailInheritsHistoricalUnits(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
`ALTER TABLE batch_main ADD target_dept_id INTEGER DEFAULT 0`,
|
||||
`INSERT INTO batch_main(batch_no,batch_type,batch_info,created_dept,target_dept_id) VALUES('ALC-HISTORY',3,'{"sourceDeptId":10,"targetDeptId":20,"sourceDeptName":"历史划拨单位","targetDeptName":"历史接收单位"}',10,20)`,
|
||||
`INSERT INTO batch_detail(batch_no,batch_type,item_id,snapshot) VALUES('ALC-HISTORY',3,'card','{}')`,
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
rows, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ALC-HISTORY"})
|
||||
if err != nil || total != 1 || len(rows) != 1 {
|
||||
t.Fatal("读取历史明细失败", total, err)
|
||||
}
|
||||
if rows[0].SourceDeptName != "历史划拨单位" || rows[0].TargetDeptName != "历史接收单位" {
|
||||
t.Fatal("历史明细未优先继承批次快照", rows[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListNormalizesFileRowNo(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "INB-1", consts.BatchTypeInbound, consts.BatchSourceTypeFile, 2, "89860000000000000001")
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
BatchNo: "INB-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("detail list: %v", err)
|
||||
}
|
||||
if total != 1 {
|
||||
t.Fatalf("total rows = %d, want 1", total)
|
||||
}
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("list length = %d, want 1", len(list))
|
||||
}
|
||||
if list[0].RowNo != 1 {
|
||||
t.Fatalf("file batch rowNo = %d, want 1", list[0].RowNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListNormalizesManualRowNo(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "INB-2", consts.BatchTypeInbound, consts.BatchSourceTypeBatchInput, 2, "89860000000000000002")
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
BatchNo: "INB-2",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("detail list: %v", err)
|
||||
}
|
||||
if total != 1 {
|
||||
t.Fatalf("total rows = %d, want 1", total)
|
||||
}
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("list length = %d, want 1", len(list))
|
||||
}
|
||||
if list[0].RowNo != 1 {
|
||||
t.Fatalf("manual batch rowNo = %d, want 1", list[0].RowNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListNumbersPages(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "ROWS", consts.BatchTypeInbound, consts.BatchSourceTypeBatchSelect, 2, "first")
|
||||
_, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(batch_no, batch_type, detail_status, row_no, item_id) VALUES (?, ?, ?, ?, ?)", "ROWS", consts.BatchTypeInbound, consts.BatchDetailStatusFailed, 9, "second")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ROWS", PageInfo: request.PageInfo{Page: 2, PageSize: 1}})
|
||||
if err != nil || total != 2 || len(list) != 1 || list[0].RowNo != 2 || list[0].Iccid != "first" {
|
||||
t.Fatalf("paged rows: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
list, total, err = NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ROWS", PageInfo: request.PageInfo{Page: 1, PageSize: 1}})
|
||||
if err != nil || total != 2 || len(list) != 1 || list[0].Iccid != "second" {
|
||||
t.Fatalf("failed card not first: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
failed := consts.BatchDetailStatusFailed
|
||||
list, total, err = NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ROWS", DetailStatus: &failed, PageInfo: request.PageInfo{Page: 1, PageSize: 1}})
|
||||
if err != nil || total != 1 || len(list) != 1 || list[0].RowNo != 1 {
|
||||
t.Fatalf("filtered rows: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListPrioritizesRealFailureOverCascade(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "PRIORITY", consts.BatchTypeAllocate, consts.BatchSourceTypeBatchInput, 1, "cascade")
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE batch_detail SET detail_status = ?, error_reason = ? WHERE batch_no = ?", consts.BatchDetailStatusFailed, "本批次存在失败明细,整批未执行", "PRIORITY"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(batch_no,batch_type,detail_status,row_no,item_id,error_reason) VALUES(?,?,?,?,?,?)", "PRIORITY", consts.BatchTypeAllocate, consts.BatchDetailStatusFailed, 2, "actual-error", "ICCID不在当前操作员授权范围内"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for page, want := range []string{"actual-error", "cascade"} {
|
||||
rows, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "PRIORITY", PageInfo: request.PageInfo{Page: page + 1, PageSize: 1}})
|
||||
if err != nil || total != 2 || len(rows) != 1 || rows[0].Iccid != want {
|
||||
t.Fatalf("page=%d want=%s rows=%+v total=%d err=%v", page+1, want, rows, total, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupBatchExecutionLogListDB(t *testing.T) context.Context {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
if err := gdb.SetConfig(gdb.Config{
|
||||
"default": gdb.ConfigGroup{{
|
||||
Type: "sqlite",
|
||||
Link: "sqlite::@file(:memory:)",
|
||||
MaxOpenConnCount: 1,
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = g.DB().Close(ctx)
|
||||
})
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
CREATE TABLE batch_main (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL,
|
||||
batch_type INTEGER NOT NULL,
|
||||
source_batch_no TEXT NOT NULL DEFAULT '',
|
||||
source_type INTEGER,
|
||||
source_channel TEXT NOT NULL DEFAULT 'legacy', execution_mode INTEGER NOT NULL DEFAULT 0,
|
||||
batch_info TEXT,
|
||||
total_count INTEGER NOT NULL DEFAULT 0,
|
||||
run_result INTEGER NOT NULL DEFAULT 0,
|
||||
start_time TEXT,
|
||||
end_time TEXT,
|
||||
created_dept INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT,
|
||||
tenant_id TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
CREATE TABLE batch_detail (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL,
|
||||
batch_type INTEGER NOT NULL,
|
||||
detail_status INTEGER NOT NULL DEFAULT 0,
|
||||
row_no INTEGER NOT NULL DEFAULT 0,
|
||||
item_id TEXT NOT NULL DEFAULT '',
|
||||
error_reason TEXT NOT NULL DEFAULT '',
|
||||
snapshot TEXT,
|
||||
tenant_id TEXT NOT NULL DEFAULT '',
|
||||
created_dept INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogListHidesAllocateChildBatches(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(
|
||||
batch_no, batch_type, source_batch_no, source_type, batch_info, total_count,
|
||||
run_result, created_dept, created_by, tenant_id
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
"ALC-PARENT", consts.BatchTypeAllocate, "", consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_parent"}`, 2, consts.BatchRunResultRunning, 0, 0, "",
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, "ALC-PARENT", consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_chunk"}`, 2, consts.BatchRunResultRunning, 0, 0, "",
|
||||
); err != nil {
|
||||
t.Fatalf("insert allocate batches: %v", err)
|
||||
}
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{
|
||||
PageInfo: request.PageInfo{Page: 1, PageSize: 20},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list execution logs: %v", err)
|
||||
}
|
||||
if total != 1 || len(list) != 1 {
|
||||
t.Fatalf("allocate child batch was not hidden: total=%d list=%d", total, len(list))
|
||||
}
|
||||
if list[0].BatchNo != "ALC-PARENT" {
|
||||
t.Fatalf("visible batch = %q, want parent", list[0].BatchNo)
|
||||
}
|
||||
if _, err := NewCommonBatchExecutionLog().Summary(ctx, &model.BatchExecutionLogDetailParam{BatchNo: "ALC-CHILD"}); err == nil {
|
||||
t.Fatal("allocate child was exposed by summary")
|
||||
}
|
||||
if _, _, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ALC-CHILD"}); err == nil {
|
||||
t.Fatal("allocate child was exposed by detail list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListShowsAllocateChildFailureReason(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(batch_no, batch_type, source_batch_no, source_type, batch_info, total_count, run_result, created_dept, created_by, tenant_id)
|
||||
VALUES (?, ?, '', ?, ?, 2, ?, 0, 0, ''), (?, ?, ?, ?, ?, 2, ?, 0, 0, '')
|
||||
`, "ALC-PARENT", consts.BatchTypeAllocate, consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_parent"}`, consts.BatchRunResultFailed,
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, "ALC-PARENT", consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_chunk"}`, consts.BatchRunResultFailed); err != nil {
|
||||
t.Fatalf("insert allocate batches: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_detail(batch_no, batch_type, detail_status, row_no, item_id, error_reason, snapshot, tenant_id, created_dept, created_by)
|
||||
VALUES (?, ?, ?, 1, '89860000000000000001', ?, '{}', '', 0, 0), (?, ?, ?, 2, '89860000000000000002', '', '{}', '', 0, 0)
|
||||
`, "ALC-CHILD", consts.BatchTypeAllocate, consts.BatchDetailStatusFailed, "套餐已过期,不能划拨",
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, consts.BatchDetailStatusSuccess); err != nil {
|
||||
t.Fatalf("insert allocate details: %v", err)
|
||||
}
|
||||
failed := consts.BatchDetailStatusFailed
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
PageInfo: request.PageInfo{Page: 1, PageSize: 20},
|
||||
BatchNo: "ALC-PARENT",
|
||||
DetailStatus: &failed,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list parent failure details: %v", err)
|
||||
}
|
||||
if total != 1 || len(list) != 1 {
|
||||
t.Fatalf("failed detail count = %d, list length = %d, want 1", total, len(list))
|
||||
}
|
||||
if list[0].Iccid != "89860000000000000001" || list[0].Error == nil || list[0].Error.Reason != "套餐已过期,不能划拨" {
|
||||
t.Fatalf("unexpected failure detail: %+v", list[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListAggregatesAllAllocateChildren(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(batch_no, batch_type, source_batch_no, source_type, batch_info, total_count, run_result, created_dept, created_by, tenant_id)
|
||||
VALUES (?, ?, '', ?, ?, 1, ?, 0, 0, ''), (?, ?, ?, ?, ?, 1, ?, 0, 0, ''), (?, ?, ?, ?, ?, 1, ?, 0, 0, '')
|
||||
`, "ALC-PARENT", consts.BatchTypeAllocate, consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_parent"}`, consts.BatchRunResultFailed,
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, "ALC-PARENT", consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_chunk"}`, consts.BatchRunResultFailed,
|
||||
"ALC-RELATED", consts.BatchTypeAllocate, "ALC-PARENT", consts.BatchSourceTypeBatchInput, `{}`, consts.BatchRunResultFailed); err != nil {
|
||||
t.Fatalf("insert allocate batches: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_detail(batch_no, batch_type, detail_status, row_no, item_id, error_reason, snapshot, tenant_id, created_dept, created_by)
|
||||
VALUES (?, ?, ?, 1, 'parent-card', '', '{}', '', 0, 0), (?, ?, ?, 1, 'chunk-card', '', '{}', '', 0, 0), (?, ?, ?, 1, 'related-card', '', '{}', '', 0, 0)
|
||||
`, "ALC-PARENT", consts.BatchTypeAllocate, consts.BatchDetailStatusSuccess,
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, consts.BatchDetailStatusSuccess,
|
||||
"ALC-RELATED", consts.BatchTypeAllocate, consts.BatchDetailStatusSuccess); err != nil {
|
||||
t.Fatalf("insert batch details: %v", err)
|
||||
}
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ALC-PARENT"})
|
||||
if err != nil || total != 3 || len(list) != 3 {
|
||||
t.Fatalf("unexpected parent details: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, item := range list {
|
||||
seen[item.Iccid] = true
|
||||
}
|
||||
for _, iccid := range []string{"parent-card", "chunk-card", "related-card"} {
|
||||
if !seen[iccid] {
|
||||
t.Fatalf("parent details missing %s: %+v", iccid, list)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestBatchExecutionLogSelfReferencedParentAggregatesChildren(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(batch_no, batch_type, source_batch_no, batch_info)
|
||||
VALUES ('SELF-PARENT', 3, 'SELF-PARENT', '{}'), ('SELF-CHILD', 3, 'SELF-PARENT', '{}')
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_detail(batch_no, batch_type, detail_status, row_no, item_id, snapshot)
|
||||
VALUES ('SELF-PARENT', 3, 2, 1, 'parent-card', '{}'), ('SELF-CHILD', 3, 2, 2, 'child-card', '{}')
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "SELF-PARENT"})
|
||||
if err != nil || total != 2 || len(list) != 2 || list[0].Iccid != "parent-card" || list[1].Iccid != "child-card" {
|
||||
t.Fatalf("self-referenced parent aggregation: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
}
|
||||
|
||||
func insertBatchExecutionLogDetail(t *testing.T, ctx context.Context, batchNo string, batchType, sourceType, rowNo int, itemID string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(
|
||||
batch_no, batch_type, source_type, total_count, run_result,
|
||||
created_dept, created_by, tenant_id
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, batchNo, batchType, sourceType, 1, 2, 0, 0, ""); err != nil {
|
||||
t.Fatalf("insert batch main: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_detail(
|
||||
batch_no, batch_type, detail_status, row_no, item_id, snapshot,
|
||||
tenant_id, created_dept, created_by
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, batchNo, batchType, consts.BatchDetailStatusSuccess, rowNo, itemID, `{"ICCID":"`+itemID+`"}`, "", 0, 0); err != nil {
|
||||
t.Fatalf("insert batch detail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 出库等批次不写 t_resource_batch_download,详情查询查不到记录时
|
||||
// 必须按「无结果文件」返回,而不是把 sql: no rows in result set 抛给页面。
|
||||
func TestBatchExecutionLogSummaryTreatsMissingDownloadAsNoResultFile(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
CREATE TABLE t_resource_batch_download (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL DEFAULT '',
|
||||
oss_id INTEGER NOT NULL DEFAULT 0,
|
||||
file_url TEXT NOT NULL DEFAULT '',
|
||||
file_path TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT,
|
||||
tenant_id TEXT NOT NULL DEFAULT '',
|
||||
created_dept INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`); err != nil {
|
||||
t.Fatalf("create download table: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(
|
||||
batch_no, batch_type, source_type, batch_info, total_count,
|
||||
run_result, created_dept, created_by, tenant_id
|
||||
) VALUES(?, ?, ?, ?, 0, ?, 0, 0, '')
|
||||
`, "OUT-NOFILE", 2, 0, `{}`, consts.BatchRunResultSuccess); err != nil {
|
||||
t.Fatalf("insert outbound batch: %v", err)
|
||||
}
|
||||
|
||||
res, err := NewCommonBatchExecutionLog().Summary(ctx, &model.BatchExecutionLogDetailParam{BatchNo: "OUT-NOFILE"})
|
||||
if err != nil {
|
||||
t.Fatalf("summary should not fail when result file is absent: %v", err)
|
||||
}
|
||||
if res == nil {
|
||||
t.Fatal("summary returned nil result")
|
||||
}
|
||||
if res.ResultFile != nil {
|
||||
t.Fatalf("resultFile = %v, want nil when no download record exists", res.ResultFile)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/allocationunits"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
@@ -87,6 +88,8 @@ func validateRequeueBatch(batch *entity.BatchMain) error {
|
||||
return nil
|
||||
case consts.BatchAsyncStatusProcessing:
|
||||
return validateProcessingRequeueBatch(batch)
|
||||
case consts.BatchAsyncStatusReview:
|
||||
return gerror.New("待核查批次不支持安全重投,请先核对上游执行结果")
|
||||
case consts.BatchAsyncStatusFailed, consts.BatchAsyncStatusSuccess, consts.BatchAsyncStatusPartialSuccess:
|
||||
return gerror.New("终态批次不支持安全重投")
|
||||
default:
|
||||
@@ -248,7 +251,17 @@ func (s *sCommonBatchMain) SuccessList(ctx context.Context, in *model.BatchMainS
|
||||
if in.BatchType != 0 {
|
||||
requestedBatchType = &in.BatchType
|
||||
}
|
||||
mod = visibleBatchTypeModel(mod, columns.BatchType, requestedBatchType)
|
||||
mod = successListBatchTypeModel(mod, columns.BatchType, requestedBatchType)
|
||||
|
||||
if chanKey := strings.TrimSpace(in.ChanKey); chanKey != "" {
|
||||
extract := "JSON_UNQUOTE(JSON_EXTRACT(%s, '%s'))"
|
||||
if strings.EqualFold(dao.BatchMain.DB().GetConfig().Type, "sqlite") {
|
||||
extract = "JSON_EXTRACT(%s, '%s')"
|
||||
}
|
||||
nested := fmt.Sprintf(extract, columns.BatchInfo, "$.inboundOptions.chanKey")
|
||||
legacy := fmt.Sprintf(extract, columns.BatchInfo, "$.chanKey")
|
||||
mod = mod.Where(fmt.Sprintf("CASE WHEN JSON_VALID(%s) THEN COALESCE(NULLIF(TRIM(%s), ''), TRIM(%s)) END = ?", columns.BatchInfo, nested, legacy), chanKey)
|
||||
}
|
||||
|
||||
if in.BatchNo != "" {
|
||||
mod = mod.Where(columns.BatchNo+" LIKE ?", "%"+in.BatchNo+"%")
|
||||
@@ -343,6 +356,7 @@ func (s *sCommonBatchMain) ListCards(ctx context.Context, in *model.BatchCardLis
|
||||
}
|
||||
|
||||
details, totalCount, err := s.listBatchDetails(ctx, batch, batchDetailQuery{
|
||||
DetailStatus: in.DetailStatus,
|
||||
BatchNo: in.BatchNo,
|
||||
BatchType: in.BatchType,
|
||||
Iccid: in.Iccid,
|
||||
@@ -370,6 +384,7 @@ func (s *sCommonBatchMain) ListCards(ctx context.Context, in *model.BatchCardLis
|
||||
Msisdn: msisdn,
|
||||
Imsi: imsi,
|
||||
DetailStatus: detail.DetailStatus,
|
||||
ErrorReason: detail.ErrorReason,
|
||||
}
|
||||
if batch.BatchType == consts.BatchTypeAllocate {
|
||||
item.Fields = allocationunits.Resolve(detail.Snapshot, parentUnits.SourceDeptId, parentUnits.TargetDeptId)
|
||||
@@ -439,13 +454,22 @@ func (s *sCommonBatchMain) listBatchDetails(ctx context.Context, batch *entity.B
|
||||
mod = onlyCurrentStockDetails(mod)
|
||||
}
|
||||
|
||||
mod = mod.Page(query.Page, query.PageSize).OrderAsc(columns.RowNo)
|
||||
mod = orderBatchCards(mod.Page(query.Page, query.PageSize), batch.BatchType)
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取批次明细列表失败,请稍后重试")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func orderBatchCards(mod *gdb.Model, batchType int) *gdb.Model {
|
||||
columns := dao.BatchDetail.Columns()
|
||||
if batchType == consts.BatchTypeInbound || batchType == consts.BatchTypeOutbound || batchType == consts.BatchTypeAllocate {
|
||||
order := fmt.Sprintf("CASE WHEN %s = %d AND COALESCE(%s, '') NOT LIKE '本批%%整批未执行%%' AND COALESCE(%s, '') NOT LIKE '同批%%本行未执行%%' AND COALESCE(%s, '') NOT LIKE '同批%%整批未入库%%' THEN 0 ELSE 1 END, %s ASC, %s ASC", columns.DetailStatus, consts.BatchDetailStatusFailed, columns.ErrorReason, columns.ErrorReason, columns.ErrorReason, columns.RowNo, columns.Id)
|
||||
return mod.Order(gdb.Raw(order))
|
||||
}
|
||||
return mod.OrderAsc(columns.RowNo).OrderAsc(columns.Id)
|
||||
}
|
||||
|
||||
func onlyCurrentStockDetails(mod *gdb.Model) *gdb.Model {
|
||||
detailTable := dao.BatchDetail.Table()
|
||||
stockTable := dao.TSimCardStock.Table()
|
||||
@@ -508,6 +532,41 @@ func batchDetailSnapshotCards(detail *entity.BatchDetail) (iccid, msisdn, imsi s
|
||||
return
|
||||
}
|
||||
|
||||
// ExportWarehousing 导出入库批次列表(异步:提交任务后到下载中心取文件)。
|
||||
// 数据来源与入库管理页保持一致:成功批次列表(batch_type=入库)。
|
||||
func (s *sCommonBatchMain) ExportWarehousing(ctx context.Context, in *model.BatchMainSuccessListParam) (err error) {
|
||||
if in == nil {
|
||||
in = &model.BatchMainSuccessListParam{}
|
||||
}
|
||||
// 与页面 WarehousingList 一致:固定只导入库批次,不接受调用方传其它类型
|
||||
in.BatchType = consts.BatchTypeInbound
|
||||
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "batchMainWarehousing",
|
||||
BizName: "入库管理",
|
||||
PermCode: "cpm:iot:tSimCardStock:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.BatchMainSuccessListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.SuccessList(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Export 导出批次主表
|
||||
func (s *sCommonBatchMain) Export(ctx context.Context, in *model.BatchMainListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
)
|
||||
|
||||
func TestInboundBatchChannelFiltering(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
"ALTER TABLE batch_main ADD target_dept_id INTEGER DEFAULT 0",
|
||||
"CREATE TABLE t_sim_card_stock(batch_no TEXT,tenant_id TEXT)",
|
||||
"INSERT INTO t_sim_card_stock VALUES('A-NESTED',''),('A-NESTED','')",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
fixtures := []struct {
|
||||
name string
|
||||
info string
|
||||
batchType int
|
||||
runResult int
|
||||
}{
|
||||
{"A-NESTED", `{"inboundOptions":{"chanKey":"A"}}`, 1, consts.BatchRunResultSuccess},
|
||||
{"A-LEGACY", `{"chanKey":"A"}`, 1, consts.BatchRunResultSuccess},
|
||||
{"B-NESTED", `{"inboundOptions":{"chanKey":"B"},"chanKey":"A"}`, 1, consts.BatchRunResultSuccess},
|
||||
{"A-EMPTY-NESTED", `{"inboundOptions":{"chanKey":""},"chanKey":"A"}`, 1, consts.BatchRunResultSuccess},
|
||||
{"NO-CHANNEL", "{}", 1, consts.BatchRunResultSuccess},
|
||||
{"INVALID-JSON", "broken", 1, consts.BatchRunResultSuccess},
|
||||
{"OTHER-TYPE", `{"chanKey":"A"}`, 2, consts.BatchRunResultSuccess},
|
||||
{"NOT-SUCCESS", `{"chanKey":"A"}`, 1, consts.BatchRunResultFailed},
|
||||
}
|
||||
for _, fixture := range fixtures {
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_main(batch_no,batch_type,batch_info,total_count,run_result) VALUES(?,?,?,?,?)", fixture.name, fixture.batchType, fixture.info, 2, fixture.runResult); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
channel string
|
||||
page int
|
||||
size int
|
||||
wantTotal int
|
||||
wantRows int
|
||||
}{
|
||||
{"渠道A兼容新旧格式", "A", 1, 20, 3, 3},
|
||||
{"渠道B优先使用嵌套编码", "B", 1, 20, 1, 1},
|
||||
{"清空恢复全部入库批次", "", 1, 20, 6, 6},
|
||||
{"前后空白不影响筛选", " A ", 1, 20, 3, 3},
|
||||
{"总数按渠道过滤后分页", "A", 2, 2, 3, 1},
|
||||
{"不存在的渠道返回空", "missing", 1, 20, 0, 0},
|
||||
{"特殊字符作为参数而不是SQL", "A' OR 1=1 --", 1, 20, 0, 0},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rows, total, err := NewCommonBatchMain().SuccessList(ctx, &model.BatchMainSuccessListParam{BatchType: consts.BatchTypeInbound, ChanKey: test.channel, PageInfo: request.PageInfo{Page: test.page, PageSize: test.size}})
|
||||
if err != nil || total != test.wantTotal || len(rows) != test.wantRows {
|
||||
t.Fatalf("total=%d rows=%d err=%v", total, len(rows), err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row.BatchNo == "A-NESTED" && row.StockCount != 2 {
|
||||
t.Fatalf("stock count changed: %+v", row)
|
||||
}
|
||||
if test.channel == "B" && row.BatchNo != "B-NESTED" {
|
||||
t.Fatalf("nested channel priority violated: %+v", row)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestBatchMainEditRejectsMissingCreationMetadata(t *testing.T) {
|
||||
for _, mode := range []int{batchmetadata.Unknown, -1, 3} {
|
||||
if err := NewCommonBatchMain().Edit(context.Background(), &model.BatchMainEditParam{ExecutionMode: &mode}); err == nil {
|
||||
t.Fatalf("accepted execution mode %d", mode)
|
||||
}
|
||||
}
|
||||
for _, input := range []*model.BatchMainEditParam{nil, {}} {
|
||||
if err := NewCommonBatchMain().Edit(context.Background(), input); err == nil {
|
||||
t.Fatal("accepted missing creation metadata")
|
||||
}
|
||||
}
|
||||
mode := batchmetadata.Single
|
||||
quantity := 2
|
||||
if err := NewCommonBatchMain().Edit(context.Background(), &model.BatchMainEditParam{ExecutionMode: &mode, TotalCount: &quantity}); err == nil {
|
||||
t.Fatal("accepted multiple cards as a single-card batch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchMainEditWritesTrustedCreationMetadata(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, column := range []string{"updated_by INTEGER", "updated_at TEXT"} {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE batch_main ADD COLUMN "+column); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fields, err := g.Model("batch_main").Ctx(ctx).TableFields("batch_main")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fields["id"].Key = "PRI"
|
||||
if _, err := g.DB().GetCore().GetTablesWithCache(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx = contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 7, TenantId: "A"}}})
|
||||
batchType := consts.BatchTypeInbound
|
||||
quantity := 1
|
||||
claimedUser := int64(999)
|
||||
claimedTenant := "B"
|
||||
for _, mode := range []int{batchmetadata.Single, batchmetadata.Bulk} {
|
||||
batchNo := "META-EDIT-SINGLE"
|
||||
if mode == batchmetadata.Bulk {
|
||||
batchNo = "META-EDIT-BULK"
|
||||
}
|
||||
input := &model.BatchMainEditParam{BatchNo: &batchNo, BatchType: &batchType, ExecutionMode: &mode, TotalCount: &quantity, CreatedBy: &claimedUser, TenantId: &claimedTenant}
|
||||
if err := NewCommonBatchMain().Edit(ctx, input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var batch *entity.BatchMain
|
||||
if err := g.Model("batch_main").Ctx(ctx).Where("batch_no", batchNo).Scan(&batch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if batch == nil || batch.SourceChannel != batchmetadata.Console || batch.ExecutionMode != uint(mode) || batch.CreatedBy != 7 || batch.TenantId != "A" {
|
||||
t.Fatalf("unexpected batch: %+v", batch)
|
||||
}
|
||||
otherMode := 3 - mode
|
||||
if err := NewCommonBatchMain().Edit(ctx, &model.BatchMainEditParam{Id: batch.Id, BatchNo: &batchNo, ExecutionMode: &otherMode}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var updated *entity.BatchMain
|
||||
if err := g.Model("batch_main").Ctx(ctx).Where("id", batch.Id).Scan(&updated); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if updated.SourceChannel != batchmetadata.Console || updated.ExecutionMode != uint(mode) {
|
||||
t.Fatalf("editing changed attribution: %+v", updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func TestBatchMainRequeuePublishesPendingBatch(t *testing.T) {
|
||||
ctx := setupBatchMainRequeueDB(t)
|
||||
insertRequeueBatch(t, ctx, "PENDING-1", consts.BatchTypeInbound, consts.BatchAsyncStatusPending, consts.BatchRunResultPending, `{}`)
|
||||
|
||||
var published []batchExecuteMessage
|
||||
restore := replaceBatchRequeuePublisher(func(_ context.Context, queue string, data any) error {
|
||||
if queue != consts.QueueBatchExecute {
|
||||
t.Fatalf("queue = %s, want %s", queue, consts.QueueBatchExecute)
|
||||
}
|
||||
message, ok := data.(batchExecuteMessage)
|
||||
if !ok {
|
||||
t.Fatalf("message type = %T, want batchExecuteMessage", data)
|
||||
}
|
||||
published = append(published, message)
|
||||
return nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "PENDING-1", Reason: "message lost"})
|
||||
if err != nil {
|
||||
t.Fatalf("Requeue() error = %v", err)
|
||||
}
|
||||
if res == nil || !res.Requeued || res.BatchNo != "PENDING-1" || res.BatchType != consts.BatchTypeInbound {
|
||||
t.Fatalf("unexpected response: %+v", res)
|
||||
}
|
||||
if len(published) != 1 || published[0].BatchNo != "PENDING-1" || published[0].BatchType != consts.BatchTypeInbound {
|
||||
t.Fatalf("unexpected published messages: %+v", published)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchMainRequeuePublishesDueRestartWaitingResume(t *testing.T) {
|
||||
ctx := setupBatchMainRequeueDB(t)
|
||||
resumeAt := gtime.Now().Add(-time.Minute).String()
|
||||
insertRequeueBatch(t, ctx, "RST-DUE", consts.BatchTypeRestart, consts.BatchAsyncStatusProcessing, consts.BatchRunResultRunning, `{"restart":{"phase":"waiting_resume","resumeAt":"`+resumeAt+`"}}`)
|
||||
|
||||
var published []batchExecuteMessage
|
||||
restore := replaceBatchRequeuePublisher(func(_ context.Context, _ string, data any) error {
|
||||
published = append(published, data.(batchExecuteMessage))
|
||||
return nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "RST-DUE", Reason: "delayed message lost"})
|
||||
if err != nil {
|
||||
t.Fatalf("Requeue() error = %v", err)
|
||||
}
|
||||
if res == nil || res.BatchNo != "RST-DUE" || res.BatchType != consts.BatchTypeRestart {
|
||||
t.Fatalf("unexpected response: %+v", res)
|
||||
}
|
||||
if len(published) != 1 || published[0].BatchNo != "RST-DUE" || published[0].BatchType != consts.BatchTypeRestart {
|
||||
t.Fatalf("unexpected published messages: %+v", published)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchMainRequeueRejectsUnsafeStatuses(t *testing.T) {
|
||||
ctx := setupBatchMainRequeueDB(t)
|
||||
future := gtime.Now().Add(time.Hour).String()
|
||||
cases := []struct {
|
||||
name string
|
||||
batchNo string
|
||||
batchType int
|
||||
status int
|
||||
runResult int
|
||||
info string
|
||||
}{
|
||||
{name: "failed", batchNo: "FAILED-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusFailed, runResult: consts.BatchRunResultFailed, info: `{}`},
|
||||
{name: "success", batchNo: "SUCCESS-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusSuccess, runResult: consts.BatchRunResultSuccess, info: `{}`},
|
||||
{name: "partial", batchNo: "PARTIAL-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusPartialSuccess, runResult: consts.BatchRunResultPartialSuccess, info: `{}`},
|
||||
{name: "review", batchNo: "REVIEW-1", batchType: consts.BatchTypeDataStart, status: consts.BatchAsyncStatusReview, runResult: consts.BatchRunResultReview, info: `{}`},
|
||||
{name: "normal processing", batchNo: "PROCESSING-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusProcessing, runResult: consts.BatchRunResultRunning, info: `{}`},
|
||||
{name: "restart future resume", batchNo: "RST-FUTURE", batchType: consts.BatchTypeRestart, status: consts.BatchAsyncStatusProcessing, runResult: consts.BatchRunResultRunning, info: `{"restart":{"phase":"waiting_resume","resumeAt":"` + future + `"}}`},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
insertRequeueBatch(t, ctx, tc.batchNo, tc.batchType, tc.status, tc.runResult, tc.info)
|
||||
}
|
||||
|
||||
var published int
|
||||
restore := replaceBatchRequeuePublisher(func(context.Context, string, any) error {
|
||||
published++
|
||||
return nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: tc.batchNo, Reason: "operator retry"})
|
||||
if err == nil {
|
||||
t.Fatal("Requeue() error = nil, want error")
|
||||
}
|
||||
})
|
||||
}
|
||||
if published != 0 {
|
||||
t.Fatalf("published messages = %d, want 0", published)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchMainRequeueHandlesMissingBatchAndPublishFailure(t *testing.T) {
|
||||
ctx := setupBatchMainRequeueDB(t)
|
||||
insertRequeueBatch(t, ctx, "PENDING-FAIL-PUBLISH", consts.BatchTypeInbound, consts.BatchAsyncStatusPending, consts.BatchRunResultPending, `{}`)
|
||||
|
||||
restore := replaceBatchRequeuePublisher(func(context.Context, string, any) error {
|
||||
return errors.New("rabbitmq unavailable")
|
||||
})
|
||||
defer restore()
|
||||
|
||||
if _, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "PENDING-FAIL-PUBLISH", Reason: "message lost"}); err == nil {
|
||||
t.Fatal("Requeue() publish failure error = nil, want error")
|
||||
}
|
||||
if _, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "MISSING", Reason: "message lost"}); err == nil {
|
||||
t.Fatal("Requeue() missing batch error = nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func setupBatchMainRequeueDB(t *testing.T) context.Context {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
if err := gdb.SetConfig(gdb.Config{
|
||||
"default": gdb.ConfigGroup{{
|
||||
Type: "sqlite",
|
||||
Link: "sqlite::@file(:memory:)",
|
||||
MaxOpenConnCount: 1,
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = g.DB().Close(ctx)
|
||||
})
|
||||
// GoFrame caches table fields by group and table, across SQLite instances.
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `CREATE TABLE batch_main (
|
||||
source_channel TEXT NOT NULL DEFAULT 'legacy',
|
||||
execution_mode INTEGER NOT NULL DEFAULT 0,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL,
|
||||
batch_type INTEGER NOT NULL,
|
||||
batch_async_status INTEGER NOT NULL,
|
||||
run_result INTEGER NOT NULL,
|
||||
batch_info TEXT,
|
||||
tenant_id TEXT NOT NULL DEFAULT '',
|
||||
dept_id INTEGER NOT NULL DEFAULT 0,
|
||||
created_dept INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER NOT NULL DEFAULT 0
|
||||
)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func insertRequeueBatch(t *testing.T, ctx context.Context, batchNo string, batchType, status, runResult int, batchInfo string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
`INSERT INTO batch_main(batch_no, batch_type, batch_async_status, run_result, batch_info, tenant_id) VALUES(?, ?, ?, ?, ?, '')`,
|
||||
batchNo, batchType, status, runResult, batchInfo,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceBatchRequeuePublisher(fn func(context.Context, string, any) error) func() {
|
||||
original := publishBatchExecuteMessage
|
||||
publishBatchExecuteMessage = fn
|
||||
return func() {
|
||||
publishBatchExecuteMessage = original
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatchManagementAllocationUnits(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
"ALTER TABLE batch_main ADD target_dept_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD dept_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD updated_by INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD batch_async_status INTEGER DEFAULT 3",
|
||||
"CREATE TABLE sys_dept(dept_id INTEGER,tenant_id TEXT,dept_key TEXT,dept_name TEXT,deleted_at TEXT)",
|
||||
"INSERT INTO sys_dept VALUES(10,'','source','当前划拨名称',NULL),(20,'','target','当前接收名称',NULL)",
|
||||
`INSERT INTO batch_main(batch_no,batch_type,batch_info,created_dept,target_dept_id,total_count,run_result) VALUES('ALC-PARENT',3,'{"jobRole":"allocate_parent","sourceDeptId":10,"targetDeptId":20,"sourceDeptName":"历史划拨单位","targetDeptName":"历史接收单位"}',10,20,1,2),('ALC-LEGACY',3,'{}',10,20,1,2),('ALC-PER-ROW',3,'{"sourceDeptId":10,"sourceDeptName":"历史划拨单位","targetDeptMode":"per_row"}',10,0,1,2)`,
|
||||
`INSERT INTO batch_main(batch_no,batch_type,source_batch_no,batch_info,created_dept,target_dept_id,total_count,run_result) VALUES('ALC-CHILD',3,'ALC-PARENT','{"jobRole":"allocate_chunk"}',10,20,1,2)`,
|
||||
`INSERT INTO batch_detail(batch_no,batch_type,item_id,snapshot,created_dept) VALUES('ALC-CHILD',3,'card','{}',10)`,
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
service := NewCommonBatchMain()
|
||||
rows, _, err := service.SuccessList(ctx, &model.BatchMainSuccessListParam{BatchType: consts.BatchTypeAllocate, PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, row := range rows {
|
||||
seen[row.BatchNo] = true
|
||||
switch row.BatchNo {
|
||||
case "ALC-PARENT":
|
||||
if row.SourceDeptName != "历史划拨单位" || row.TargetDeptName != "历史接收单位" {
|
||||
t.Fatal(row)
|
||||
}
|
||||
encoded, err := json.Marshal(row)
|
||||
if err != nil || !strings.Contains(string(encoded), `"targetDeptId":20`) || strings.Contains(string(encoded), "StoredTargetDeptId") {
|
||||
t.Fatal("接口契约错误", string(encoded), err)
|
||||
}
|
||||
case "ALC-LEGACY":
|
||||
if row.SourceDeptName != "当前划拨名称" || row.TargetDeptName != "当前接收名称" {
|
||||
t.Fatal(row)
|
||||
}
|
||||
case "ALC-PER-ROW":
|
||||
if row.TargetDeptMode != "per_row" || row.TargetDeptName != "" {
|
||||
t.Fatal(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !seen["ALC-PARENT"] || !seen["ALC-LEGACY"] || !seen["ALC-PER-ROW"] {
|
||||
t.Fatal(seen)
|
||||
}
|
||||
all, _, err := service.List(ctx, &model.BatchMainListParam{BatchNo: "ALC-PARENT", PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil || len(all) != 1 || all[0].TargetDeptName != "历史接收单位" {
|
||||
t.Fatal("主表列表单位缺失", all, err)
|
||||
}
|
||||
view, err := service.View(ctx, &model.BatchMainViewParam{Id: all[0].Id})
|
||||
if err != nil || view.SourceDeptName != "历史划拨单位" || view.TargetDeptName != "历史接收单位" {
|
||||
t.Fatal("批次详情单位缺失", view, err)
|
||||
}
|
||||
cards, total, err := service.ListCards(ctx, &model.BatchCardListParam{BatchNo: "ALC-PARENT", PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil || total != 1 || len(cards) != 1 {
|
||||
t.Fatal("父批次未展示子批次卡清单", total, err)
|
||||
}
|
||||
if cards[0].Iccid != "card" || cards[0].SourceDeptName != "历史划拨单位" || cards[0].TargetDeptName != "历史接收单位" {
|
||||
t.Fatal(cards[0])
|
||||
}
|
||||
if _, _, err := service.ListCards(context.Background(), &model.BatchCardListParam{BatchNo: "missing"}); err == nil {
|
||||
t.Fatal("不存在批次被放行")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExecutionLogMetadataFilters(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "META-SINGLE", consts.BatchTypeInbound, 0, 1, "card1")
|
||||
insertBatchExecutionLogDetail(t, ctx, "META-BULK", consts.BatchTypeInbound, 3, 1, "card2")
|
||||
for _, sql := range []string{
|
||||
"UPDATE batch_main SET source_channel='mini_program',execution_mode=1 WHERE batch_no='META-SINGLE'",
|
||||
"UPDATE batch_main SET source_channel='console',execution_mode=2 WHERE batch_no='META-BULK'",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, sql); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
mode := 1
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{PageInfo: request.PageInfo{Page: 1, PageSize: 20}, SourceChannel: "mini_program", ExecutionMode: &mode})
|
||||
if err != nil || total != 1 || len(list) != 1 || list[0].BatchNo != "META-SINGLE" || list[0].SourceChannel != "mini_program" || list[0].ExecutionMode != 1 {
|
||||
t.Fatalf("%+v %d %v", list, total, err)
|
||||
}
|
||||
mode = 2
|
||||
_, total, err = NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{PageInfo: request.PageInfo{Page: 1, PageSize: 20}, SourceChannel: "mini_program", ExecutionMode: &mode})
|
||||
if err != nil || total != 0 {
|
||||
t.Fatal(total, err)
|
||||
}
|
||||
}
|
||||
@@ -22,3 +22,13 @@ func visibleBatchTypeMatches(actual int, requested *int) bool {
|
||||
}
|
||||
return requested == nil || actual == *requested
|
||||
}
|
||||
|
||||
// successListBatchTypeModel keeps export execution logs out of business batch selectors.
|
||||
func successListBatchTypeModel(mod *gdb.Model, column string, requested *int) *gdb.Model {
|
||||
mod = visibleBatchTypeModel(mod, column, requested)
|
||||
return mod.Where(column+" <> ?", consts.BatchTypeExport)
|
||||
}
|
||||
|
||||
func successListBatchTypeMatches(actual int, requested *int) bool {
|
||||
return actual != consts.BatchTypeExport && visibleBatchTypeMatches(actual, requested)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
)
|
||||
|
||||
func TestSuccessListBatchTypeMatches(t *testing.T) {
|
||||
prepare := consts.BatchTypePrepare
|
||||
renew := consts.BatchTypeRenew
|
||||
export := consts.BatchTypeExport
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
actual int
|
||||
requested *int
|
||||
want bool
|
||||
}{
|
||||
{name: "unfiltered business batch", actual: consts.BatchTypeChange, want: true},
|
||||
{name: "matching business filter", actual: consts.BatchTypeRenew, requested: &renew, want: true},
|
||||
{name: "different business filter", actual: consts.BatchTypeChange, requested: &renew, want: false},
|
||||
{name: "unfiltered prepare batch", actual: consts.BatchTypePrepare, want: false},
|
||||
{name: "explicit prepare filter remains hidden", actual: consts.BatchTypePrepare, requested: &prepare, want: false},
|
||||
{name: "unfiltered export batch remains hidden", actual: consts.BatchTypeExport, want: false},
|
||||
{name: "explicit export filter remains hidden", actual: consts.BatchTypeExport, requested: &export, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := successListBatchTypeMatches(tt.actual, tt.requested); got != tt.want {
|
||||
t.Fatalf("successListBatchTypeMatches(%d) = %v, want %v", tt.actual, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
type sCommonDownloadCenter struct{}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonDownloadCenter(NewCommonDownloadCenter())
|
||||
}
|
||||
|
||||
func NewCommonDownloadCenter() *sCommonDownloadCenter {
|
||||
return &sCommonDownloadCenter{}
|
||||
}
|
||||
|
||||
// List 下载中心列表:K2 仅本人 + K1 业务权限过滤
|
||||
func (s *sCommonDownloadCenter) List(ctx context.Context, in *model.DownloadCenterListParam) (list []*model.DownloadCenterListModel, totalCount int, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil || user.ID <= 0 {
|
||||
return nil, 0, gerror.New("登录状态已失效,请重新登录")
|
||||
}
|
||||
|
||||
mod := dao.TExportTask.Ctx(ctx).
|
||||
Fields(model.DownloadCenterListModel{}).
|
||||
Where(dao.TExportTask.Columns().OwnerUserId, user.ID)
|
||||
mod = applyExportPermissionFilter(ctx, mod)
|
||||
|
||||
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
mod = mod.Where(
|
||||
"("+dao.TExportTask.Columns().TaskNo+" LIKE ? OR "+
|
||||
dao.TExportTask.Columns().FileName+" LIKE ? OR "+
|
||||
dao.TExportTask.Columns().BizName+" LIKE ?)",
|
||||
like, like, like,
|
||||
)
|
||||
}
|
||||
if in.BizKey != "" {
|
||||
mod = mod.Where(dao.TExportTask.Columns().BizKey, in.BizKey)
|
||||
}
|
||||
if statusIds := parseStatusIds(in.Status); len(statusIds) > 0 {
|
||||
mod = mod.WhereIn(dao.TExportTask.Columns().Status, statusIds)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
mod = mod.WhereBetween(dao.TExportTask.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
err = mod.Page(in.Page, in.PageSize).
|
||||
OrderDesc(dao.TExportTask.Columns().Id).
|
||||
ScanAndCount(&list, &totalCount, false)
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取导出任务列表失败,请稍后重试")
|
||||
}
|
||||
if list == nil {
|
||||
list = []*model.DownloadCenterListModel{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Recent 右上角全局入口:最近任务与进行中/已完成数量
|
||||
func (s *sCommonDownloadCenter) Recent(ctx context.Context, in *model.DownloadCenterRecentParam) (res *model.DownloadCenterRecentModel, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil || user.ID <= 0 {
|
||||
return nil, gerror.New("登录状态已失效,请重新登录")
|
||||
}
|
||||
limit := in.Limit
|
||||
if limit <= 0 || limit > 20 {
|
||||
limit = 5
|
||||
}
|
||||
|
||||
items := make([]*model.DownloadCenterListModel, 0, limit)
|
||||
mod := dao.TExportTask.Ctx(ctx).
|
||||
WhereNull(dao.TExportTask.Columns().DeletedTime).
|
||||
Fields(model.DownloadCenterListModel{}).
|
||||
Where(dao.TExportTask.Columns().OwnerUserId, user.ID)
|
||||
mod = applyExportPermissionFilter(ctx, mod)
|
||||
if err = mod.OrderDesc(dao.TExportTask.Columns().Id).Limit(limit).Scan(&items); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取最近导出任务失败")
|
||||
}
|
||||
|
||||
running, err := s.countByStatus(ctx, user.ID, []int{consts.DownloadStatusPending, consts.DownloadStatusRunning})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
finished, err := s.countByStatus(ctx, user.ID, []int{consts.DownloadStatusGenerated})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.DownloadCenterRecentModel{
|
||||
Items: items,
|
||||
RunningCount: running,
|
||||
FinishedCount: finished,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sCommonDownloadCenter) countByStatus(ctx context.Context, userId int64, status []int) (int, error) {
|
||||
mod := dao.TExportTask.Ctx(ctx).
|
||||
WhereNull(dao.TExportTask.Columns().DeletedTime).
|
||||
Where(dao.TExportTask.Columns().OwnerUserId, userId).
|
||||
WhereIn(dao.TExportTask.Columns().Status, status)
|
||||
mod = applyExportPermissionFilter(ctx, mod)
|
||||
count, err := mod.Count()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "统计导出任务数量失败")
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Download 下载导出文件:重新校验 K1+K2,然后直接写文件流
|
||||
func (s *sCommonDownloadCenter) Download(ctx context.Context, in *model.DownloadCenterTaskParam) (err error) {
|
||||
task, err := s.loadOwnedTask(ctx, in.TaskId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasExportPermission(ctx, task.PermCode) {
|
||||
return gerror.New("你的导出权限已变更,无法下载该文件")
|
||||
}
|
||||
if task.DeletedTime != nil {
|
||||
return gerror.New("文件已删除或已过期清理,请重新导出")
|
||||
}
|
||||
if task.Status != consts.DownloadStatusGenerated {
|
||||
return gerror.New("文件尚未生成完成,请稍后再试")
|
||||
}
|
||||
if task.ExpireTime == nil || !task.ExpireTime.After(gtime.Now()) {
|
||||
return gerror.New("文件已过期,请重新导出")
|
||||
}
|
||||
if task.OssId <= 0 {
|
||||
return gerror.New("文件不存在")
|
||||
}
|
||||
|
||||
oss := &entity.SysOss{}
|
||||
if err = dao.SysOss.Ctx(ctx).Where(dao.SysOss.Columns().OssId, task.OssId).Scan(oss); err != nil {
|
||||
return gerror.Wrap(err, "获取文件信息失败")
|
||||
}
|
||||
if oss.OssId <= 0 {
|
||||
return gerror.New("文件不存在或已被清理")
|
||||
}
|
||||
|
||||
return service.SysOss().DownloadLocal(ctx, oss, func() error {
|
||||
columns := dao.TExportTask.Columns()
|
||||
// 先计算有效期再递增次数,避免 MySQL 从左到右赋值影响首次下载判断。
|
||||
affected, updateErr := dao.TExportTask.Ctx(ctx).
|
||||
Data("expire_time = CASE WHEN download_count = 0 THEN LEAST(expire_time, DATE_ADD(NOW(), INTERVAL 1 HOUR)) ELSE expire_time END, last_download_time = NOW(), download_count = download_count + 1").
|
||||
Where(columns.Id, task.Id).
|
||||
WhereNull(columns.DeletedTime).
|
||||
Where(columns.OwnerUserId, task.OwnerUserId).
|
||||
Where(columns.Status, consts.DownloadStatusGenerated).
|
||||
Where(columns.OssId, oss.OssId).
|
||||
Where(columns.ExpireTime + " > NOW()").
|
||||
UpdateAndGetAffected()
|
||||
if updateErr != nil {
|
||||
return gerror.Wrap(updateErr, "更新下载有效期失败,请稍后重试")
|
||||
}
|
||||
if affected != 1 {
|
||||
return gerror.New("文件已过期或状态已变更,请刷新后重试")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Remove 物理清理文件与OSS记录,软删除并保留导出任务历史
|
||||
func (s *sCommonDownloadCenter) Remove(ctx context.Context, in *model.DownloadCenterTaskParam) (err error) {
|
||||
task, err := s.loadOwnedTask(ctx, in.TaskId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if task.Status == consts.DownloadStatusRunning {
|
||||
return gerror.New("文件正在生成中,暂时不能删除")
|
||||
}
|
||||
if !hasExportPermission(ctx, task.PermCode) {
|
||||
return gerror.New("你的导出权限已变更,无法删除该任务")
|
||||
}
|
||||
|
||||
userId := contexts.GetUserId(ctx)
|
||||
_, err = exportcenter.RemoveTaskFile(ctx, task.Id, "user", func(locked *entity.TExportTask) error {
|
||||
if locked.OwnerUserId != userId {
|
||||
return gerror.New("无权访问该导出任务")
|
||||
}
|
||||
if locked.Status == consts.DownloadStatusRunning {
|
||||
return gerror.New("任务已开始生成,暂时不能删除")
|
||||
}
|
||||
if !hasExportPermission(ctx, locked.PermCode) {
|
||||
return gerror.New("你的导出权限已变更,无法删除该任务")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "删除导出任务失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadOwnedTask 取任务并校验归属(K2:仅本人)
|
||||
func (s *sCommonDownloadCenter) loadOwnedTask(ctx context.Context, taskId int64) (*entity.TExportTask, error) {
|
||||
if taskId <= 0 {
|
||||
return nil, gerror.New("任务ID不能为空")
|
||||
}
|
||||
task := &entity.TExportTask{}
|
||||
if err := dao.TExportTask.Ctx(ctx).
|
||||
Where(dao.TExportTask.Columns().Id, taskId).
|
||||
Scan(task); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取导出任务失败")
|
||||
}
|
||||
if task.Id <= 0 {
|
||||
return nil, gerror.New("导出任务不存在")
|
||||
}
|
||||
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil || user.ID <= 0 {
|
||||
return nil, gerror.New("登录状态已失效,请重新登录")
|
||||
}
|
||||
if task.OwnerUserId != user.ID {
|
||||
return nil, gerror.New("无权访问该导出任务")
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// applyExportPermissionFilter K1:只显示「我仍持有对应导出权限」的任务
|
||||
func applyExportPermissionFilter(ctx context.Context, mod *gdb.Model) *gdb.Model {
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
return mod
|
||||
}
|
||||
codes := exportPermissionCodes(contexts.GetAccessCodeList(ctx))
|
||||
if len(codes) == 0 {
|
||||
return mod.Where("1 = 0")
|
||||
}
|
||||
|
||||
exact := make([]string, 0, len(codes))
|
||||
prefixes := make([]string, 0, len(codes))
|
||||
for _, code := range codes {
|
||||
if strings.HasSuffix(code, "*") {
|
||||
prefixes = append(prefixes, strings.TrimSuffix(code, "*"))
|
||||
continue
|
||||
}
|
||||
exact = append(exact, code)
|
||||
}
|
||||
|
||||
conditions := make([]string, 0, len(exact)+len(prefixes))
|
||||
args := make([]interface{}, 0, len(exact)+len(prefixes))
|
||||
column := dao.TExportTask.Columns().PermCode
|
||||
if len(exact) > 0 {
|
||||
conditions = append(conditions, column+" IN (?)")
|
||||
args = append(args, exact)
|
||||
}
|
||||
for _, prefix := range prefixes {
|
||||
conditions = append(conditions, column+" LIKE ?")
|
||||
args = append(args, prefix+"%")
|
||||
}
|
||||
if len(conditions) == 0 {
|
||||
return mod.Where("1 = 0")
|
||||
}
|
||||
return mod.Where("("+strings.Join(conditions, " OR ")+")", args...)
|
||||
}
|
||||
|
||||
// hasExportPermission 下载/删除时的二次校验
|
||||
func hasExportPermission(ctx context.Context, required string) bool {
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(required) == "" {
|
||||
// 提交时接口未声明权限码时,退化为只校验归属,避免历史任务全部不可用
|
||||
return true
|
||||
}
|
||||
for _, code := range contexts.GetAccessCodeList(ctx) {
|
||||
if matchExportCode(code, required) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchExportCode(pattern, code string) bool {
|
||||
if pattern == code {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(pattern, "*") {
|
||||
return strings.HasPrefix(code, strings.TrimSuffix(pattern, "*"))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// exportPermissionCodes 取「可用于判断导出任务可见性」的权限码。
|
||||
//
|
||||
// 历史实现只认 :export 结尾,但导出任务的 perm_code 是提交时声明的权限码,
|
||||
// 不同业务可能声明为 :view / :download / :list(例如卡基础信息查询、月历史用量查询
|
||||
// 把查询与导出合并成一个 :view 码)。只认 :export 会让这些任务对提交人自己也不可见。
|
||||
//
|
||||
// 放宽到常见后缀不影响安全:真正下载时 hasExportPermission 会拿任务记录的 perm_code
|
||||
// 再校验一次,用户失去权限后即使看得到记录也拿不到文件。
|
||||
func exportPermissionCodes(list []string) []string {
|
||||
suffixes := []string{":export", ":view", ":download", ":list"}
|
||||
out := make([]string, 0, len(list))
|
||||
for _, code := range list {
|
||||
if strings.HasSuffix(code, "*") {
|
||||
out = append(out, code)
|
||||
continue
|
||||
}
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(code, suffix) {
|
||||
out = append(out, code)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// parseStatusIds 解析逗号分隔的状态筛选
|
||||
func parseStatusIds(raw string) []int {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
ids := make([]int, 0, 4)
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if v, err := strconv.Atoi(part); err == nil {
|
||||
ids = append(ids, v)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTBalanceDetail struct{}
|
||||
@@ -137,31 +134,38 @@ func (s *sCommonTBalanceDetail) List(ctx context.Context, in *model.TBalanceDeta
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出余额明细表
|
||||
// Export 导出余额明细(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTBalanceDetail) Export(ctx context.Context, in *model.TBalanceDetailListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
var req exportcenter.Request
|
||||
req = exportcenter.Request{
|
||||
BizKey: "tBalanceDetail",
|
||||
BizName: "余额明细",
|
||||
PermCode: "cpm:common:tBalanceDetail:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TBalanceDetailListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
// 与列表 getDisplayTransactionChannel 保持一致,先归一化历史渠道值。
|
||||
if len(req.Columns) > 0 && (item.TransactionChannel == 4 || (item.TransactionChannel == 0 && item.IncomeExpenseType == 1 && item.TransactionType == 1)) {
|
||||
item.TransactionChannel = 3
|
||||
}
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TBalanceDetailExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出余额明细表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TBalanceDetailExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
_, err = exportcenter.Submit(ctx, &req)
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增余额明细表
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package iot
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTFinWithdrawableChange struct{}
|
||||
@@ -113,31 +110,32 @@ func (s *sCommonTFinWithdrawableChange) List(ctx context.Context, in *model.TFin
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出可提现变动明细表
|
||||
// Export 导出可提现变动明细(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTFinWithdrawableChange) Export(ctx context.Context, in *model.TFinWithdrawableChangeListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TFinWithdrawableChangeExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出可提现变动明细表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TFinWithdrawableChangeExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tFinWithdrawableChange",
|
||||
BizName: "可提现变动明细",
|
||||
PermCode: "cpm:common:tFinWithdrawableChange:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TFinWithdrawableChangeListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增可提现变动明细表
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
@@ -18,14 +18,11 @@ import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTFinanceApply struct{}
|
||||
@@ -105,31 +102,32 @@ func (s *sCommonTFinanceApply) List(ctx context.Context, in *model.TFinanceApply
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出财务申请表
|
||||
// Export 导出财务申请(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTFinanceApply) Export(ctx context.Context, in *model.TFinanceApplyListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TFinanceApplyExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出财务申请表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TFinanceApplyExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tFinanceApply",
|
||||
BizName: "财务申请",
|
||||
PermCode: "cpm:common:tFinanceApply:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TFinanceApplyListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增财务申请表
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTFinanceAuditRecord struct{}
|
||||
@@ -98,31 +95,32 @@ func (s *sCommonTFinanceAuditRecord) List(ctx context.Context, in *model.TFinanc
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出财务审核记录表
|
||||
// Export 导出财务审核记录(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTFinanceAuditRecord) Export(ctx context.Context, in *model.TFinanceAuditRecordListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TFinanceAuditRecordExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出财务审核记录表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TFinanceAuditRecordExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tFinanceAuditRecord",
|
||||
BizName: "财务审核记录",
|
||||
PermCode: "cpm:common:tFinanceAuditRecord:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TFinanceAuditRecordListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增财务审核记录表
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/websocket"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
@@ -18,13 +19,11 @@ import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
@@ -123,31 +122,32 @@ func (s *sCommonTMessageLog) List(ctx context.Context, in *model.TMessageLogList
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出消息日志
|
||||
// Export 导出消息日志(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTMessageLog) Export(ctx context.Context, in *model.TMessageLogListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TMessageLogExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出消息日志-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TMessageLogExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tMessageLog",
|
||||
BizName: "消息日志",
|
||||
PermCode: "cpm:common:tMessageLog:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TMessageLogListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增消息日志
|
||||
@@ -321,3 +321,119 @@ func (s *sCommonTMessageLog) View(ctx context.Context, in *model.TMessageLogView
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 未读通知的默认与最大返回条数,避免一次拉取过多消息。
|
||||
const (
|
||||
messageUnreadDefaultLimit = 50
|
||||
messageUnreadMaxLimit = 200
|
||||
)
|
||||
|
||||
// unreadQuery 构造当前用户未读消息查询。
|
||||
// 以用户消息表为准:只查本人的未读记录,关联消息日志排除已撤回消息,
|
||||
// 消息日志已删除(关联为空)时也不提醒。
|
||||
// 这里不按 send_user_type 过滤:收件人 user_id 全部来自 sys_user 单表,
|
||||
// 该字段只表示消息面向哪个控制台,按其过滤会漏掉真正发给本人的消息。
|
||||
func (s *sCommonTMessageLog) unreadQuery(ctx context.Context, userId int64) *gdb.Model {
|
||||
var (
|
||||
userTable = dao.TUserMessage.Table()
|
||||
logTable = dao.TMessageLog.Table()
|
||||
userColumns = dao.TUserMessage.Columns()
|
||||
logColumns = dao.TMessageLog.Columns()
|
||||
)
|
||||
return dao.TUserMessage.Ctx(ctx).
|
||||
LeftJoin(logTable+" ON "+logTable+"."+logColumns.MessageLogId+" = "+userTable+"."+userColumns.MessageLogId).
|
||||
Where(userTable+"."+userColumns.UserId, userId).
|
||||
Where(userTable+"."+userColumns.IsRead, 0).
|
||||
Where(logTable+"."+logColumns.IsRecall, 0)
|
||||
}
|
||||
|
||||
// UnreadList 获取当前登录用户的未读消息通知。
|
||||
// 这是右上角消息提醒的数据来源:登录、刷新、断线重连后都必须重新拉取,
|
||||
// 否则离线期间的消息会因错过 websocket 推送而不再出现提醒。
|
||||
func (s *sCommonTMessageLog) UnreadList(ctx context.Context, in *model.TMessageLogUnreadListParam) (list []*model.TMessageLogUnreadListModel, totalCount int, err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
err = gerror.New("未获取到当前登录用户信息")
|
||||
return
|
||||
}
|
||||
|
||||
limit := messageUnreadDefaultLimit
|
||||
if in != nil && in.Limit > 0 {
|
||||
limit = in.Limit
|
||||
}
|
||||
if limit > messageUnreadMaxLimit {
|
||||
limit = messageUnreadMaxLimit
|
||||
}
|
||||
|
||||
var (
|
||||
userTable = dao.TUserMessage.Table()
|
||||
logTable = dao.TMessageLog.Table()
|
||||
userColumn = dao.TUserMessage.Columns()
|
||||
logColumn = dao.TMessageLog.Columns()
|
||||
)
|
||||
if err = s.unreadQuery(ctx, userId).
|
||||
Fields(userTable + "." + userColumn.MessageLogId + "," +
|
||||
logTable + "." + logColumn.MessageType + "," +
|
||||
logTable + "." + logColumn.SendUserType + "," +
|
||||
logTable + "." + logColumn.MessageTitle + "," +
|
||||
logTable + "." + logColumn.MessageContent + "," +
|
||||
logTable + "." + logColumn.MessageLink + "," +
|
||||
logTable + "." + logColumn.SendTime + "," +
|
||||
userTable + "." + userColumn.IsRead).
|
||||
OrderDesc(userTable + "." + userColumn.MessageId).
|
||||
Limit(limit).
|
||||
Scan(&list); err != nil {
|
||||
err = gerror.Wrap(err, "获取未读消息失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*model.TMessageLogUnreadListModel{}
|
||||
}
|
||||
// 总数不受列表条数上限影响,用于判断是否还有更多未读。
|
||||
if totalCount, err = s.unreadQuery(ctx, userId).Count(); err != nil {
|
||||
err = gerror.Wrap(err, "获取未读消息数量失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Read 标记当前登录用户的指定消息为已读。
|
||||
// 只允许操作本人的用户消息记录,避免越权修改他人已读状态。
|
||||
func (s *sCommonTMessageLog) Read(ctx context.Context, in *model.TMessageLogReadParam) (err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
return gerror.New("未获取到当前登录用户信息")
|
||||
}
|
||||
if in == nil || in.MessageLogId <= 0 {
|
||||
return gerror.New("消息ID不能为空")
|
||||
}
|
||||
if _, err = dao.TUserMessage.Ctx(ctx).Where(g.Map{
|
||||
dao.TUserMessage.Columns().MessageLogId: in.MessageLogId,
|
||||
dao.TUserMessage.Columns().UserId: userId,
|
||||
}).Data(g.Map{
|
||||
dao.TUserMessage.Columns().IsRead: 1,
|
||||
dao.TUserMessage.Columns().UpdatedBy: userId,
|
||||
dao.TUserMessage.Columns().UpdatedAt: gtime.Now(),
|
||||
}).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "标记消息已读失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll 标记当前登录用户的全部未读消息为已读。
|
||||
func (s *sCommonTMessageLog) ReadAll(ctx context.Context, in *model.TMessageLogReadAllParam) (err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
return gerror.New("未获取到当前登录用户信息")
|
||||
}
|
||||
if _, err = dao.TUserMessage.Ctx(ctx).
|
||||
Where(dao.TUserMessage.Columns().UserId, userId).
|
||||
Where(dao.TUserMessage.Columns().IsRead, 0).
|
||||
Data(g.Map{
|
||||
dao.TUserMessage.Columns().IsRead: 1,
|
||||
dao.TUserMessage.Columns().UpdatedBy: userId,
|
||||
dao.TUserMessage.Columns().UpdatedAt: gtime.Now(),
|
||||
}).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "标记全部消息已读失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/websocket"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
@@ -18,13 +19,11 @@ import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
@@ -120,31 +119,32 @@ func (s *sCommonTNotifyLog) List(ctx context.Context, in *model.TNotifyLogListPa
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出通知日志
|
||||
// Export 导出通知日志(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTNotifyLog) Export(ctx context.Context, in *model.TNotifyLogListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TNotifyLogExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出通知日志-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []*model.TNotifyLogExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tNotifyLog",
|
||||
BizName: "通知日志",
|
||||
PermCode: "cpm:common:tNotifyLog:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TNotifyLogListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增通知日志
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
@@ -277,6 +277,7 @@ func (s *sCommonTWoMain) Edit(ctx context.Context, in *model.TWoMainEditParam) (
|
||||
return service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
|
||||
WoNo: *in.WoNo,
|
||||
OperationType: workOrderOperationCreate,
|
||||
AfterStatus: currentStatus,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTWoRecord struct{}
|
||||
@@ -135,31 +132,32 @@ func (s *sCommonTWoRecord) List(ctx context.Context, in *model.TWoRecordListPara
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出工单记录表
|
||||
// Export 导出工单记录(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTWoRecord) Export(ctx context.Context, in *model.TWoRecordListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TWoRecordExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出工单记录表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TWoRecordExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tWoRecord",
|
||||
BizName: "工单记录",
|
||||
PermCode: "cpm:gen:tWoRecord:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TWoRecordListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增工单记录表
|
||||
@@ -205,17 +203,13 @@ func (s *sCommonTWoRecord) AddOperation(ctx context.Context, in *model.TWoRecord
|
||||
OperationType: &in.OperationType,
|
||||
OperationDeptId: &operationDeptId,
|
||||
OperationUserId: &operationUserId,
|
||||
BeforeStatus: &in.BeforeStatus,
|
||||
AfterStatus: &in.AfterStatus,
|
||||
}
|
||||
if in.TargetDeptId > 0 {
|
||||
param.TargetDeptId = &in.TargetDeptId
|
||||
param.TargetUserId = &in.TargetUserId
|
||||
}
|
||||
if in.BeforeStatus > 0 {
|
||||
param.BeforeStatus = &in.BeforeStatus
|
||||
}
|
||||
if in.AfterStatus > 0 {
|
||||
param.AfterStatus = &in.AfterStatus
|
||||
}
|
||||
return s.Edit(ctx, param)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWorkOrderAccess(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, "CREATE TABLE t_wo_main (id INTEGER, wo_no TEXT, wo_type TEXT, related_id TEXT, title TEXT, content TEXT, attachment TEXT, creator_type TEXT, creator_dept_id TEXT, creator_user_id INTEGER, creator_name TEXT, creator_phone TEXT, assign_dept_id TEXT, assign_user_id TEXT, current_status TEXT, handler_dept_id INTEGER, handler_user_id INTEGER, first_response_time TEXT, resolve_time TEXT, close_time TEXT, satisfaction_level TEXT, eval_content TEXT, eval_time TEXT, remark TEXT, tenant_id TEXT, created_dept TEXT, created_by TEXT, created_at TEXT, updated_by TEXT, updated_at TEXT)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO t_wo_main (id,wo_no,tenant_id,creator_user_id,handler_user_id,handler_dept_id) VALUES (1, 'WO', 'A', 10, 20, 30)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, item := range []struct {
|
||||
name, tenant string
|
||||
user, dept int64
|
||||
allowed bool
|
||||
}{
|
||||
{"creator", "A", 10, 40, true}, {"handler", "A", 20, 40, true}, {"handler department", "A", 21, 30, true}, {"unrelated", "A", 11, 40, false}, {"other tenant", "B", 10, 30, false}, {"anonymous", "A", 0, 30, false},
|
||||
} {
|
||||
t.Run(item.name, func(t *testing.T) {
|
||||
scoped := contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: item.user, DeptId: item.dept, TenantId: item.tenant}}})
|
||||
_, err := getWorkOrder(scoped, "WO")
|
||||
if (err == nil) != item.allowed {
|
||||
t.Fatalf("allowed=%v error=%v", item.allowed, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("状态记录", func(t *testing.T) {
|
||||
testWorkOrderRecordStatuses(t, ctx)
|
||||
})
|
||||
}
|
||||
@@ -2,13 +2,13 @@ package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func testWorkOrderRecordStatuses(t *testing.T, ctx context.Context) {
|
||||
t.Helper()
|
||||
for _, statement := range []string{
|
||||
"CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, parent_id INTEGER, leader TEXT, tenant_id TEXT)",
|
||||
"CREATE TABLE t_wo_record (id INTEGER PRIMARY KEY, wo_no TEXT, operation_type INTEGER, operation_dept_id INTEGER, operation_user_id INTEGER, target_dept_id INTEGER, target_user_id INTEGER, before_status INTEGER, after_status INTEGER, tenant_id TEXT, created_dept INTEGER, created_by INTEGER, created_at TEXT, updated_by INTEGER, updated_at TEXT)",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx = contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 7, DeptId: 10, TenantId: "A"}}})
|
||||
if _, err := dao.SysDept.Ctx(ctx).Data(do.SysDept{DeptId: 10, ParentId: 0, Leader: "7", TenantId: "A"}).Insert(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("创建记录与主表状态一致", func(t *testing.T) {
|
||||
title := "工单状态回归"
|
||||
input := &model.TWoMainEditParam{Title: &title}
|
||||
if err := NewCommonTWoMain().Edit(ctx, input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var main entity.TWoMain
|
||||
if err := dao.TWoMain.Ctx(ctx).Where(dao.TWoMain.Columns().WoNo, *input.WoNo).Scan(&main); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var record entity.TWoRecord
|
||||
if err := dao.TWoRecord.Ctx(ctx).Where(dao.TWoRecord.Columns().WoNo, *input.WoNo).Scan(&record); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if main.CurrentStatus != workOrderStatusProcessing || record.AfterStatus != main.CurrentStatus || record.BeforeStatus != 0 || record.OperationType != workOrderOperationCreate {
|
||||
t.Fatalf("主表与创建记录状态不一致: main=%+v record=%+v", main, record)
|
||||
}
|
||||
if record.OperationUserId != 7 || record.OperationDeptId != 10 || record.TenantId != "A" {
|
||||
t.Fatalf("操作人或租户信息错误: %+v", record)
|
||||
}
|
||||
missing, err := dao.TWoRecord.Ctx(ctx).Where(dao.TWoRecord.Columns().Id, record.Id).WhereNull(dao.TWoRecord.Columns().BeforeStatus).Count()
|
||||
if err != nil || missing != 0 {
|
||||
t.Fatalf("创建前状态应显式写入0: missing=%d err=%v", missing, err)
|
||||
}
|
||||
})
|
||||
|
||||
for _, item := range []struct {
|
||||
name string
|
||||
operation, before, after int
|
||||
}{
|
||||
{"分派", workOrderOperationAssign, workOrderStatusAssigned, workOrderStatusProcessing},
|
||||
{"处理", workOrderOperationProcess, workOrderStatusResolved, workOrderStatusProcessing},
|
||||
{"完成", workOrderOperationFinish, workOrderStatusProcessing, workOrderStatusResolved},
|
||||
{"关单", workOrderOperationClose, workOrderStatusResolved, workOrderStatusClosed},
|
||||
{"取消", workOrderOperationCancel, workOrderStatusProcessing, workOrderStatusCanceled},
|
||||
} {
|
||||
t.Run(item.name, func(t *testing.T) {
|
||||
input := &model.TWoRecordAddOperationParam{WoNo: item.name, OperationType: item.operation, BeforeStatus: item.before, AfterStatus: item.after}
|
||||
if err := NewCommonTWoRecord().AddOperation(ctx, input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var record entity.TWoRecord
|
||||
if err := dao.TWoRecord.Ctx(ctx).Where(dao.TWoRecord.Columns().WoNo, item.name).Scan(&record); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if record.OperationType != item.operation || record.BeforeStatus != item.before || record.AfterStatus != item.after {
|
||||
t.Fatalf("操作记录状态错误: %+v", record)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user