chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)

This commit is contained in:
MiFaZhan
2026-11-02 13:27:59 +08:00
1250 changed files with 154111 additions and 0 deletions
@@ -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)
}
}
}
@@ -0,0 +1,764 @@
package common
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
"strconv"
"strings"
"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/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"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"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"
)
type sCommonBatchExecutionLog struct{}
type batchExecutionLogSummaryBatch struct {
CreatedDept int64 `orm:"created_dept"`
TargetDeptId int64 `orm:"target_dept_id"`
SourceChannel string `orm:"source_channel"`
ExecutionMode int `orm:"execution_mode"`
Id int64 `orm:"id"`
BatchNo string `orm:"batch_no"`
BatchType int `orm:"batch_type"`
SourceBatchNo string `orm:"source_batch_no"`
SourceType *int `orm:"source_type"`
BatchAsyncStatus int `orm:"batch_async_status"`
RunResult int `orm:"run_result"`
FailReason string `orm:"fail_reason"`
TotalCount int `orm:"total_count"`
SourceOssId int64 `orm:"source_oss_id"`
ResultOssId int64 `orm:"result_oss_id"`
BatchInfo *gjson.Json `orm:"batch_info"`
StartTime *gtime.Time `orm:"start_time"`
EndTime *gtime.Time `orm:"end_time"`
}
func NewCommonBatchExecutionLog() *sCommonBatchExecutionLog {
return &sCommonBatchExecutionLog{}
}
func init() {
service.RegisterCommonBatchExecutionLog(NewCommonBatchExecutionLog())
}
func (s *sCommonBatchExecutionLog) batchModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.BatchMain.Ctx(ctx), option...)
}
func (s *sCommonBatchExecutionLog) detailModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.BatchDetail.Ctx(ctx), option...)
}
func (s *sCommonBatchExecutionLog) downloadModel(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.TResourceBatchDownload.Ctx(ctx), option...)
}
func (s *sCommonBatchExecutionLog) ossModel(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.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{})
if in.BatchNo != "" {
mod = mod.Where(dao.BatchMain.Columns().BatchNo+" LIKE ?", "%"+in.BatchNo+"%")
}
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
like := "%" + keyword + "%"
mod = mod.Where(
"("+dao.BatchMain.Columns().BatchNo+" LIKE ? OR "+dao.BatchMain.Columns().OrderSn+" LIKE ? OR "+dao.BatchMain.Columns().FailReason+" LIKE ?)",
like,
like,
like,
)
}
if in.BatchAsyncStatus != nil {
mod = mod.Where(dao.BatchMain.Columns().BatchAsyncStatus, *in.BatchAsyncStatus)
}
if in.SourceType != nil {
mod = mod.Where(dao.BatchMain.Columns().SourceType, *in.SourceType)
}
if in.SourceChannel != "" {
mod = mod.Where(dao.BatchMain.Columns().SourceChannel, in.SourceChannel)
}
if in.ExecutionMode != nil {
mod = mod.Where(dao.BatchMain.Columns().ExecutionMode, *in.ExecutionMode)
}
mod = mod.Where(
fmt.Sprintf("NOT (%s = ? AND COALESCE(%s, '') <> '' AND %s <> %s)", dao.BatchMain.Columns().BatchType, dao.BatchMain.Columns().SourceBatchNo, dao.BatchMain.Columns().SourceBatchNo, dao.BatchMain.Columns().BatchNo),
consts.BatchTypeAllocate,
)
mod = executionLogBatchTypeModel(mod, dao.BatchMain.Columns().BatchType, dao.BatchMain.Columns().BatchInfo, in.BatchType)
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
mod = mod.WhereBetween(dao.BatchMain.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
mod = mod.Page(in.Page, in.PageSize).
OrderDesc(dao.BatchMain.Columns().Id).
Hook(hook.MemberSummary)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取执行日志列表失败,请稍后重试!")
return
}
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)
}
}
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), fields); err != nil {
return nil, 0, err
}
return
}
func (s *sCommonBatchExecutionLog) Summary(ctx context.Context, in *model.BatchExecutionLogDetailParam) (res *model.BatchExecutionLogDetailModel, err error) {
if in == nil || in.BatchNo == "" {
return nil, gerror.New("批次号不能为空")
}
batch := &batchExecutionLogSummaryBatch{}
mod := executionLogBatchTypeModel(
s.batchModel(ctx).Where(dao.BatchMain.Columns().BatchNo, in.BatchNo),
dao.BatchMain.Columns().BatchType,
dao.BatchMain.Columns().BatchInfo,
nil,
)
if err = mod.Scan(batch); err != nil {
return nil, gerror.Wrap(err, "获取执行日志详情失败,请稍后重试!")
}
if batch.Id == 0 {
return nil, gerror.New("执行日志不存在")
}
if isAllocateChildBatch(batch.BatchType, batch.BatchNo, batch.SourceBatchNo) {
return nil, gerror.New("执行日志不存在")
}
res = &model.BatchExecutionLogDetailModel{
SourceChannel: batch.SourceChannel,
ExecutionMode: batch.ExecutionMode,
Id: batch.Id,
BatchNo: batch.BatchNo,
BatchType: batch.BatchType,
SourceType: batch.SourceType,
BatchAsyncStatus: batch.BatchAsyncStatus,
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)
if err := allocationunits.Fill(ctx, contexts.GetTenantId(ctx), []*allocationunits.Fields{&res.Fields}); err != nil {
return nil, err
}
}
if sourceFile, err := s.getSourceFile(ctx, batch.SourceOssId, batch.BatchInfo); err != nil {
return nil, err
} else {
res.SourceFile = sourceFile
}
// 导出任务的成品文件落在 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 {
resultFile, err = s.getResultFile(ctx, batch.BatchNo, batch.ResultOssId, batch.BatchInfo)
}
if err != nil {
return nil, err
}
res.ResultFile = resultFile
return res, nil
}
func (s *sCommonBatchExecutionLog) DetailList(ctx context.Context, in *model.BatchDetailListParam) (list []*model.BatchDetailListModel, totalCount int, err error) {
if in == nil || in.BatchNo == "" {
return nil, 0, gerror.New("批次号不能为空")
}
batch, err := s.getDetailBatch(ctx, in.BatchNo, in.BatchType)
if err != nil {
return nil, 0, err
}
columns := dao.BatchDetail.Columns()
detailBatchType := batch.BatchType
mod := s.detailModel(ctx).
Fields(
columns.Id,
columns.BatchNo,
columns.BatchType,
columns.DetailStatus,
columns.RowNo,
columns.ItemId,
columns.ErrorReason,
columns.Snapshot,
columns.TenantId,
columns.CreatedDept,
columns.CreatedBy,
).
Where(columns.BatchType, detailBatchType)
if isAllocateParentBatch(batch) {
childBatches := s.batchModel(ctx).
Fields(dao.BatchMain.Columns().BatchNo).
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeAllocate).
Where(dao.BatchMain.Columns().SourceBatchNo, batch.BatchNo).
Where(dao.BatchMain.Columns().SourceBatchNo + " <> " + dao.BatchMain.Columns().BatchNo)
mod = mod.Where("("+columns.BatchNo+" = ? OR "+columns.BatchNo+" IN (?))", batch.BatchNo, childBatches)
} else {
mod = mod.Where(columns.BatchNo, batch.BatchNo)
}
if in.DetailStatus != nil {
mod = mod.Where(columns.DetailStatus, *in.DetailStatus)
}
if in.Iccid != "" {
mod = mod.Where(columns.ItemId+" LIKE ?", "%"+in.Iccid+"%")
}
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
like := "%" + keyword + "%"
mod = mod.Where("("+columns.ItemId+" LIKE ? OR "+columns.ErrorReason+" LIKE ?)", like, like)
}
var details []*entity.BatchDetail
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, "获取批次执行明细列表失败,请稍后重试")
}
list = make([]*model.BatchDetailListModel, 0, len(details))
displayRowStart := 0
if in.Page > 0 && in.PageSize > 0 {
displayRowStart = (in.Page - 1) * in.PageSize
}
for index, detail := range details {
iccid, msisdn, imsi := batchExecutionLogDetailSnapshotCards(detail)
item := &model.BatchDetailListModel{
RowNo: displayRowStart + index + 1,
Iccid: iccid,
Msisdn: msisdn,
Imsi: imsi,
Imei: batchExecutionLogSnapshotValue(detail, "IMEI", "imei", "rawFields.IMEI"),
ChanKey: batchExecutionLogSnapshotValue(detail, "chanKey", "chan_key", "rawFields.chanKey", "rawFields.chan_key", "rawFields.通道编号", "rawFields.通道编码", "rawFields.通道编号*"),
CardType: batchExecutionLogSnapshotValue(detail, "cardType", "rawFields.cardType", "rawFields.卡类型"),
UpPkgCode: batchExecutionLogSnapshotValue(detail, "upPkgCode", "rawFields.upPkgCode", "rawFields.上游套餐编码", "rawFields.上游套餐"),
SuppliersCode: batchExecutionLogSnapshotValue(detail, "suppliersCode", "rawFields.suppliersCode", "rawFields.供应商编码", "rawFields.供应商"),
UpstreamActivationTime: batchExecutionLogSnapshotValue(detail, "upstreamActivationTime", "rawFields.upstreamActivationTime", "rawFields.上游激活时间", "rawFields.上游给出激活时间"),
InsertToPlugEnabled: batchExecutionLogSnapshotValue(detail, "insertToPlugEnabled", "rawFields.insertToPlugEnabled", "rawFields.是否贴片转插拔"),
CarrierTrafficCutoffEnabled: batchExecutionLogSnapshotValue(detail, "carrierTrafficCutoffEnabled", "rawFields.carrierTrafficCutoffEnabled", "rawFields.是否运营商达量断网"),
AgreementExpiryDate: batchExecutionLogSnapshotValue(detail, "agreementExpiryDate", "rawFields.agreementExpiryDate", "rawFields.协议到期时间", "rawFields.协议期到期时间"),
HasTestPeriod: batchExecutionLogSnapshotValue(detail, "hasTestPeriod", "rawFields.hasTestPeriod", "rawFields.是否有测试期"),
TestPeriodDays: batchExecutionLogSnapshotValue(detail, "testPeriodDays", "rawFields.testPeriodDays", "rawFields.测试期天数"),
TestPeriodLatestExpiryDate: batchExecutionLogSnapshotValue(detail, "testPeriodLatestExpiryDate", "rawFields.testPeriodLatestExpiryDate", "rawFields.测试期最晚过期日期"),
HasSilentPeriod: batchExecutionLogSnapshotValue(detail, "hasSilentPeriod", "rawFields.hasSilentPeriod", "rawFields.是否有沉默期"),
SilentPeriodDays: batchExecutionLogSnapshotValue(detail, "silentPeriodDays", "rawFields.silentPeriodDays", "rawFields.沉默期天数"),
SilentPeriodLatestExpiryDate: batchExecutionLogSnapshotValue(detail, "silentPeriodLatestExpiryDate", "rawFields.silentPeriodLatestExpiryDate", "rawFields.沉默期最晚过期日期"),
DetailStatus: detail.DetailStatus,
}
if batch.BatchType == consts.BatchTypeAllocate {
parentUnits := allocationunits.Resolve(batch.BatchInfo, batch.CreatedDept, batch.TargetDeptId)
item.Fields = allocationunits.Resolve(detail.Snapshot, parentUnits.SourceDeptId, parentUnits.TargetDeptId)
if item.SourceDeptName == "" && item.SourceDeptId == parentUnits.SourceDeptId {
item.SourceDeptName = parentUnits.SourceDeptName
}
if item.TargetDeptName == "" && item.TargetDeptId == parentUnits.TargetDeptId {
item.TargetDeptName = parentUnits.TargetDeptName
}
}
if detail.ErrorReason != "" {
item.Error = &model.BatchDetailErrorModel{
Reason: detail.ErrorReason,
}
}
list = append(list, item)
}
fields := []*allocationunits.Fields{}
for _, item := range list {
if batch.BatchType == consts.BatchTypeAllocate {
fields = append(fields, &item.Fields)
}
}
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), fields); err != nil {
return nil, 0, err
}
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("批次号不能为空")
}
columns := dao.BatchMain.Columns()
batch := &entity.BatchMain{}
mod := s.batchModel(ctx).
Fields(
columns.Id,
columns.BatchNo,
columns.BatchType,
columns.SourceType,
columns.SourceBatchNo,
columns.BatchInfo,
columns.CreatedDept, columns.TargetDeptId,
).
Where(columns.BatchNo, batchNo)
mod = executionLogBatchTypeModel(mod, columns.BatchType, columns.BatchInfo, batchType)
if err := mod.Scan(batch); err != nil {
return nil, gerror.Wrap(err, "获取批次信息失败,请稍后重试")
}
if batch.Id == 0 {
return nil, gerror.New("批次不存在")
}
if isAllocateChildBatch(batch.BatchType, batch.BatchNo, batch.SourceBatchNo) {
return nil, gerror.New("批次不存在")
}
if !executionLogBatchTypeMatches(batch.BatchType, batch.BatchInfo, batchType) {
return nil, gerror.New("批次类型不匹配")
}
return batch, nil
}
func executionLogBatchTypeModel(mod *gdb.Model, batchTypeColumn, batchInfoColumn string, requested *int) *gdb.Model {
if requested == nil {
return mod
}
if *requested == consts.BatchTypePrepare {
return mod.Where(batchTypeColumn, consts.BatchTypePrepare)
}
if *requested == consts.BatchTypeAllocate {
return mod.Where(
fmt.Sprintf("(%s = ? OR (%s = ? AND JSON_UNQUOTE(JSON_EXTRACT(%s, '$.targetBatchType')) = ?))", batchTypeColumn, batchTypeColumn, batchInfoColumn),
consts.BatchTypeAllocate,
consts.BatchTypePrepare,
strconv.Itoa(*requested),
)
}
return mod.Where(
fmt.Sprintf("(%s = ? OR (%s = ? AND JSON_UNQUOTE(JSON_EXTRACT(%s, '$.targetBatchType')) = ?))", batchTypeColumn, batchTypeColumn, batchInfoColumn),
*requested,
consts.BatchTypePrepare,
strconv.Itoa(*requested),
)
}
func executionLogBatchTypeMatches(actual int, info *gjson.Json, requested *int) bool {
if requested == nil {
return true
}
if actual == *requested {
return true
}
return actual == consts.BatchTypePrepare && info != nil && info.Get("targetBatchType").Int() == *requested
}
func isAllocateParentBatch(batch *entity.BatchMain) bool {
if batch == nil || batch.BatchType != consts.BatchTypeAllocate || isAllocateChildBatch(batch.BatchType, batch.BatchNo, batch.SourceBatchNo) {
return false
}
return true
}
func isAllocateChildBatch(batchType int, batchNo, sourceBatchNo string) bool {
return batchType == consts.BatchTypeAllocate &&
strings.TrimSpace(sourceBatchNo) != "" &&
strings.TrimSpace(sourceBatchNo) != strings.TrimSpace(batchNo)
}
func batchExecutionLogDetailSnapshotCards(detail *entity.BatchDetail) (iccid, msisdn, imsi string) {
if detail == nil {
return
}
if detail.Snapshot != nil {
iccid = strings.TrimSpace(detail.Snapshot.Get("ICCID").String())
msisdn = strings.TrimSpace(detail.Snapshot.Get("MSISDN").String())
imsi = strings.TrimSpace(detail.Snapshot.Get("IMSI").String())
if iccid == "" {
iccid = strings.TrimSpace(detail.Snapshot.Get("iccid").String())
}
if msisdn == "" {
msisdn = strings.TrimSpace(detail.Snapshot.Get("msisdn").String())
}
if imsi == "" {
imsi = strings.TrimSpace(detail.Snapshot.Get("imsi").String())
}
}
if iccid == "" {
iccid = detail.ItemId
}
return
}
func batchExecutionLogSnapshotValue(detail *entity.BatchDetail, paths ...string) string {
if detail == nil || detail.Snapshot == nil {
return ""
}
for _, path := range paths {
value := strings.TrimSpace(detail.Snapshot.Get(path).String())
if value != "" {
return value
}
}
return ""
}
func (s *sCommonBatchExecutionLog) getSourceFile(ctx context.Context, sourceOssId int64, batchInfo *gjson.Json) (*model.BatchExecutionLogFileModel, error) {
ossId := sourceOssId
if ossId == 0 {
ossId = legacySourceOssId(batchInfo)
}
if ossId == 0 {
return nil, nil
}
oss := &entity.SysOss{}
if err := s.ossModel(ctx).
Where(dao.SysOss.Columns().OssId, ossId).
Scan(oss); err != nil {
return nil, gerror.Wrap(err, "获取原始文件信息失败,请稍后重试!")
}
if oss.OssId == 0 {
return nil, nil
}
return &model.BatchExecutionLogFileModel{
OssId: oss.OssId,
Url: batchExecutionLogFileURL(ctx, oss.Url, oss.Path),
}, nil
}
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 && !errors.Is(err, sql.ErrNoRows) {
return nil, gerror.Wrap(err, "获取结果文件信息失败,请稍后重试!")
}
ossId := resultOssId
if ossId == 0 && download.OssId > 0 {
ossId = download.OssId
}
if ossId == 0 {
ossId = legacyResultOssId(batchInfo)
}
if download.Id == 0 && ossId == 0 {
return nil, nil
}
url := batchExecutionLogFileURL(ctx, download.FileUrl, download.FilePath)
originalName := ""
if ossId > 0 {
oss := &entity.SysOss{}
if scanErr := s.ossModel(ctx).
Where(dao.SysOss.Columns().OssId, ossId).
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
}
}
}
return &model.BatchExecutionLogResultFileModel{
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
}
for _, path := range []string{"importFileOssId", "source.importFileOssId", "source.inputFileOssId"} {
if ossId := batchInfo.Get(path).Int64(); ossId > 0 {
return ossId
}
}
return 0
}
func legacyResultOssId(batchInfo *gjson.Json) int64 {
if batchInfo == nil {
return 0
}
return batchInfo.Get("resultFileOssId").Int64()
}
func resultDownloadStatus(download *entity.TResourceBatchDownload) int {
if download != nil && download.Id > 0 {
return download.DownloadStatus
}
return consts.DownloadStatusGenerated
}
func batchExecutionLogFileURL(ctx context.Context, storedURL string, storedPath string) string {
if url := normalizeBatchLocalUploadURL(ctx, storedURL); url != "" {
return url
}
if url := normalizeBatchLocalUploadURL(ctx, storedPath); url != "" {
return url
}
return strings.TrimSpace(storedURL)
}
func normalizeBatchLocalUploadURL(ctx context.Context, value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
lower := strings.ToLower(value)
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
return value
}
normalized := filepath.ToSlash(filepath.Clean(value))
if normalized == "/upload" || strings.HasPrefix(normalized, "/upload/") {
return normalized
}
if configType, err := service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssUrlPath); err == nil && configType != nil {
configValue := strings.TrimSpace(configType.ConfigValue)
if configValue != "" {
if strings.Contains(configValue, "|") {
split := strings.SplitN(configValue, "|", 2)
from := strings.TrimSpace(filepath.ToSlash(split[0]))
to := strings.TrimSpace(filepath.ToSlash(split[1]))
if suffix, ok := batchLocalUploadMappedSuffix(normalized, from); ok {
return batchJoinUploadURL(to, suffix)
}
} else if suffix := batchLocalUploadRelativePath(normalized); suffix != "" {
return batchJoinUploadURL(configValue, suffix)
}
}
}
if suffix := batchLocalUploadRelativePath(normalized); suffix != "" {
return batchJoinUploadURL("/upload", suffix)
}
if strings.HasPrefix(normalized, "/") {
return normalized
}
return ""
}
func batchLocalUploadMappedSuffix(filePath string, from string) (string, bool) {
from = strings.Trim(strings.TrimSpace(filepath.ToSlash(from)), "/")
if from == "" {
return "", false
}
normalized := strings.TrimSpace(filepath.ToSlash(filePath))
marker := "/" + from + "/"
if idx := strings.Index(normalized, marker); idx >= 0 {
return strings.TrimPrefix(normalized[idx+len(marker):], "/"), true
}
prefix := from + "/"
if strings.HasPrefix(normalized, prefix) {
return strings.TrimPrefix(normalized[len(prefix):], "/"), true
}
if strings.Trim(normalized, "/") == from {
return "", true
}
return "", false
}
func batchLocalUploadRelativePath(filePath string) string {
normalized := filepath.ToSlash(filepath.Clean(strings.TrimSpace(filePath)))
for _, marker := range []string{
"resource/upload/",
"/resource/upload/",
"/app/resource/upload/",
"/app/upload/",
"upload/",
"/upload/",
} {
if idx := strings.Index(normalized, marker); idx >= 0 {
return strings.TrimPrefix(normalized[idx+len(marker):], "/")
}
}
return ""
}
func batchJoinUploadURL(base string, suffix string) string {
base = strings.TrimRight(strings.TrimSpace(filepath.ToSlash(base)), "/")
suffix = strings.TrimLeft(strings.TrimSpace(filepath.ToSlash(suffix)), "/")
if base == "" {
base = "/upload"
}
if suffix == "" {
return base
}
return base + "/" + suffix
}
@@ -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)
}
}
@@ -0,0 +1,726 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package common
import (
"context"
"fmt"
"strings"
"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/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"
"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/model/do"
"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 sCommonBatchMain struct{}
const restartPhaseWaitingResume = "waiting_resume"
type batchExecuteMessage struct {
BatchNo string `json:"batchNo"`
BatchType int `json:"batchType"`
}
var publishBatchExecuteMessage = rabbitmq.Publish
func NewCommonBatchMain() *sCommonBatchMain {
return &sCommonBatchMain{}
}
func (s *sCommonBatchMain) getRequeueBatch(ctx context.Context, batchNo string) (*entity.BatchMain, error) {
columns := dao.BatchMain.Columns()
batch := &entity.BatchMain{}
err := s.Model(ctx).
Fields(
columns.Id,
columns.BatchNo,
columns.BatchType,
columns.BatchAsyncStatus,
columns.RunResult,
columns.BatchInfo,
columns.TenantId,
columns.DeptId,
columns.CreatedDept,
columns.CreatedBy,
).
Where(columns.BatchNo, batchNo).
Where(columns.BatchType+" <> ?", consts.BatchTypePrepare).
Scan(batch)
if err != nil {
return nil, gerror.Wrap(err, "查询批次信息失败")
}
if batch.Id == 0 {
return nil, gerror.New("批次不存在或无权限")
}
return batch, nil
}
func validateRequeueBatch(batch *entity.BatchMain) error {
if batch == nil {
return gerror.New("批次不存在")
}
switch batch.BatchAsyncStatus {
case consts.BatchAsyncStatusPending:
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:
return gerror.Newf("当前批次状态不支持安全重投,status=%d", batch.BatchAsyncStatus)
}
}
func validateProcessingRequeueBatch(batch *entity.BatchMain) error {
if batch.BatchType != consts.BatchTypeRestart {
return gerror.New("处理中批次不支持安全重投,避免重复调用运营商")
}
if restartPhase(batch) != restartPhaseWaitingResume {
return gerror.New("RST 批次当前不在等待复机阶段,不能安全重投")
}
resumeAt := restartResumeAt(batch)
if resumeAt == nil {
return gerror.New("RST 批次缺少计划复机时间,不能安全重投")
}
if resumeAt.After(gtime.Now()) {
return gerror.Newf("未到计划复机时间,resumeAt=%s", resumeAt.String())
}
return nil
}
func restartPhase(batch *entity.BatchMain) string {
if batch == nil || batch.BatchInfo == nil {
return ""
}
return strings.TrimSpace(batch.BatchInfo.Get("restart.phase").String())
}
func restartResumeAt(batch *entity.BatchMain) *gtime.Time {
if batch == nil || batch.BatchInfo == nil {
return nil
}
value := strings.TrimSpace(batch.BatchInfo.Get("restart.resumeAt").String())
if value == "" {
return nil
}
parsed := gtime.NewFromStr(value)
if parsed == nil || parsed.IsZero() {
return nil
}
return parsed
}
func init() {
service.RegisterCommonBatchMain(NewCommonBatchMain())
}
// Model 批次主表ORM模型
func (s *sCommonBatchMain) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.BatchMain.Ctx(ctx), option...)
}
func (s *sCommonBatchMain) detailModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.BatchDetail.Ctx(ctx), option...)
}
// List 获取批次主表列表
func (s *sCommonBatchMain) List(ctx context.Context, in *model.BatchMainListParam) (list []*model.BatchMainListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(model.BatchMainListModel{})
// 查询主键ID
if in.Id > 0 {
mod = mod.Where(dao.BatchMain.Columns().Id, in.Id)
}
// 查询批次编号
if in.BatchNo != "" {
mod = mod.Where(dao.BatchMain.Columns().BatchNo+" LIKE ?", "%"+in.BatchNo+"%")
}
// 查询订单编号
if in.OrderSn != "" {
mod = mod.Where(dao.BatchMain.Columns().OrderSn+" LIKE ?", "%"+in.OrderSn+"%")
}
// 查询批次类型 枚举
mod = visibleBatchTypeModel(mod, dao.BatchMain.Columns().BatchType, in.BatchType)
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.BatchMain.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.BatchMain.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取批次主表列表失败,请稍后重试!")
return
}
units := make([]*allocationunits.Fields, 0, len(list))
for _, item := range list {
if item.BatchType == consts.BatchTypeAllocate {
fields := allocationunits.Resolve(item.BatchInfo, item.CreatedDept, item.TargetDeptId)
units = append(units, &fields)
}
}
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), units); err != nil {
return nil, 0, err
}
unitIndex := 0
for _, item := range list {
if item.BatchType == consts.BatchTypeAllocate {
fields := units[unitIndex]
item.SourceDeptId, item.SourceDeptName = fields.SourceDeptId, fields.SourceDeptName
item.TargetDeptId, item.TargetDeptName, item.TargetDeptMode = fields.TargetDeptId, fields.TargetDeptName, fields.TargetDeptMode
unitIndex++
}
}
return
}
func (s *sCommonBatchMain) SuccessList(ctx context.Context, in *model.BatchMainSuccessListParam) (list []*model.BatchMainSuccessListModel, totalCount int, err error) {
if in == nil {
in = &model.BatchMainSuccessListParam{}
}
columns := dao.BatchMain.Columns()
mod := s.Model(ctx).
Fields(
columns.BatchInfo,
columns.BatchNo,
columns.BatchType,
columns.StartTime+" AS batch_time",
columns.TotalCount,
columns.CreatedDept,
columns.TargetDeptId,
).
Where(columns.RunResult, consts.BatchRunResultSuccess).
WhereGT(columns.TotalCount, 0)
var requestedBatchType *int
if in.BatchType != 0 {
requestedBatchType = &in.BatchType
}
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+"%")
}
if len(in.Time) == 2 {
in.Time[1] = in.Time[1].EndOfDay()
mod = mod.WhereBetween(columns.StartTime, in.Time[0], in.Time[1])
}
mod = mod.Page(in.Page, in.PageSize).OrderDesc(columns.Id)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取成功批次列表失败,请稍后重试")
}
if err = fillInboundBatchStockCounts(ctx, list); err != nil {
return nil, 0, err
}
units := make([]*allocationunits.Fields, 0, len(list))
for _, item := range list {
if item.BatchType == consts.BatchTypeAllocate {
item.Fields = allocationunits.Resolve(item.BatchInfo, item.StoredSourceDeptId, item.StoredTargetDeptId)
units = append(units, &item.Fields)
}
}
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), units); err != nil {
return nil, 0, err
}
return
}
func fillInboundBatchStockCounts(ctx context.Context, list []*model.BatchMainSuccessListModel) error {
if len(list) == 0 {
return nil
}
batchNos := make([]string, 0, len(list))
for _, item := range list {
if item == nil || item.BatchType != consts.BatchTypeInbound || strings.TrimSpace(item.BatchNo) == "" {
continue
}
batchNos = append(batchNos, item.BatchNo)
}
if len(batchNos) == 0 {
return nil
}
type stockCountRow struct {
BatchNo string `orm:"batch_no"`
StockCount int `orm:"stock_count"`
}
var rows []*stockCountRow
stockColumns := dao.TSimCardStock.Columns()
if err := dao.TSimCardStock.Ctx(ctx).
Fields(stockColumns.BatchNo, "COUNT(1) AS stock_count").
Where(stockColumns.TenantId, contexts.GetTenantId(ctx)).
WhereIn(stockColumns.BatchNo, batchNos).
Group(stockColumns.BatchNo).
Scan(&rows); err != nil {
return gerror.Wrap(err, "统计入库批次在库数量失败")
}
stockCountMap := make(map[string]int, len(rows))
for _, row := range rows {
if row != nil {
stockCountMap[row.BatchNo] = row.StockCount
}
}
for _, item := range list {
if item != nil && item.BatchType == consts.BatchTypeInbound {
item.StockCount = stockCountMap[item.BatchNo]
}
}
return nil
}
type batchDetailQuery struct {
BatchNo string
BatchType *int
DetailStatus *int
Iccid string
Msisdn string
CurrentStockOnly bool
Page int
PageSize int
}
func (s *sCommonBatchMain) ListCards(ctx context.Context, in *model.BatchCardListParam) (list []*model.BatchCardListModel, totalCount int, err error) {
if in == nil || in.BatchNo == "" {
return nil, 0, gerror.New("批次号不能为空")
}
batch, err := s.getVisibleBatch(ctx, in.BatchNo, in.BatchType)
if err != nil {
return nil, 0, err
}
details, totalCount, err := s.listBatchDetails(ctx, batch, batchDetailQuery{
DetailStatus: in.DetailStatus,
BatchNo: in.BatchNo,
BatchType: in.BatchType,
Iccid: in.Iccid,
Msisdn: in.Msisdn,
CurrentStockOnly: in.CurrentStockOnly,
Page: in.Page,
PageSize: in.PageSize,
})
if err != nil {
return nil, 0, err
}
list = make([]*model.BatchCardListModel, 0, len(details))
displayRowStart := 0
if in.Page > 0 && in.PageSize > 0 {
displayRowStart = (in.Page - 1) * in.PageSize
}
units := make([]*allocationunits.Fields, 0, len(details))
parentUnits := allocationunits.Resolve(batch.BatchInfo, batch.CreatedDept, batch.TargetDeptId)
for index, detail := range details {
iccid, msisdn, imsi := batchDetailSnapshotCards(detail)
item := &model.BatchCardListModel{
RowNo: displayRowStart + index + 1,
Iccid: iccid,
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)
if item.SourceDeptName == "" && item.SourceDeptId == parentUnits.SourceDeptId {
item.SourceDeptName = parentUnits.SourceDeptName
}
if item.TargetDeptName == "" && item.TargetDeptId == parentUnits.TargetDeptId {
item.TargetDeptName = parentUnits.TargetDeptName
}
units = append(units, &item.Fields)
}
list = append(list, item)
}
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), units); err != nil {
return nil, 0, err
}
return
}
// listBatchDetails 获取批次卡清单明细。
func (s *sCommonBatchMain) listBatchDetails(ctx context.Context, batch *entity.BatchMain, query batchDetailQuery) (list []*entity.BatchDetail, totalCount int, err error) {
columns := dao.BatchDetail.Columns()
mod := s.detailModel(ctx).
Fields(
columns.Id,
columns.BatchNo,
columns.BatchType,
columns.DetailStatus,
columns.RowNo,
columns.ItemId,
columns.ErrorReason,
columns.Snapshot,
columns.TenantId,
columns.CreatedDept,
columns.CreatedBy,
).
Where(columns.BatchType, batch.BatchType)
if batch.BatchType == consts.BatchTypeAllocate && (batch.SourceBatchNo == "" || batch.SourceBatchNo == batch.BatchNo) {
mainColumns := dao.BatchMain.Columns()
childBatches := s.Model(ctx).Fields(mainColumns.BatchNo).
Where(mainColumns.BatchType, consts.BatchTypeAllocate).
Where(mainColumns.SourceBatchNo, batch.BatchNo).
Where(mainColumns.SourceBatchNo + " <> " + mainColumns.BatchNo)
mod = mod.Where("("+columns.BatchNo+" = ? OR "+columns.BatchNo+" IN (?))", batch.BatchNo, childBatches)
} else {
mod = mod.Where(columns.BatchNo, batch.BatchNo)
}
if query.DetailStatus != nil {
mod = mod.Where(columns.DetailStatus, *query.DetailStatus)
}
if query.Iccid != "" {
like := "%" + query.Iccid + "%"
mod = mod.Where(
"("+columns.ItemId+" LIKE ? OR JSON_UNQUOTE(JSON_EXTRACT("+columns.Snapshot+", '$.ICCID')) LIKE ?)",
like,
like,
)
}
if query.Msisdn != "" {
mod = mod.Where(
"JSON_UNQUOTE(JSON_EXTRACT("+columns.Snapshot+", '$.MSISDN')) LIKE ?",
"%"+query.Msisdn+"%",
)
}
if query.CurrentStockOnly {
mod = onlyCurrentStockDetails(mod)
}
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()
return mod.Where(fmt.Sprintf(
"EXISTS (SELECT 1 FROM `%s` stock WHERE stock.`tenant_id` = `%s`.`tenant_id` AND stock.`batch_no` = `%s`.`batch_no` AND stock.`iccid` = COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`%s`.`snapshot`, '$.ICCID')), ''), `%s`.`item_id`))",
stockTable,
detailTable,
detailTable,
detailTable,
detailTable,
))
}
func (s *sCommonBatchMain) getVisibleBatch(ctx context.Context, batchNo string, batchType *int) (*entity.BatchMain, error) {
if batchNo == "" {
return nil, gerror.New("批次号不能为空")
}
columns := dao.BatchMain.Columns()
batch := &entity.BatchMain{}
mod := s.Model(ctx).
Fields(
columns.Id,
columns.BatchNo,
columns.BatchType,
columns.TenantId,
columns.DeptId,
columns.BatchInfo,
columns.TargetDeptId,
columns.SourceBatchNo,
columns.CreatedDept,
columns.CreatedBy,
).
Where(columns.BatchNo, batchNo)
mod = visibleBatchTypeModel(mod, columns.BatchType, batchType)
if err := mod.Scan(batch); err != nil {
return nil, gerror.Wrap(err, "获取批次信息失败,请稍后重试")
}
if batch.Id == 0 {
return nil, gerror.New("批次不存在")
}
if !visibleBatchTypeMatches(batch.BatchType, batchType) {
return nil, gerror.New("批次类型不匹配")
}
return batch, nil
}
func batchDetailSnapshotCards(detail *entity.BatchDetail) (iccid, msisdn, imsi string) {
if detail == nil {
return
}
if detail.Snapshot != nil {
iccid = strings.TrimSpace(detail.Snapshot.Get("ICCID").String())
msisdn = strings.TrimSpace(detail.Snapshot.Get("MSISDN").String())
imsi = strings.TrimSpace(detail.Snapshot.Get("IMSI").String())
}
if iccid == "" {
iccid = detail.ItemId
}
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)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.BatchMainExportModel{})
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.BatchMainExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增批次主表
func (s *sCommonBatchMain) Edit(ctx context.Context, in *model.BatchMainEditParam) (err error) {
if in == nil {
return gerror.New("批次参数不能为空")
}
if in.Id <= 0 {
if in.ExecutionMode == nil {
return gerror.New("新建批次必须指定单卡或批量操作规模")
}
if err = batchmetadata.ValidateCreation(batchmetadata.Console, *in.ExecutionMode); err != nil {
return err
}
if *in.ExecutionMode == batchmetadata.Single && (in.TotalCount == nil || *in.TotalCount != 1) {
return gerror.New("单卡批次总数量必须为1")
}
}
data := do.BatchMain{}
if err = gconv.Struct(in, &data); err != nil {
return gerror.Wrap(err, "转换批次参数失败")
}
data.CreatedAt = nil
data.UpdatedAt = nil
// 验证'BatchNo'唯一
if err = xgorm.IsUnique(ctx, &dao.BatchMain, g.Map{dao.BatchMain.Columns().BatchNo: in.BatchNo}, "批次编号已存在", in.Id); err != nil {
return
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
data.UpdatedBy = contexts.GetUserId(ctx)
data.SourceChannel = nil
data.ExecutionMode = nil
if _, err = s.Model(ctx).TX(tx).
Fields(model.BatchMainUpdateFields{}).
WherePri(in.Id).Data(data).Update(); err != nil {
err = gerror.Wrap(err, "修改批次主表失败,请稍后重试!")
}
return
}
// 新增
data.Id = nil
data.TenantId = contexts.GetTenantId(ctx)
data.CreatedDept = contexts.GetDeptId(ctx)
data.CreatedBy = contexts.GetUserId(ctx)
data.SourceChannel = batchmetadata.Console
data.ExecutionMode = *in.ExecutionMode
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx).
Fields(model.BatchMainInsertFields{}).
Data(data).Insert(); err != nil {
err = gerror.Wrap(err, "新增批次主表失败,请稍后重试!")
}
return
})
}
// Delete 删除批次主表
func (s *sCommonBatchMain) Delete(ctx context.Context, in *model.BatchMainDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除批次主表失败,请稍后重试!")
return
}
return
}
// View 获取批次主表指定信息
func (s *sCommonBatchMain) Requeue(ctx context.Context, in *model.BatchMainRequeueParam) (res *model.BatchMainRequeueModel, err error) {
if in == nil {
in = &model.BatchMainRequeueParam{}
}
batchNo := strings.TrimSpace(in.BatchNo)
reason := strings.TrimSpace(in.Reason)
if batchNo == "" {
return nil, gerror.New("批次编号不能为空")
}
if reason == "" {
return nil, gerror.New("重投原因不能为空")
}
batch, err := s.getRequeueBatch(ctx, batchNo)
if err != nil {
return nil, err
}
if err = validateRequeueBatch(batch); err != nil {
return nil, err
}
message := batchExecuteMessage{BatchNo: batch.BatchNo, BatchType: batch.BatchType}
if err = publishBatchExecuteMessage(ctx, consts.QueueBatchExecute, message); err != nil {
g.Log().Errorf(ctx, "batch requeue publish failed, batchNo=%s, batchType=%d, tenantId=%s, userId=%d, reason=%s, err=%+v",
batch.BatchNo, batch.BatchType, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason, err)
return nil, gerror.Wrap(err, "发布批次重投消息失败")
}
g.Log().Infof(ctx, "batch requeue published, batchNo=%s, batchType=%d, status=%d, runResult=%d, tenantId=%s, userId=%d, reason=%s",
batch.BatchNo, batch.BatchType, batch.BatchAsyncStatus, batch.RunResult, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason)
return &model.BatchMainRequeueModel{
BatchNo: batch.BatchNo,
BatchType: batch.BatchType,
BatchAsyncStatus: batch.BatchAsyncStatus,
RunResult: batch.RunResult,
Requeued: true,
}, nil
}
func (s *sCommonBatchMain) View(ctx context.Context, in *model.BatchMainViewParam) (res *model.BatchMainViewModel, err error) {
mod := visibleBatchTypeModel(
s.Model(ctx).WherePri(in.Id),
dao.BatchMain.Columns().BatchType,
nil,
)
if err = mod.Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取批次主表信息,请稍后重试!")
return
}
if res == nil || res.Id == 0 {
return nil, gerror.New("批次不存在")
}
if res.BatchType == consts.BatchTypeAllocate {
fields := allocationunits.Resolve(res.BatchInfo, res.CreatedDept, res.TargetDeptId)
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), []*allocationunits.Fields{&fields}); err != nil {
return nil, err
}
res.SourceDeptId, res.SourceDeptName = fields.SourceDeptId, fields.SourceDeptName
res.TargetDeptId, res.TargetDeptName, res.TargetDeptMode = fields.TargetDeptId, fields.TargetDeptName, fields.TargetDeptMode
}
return
}
@@ -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)
}
}
@@ -0,0 +1,34 @@
package common
import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"github.com/gogf/gf/v2/database/gdb"
)
// visibleBatchTypeModel always excludes internal prepare batches before applying
// an optional caller-provided batch type filter.
func visibleBatchTypeModel(mod *gdb.Model, column string, requested *int) *gdb.Model {
mod = mod.Where(column+" <> ?", consts.BatchTypePrepare)
if requested != nil {
mod = mod.Where(column, *requested)
}
return mod
}
func visibleBatchTypeMatches(actual int, requested *int) bool {
if actual == consts.BatchTypePrepare {
return false
}
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
}
@@ -0,0 +1,837 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"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/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"
"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"
"github.com/google/uuid"
)
type sCommonAgentBalance struct{}
func NewCommonAgentBalance() *sCommonAgentBalance {
return &sCommonAgentBalance{}
}
func init() {
service.RegisterCommonAgentBalance(NewCommonAgentBalance())
}
// Model 企业客户财务信息ORM模型
func (s *sCommonAgentBalance) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TAgentBalance.Ctx(ctx), option...)
}
// List 获取企业客户财务信息列表
func (s *sCommonAgentBalance) List(ctx context.Context, in *model.TAgentBalanceListParam) (list []*model.TAgentBalanceListModel, totalCount int, err error) {
mod := s.Model(ctx)
deptId := contexts.GetDeptId(ctx)
mod = mod.Fields(model.TAgentBalanceListModel{})
mod = mod.Where(dao.TAgentBalance.Columns().DeptId, deptId)
if in.Id > 0 {
mod = mod.Where(dao.TAgentBalance.Columns().Id, in.Id)
}
if in.WithdrawAmountStatus > 0 {
mod = mod.Where(dao.TAgentBalance.Columns().WithdrawAmountStatus, in.WithdrawAmountStatus)
}
if in.Status > 0 {
mod = mod.Where(dao.TAgentBalance.Columns().Status, in.Status)
}
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TAgentBalance.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
mod = mod.Page(in.Page, in.PageSize)
mod = mod.OrderDesc(dao.TAgentBalance.Columns().Id)
mod = mod.Hook(hook.MemberSummary)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取企业客户财务信息列表失败,请稍后重试!")
return
}
return
}
// Export 导出企业客户财务信息
func (s *sCommonAgentBalance) Export(ctx context.Context, in *model.TAgentBalanceListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
tags, err := convert.GetEntityDescTags(model.TAgentBalanceExportModel{})
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.TAgentBalanceExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增企业客户财务信息
func (s *sCommonAgentBalance) Edit(ctx context.Context, in *model.TAgentBalanceEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TAgentBalanceUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改企业客户财务信息失败,请稍后重试!")
}
return
}
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TAgentBalanceInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增企业客户财务信息失败,请稍后重试!")
}
return
})
}
// Delete 删除企业客户财务信息
func (s *sCommonAgentBalance) Delete(ctx context.Context, in *model.TAgentBalanceDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除企业客户财务信息失败,请稍后重试!")
return
}
return
}
// View 获取企业客户财务信息指定信息
func (s *sCommonAgentBalance) View(ctx context.Context, in *model.TAgentBalanceViewParam) (res *model.TAgentBalanceViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取企业客户财务信息信息,请稍后重试!")
return
}
return
}
// Status 更新企业客户财务信息状态
func (s *sCommonAgentBalance) Status(ctx context.Context, in *model.TAgentBalanceStatusParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.TAgentBalance.Columns().Status: in.Status,
dao.TAgentBalance.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新企业客户财务信息状态失败,请稍后重试!")
return
}
return
}
// GetAccountBalance 获取当前登录企业的账户余额信息。
func (s *sCommonAgentBalance) GetAccountBalance(ctx context.Context, in *model.TAgentBalanceTrendParam) (res *model.TAgentBalanceChartData, err error) {
res = &model.TAgentBalanceChartData{}
deptId := contexts.GetDeptId(ctx)
months, startTime, endTime, err := agentBalanceMonthRange(in.StartMonth, in.EndMonth)
if err != nil {
return nil, err
}
balanceMap, err := s.getMonthlyLastBalance(ctx, dao.TBalanceDetail.Ctx(ctx),
dao.TBalanceDetail.Columns().TransactionTime, dao.TBalanceDetail.Columns().BalanceAfter,
dao.TBalanceDetail.Columns().DeptId, deptId, startTime, endTime, "余额趋势")
if err != nil {
return nil, err
}
withdrawableMap, err := s.getMonthlyLastBalance(ctx, dao.TFinWithdrawableChange.Ctx(ctx),
dao.TFinWithdrawableChange.Columns().CreatedAt, dao.TFinWithdrawableChange.Columns().AfterBalance,
dao.TFinWithdrawableChange.Columns().DeptId, deptId, startTime, endTime, "可提现金额趋势")
if err != nil {
return nil, err
}
creditMap, err := s.getMonthlyLastBalance(ctx, dao.TCreditDetail.Ctx(ctx),
dao.TCreditDetail.Columns().TransactionTime, dao.TCreditDetail.Columns().CreditBalanceAfter,
dao.TCreditDetail.Columns().DeptId, deptId, startTime, endTime, "信用额度趋势")
if err != nil {
return nil, err
}
balanceValues := make([]float64, 0, len(months))
withdrawableValues := make([]float64, 0, len(months))
creditValues := make([]float64, 0, len(months))
for _, month := range months {
balanceValues = append(balanceValues, balanceMap[month])
withdrawableValues = append(withdrawableValues, withdrawableMap[month])
creditValues = append(creditValues, creditMap[month])
}
res.Labels = months
res.Series = []*model.TAgentBalanceChartSeries{
{Name: "余额", Values: balanceValues},
{Name: "可提现金额", Values: withdrawableValues},
{Name: "信用额度", Values: creditValues},
}
return
}
// getMonthlyLastBalance 查询指定月份范围内每月最后一条记录的余额。
func (s *sCommonAgentBalance) getMonthlyLastBalance(ctx context.Context, mod *gdb.Model, timeColumn string, balanceColumn string, deptIdColumn string, deptId interface{}, startTime, endTime time.Time, errMsg string) (map[string]float64, error) {
type maxTimeRow struct {
AccountPeriod string `orm:"account_period"`
MaxTime string `orm:"max_time"`
}
var maxTimes []*maxTimeRow
if err := mod.Fields("DATE_FORMAT("+timeColumn+", '%Y-%m') as account_period, MAX("+timeColumn+") as max_time").
Where(deptIdColumn, deptId).
WhereGTE(timeColumn, startTime).
WhereLT(timeColumn, endTime.AddDate(0, 1, 0)).
Group("DATE_FORMAT(" + timeColumn + ", '%Y-%m')").
Scan(&maxTimes); err != nil {
return nil, gerror.Wrap(err, "查询"+errMsg+"失败,请稍后重试!")
}
result := make(map[string]float64)
if len(maxTimes) == 0 {
return result, nil
}
maxTimeList := make([]string, len(maxTimes))
for i, mt := range maxTimes {
maxTimeList[i] = mt.MaxTime
}
type balanceRow struct {
AccountPeriod string `orm:"account_period"`
BalanceAfter float64 `orm:"balance_after"`
}
var allResults []*balanceRow
if err := mod.Fields("DATE_FORMAT("+timeColumn+", '%Y-%m') as account_period, "+balanceColumn+" as balance_after").
Where(deptIdColumn, deptId).
WhereIn(timeColumn, maxTimeList).
Order(timeColumn + " ASC").
Scan(&allResults); err != nil {
return nil, gerror.Wrap(err, "查询"+errMsg+"失败,请稍后重试!")
}
for _, item := range allResults {
result[item.AccountPeriod] = item.BalanceAfter
}
return result, nil
}
// AccountTrend 获取收入金额、支出金额折线图。
func (s *sCommonAgentBalance) AccountTrend(ctx context.Context, in *model.TAgentBalanceTrendParam) (res *model.TAgentBalanceChartData, err error) {
months, startMonth, endMonth, err := agentBalanceMonthRange(in.StartMonth, in.EndMonth)
if err != nil {
return nil, err
}
deptId := contexts.GetDeptId(ctx)
type billRow struct {
AccountPeriod string `orm:"account_period"`
TotalIncome float64 `orm:"total_income"`
TotalExpense float64 `orm:"total_expense"`
}
var bills []*billRow
accountPeriodColumn := dao.TProfitAccountMonthBill.Columns().AccountPeriod
deptIdColumn := dao.TProfitAccountMonthBill.Columns().DeptId
totalIncomeColumn := dao.TProfitAccountMonthBill.Columns().TotalIncome
totalExpenseColumn := dao.TProfitAccountMonthBill.Columns().TotalExpense
mod := handler.Model(dao.TProfitAccountMonthBill.Ctx(ctx))
if in.AccountType == "2" {
accountPeriodColumn = dao.TConsumeAccountMonthBill.Columns().AccountPeriod
deptIdColumn = dao.TConsumeAccountMonthBill.Columns().DeptId
totalIncomeColumn = dao.TConsumeAccountMonthBill.Columns().TotalIncome
totalExpenseColumn = dao.TConsumeAccountMonthBill.Columns().TotalExpense
mod = handler.Model(dao.TConsumeAccountMonthBill.Ctx(ctx))
}
if err = mod.Fields(accountPeriodColumn, totalIncomeColumn, totalExpenseColumn).
Where(deptIdColumn, deptId).
WhereBetween(accountPeriodColumn, startMonth, endMonth).
Scan(&bills); err != nil {
return nil, gerror.Wrap(err, "查询账户总览趋势失败,请稍后重试!")
}
billMap := make(map[string]*billRow, len(bills))
for _, item := range bills {
billMap[item.AccountPeriod] = item
}
incomeValues := make([]float64, 0, len(months))
expenseValues := make([]float64, 0, len(months))
for _, month := range months {
if item, ok := billMap[month]; ok {
incomeValues = append(incomeValues, item.TotalIncome)
expenseValues = append(expenseValues, item.TotalExpense)
continue
}
incomeValues = append(incomeValues, 0)
expenseValues = append(expenseValues, 0)
}
return &model.TAgentBalanceChartData{
Labels: months,
Series: []*model.TAgentBalanceChartSeries{
{Name: "收入金额", Values: incomeValues},
{Name: "支出金额", Values: expenseValues},
},
}, nil
}
// agentBalanceMonthRange 返回月份范围。
func agentBalanceMonthRange(startMonth, endMonth string) (months []string, startTime time.Time, endTime time.Time, err error) {
now := time.Now()
if endMonth == "" {
endMonth = now.Format("2006-01")
}
if startMonth == "" {
tempEndTime, parseErr := time.Parse("2006-01", endMonth)
if parseErr != nil {
err = gerror.New("结束月份格式不正确,应为YYYY-MM")
return
}
startMonth = tempEndTime.AddDate(0, -11, 0).Format("2006-01")
}
startTime, err = time.Parse("2006-01", startMonth)
if err != nil {
err = gerror.New("开始月份格式不正确,应为YYYY-MM")
return
}
endTime, err = time.Parse("2006-01", endMonth)
if err != nil {
err = gerror.New("结束月份格式不正确,应为YYYY-MM")
return
}
if startTime.After(endTime) {
err = gerror.New("开始月份不能大于结束月份")
return
}
for t := startTime; !t.After(endTime); t = t.AddDate(0, 1, 0) {
months = append(months, t.Format("2006-01"))
}
return months, startTime, endTime, nil
}
const (
financeIncomeExpenseIncome = 0
financeIncomeExpenseExpense = 1
financeBalanceTransactionTypeTransferToWithdraw = 2
financeBalanceTransactionTypeManualAdjust = 4
financeBalanceTransactionChannelBalance = 3
financeWithdrawableChangeTypeCommission = 0
financeWithdrawableChangeTypeBalanceTransferIn = 3
financeApplyTypeBalanceToWithdraw = 2
financeAuditTypeFirst = 1
financeApplyStatusPending = 0
financeApplyStatusApproved = 1
financeApplyStatusRejected = 2
financeAuditResultPass = 0
financeAuditResultReject = 1
financeAccountStatusNormal = 0
financeWithdrawAmountStatusNormal = 0
financeCommissionStatusPending = 0
financeCommissionStatusSettled = 1
)
// BalanceTransferApply 发起余额转可提现申请。申请时先扣余额并写余额明细,申请单保持待审核。
func (s *sCommonAgentBalance) BalanceTransferApply(ctx context.Context, in *model.TAgentBalanceTransferApplyParam) (*model.TAgentBalanceTransferApplyModel, error) {
if in == nil {
in = &model.TAgentBalanceTransferApplyParam{}
}
if err := in.Filter(ctx); err != nil {
return nil, err
}
applyNo := "BWT" + strings.ReplaceAll(uuid.New().String(), "-", "")
deptId := contexts.GetDeptId(ctx)
tenantId := contexts.GetTenantId(ctx)
userId := contexts.GetUserId(ctx)
now := gtime.Now()
res := &model.TAgentBalanceTransferApplyModel{
ApplyNo: applyNo,
ApplyAmount: in.Amount,
}
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
account := &entity.TAgentBalance{}
columns := dao.TAgentBalance.Columns()
if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
Where(columns.DeptId, deptId).
Where(columns.TenantId, tenantId).
Scan(account); err != nil {
return gerror.Wrap(err, "查询企业客户财务账户失败,请稍后重试!")
}
if account.Id == 0 {
return gerror.New("企业客户财务账户不存在")
}
if account.Status != financeAccountStatusNormal {
return gerror.New("企业客户财务账户状态异常")
}
if account.WithdrawAmountStatus != financeWithdrawAmountStatusNormal {
return gerror.New("可提现金额已冻结")
}
if account.Balance < in.Amount {
return gerror.New("账户余额不足")
}
balanceAfter := account.Balance - in.Amount
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
WherePri(account.Id).
Data(g.Map{
columns.Balance: balanceAfter,
columns.UpdatedBy: userId,
columns.UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrap(err, "扣减账户余额失败,请稍后重试!")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "获取账户余额更新结果失败")
}
if affected == 0 {
return gerror.New("账户余额未更新,请稍后重试")
}
if err = service.CommonTBalanceDetail().CreateBalanceTransferDetail(ctx, tx, &model.TBalanceTransferDetailCreateParam{
DeptId: deptId,
SerialNo: applyNo,
TransactionTime: now,
IncomeExpenseType: financeIncomeExpenseExpense,
TransactionType: financeBalanceTransactionTypeTransferToWithdraw,
TransactionChannel: financeBalanceTransactionChannelBalance,
Amount: in.Amount,
BeforeBalance: account.Balance,
BalanceAfter: balanceAfter,
OrderSn: applyNo,
Remark: "余额转可提现申请扣减余额",
TenantId: tenantId,
CreatedDept: deptId,
CreatedBy: userId,
CreatedAt: now,
}); err != nil {
return err
}
var topDept entity.SysDept
if err := dao.SysDept.Ctx(ctx).
Fields(dao.SysDept.Columns().DeptId, dao.SysDept.Columns().ParentId).
Where(dao.SysDept.Columns().ParentId, 0).
Scan(&topDept); err != nil {
return gerror.Wrap(err, "查询审核机构失败,请稍后重试!")
}
if topDept.DeptId == 0 {
return gerror.New("审核机构不存在")
}
if err = service.CommonTFinanceApply().CreateBalanceTransferApply(ctx, tx, &model.TFinanceApplyCreateBalanceTransferParam{
ApplyNo: applyNo,
ApplyDeptId: deptId,
AuditDeptId: topDept.DeptId,
AvailableAmountSnapshot: account.WithdrawAmount,
ApplyAmount: in.Amount,
Remark: in.Remark,
TenantId: tenantId,
CreatedDept: deptId,
CreatedBy: userId,
CreatedAt: now,
}); err != nil {
return err
}
res.BalanceAfter = balanceAfter
return nil
})
if err != nil {
return nil, gerror.Wrap(err, "余额转可提现申请失败,请稍后重试!")
}
return res, nil
}
// BalanceTransferAudit 审核余额转可提现申请。通过时加可提现金额,驳回时退回申请时扣掉的余额。
func (s *sCommonAgentBalance) BalanceTransferAudit(ctx context.Context, in *model.TAgentBalanceTransferAuditParam) error {
if in == nil {
in = &model.TAgentBalanceTransferAuditParam{}
}
if err := in.Filter(ctx); err != nil {
return err
}
auditNo, err := batchimport.NextRandomNo("BTA")
if err != nil {
return gerror.Wrap(err, "生成审核单号失败")
}
tenantId := contexts.GetTenantId(ctx)
currentDeptId := contexts.GetDeptId(ctx)
currentUserId := contexts.GetUserId(ctx)
if tenantId == "" {
return gerror.New("当前租户不能为空")
}
if currentDeptId <= 0 {
return gerror.New("当前登录机构不能为空")
}
now := gtime.Now()
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
apply, err := service.CommonTFinanceApply().GetPendingByApplyNo(ctx, tx, in.ApplyNo, tenantId)
if err != nil {
return err
}
if err = service.CommonTFinanceAuditRecord().CreateRecord(ctx, tx, &model.TFinanceAuditRecordCreateParam{
AuditNo: auditNo,
ApplyNo: apply.ApplyNo,
AuditDeptId: currentDeptId,
AuditorUserId: currentUserId,
AuditResult: in.AuditResult,
AuditOpinion: in.AuditOpinion,
AuditTime: now,
}); err != nil {
return err
}
applyStatus := financeApplyStatusApproved
if in.AuditResult == financeAuditResultReject {
applyStatus = financeApplyStatusRejected
}
if err = service.CommonTFinanceApply().UpdateAuditStatus(ctx, tx, &model.TFinanceApplyUpdateAuditStatusParam{
ApplyNo: apply.ApplyNo,
ApplyStatus: applyStatus,
AuditTime: now,
UpdatedBy: currentUserId,
UpdatedAt: now,
}); err != nil {
return err
}
account := &entity.TAgentBalance{}
columns := dao.TAgentBalance.Columns()
if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
Where(columns.DeptId, apply.ApplyDeptId).
Where(columns.TenantId, apply.TenantId).
Scan(account); err != nil {
return gerror.Wrap(err, "查询企业客户财务账户失败,请稍后重试!")
}
if account.Id == 0 {
return gerror.New("企业客户财务账户不存在")
}
if account.Status != financeAccountStatusNormal {
return gerror.New("企业客户财务账户状态异常")
}
if in.AuditResult == financeAuditResultPass {
if account.WithdrawAmountStatus != financeWithdrawAmountStatusNormal {
return gerror.New("可提现金额已冻结")
}
before := account.WithdrawAmount
after := before + apply.ApplyAmount
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
WherePri(account.Id).
Data(g.Map{
columns.WithdrawAmount: after,
columns.UpdatedBy: currentUserId,
columns.UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrap(err, "增加可提现金额失败,请稍后重试!")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "获取账户余额更新结果失败")
}
if affected == 0 {
return gerror.New("账户余额未更新,请稍后重试")
}
return service.CommonTFinWithdrawableChange().CreateBalanceTransferInChange(ctx, tx, &model.TFinWithdrawableBalanceTransferInParam{
DeptId: apply.ApplyDeptId,
ChangeAmount: apply.ApplyAmount,
BeforeBalance: before,
AfterBalance: after,
AccountType: apply.AccountType,
BankName: apply.BankName,
BankAccount: apply.BankAccount,
AccountName: apply.AccountName,
IdentityCard: apply.IdentityCard,
SourceNo: apply.ApplyNo,
Remark: "余额转可提现审核通过",
AccountPeriod: now.Format("Y-m"),
TenantId: apply.TenantId,
CreatedDept: apply.ApplyDeptId,
CreatedBy: currentUserId,
CreatedAt: now,
})
}
before := account.Balance
after := before + apply.ApplyAmount
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
WherePri(account.Id).
Data(g.Map{
columns.Balance: after,
columns.UpdatedBy: currentUserId,
columns.UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrap(err, "退回账户余额失败,请稍后重试!")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "获取账户余额更新结果失败")
}
if affected == 0 {
return gerror.New("账户余额未更新,请稍后重试")
}
remark := "余额转可提现申请驳回退款"
if in.AuditOpinion != "" {
remark += ":" + in.AuditOpinion
}
return service.CommonTBalanceDetail().CreateBalanceTransferDetail(ctx, tx, &model.TBalanceTransferDetailCreateParam{
DeptId: apply.ApplyDeptId,
SerialNo: apply.ApplyNo,
TransactionTime: now,
IncomeExpenseType: financeIncomeExpenseIncome,
TransactionType: financeBalanceTransactionTypeManualAdjust,
TransactionChannel: financeBalanceTransactionChannelBalance,
Amount: apply.ApplyAmount,
BeforeBalance: before,
BalanceAfter: after,
OrderSn: apply.ApplyNo,
Remark: remark,
TenantId: apply.TenantId,
CreatedDept: apply.ApplyDeptId,
CreatedBy: currentUserId,
CreatedAt: now,
})
})
}
// SettleCommissionByPeriod 按账期区间结算待结算分润,同步到企业账户可提现金额并写入可提现明细。
func (s *sCommonAgentBalance) SettleCommissionByPeriod(ctx context.Context, in *model.TAgentBalanceCommissionSettlementParam) (*model.TAgentBalanceCommissionSettlementResult, error) {
if in == nil {
in = &model.TAgentBalanceCommissionSettlementParam{}
}
if err := in.Filter(ctx); err != nil {
return nil, err
}
res := &model.TAgentBalanceCommissionSettlementResult{
StartPeriod: in.StartPeriod,
EndPeriod: in.EndPeriod,
}
endPeriodTime, _ := time.Parse("2006-01", in.EndPeriod)
endNextPeriod := endPeriodTime.AddDate(0, 1, 0).Format("2006-01")
var lastId int64
tenantId := contexts.GetTenantId(ctx)
commissionColumns := dao.TFinCommissionDetail.Columns()
for {
var details []*entity.TFinCommissionDetail
mod := handler.Model(dao.TFinCommissionDetail.Ctx(ctx), &handler.Option{
FilterTenant: false,
FilterAuth: false,
}).
Fields(
commissionColumns.Id,
commissionColumns.BenefitDeptId,
commissionColumns.CommissionAmount,
commissionColumns.AccountPeriod,
commissionColumns.TenantId,
commissionColumns.CreatedDept,
).
Where(commissionColumns.CommissionStatus, financeCommissionStatusPending).
WhereGTE(commissionColumns.AccountPeriod, in.StartPeriod).
WhereLT(commissionColumns.AccountPeriod, endNextPeriod).
WhereGT(commissionColumns.Id, lastId).
OrderAsc(commissionColumns.Id).
Limit(1000)
if tenantId != "" {
mod = mod.Where(commissionColumns.TenantId, tenantId)
}
if err := mod.Scan(&details); err != nil {
return res, gerror.Wrap(err, "扫描待结算分润明细失败,请稍后重试!")
}
if len(details) == 0 {
break
}
res.ScanCount += len(details)
for _, item := range details {
if item.Id > lastId {
lastId = item.Id
}
}
groups := make(map[model.TAgentBalanceCommissionSettlementGroupKey]*model.TAgentBalanceCommissionSettlementGroup)
for _, item := range details {
if item.BenefitDeptId <= 0 || item.AccountPeriod == "" || item.CommissionAmount <= 0 {
continue
}
key := model.TAgentBalanceCommissionSettlementGroupKey{
DeptId: item.BenefitDeptId,
AccountPeriod: item.AccountPeriod,
TenantId: item.TenantId,
}
group := groups[key]
if group == nil {
group = &model.TAgentBalanceCommissionSettlementGroup{
DeptId: item.BenefitDeptId,
AccountPeriod: item.AccountPeriod,
TenantId: item.TenantId,
CreatedDept: item.CreatedDept,
}
groups[key] = group
}
group.Ids = append(group.Ids, item.Id)
group.Amount += item.CommissionAmount
}
for _, group := range groups {
if err := s.settleCommissionGroup(ctx, group); err != nil {
res.FailGroupCount++
g.Log().Errorf(ctx, "分润结算失败 deptId:%d accountPeriod:%s amount:%v err:%+v", group.DeptId, group.AccountPeriod, group.Amount, err)
continue
}
res.SuccessGroupCount++
res.SettledCount += len(group.Ids)
res.SettledAmount += group.Amount
}
}
return res, nil
}
func (s *sCommonAgentBalance) settleCommissionGroup(ctx context.Context, group *model.TAgentBalanceCommissionSettlementGroup) error {
if group == nil || len(group.Ids) == 0 {
return nil
}
now := gtime.Now()
userId := contexts.GetUserId(ctx)
accountColumns := dao.TAgentBalance.Columns()
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
account := &entity.TAgentBalance{}
if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
Where(accountColumns.DeptId, group.DeptId).
Where(accountColumns.TenantId, group.TenantId).
LockUpdate().
Scan(account); err != nil {
return gerror.Wrap(err, "锁定企业客户财务账户失败,请稍后重试!")
}
if account.Id == 0 {
return gerror.New("企业客户财务账户不存在")
}
if account.Status != financeAccountStatusNormal {
return gerror.New("企业客户财务账户状态异常")
}
if account.WithdrawAmountStatus != financeWithdrawAmountStatusNormal {
return gerror.New("可提现金额已冻结")
}
if err := service.CommonTFinCommissionDetail().MarkSettled(ctx, tx, &model.TFinCommissionSettleUpdateParam{
Ids: group.Ids,
SettlementTime: now,
}); err != nil {
return err
}
before := account.WithdrawAmount
after := before + group.Amount
sourceNo := fmt.Sprintf("CMS%sD%d%s", strings.ReplaceAll(group.AccountPeriod, "-", ""), group.DeptId, now.Format("150405"))
if err := service.CommonTFinWithdrawableChange().CreateCommissionSettlementChange(ctx, tx, &model.TFinWithdrawableCommissionSettlementParam{
DeptId: group.DeptId,
ChangeAmount: group.Amount,
BeforeBalance: before,
AfterBalance: after,
SourceNo: sourceNo,
Remark: "分润结算转可提现",
AccountPeriod: group.AccountPeriod,
TenantId: group.TenantId,
CreatedDept: group.CreatedDept,
CreatedBy: userId,
CreatedAt: now,
}); err != nil {
return err
}
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
WherePri(account.Id).
Data(g.Map{
accountColumns.WithdrawAmount: after,
accountColumns.UpdatedBy: userId,
accountColumns.UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrap(err, "更新企业客户可提现金额失败,请稍后重试!")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "获取企业客户可提现金额更新结果失败")
}
if affected == 0 {
return gerror.New("企业客户可提现金额未更新,请稍后重试")
}
return nil
})
}
@@ -0,0 +1,254 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package common
import (
"context"
"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"
"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/gtime"
)
type sCommonTBalanceDetail struct{}
func NewCommonTBalanceDetail() *sCommonTBalanceDetail {
return &sCommonTBalanceDetail{}
}
func init() {
service.RegisterCommonTBalanceDetail(NewCommonTBalanceDetail())
}
// Model 余额明细表ORM模型
func (s *sCommonTBalanceDetail) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TBalanceDetail.Ctx(ctx), option...)
}
// List 获取余额明细表列表
func (s *sCommonTBalanceDetail) List(ctx context.Context, in *model.TBalanceDetailListParam) (list []*model.TBalanceDetailListModel, totalCount int, err error) {
mod := s.Model(ctx)
sysDeptTable := dao.SysDept.Table()
orderTable := dao.TOrder.Table()
// 字段过滤
mod = mod.FieldsPrefix(dao.TBalanceDetail.Table(), model.TBalanceDetailListModel{})
mod = mod.Fields(sysDeptTable + ".dept_name as deptName")
mod = mod.Fields(orderTable + ".order_type as orderType")
mod = mod.Fields(orderTable + ".pay_type as payType")
deptId := contexts.GetDeptId(ctx)
// 字段过滤
mod = mod.Where(dao.TBalanceDetail.Columns().DeptId, deptId)
// 查询明细ID
if in.Id > 0 {
mod = mod.Where(dao.TBalanceDetail.Columns().Id, in.Id)
}
g.Log().Info(ctx, "month==:", in.Month, " len:", len(in.Month))
// 查询收支类型
if in.IncomeExpenseType != nil && *in.IncomeExpenseType >= 0 {
mod = mod.Where(dao.TBalanceDetail.Columns().IncomeExpenseType, *in.IncomeExpenseType)
}
// 查询交易类型
if in.TransactionType != nil && *in.TransactionType >= 0 {
mod = mod.Where(dao.TBalanceDetail.Columns().TransactionType, *in.TransactionType)
}
// 查询交易渠道
if in.TransactionChannel != nil && *in.TransactionChannel >= 0 {
mod = mod.Where(dao.TBalanceDetail.Columns().TransactionChannel, *in.TransactionChannel)
}
// // 查询支付类型
// if in.PayType != nil && *in.PayType >= 0 {
// mod = mod.Where(dao.TBalanceDetail.Columns().PayType, *in.PayType)
// }
// 查询交易时间范围
if len(in.Month) == 2 {
t1, err := time.ParseInLocation("2006-01", in.Month[0], time.Local)
t2, err2 := time.ParseInLocation("2006-01", in.Month[1], time.Local)
if err == nil && err2 == nil {
startOfMonth := gtime.NewFromTime(t1).StartOfMonth()
endOfMonth := gtime.NewFromTime(t2).EndOfMonth()
mod = mod.WhereGTE(dao.TBalanceDetail.Columns().TransactionTime, startOfMonth)
mod = mod.WhereLTE(dao.TBalanceDetail.Columns().TransactionTime, endOfMonth)
}
}
// 查询账期(根据 transaction_time 筛选月份)
if in.AccountPeriod != "" {
// 统一转换为 "2026-06" 格式后解析
accountPeriodStr := in.AccountPeriod
if len(accountPeriodStr) == 6 {
accountPeriodStr = accountPeriodStr[:4] + "-" + accountPeriodStr[4:]
}
t := gtime.NewFromStr(accountPeriodStr + "-01")
startOfMonth := t.StartOfDay()
endOfMonth := startOfMonth.EndOfMonth()
mod = mod.WhereBetween(dao.TBalanceDetail.Columns().TransactionTime, startOfMonth, endOfMonth)
}
mod = mod.LeftJoin(sysDeptTable, dao.TBalanceDetail.Table()+"."+dao.TBalanceDetail.Columns().DeptId+" COLLATE utf8mb4_general_ci="+sysDeptTable+"."+dao.SysDept.Columns().DeptId)
mod = mod.LeftJoin(orderTable, dao.TBalanceDetail.Table()+"."+dao.TBalanceDetail.Columns().OrderSn+" COLLATE utf8mb4_general_ci="+orderTable+"."+dao.TOrder.Columns().OrderSn)
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TBalanceDetail.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取余额明细表列表失败,请稍后重试!")
return
}
// 根据 TransactionTime 赋值账期
for i := range list {
if list[i].TransactionTime != nil {
list[i].TradingMonth = list[i].TransactionTime.Format("Y-m")
}
}
return
}
// Export 导出余额明细(异步:提交任务后到下载中心取文件)
func (s *sCommonTBalanceDetail) Export(ctx context.Context, in *model.TBalanceDetailListParam) (err error) {
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
},
}
_, err = exportcenter.Submit(ctx, &req)
return err
}
// Edit 修改/新增余额明细表
func (s *sCommonTBalanceDetail) Edit(ctx context.Context, in *model.TBalanceDetailEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TBalanceDetailUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改余额明细表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TBalanceDetailInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增余额明细表失败,请稍后重试!")
}
return
})
}
// Delete 删除余额明细表
func (s *sCommonTBalanceDetail) Delete(ctx context.Context, in *model.TBalanceDetailDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除余额明细表失败,请稍后重试!")
return
}
return
}
// View 获取余额明细表指定信息
func (s *sCommonTBalanceDetail) View(ctx context.Context, in *model.TBalanceDetailViewParam) (res *model.TBalanceDetailViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取余额明细表信息,请稍后重试!")
return
}
return
}
// CreateBalanceTransferDetail 写入余额转可提现相关的余额明细。
func (s *sCommonTBalanceDetail) CreateBalanceTransferDetail(ctx context.Context, tx gdb.TX, in *model.TBalanceTransferDetailCreateParam) error {
if in == nil {
return gerror.New("余额明细参数不能为空")
}
tenantId := contexts.GetTenantId(ctx)
userId := contexts.GetUserId(ctx)
deptId := contexts.GetDeptId(ctx)
createdAt := gtime.Now()
columns := dao.TBalanceDetail.Columns()
if _, err := tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()).Data(g.Map{
columns.DeptId: in.DeptId,
columns.SerialNo: in.SerialNo,
columns.TransactionTime: in.TransactionTime,
columns.IncomeExpenseType: in.IncomeExpenseType,
columns.TransactionType: in.TransactionType,
columns.TransactionChannel: in.TransactionChannel,
columns.Amount: in.Amount,
columns.BeforeBalance: in.BeforeBalance,
columns.BalanceAfter: in.BalanceAfter,
columns.OrderSn: in.OrderSn,
columns.Remark: in.Remark,
columns.TenantId: tenantId,
columns.CreatedDept: deptId,
columns.CreatedBy: userId,
columns.CreatedAt: createdAt,
}).Insert(); err != nil {
return gerror.Wrap(err, "新增余额转可提现余额明细失败,请稍后重试!")
}
return nil
}
@@ -0,0 +1,458 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package common
import (
"context"
"database/sql"
"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/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"
"math"
"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 sCommonConsumeAccountMonthBill struct{}
func NewCommonConsumeAccountMonthBill() *sCommonConsumeAccountMonthBill {
return &sCommonConsumeAccountMonthBill{}
}
func init() {
service.RegisterCommonConsumeAccountMonthBill(NewCommonConsumeAccountMonthBill())
}
// Model 消费账户月账单表ORM模型
func (s *sCommonConsumeAccountMonthBill) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TConsumeAccountMonthBill.Ctx(ctx), option...)
}
// List 获取消费账户月账单表列表
func (s *sCommonConsumeAccountMonthBill) List(ctx context.Context, in *model.TConsumeAccountMonthBillListParam) (list []*model.TConsumeAccountMonthBillListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(model.TConsumeAccountMonthBillListModel{})
// 查询账单ID
if in.Id > 0 {
mod = mod.Where(dao.TConsumeAccountMonthBill.Columns().Id, in.Id)
}
// 查询账单状态:1正常,2已调整,3已作废
if in.BillStatus > 0 {
mod = mod.Where(dao.TConsumeAccountMonthBill.Columns().BillStatus, in.BillStatus)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TConsumeAccountMonthBill.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TConsumeAccountMonthBill.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取消费账户月账单表列表失败,请稍后重试!")
return
}
return
}
// Export 导出消费账户月账单表
func (s *sCommonConsumeAccountMonthBill) Export(ctx context.Context, in *model.TConsumeAccountMonthBillListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TConsumeAccountMonthBillExportModel{})
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.TConsumeAccountMonthBillExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增消费账户月账单表
func (s *sCommonConsumeAccountMonthBill) Edit(ctx context.Context, in *model.TConsumeAccountMonthBillEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TConsumeAccountMonthBillUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改消费账户月账单表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TConsumeAccountMonthBillInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增消费账户月账单表失败,请稍后重试!")
}
return
})
}
// Delete 删除消费账户月账单表
func (s *sCommonConsumeAccountMonthBill) Delete(ctx context.Context, in *model.TConsumeAccountMonthBillDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除消费账户月账单表失败,请稍后重试!")
return
}
return
}
// View 获取消费账户月账单表指定信息
func (s *sCommonConsumeAccountMonthBill) View(ctx context.Context, in *model.TConsumeAccountMonthBillViewParam) (res *model.TConsumeAccountMonthBillViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取消费账户月账单表信息,请稍后重试!")
return
}
return
}
// GetSummary 获取月账单消费账户汇总信息
func (s *sCommonConsumeAccountMonthBill) GetConsumptionBillSummary(ctx context.Context, month string) (res *model.TConsumptionBillSummaryModel, err error) {
// 计算上月账期(基于当前时间)
// now := time.Now()
// lastMonthTime := now.AddDate(0, -1, 0)
// lastMonthPeriod := lastMonthTime.Format("2006-01")
deptId := contexts.GetDeptId(ctx)
var bill *model.TConsumeAccountMonthBillViewModel
if err = dao.TConsumeAccountMonthBill.Ctx(ctx).
Where(dao.TConsumeAccountMonthBill.Columns().AccountPeriod, month).
Where(dao.TConsumeAccountMonthBill.Columns().DeptId, deptId).
Scan(&bill); err != nil {
err = gerror.Wrap(err, "查询上月账单失败,请稍后重试!")
return
}
if bill == nil {
return
}
res = &model.TConsumptionBillSummaryModel{}
res.TotalIncome = bill.TotalIncome
res.TotalExpense = bill.TotalExpense
return
}
// SettleConsumeAccountMonthBill 按账期统计消费账户月账单。
func (s *sCommonConsumeAccountMonthBill) SettleConsumeAccountMonthBill(ctx context.Context, in *model.TConsumeAccountMonthBillSettleParam) (res *model.TConsumeAccountMonthBillSettleResult, err error) {
if in == nil {
in = &model.TConsumeAccountMonthBillSettleParam{}
}
if err = in.Filter(ctx); err != nil {
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
}
if _, err = time.Parse("2006-01", in.AccountPeriod); err != nil {
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
}
startTime, err := time.Parse("2006-01", in.AccountPeriod)
if err != nil {
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
}
endTime := startTime.AddDate(0, 1, 0)
now := gtime.Now()
userId := contexts.GetUserId(ctx)
tenantId := contexts.GetTenantId(ctx)
billColumns := dao.TConsumeAccountMonthBill.Columns()
detailColumns := dao.TBalanceDetail.Columns()
type detailRow struct {
Id int64 `orm:"id"`
DeptId int64 `orm:"dept_id"`
TenantId string `orm:"tenant_id"`
CreatedDept int64 `orm:"created_dept"`
IncomeExpenseType int `orm:"income_expense_type"`
TransactionType int `orm:"transaction_type"`
Amount float64 `orm:"amount"`
BeforeBalance float64 `orm:"before_balance"`
BalanceAfter float64 `orm:"balance_after"`
TransactionTime *gtime.Time `orm:"transaction_time"`
}
var rows []*detailRow
mod := handler.Model(dao.TBalanceDetail.Ctx(ctx), &handler.Option{
FilterTenant: false,
FilterAuth: false,
}).
Fields(
detailColumns.Id,
detailColumns.DeptId,
detailColumns.TenantId,
detailColumns.CreatedDept,
detailColumns.IncomeExpenseType,
detailColumns.TransactionType,
detailColumns.TransactionStatus,
detailColumns.Amount,
detailColumns.BeforeBalance,
detailColumns.BalanceAfter,
detailColumns.TransactionTime,
).
WhereGTE(detailColumns.TransactionTime, startTime).
WhereLT(detailColumns.TransactionTime, endTime).
Where(detailColumns.TransactionStatus, 1)
if tenantId != "" {
mod = mod.Where(detailColumns.TenantId, tenantId)
}
if err = mod.OrderAsc(detailColumns.TenantId).OrderAsc(detailColumns.DeptId).OrderAsc(detailColumns.TransactionTime).OrderAsc(detailColumns.Id).Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "查询消费流水失败,请稍后重试!")
}
type billGroup struct {
DeptId int64
TenantId string
CreatedDept int64
AccountPeriod string
OpeningBalance float64
ClosingBalance float64
RechargeIncome float64
WithdrawableTransferInIncome float64
ManualAdjustIncome float64
RefundIncome float64
ConsumeExpense float64
BalanceTransferOutExpense float64
ManualAdjustExpense float64
ConsumeCreditRepayExpense float64
TotalIncome float64
TotalExpense float64
IncomeCount int
ExpenseCount int
DetailCount int
}
groups := make(map[string]*billGroup)
keys := make([]string, 0, 16)
for _, row := range rows {
if row == nil || row.DeptId <= 0 || row.TransactionTime == nil {
continue
}
switch row.TransactionType {
case 0, 1, 2, 3, 4:
default:
continue
}
key := fmt.Sprintf("%s:%d", row.TenantId, row.DeptId)
group := groups[key]
if group == nil {
group = &billGroup{
DeptId: row.DeptId,
TenantId: row.TenantId,
CreatedDept: row.CreatedDept,
AccountPeriod: in.AccountPeriod,
OpeningBalance: row.BeforeBalance,
}
groups[key] = group
keys = append(keys, key)
}
group.ClosingBalance = row.BalanceAfter
group.DetailCount++
switch row.TransactionType {
case 0:
if row.IncomeExpenseType == 0 {
group.RechargeIncome += row.Amount
}
case 1:
if row.IncomeExpenseType == 1 {
group.ConsumeExpense += row.Amount
}
case 2:
if row.IncomeExpenseType == 1 {
group.BalanceTransferOutExpense += row.Amount
}
case 3:
if row.IncomeExpenseType == 0 {
group.WithdrawableTransferInIncome += row.Amount
}
case 4:
if row.IncomeExpenseType == 0 {
group.ManualAdjustIncome += row.Amount
} else if row.IncomeExpenseType == 1 {
group.ManualAdjustExpense += row.Amount
}
}
if row.IncomeExpenseType == 0 {
group.TotalIncome += row.Amount
group.IncomeCount++
} else if row.IncomeExpenseType == 1 {
group.TotalExpense += row.Amount
group.ExpenseCount++
}
}
res = &model.TConsumeAccountMonthBillSettleResult{
AccountPeriod: in.AccountPeriod,
}
if len(groups) == 0 {
return res, nil
}
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
for _, key := range keys {
group := groups[key]
if group == nil {
continue
}
closingBalance := group.OpeningBalance + group.TotalIncome - group.TotalExpense
if math.Abs(closingBalance-group.ClosingBalance) > 0.01 {
return gerror.New(fmt.Sprintf("消费账单余额勾稽失败:部门 %d,账期 %s,计算余额 %.2f,流水余额 %.2f", group.DeptId, group.AccountPeriod, closingBalance, group.ClosingBalance))
}
lookup := tx.Ctx(ctx).Model(dao.TConsumeAccountMonthBill.Table()).
Fields(billColumns.Id).
Where(billColumns.DeptId, group.DeptId).
Where(billColumns.AccountPeriod, group.AccountPeriod)
if group.TenantId != "" {
lookup = lookup.Where(billColumns.TenantId, group.TenantId)
}
var existing struct {
Id int64 `orm:"id"`
}
if err := lookup.LockUpdate().Scan(&existing); err != nil && err != sql.ErrNoRows {
return gerror.Wrap(err, "查询消费月账单失败,请稍后重试!")
}
tenantValue := group.TenantId
if tenantValue == "" {
tenantValue = tenantId
}
if tenantValue == "" {
return gerror.New("租户ID不能为空")
}
createdDeptValue := group.CreatedDept
if createdDeptValue == 0 {
createdDeptValue = contexts.GetDeptId(ctx)
}
if createdDeptValue == 0 {
return gerror.New("创建部门不能为空")
}
data := g.Map{
billColumns.DeptId: group.DeptId,
billColumns.AccountPeriod: group.AccountPeriod,
billColumns.OpeningBalance: group.OpeningBalance,
billColumns.RechargeIncome: group.RechargeIncome,
billColumns.WithdrawableTransferInIncome: group.WithdrawableTransferInIncome,
billColumns.ManualAdjustIncome: group.ManualAdjustIncome,
billColumns.RefundIncome: 0,
billColumns.ConsumeExpense: group.ConsumeExpense,
billColumns.BalanceTransferOutExpense: group.BalanceTransferOutExpense,
billColumns.ManualAdjustExpense: group.ManualAdjustExpense,
billColumns.ConsumeCreditRepayExpense: group.ConsumeCreditRepayExpense,
billColumns.TotalIncome: group.TotalIncome,
billColumns.TotalExpense: group.TotalExpense,
billColumns.ClosingBalance: closingBalance,
billColumns.IncomeCount: group.IncomeCount,
billColumns.ExpenseCount: group.ExpenseCount,
billColumns.CreditUsed: 0,
billColumns.CreditRepaid: 0,
billColumns.BillStatus: 1,
billColumns.SettleTime: now,
billColumns.Remark: "根据余额明细统计生成",
billColumns.TenantId: tenantValue,
billColumns.UpdatedBy: userId,
billColumns.UpdatedAt: now,
}
if existing.Id > 0 {
if _, err := tx.Ctx(ctx).Model(dao.TConsumeAccountMonthBill.Table()).
WherePri(existing.Id).
Data(data).
Update(); err != nil {
return gerror.Wrap(err, "更新消费月账单失败,请稍后重试!")
}
res.UpdateCount++
} else {
data[billColumns.CreatedDept] = createdDeptValue
data[billColumns.CreatedBy] = userId
data[billColumns.CreatedAt] = now
if _, err := tx.Ctx(ctx).Model(dao.TConsumeAccountMonthBill.Table()).
Data(data).
Insert(); err != nil {
return gerror.Wrap(err, "新增消费月账单失败,请稍后重试!")
}
res.InsertCount++
}
res.DeptCount++
res.DetailCount += group.DetailCount
res.TotalIncome += group.TotalIncome
res.TotalExpense += group.TotalExpense
}
return nil
})
if err != nil {
return nil, err
}
return res, nil
}
@@ -0,0 +1,165 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/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 sCommonTCreditDetail struct{}
func NewCommonTCreditDetail() *sCommonTCreditDetail {
return &sCommonTCreditDetail{}
}
func init() {
service.RegisterCommonTCreditDetail(NewCommonTCreditDetail())
}
// Model 信用额度明细表ORM模型
func (s *sCommonTCreditDetail) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TCreditDetail.Ctx(ctx), option...)
}
// List 获取信用额度明细表列表
func (s *sCommonTCreditDetail) List(ctx context.Context, in *model.TCreditDetailListParam) (list []*model.TCreditDetailListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(model.TCreditDetailListModel{})
// 查询明细ID
if in.Id > 0 {
mod = mod.Where(dao.TCreditDetail.Columns().Id, in.Id)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TCreditDetail.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TCreditDetail.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取信用额度明细表列表失败,请稍后重试!")
return
}
return
}
// Export 导出信用额度明细表
func (s *sCommonTCreditDetail) Export(ctx context.Context, in *model.TCreditDetailListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TCreditDetailExportModel{})
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.TCreditDetailExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增信用额度明细表
func (s *sCommonTCreditDetail) Edit(ctx context.Context, in *model.TCreditDetailEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TCreditDetailUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改信用额度明细表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TCreditDetailInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增信用额度明细表失败,请稍后重试!")
}
return
})
}
// Delete 删除信用额度明细表
func (s *sCommonTCreditDetail) Delete(ctx context.Context, in *model.TCreditDetailDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除信用额度明细表失败,请稍后重试!")
return
}
return
}
// View 获取信用额度明细表指定信息
func (s *sCommonTCreditDetail) View(ctx context.Context, in *model.TCreditDetailViewParam) (res *model.TCreditDetailViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取信用额度明细表信息,请稍后重试!")
return
}
return
}
@@ -0,0 +1,156 @@
// Package iot
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/xgorm/handler"
"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/util/gconv"
)
type sIotTDivisionCode struct{}
func NewIotTDivisionCode() *sIotTDivisionCode {
return &sIotTDivisionCode{}
}
func init() {
service.RegisterIotTDivisionCode(NewIotTDivisionCode())
}
// Model 村级行政区划表ORM模型
func (s *sIotTDivisionCode) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TDivisionCode.Ctx(ctx), option...)
}
// List 获取村级行政区划表列表
func (s *sIotTDivisionCode) List(ctx context.Context, in *model.TDivisionCodeListParam) (list []*model.TDivisionCodeListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 根据父级代码查询子级列表
if in.RegionParent != "" {
mod = mod.Where(dao.TDivisionCode.Columns().RegionParent, in.RegionParent)
}
// 根据名称查询
if in.RegionName != "" {
mod = mod.WhereLike(dao.TDivisionCode.Columns().RegionName, "%"+in.RegionName+"%")
}
// 根据代码查询
if in.RegionCode != "" {
mod = mod.Where(dao.TDivisionCode.Columns().RegionCode, in.RegionCode)
}
// 根据等级查询
if in.RegionLevel > 0 {
mod = mod.Where(dao.TDivisionCode.Columns().RegionLevel, in.RegionLevel)
}
// 字段过滤
mod = mod.Fields(model.TDivisionCodeListModel{})
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TDivisionCode.Columns().RegionId)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取村级行政区划表列表失败,请稍后重试!")
return
}
return
}
// Export 导出村级行政区划表
func (s *sIotTDivisionCode) Export(ctx context.Context, in *model.TDivisionCodeListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TDivisionCodeExportModel{})
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.TDivisionCodeExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增村级行政区划表
func (s *sIotTDivisionCode) Edit(ctx context.Context, in *model.TDivisionCodeEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.RegionId > 0 {
if _, err = s.Model(ctx).
Fields(model.TDivisionCodeUpdateFields{}).
WherePri(in.RegionId).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改村级行政区划表失败,请稍后重试!")
}
return
}
// 新增
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TDivisionCodeInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增村级行政区划表失败,请稍后重试!")
}
return
})
}
// Delete 删除村级行政区划表
func (s *sIotTDivisionCode) Delete(ctx context.Context, in *model.TDivisionCodeDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.RegionId).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除村级行政区划表失败,请稍后重试!")
return
}
return
}
// View 获取村级行政区划表指定信息
func (s *sIotTDivisionCode) View(ctx context.Context, in *model.TDivisionCodeViewParam) (res *model.TDivisionCodeViewModel, err error) {
if err = s.Model(ctx).WherePri(in.RegionId).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取村级行政区划表信息,请稍后重试!")
return
}
return
}
@@ -0,0 +1,435 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/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"
"strconv"
"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 sCommonTFinCommissionDetail struct{}
func NewCommonTFinCommissionDetail() *sCommonTFinCommissionDetail {
return &sCommonTFinCommissionDetail{}
}
func init() {
service.RegisterCommonTFinCommissionDetail(NewCommonTFinCommissionDetail())
}
// Model 分润明细表ORM模型
func (s *sCommonTFinCommissionDetail) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TFinCommissionDetail.Ctx(ctx), option...)
}
// List 获取分润明细表列表
func (s *sCommonTFinCommissionDetail) List(ctx context.Context, in *model.TFinCommissionDetailListParam) (list []*model.TFinCommissionDetailListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(model.TFinCommissionDetailListModel{})
// 查询主键ID
if in.Id > 0 {
mod = mod.Where(dao.TFinCommissionDetail.Columns().Id, in.Id)
}
// 查询分润状态:0待结算,1可提现
if in.CommissionStatus > 0 {
mod = mod.Where(dao.TFinCommissionDetail.Columns().CommissionStatus, in.CommissionStatus)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TFinCommissionDetail.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TFinCommissionDetail.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取分润明细表列表失败,请稍后重试!")
return
}
return
}
// Export 导出分润明细表
func (s *sCommonTFinCommissionDetail) Export(ctx context.Context, in *model.TFinCommissionDetailListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TFinCommissionDetailExportModel{})
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.TFinCommissionDetailExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增分润明细表
func (s *sCommonTFinCommissionDetail) Edit(ctx context.Context, in *model.TFinCommissionDetailEditParam) (err error) {
// 验证'CommissionNo'唯一
if err = xgorm.IsUnique(ctx, &dao.TFinCommissionDetail, g.Map{dao.TFinCommissionDetail.Columns().CommissionNo: in.CommissionNo}, "分润明细编号已存在", in.Id); err != nil {
return
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TFinCommissionDetailUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改分润明细表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TFinCommissionDetailInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增分润明细表失败,请稍后重试!")
}
return
})
}
// Delete 删除分润明细表
func (s *sCommonTFinCommissionDetail) Delete(ctx context.Context, in *model.TFinCommissionDetailDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除分润明细表失败,请稍后重试!")
return
}
return
}
// View 获取分润明细表指定信息
func (s *sCommonTFinCommissionDetail) View(ctx context.Context, in *model.TFinCommissionDetailViewParam) (res *model.TFinCommissionDetailViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取分润明细表信息,请稍后重试!")
return
}
return
}
// MarkSettled 批量标记分润明细为可提现状态。
func (s *sCommonTFinCommissionDetail) MarkSettled(ctx context.Context, tx gdb.TX, in *model.TFinCommissionSettleUpdateParam) error {
if in == nil {
return gerror.New("分润结算更新参数不能为空")
}
if len(in.Ids) == 0 {
return gerror.New("分润明细ID不能为空")
}
settlementTime := in.SettlementTime
if settlementTime == nil {
settlementTime = gtime.Now()
}
userId := contexts.GetUserId(ctx)
columns := dao.TFinCommissionDetail.Columns()
result, err := tx.Ctx(ctx).Model(dao.TFinCommissionDetail.Table()).
WhereIn(columns.Id, in.Ids).
Where(columns.CommissionStatus, financeCommissionStatusPending).
Data(g.Map{
columns.CommissionStatus: financeCommissionStatusSettled,
columns.SettlementTime: settlementTime,
columns.UpdatedBy: userId,
columns.UpdatedAt: settlementTime,
}).
Update()
if err != nil {
return gerror.Wrap(err, "更新分润明细结算状态失败,请稍后重试!")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "获取分润明细结算状态更新结果失败")
}
if affected != int64(len(in.Ids)) {
return gerror.New("部分分润明细状态已变化,请下次任务重试")
}
return nil
}
// 分层分润概览 分润表
// 一级客户收入 benefit_dept_level=1 commission_amount相加
// 二级客户收入 benefit_dept_level=2 commission_amount相加
// 三级客户收入 benefit_dept_level=3 commission_amount相加
// 总收入 一级二级三级加起来总和
func (s *sCommonTFinCommissionDetail) GetCommissionOverview(ctx context.Context, in *model.TCommissionOverviewParam) (res *model.TCommissionOverviewModel, err error) {
res = &model.TCommissionOverviewModel{}
deptId := contexts.GetDeptId(ctx)
// 解析账期参数
var startPeriod, endPeriod string
if in.Type == 1 { // 年
startPeriod = in.Year + "-01"
endPeriod = in.Year + "-12"
} else if in.Type == 2 { // 月
startPeriod = in.Month
endPeriod = in.Month
}
// 查询一级客户收入
type levelRow struct {
CommissionAmount float64 `orm:"commission_amount"`
}
var level1Result *levelRow
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptLevel, 1).
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount").
Scan(&level1Result); err != nil {
err = gerror.Wrap(err, "查询一级客户收入失败,请稍后重试!")
return
}
if level1Result != nil {
res.Level1Income = level1Result.CommissionAmount
}
// 查询二级客户收入
var level2Result *levelRow
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptLevel, 2).
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount").
Scan(&level2Result); err != nil {
err = gerror.Wrap(err, "查询二级客户收入失败,请稍后重试!")
return
}
if level2Result != nil {
res.Level2Income = level2Result.CommissionAmount
}
// 查询三级客户收入
var level3Result *levelRow
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptLevel, 3).
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount").
Scan(&level3Result); err != nil {
err = gerror.Wrap(err, "查询三级客户收入失败,请稍后重试!")
return
}
if level3Result != nil {
res.Level3Income = level3Result.CommissionAmount
}
// 计算总收入
res.TotalIncome = res.Level1Income + res.Level2Income + res.Level3Income
return
}
// GetLevelSummary 获取层级汇总表
// 客户数 根据 benefit_dept_level 收益机构层级分组
// 收入金额 根据收益机构id去 t_profit_account_month_bill 查,dept_id=收益机构id,收入=分润结算+余额转提现
// 分润金额 根据收益机构id去 t_profit_account_month_bill 查,commission_income
// 顶级利润贡献 等于分润金额
// 收入占比
// 环比 选年与去年对比,选月与上个月对比
func (s *sCommonTFinCommissionDetail) GetLevelSummary(ctx context.Context, in *model.TLevelSummaryParam) (res *model.TLevelSummaryModel, err error) {
res = &model.TLevelSummaryModel{}
deptId := contexts.GetDeptId(ctx)
// 解析账期参数
var startPeriod, endPeriod string
if in.Type == 1 { // 年
startPeriod = in.Year + "-01"
endPeriod = in.Year + "-12"
} else if in.Type == 2 { // 月
startPeriod = in.Month
endPeriod = in.Month
}
// 查询各层级的客户数和分润金额(从分润表)
type levelData struct {
Level int `orm:"level"`
CustomerCount int `orm:"customer_count"`
CommissionAmount float64 `orm:"commission_amount"`
}
var levelResults []*levelData
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount, COUNT(DISTINCT " + dao.TFinCommissionDetail.Columns().PayerDeptId + ") as customer_count, " + dao.TFinCommissionDetail.Columns().BenefitDeptLevel + " as level").
Group(dao.TFinCommissionDetail.Columns().BenefitDeptLevel).
Scan(&levelResults); err != nil {
err = gerror.Wrap(err, "查询层级数据失败,请稍后重试!")
return
}
// 构建层级与分润金额的映射
levelCommissionMap := make(map[int]float64)
for _, lr := range levelResults {
levelCommissionMap[lr.Level] = lr.CommissionAmount
}
// 查询各层级的收入金额(从盈利月账单表:total_income)
// 先从分润表获取各层级的机构ID列表
type deptLevelRow struct {
Level int `orm:"level"`
DeptId int64 `orm:"dept_id"`
}
var deptLevelResults []*deptLevelRow
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
Fields("DISTINCT " + dao.TFinCommissionDetail.Columns().BenefitDeptLevel + " as level, " + dao.TFinCommissionDetail.Columns().BenefitDeptId + " as dept_id").
Scan(&deptLevelResults); err != nil {
err = gerror.Wrap(err, "查询层级机构失败,请稍后重试!")
return
}
// 根据层级汇总机构ID
levelDeptMap := make(map[int][]int64)
for _, dlr := range deptLevelResults {
levelDeptMap[dlr.Level] = append(levelDeptMap[dlr.Level], dlr.DeptId)
}
// 构建收入金额映射
incomeMap := make(map[int]float64)
for level, deptIds := range levelDeptMap {
if len(deptIds) == 0 {
continue
}
incomeResult, err := s.getProfitAccountIncome(ctx, &model.IncomeQueryParams{
DeptIds: deptIds,
StartPeriod: startPeriod,
EndPeriod: endPeriod,
})
if err != nil {
return nil, err
}
incomeMap[level] = incomeResult
}
// 计算总收入
var totalIncome float64
for _, item := range levelResults {
totalIncome += item.CommissionAmount
}
// 构建结果
for level := 1; level <= 3; level++ {
item := model.TLevelSummaryItem{
Level: level,
}
// 设置客户数、分润金额
for _, lr := range levelResults {
if lr.Level == level {
item.CustomerCount = lr.CustomerCount
item.CommissionAmount = lr.CommissionAmount
break
}
}
// 设置收入金额
item.IncomeAmount = incomeMap[level]
// 计算收入占比
if totalIncome > 0 {
item.IncomeRatio = item.CommissionAmount / totalIncome * 100
}
res.Items = append(res.Items, item)
}
return
}
// getProfitAccountIncome 查询盈利账户收入
func (s *sCommonTFinCommissionDetail) getProfitAccountIncome(ctx context.Context, params *model.IncomeQueryParams) (float64, error) {
type incomeRow struct {
TotalIncome float64 `orm:"total_income"`
}
var row *incomeRow
if err := handler.Model(dao.TProfitAccountMonthBill.Ctx(ctx)).
WhereIn(dao.TProfitAccountMonthBill.Columns().DeptId, params.DeptIds).
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, params.StartPeriod).
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, params.EndPeriod).
Fields("SUM(" + dao.TProfitAccountMonthBill.Columns().TotalIncome + ") as total_income").
Scan(&row); err != nil {
return 0, gerror.Wrap(err, "查询盈利账户收入失败")
}
if row == nil {
return 0, nil
}
return row.TotalIncome, nil
}
// mustAtoi 安全转换字符串为int
func mustAtoi(s string) int {
i, _ := strconv.Atoi(s)
return i
}
@@ -0,0 +1,287 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package common
import (
"context"
"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"
"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/gtime"
)
type sCommonTFinWithdrawableChange struct{}
func NewCommonTFinWithdrawableChange() *sCommonTFinWithdrawableChange {
return &sCommonTFinWithdrawableChange{}
}
func init() {
service.RegisterCommonTFinWithdrawableChange(NewCommonTFinWithdrawableChange())
}
// Model 可提现变动明细表ORM模型
func (s *sCommonTFinWithdrawableChange) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TFinWithdrawableChange.Ctx(ctx), option...)
}
// List 获取可提现变动明细表列表
func (s *sCommonTFinWithdrawableChange) List(ctx context.Context, in *model.TFinWithdrawableChangeListParam) (list []*model.TFinWithdrawableChangeListModel, totalCount int, err error) {
mod := dao.TFinWithdrawableChange.Ctx(ctx)
sysDeptTable := dao.SysDept.Table()
orderTable := dao.TOrder.Table()
// mod = mod.FieldsPrefix(dao.TSimCardInfo.Table(), model.TSimCardInfoListModel{})
// mod = mod.Fields(simChannels + ".chan_name as chanName, chan_alias as chanAlias")
mod = mod.FieldsPrefix(dao.TFinWithdrawableChange.Table(), model.TFinWithdrawableChangeListModel{})
mod = mod.Fields(sysDeptTable + ".dept_name as deptName")
mod = mod.Fields(orderTable + ".order_type as orderType")
deptId := contexts.GetDeptId(ctx)
// 字段过滤
mod = mod.Where(dao.TFinWithdrawableChange.Columns().DeptId, deptId)
// 查询主键ID
if in.Id > 0 {
mod = mod.Where(dao.TFinWithdrawableChange.Columns().Id, in.Id)
}
// 查询月份范围(根据 account_period 字段)
if len(in.Month) == 2 {
mod = mod.WhereGTE(dao.TFinWithdrawableChange.Columns().AccountPeriod, in.Month[0])
mod = mod.WhereLTE(dao.TFinWithdrawableChange.Columns().AccountPeriod, in.Month[1])
}
// 查询收支类型
if in.IncomeExpenseType != nil {
mod = mod.Where(dao.TFinWithdrawableChange.Columns().IncomeExpenseType, *in.IncomeExpenseType)
}
// 查询变动类型
if in.ChangeType != nil {
mod = mod.Where(dao.TFinWithdrawableChange.Columns().ChangeType, *in.ChangeType)
}
// 查询账期
if in.AccountPeriod != "" {
mod = mod.Where(dao.TFinWithdrawableChange.Columns().AccountPeriod, in.AccountPeriod)
}
mod = mod.LeftJoin(sysDeptTable, dao.TFinWithdrawableChange.Table()+"."+dao.TFinWithdrawableChange.Columns().DeptId+" COLLATE utf8mb4_general_ci="+sysDeptTable+"."+dao.SysDept.Columns().DeptId)
mod = mod.LeftJoin(orderTable, dao.TFinWithdrawableChange.Table()+"."+dao.TFinWithdrawableChange.Columns().SourceNo+" COLLATE utf8mb4_general_ci="+orderTable+"."+dao.TOrder.Columns().OrderSn)
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TFinWithdrawableChange.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取可提现变动明细表列表失败,请稍后重试!")
return
}
// 根据 TransactionTime 赋值账期
for i := range list {
if list[i].CreatedAt != nil {
list[i].TradingMonth = list[i].CreatedAt.Format("Y-m")
}
}
return
}
// Export 导出可提现变动明细(异步:提交任务后到下载中心取文件)
func (s *sCommonTFinWithdrawableChange) Export(ctx context.Context, in *model.TFinWithdrawableChangeListParam) (err error) {
_, 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 修改/新增可提现变动明细表
func (s *sCommonTFinWithdrawableChange) Edit(ctx context.Context, in *model.TFinWithdrawableChangeEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TFinWithdrawableChangeUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改可提现变动明细表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TFinWithdrawableChangeInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增可提现变动明细表失败,请稍后重试!")
}
return
})
}
// Delete 删除可提现变动明细表
func (s *sCommonTFinWithdrawableChange) Delete(ctx context.Context, in *model.TFinWithdrawableChangeDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除可提现变动明细表失败,请稍后重试!")
return
}
return
}
// View 获取可提现变动明细表指定信息
func (s *sCommonTFinWithdrawableChange) View(ctx context.Context, in *model.TFinWithdrawableChangeViewParam) (res *model.TFinWithdrawableChangeViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取可提现变动明细表信息,请稍后重试!")
return
}
return
}
// CreateBalanceTransferInChange 写入余额转可提现审核通过后的可提现金额变动明细。
func (s *sCommonTFinWithdrawableChange) CreateBalanceTransferInChange(ctx context.Context, tx gdb.TX, in *model.TFinWithdrawableBalanceTransferInParam) error {
if in == nil {
return gerror.New("可提现变动明细参数不能为空")
}
tenantId := contexts.GetTenantId(ctx)
userId := contexts.GetUserId(ctx)
deptId := contexts.GetDeptId(ctx)
createdAt := gtime.Now()
columns := dao.TFinWithdrawableChange.Columns()
if _, err := tx.Ctx(ctx).Model(dao.TFinWithdrawableChange.Table()).Data(g.Map{
columns.DeptId: in.DeptId,
columns.ChangeType: financeWithdrawableChangeTypeBalanceTransferIn,
columns.IncomeExpenseType: financeIncomeExpenseIncome,
columns.ChangeAmount: in.ChangeAmount,
columns.BeforeBalance: in.BeforeBalance,
columns.AfterBalance: in.AfterBalance,
columns.AccountType: in.AccountType,
columns.BankName: in.BankName,
columns.BankAccount: in.BankAccount,
columns.AccountName: in.AccountName,
columns.IdentityCard: in.IdentityCard,
columns.SourceNo: in.SourceNo,
columns.Remark: in.Remark,
columns.AccountPeriod: in.AccountPeriod,
columns.TenantId: tenantId,
columns.CreatedDept: deptId,
columns.CreatedBy: userId,
columns.CreatedAt: createdAt,
}).Insert(); err != nil {
return gerror.Wrap(err, "新增可提现变动明细失败,请稍后重试!")
}
return nil
}
// CreateCommissionSettlementChange 写入分润结算产生的可提现明细。
func (s *sCommonTFinWithdrawableChange) CreateCommissionSettlementChange(ctx context.Context, tx gdb.TX, in *model.TFinWithdrawableCommissionSettlementParam) error {
if in == nil {
return gerror.New("分润结算可提现明细参数不能为空")
}
if in.DeptId <= 0 {
return gerror.New("机构ID不能为空")
}
if in.ChangeAmount <= 0 {
return gerror.New("分润结算金额必须大于0")
}
if in.AccountPeriod == "" {
return gerror.New("账期不能为空")
}
if in.SourceNo == "" {
return gerror.New("结算批次号不能为空")
}
tenantId := in.TenantId
if tenantId == "" {
tenantId = contexts.GetTenantId(ctx)
}
userId := in.CreatedBy
if userId <= 0 {
userId = contexts.GetUserId(ctx)
}
deptId := in.CreatedDept
if deptId <= 0 {
deptId = contexts.GetDeptId(ctx)
}
if deptId <= 0 {
deptId = in.DeptId
}
createdAt := in.CreatedAt
if createdAt == nil {
createdAt = gtime.Now()
}
columns := dao.TFinWithdrawableChange.Columns()
if _, err := tx.Ctx(ctx).Model(dao.TFinWithdrawableChange.Table()).Data(g.Map{
columns.DeptId: in.DeptId,
columns.ChangeType: financeWithdrawableChangeTypeCommission,
columns.IncomeExpenseType: financeIncomeExpenseIncome,
columns.ChangeAmount: in.ChangeAmount,
columns.BeforeBalance: in.BeforeBalance,
columns.AfterBalance: in.AfterBalance,
columns.AccountType: 0,
columns.SourceNo: in.SourceNo,
columns.Remark: in.Remark,
columns.AccountPeriod: in.AccountPeriod,
columns.TenantId: tenantId,
columns.CreatedDept: deptId,
columns.CreatedBy: userId,
columns.CreatedAt: createdAt,
}).Insert(); err != nil {
return gerror.Wrap(err, "新增分润结算可提现明细失败,请稍后重试!")
}
return nil
}
@@ -0,0 +1,261 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package common
import (
"context"
"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/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
"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/gtime"
)
type sCommonTFinanceApply struct{}
func NewCommonTFinanceApply() *sCommonTFinanceApply {
return &sCommonTFinanceApply{}
}
func init() {
service.RegisterCommonTFinanceApply(NewCommonTFinanceApply())
}
// Model 财务申请表ORM模型
func (s *sCommonTFinanceApply) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TFinanceApply.Ctx(ctx), option...)
}
// List 获取财务申请表列表
func (s *sCommonTFinanceApply) List(ctx context.Context, in *model.TFinanceApplyListParam) (list []*model.TFinanceApplyListModel, totalCount int, err error) {
mod := s.Model(ctx)
applyTable := dao.TFinanceApply.Table()
applyDeptAlias := "apply_dept"
auditorDeptAlias := "auditor_dept"
sysDeptTable := dao.SysDept.Table()
mod = mod.LeftJoin(sysDeptTable+" as "+applyDeptAlias, applyDeptAlias+"."+dao.SysDept.Columns().DeptId+"="+applyTable+"."+dao.TFinanceApply.Columns().ApplyDeptId)
mod = mod.LeftJoin(sysDeptTable+" as "+auditorDeptAlias, auditorDeptAlias+"."+dao.SysDept.Columns().DeptId+"="+applyTable+"."+dao.TFinanceApply.Columns().AuditDeptId)
// 字段过滤
mod = mod.FieldsPrefix(applyTable, model.TFinanceApplyListModel{})
mod = mod.Fields(applyDeptAlias + ".dept_name as applyDeptName")
mod = mod.Fields(auditorDeptAlias + ".dept_name as auditDeptName")
// 查询主键ID
if in.Id > 0 {
mod = mod.Where(dao.TFinanceApply.Columns().Id, in.Id)
}
if in.ApplyNo != nil {
mod = mod.Where(dao.TFinanceApply.Columns().ApplyNo, *in.ApplyNo)
}
// 查询状态:0待审核,1已通过,2已驳回,3已打款,4已取消
if in.ApplyStatus != nil {
mod = mod.Where(dao.TFinanceApply.Columns().ApplyStatus, *in.ApplyStatus)
}
if in.ApplyType != nil {
mod = mod.Where(dao.TFinanceApply.Columns().ApplyType, *in.ApplyType)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TFinanceApply.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TFinanceApply.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取财务申请表列表失败,请稍后重试!")
return
}
return
}
// Export 导出财务申请(异步:提交任务后到下载中心取文件)
func (s *sCommonTFinanceApply) Export(ctx context.Context, in *model.TFinanceApplyListParam) (err error) {
_, 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 修改/新增财务申请表
func (s *sCommonTFinanceApply) Edit(ctx context.Context, in *model.TFinanceApplyEditParam) (err error) {
// 验证'ApplyNo'唯一
if err = xgorm.IsUnique(ctx, &dao.TFinanceApply, g.Map{dao.TFinanceApply.Columns().ApplyNo: in.ApplyNo}, "申请单号已存在", in.Id); err != nil {
return
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
if _, err = s.Model(ctx).
Fields(model.TFinanceApplyUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改财务申请表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TFinanceApplyInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增财务申请表失败,请稍后重试!")
}
return
})
}
// Delete 删除财务申请表
func (s *sCommonTFinanceApply) Delete(ctx context.Context, in *model.TFinanceApplyDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除财务申请表失败,请稍后重试!")
return
}
return
}
// View 获取财务申请表指定信息
func (s *sCommonTFinanceApply) View(ctx context.Context, in *model.TFinanceApplyViewParam) (res *model.TFinanceApplyViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取财务申请表信息,请稍后重试!")
return
}
return
}
// CreateBalanceTransferApply 创建余额转可提现的待审核申请单。
func (s *sCommonTFinanceApply) CreateBalanceTransferApply(ctx context.Context, tx gdb.TX, in *model.TFinanceApplyCreateBalanceTransferParam) error {
if in == nil {
return gerror.New("财务申请参数不能为空")
}
tenantId := contexts.GetTenantId(ctx)
userId := contexts.GetUserId(ctx)
deptId := contexts.GetDeptId(ctx)
createdAt := gtime.Now()
columns := dao.TFinanceApply.Columns()
if _, err := tx.Ctx(ctx).Model(dao.TFinanceApply.Table()).Data(g.Map{
columns.ApplyNo: in.ApplyNo,
columns.ApplyType: financeApplyTypeBalanceToWithdraw,
columns.AuditType: financeAuditTypeFirst,
columns.ApplyDeptId: in.ApplyDeptId,
columns.AuditDeptId: in.AuditDeptId,
columns.AvailableAmountSnapshot: in.AvailableAmountSnapshot,
columns.ApplyAmount: in.ApplyAmount,
columns.AccountType: 0,
columns.ApplyStatus: financeApplyStatusPending,
columns.ApplyTime: createdAt,
columns.Remark: in.Remark,
columns.TenantId: tenantId,
columns.CreatedDept: deptId,
columns.CreatedBy: userId,
columns.CreatedAt: createdAt,
}).Insert(); err != nil {
return gerror.Wrap(err, "新增余额转可提现申请失败,请稍后重试!")
}
return nil
}
// GetPendingByApplyNo 按申请单号查询待审核申请。
func (s *sCommonTFinanceApply) GetPendingByApplyNo(ctx context.Context, tx gdb.TX, applyNo string, tenantId string) (*entity.TFinanceApply, error) {
apply := &entity.TFinanceApply{}
columns := dao.TFinanceApply.Columns()
if err := tx.Ctx(ctx).Model(dao.TFinanceApply.Table()).
Where(columns.ApplyNo, applyNo).
Where(columns.ApplyStatus, financeApplyStatusPending).
Where(columns.TenantId, tenantId).
Scan(apply); err != nil {
return nil, gerror.Wrap(err, "查询财务申请失败,请稍后重试!")
}
if apply.Id == 0 {
return nil, gerror.New("财务申请不存在或已审核")
}
return apply, nil
}
// UpdateAuditStatus 更新财务申请的审核状态。
func (s *sCommonTFinanceApply) UpdateAuditStatus(ctx context.Context, tx gdb.TX, in *model.TFinanceApplyUpdateAuditStatusParam) error {
if in == nil {
return gerror.New("财务申请审核状态参数不能为空")
}
userId := contexts.GetUserId(ctx)
updatedAt := gtime.Now()
columns := dao.TFinanceApply.Columns()
if _, err := tx.Ctx(ctx).Model(dao.TFinanceApply.Table()).
Where(columns.ApplyNo, in.ApplyNo).
Data(g.Map{
columns.ApplyStatus: in.ApplyStatus,
columns.AuditTime: in.AuditTime,
columns.UpdatedBy: userId,
columns.UpdatedAt: updatedAt,
}).
Update(); err != nil {
return gerror.Wrap(err, "更新财务申请审核状态失败,请稍后重试!")
}
return nil
}
@@ -0,0 +1,210 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package common
import (
"context"
"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"
"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/gtime"
)
type sCommonTFinanceAuditRecord struct{}
func NewCommonTFinanceAuditRecord() *sCommonTFinanceAuditRecord {
return &sCommonTFinanceAuditRecord{}
}
func init() {
service.RegisterCommonTFinanceAuditRecord(NewCommonTFinanceAuditRecord())
}
// Model 财务审核记录表ORM模型
func (s *sCommonTFinanceAuditRecord) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TFinanceAuditRecord.Ctx(ctx), option...)
}
// List 获取财务审核记录表列表
func (s *sCommonTFinanceAuditRecord) List(ctx context.Context, in *model.TFinanceAuditRecordListParam) (list []*model.TFinanceAuditRecordListModel, totalCount int, err error) {
mod := s.Model(ctx)
auditRecordTable := dao.TFinanceAuditRecord.Table()
sysDept := dao.SysDept.Table()
sysUser := dao.SysUser.Table()
mod = mod.LeftJoin(sysDept, sysDept+"."+dao.SysDept.Columns().DeptId+"="+auditRecordTable+"."+dao.TFinanceAuditRecord.Columns().AuditDeptId)
mod = mod.LeftJoin(sysUser, sysUser+"."+dao.SysUser.Columns().UserId+"="+auditRecordTable+"."+dao.TFinanceAuditRecord.Columns().AuditorUserId)
// 字段过滤
mod = mod.FieldsPrefix(auditRecordTable, model.TFinanceAuditRecordListModel{})
mod = mod.Fields(sysDept + ".dept_name as auditorDeptName")
mod = mod.Fields(sysUser + ".user_name as auditorUserName")
// 查询主键ID
if in.Id > 0 {
mod = mod.Where(dao.TFinanceAuditRecord.Columns().Id, in.Id)
}
if in.ApplyNo != nil {
mod = mod.Where(dao.TFinanceAuditRecord.Columns().ApplyNo, *in.ApplyNo)
}
if in.AuditResult != nil {
mod = mod.Where(dao.TFinanceAuditRecord.Columns().AuditResult, *in.AuditResult)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TFinanceAuditRecord.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TFinanceAuditRecord.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取财务审核记录表列表失败,请稍后重试!")
return
}
return
}
// Export 导出财务审核记录(异步:提交任务后到下载中心取文件)
func (s *sCommonTFinanceAuditRecord) Export(ctx context.Context, in *model.TFinanceAuditRecordListParam) (err error) {
_, 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 修改/新增财务审核记录表
func (s *sCommonTFinanceAuditRecord) Edit(ctx context.Context, in *model.TFinanceAuditRecordEditParam) (err error) {
// 验证'AuditNo'唯一
if err = xgorm.IsUnique(ctx, &dao.TFinanceAuditRecord, g.Map{dao.TFinanceAuditRecord.Columns().AuditNo: in.AuditNo}, "审核记录编号已存在", in.Id); err != nil {
return
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TFinanceAuditRecordUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改财务审核记录表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TFinanceAuditRecordInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增财务审核记录表失败,请稍后重试!")
}
return
})
}
// Delete 删除财务审核记录表
func (s *sCommonTFinanceAuditRecord) Delete(ctx context.Context, in *model.TFinanceAuditRecordDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除财务审核记录表失败,请稍后重试!")
return
}
return
}
// View 获取财务审核记录表指定信息
func (s *sCommonTFinanceAuditRecord) View(ctx context.Context, in *model.TFinanceAuditRecordViewParam) (res *model.TFinanceAuditRecordViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取财务审核记录表信息,请稍后重试!")
return
}
return
}
// CreateRecord 写入财务审核记录。
func (s *sCommonTFinanceAuditRecord) CreateRecord(ctx context.Context, tx gdb.TX, in *model.TFinanceAuditRecordCreateParam) error {
if in == nil {
return gerror.New("财务审核记录参数不能为空")
}
columns := dao.TFinanceAuditRecord.Columns()
tenantId := contexts.GetTenantId(ctx)
userId := contexts.GetUserId(ctx)
deptId := contexts.GetDeptId(ctx)
createdAt := gtime.Now()
if _, err := tx.Ctx(ctx).Model(dao.TFinanceAuditRecord.Table()).Data(g.Map{
columns.AuditNo: in.AuditNo,
columns.ApplyNo: in.ApplyNo,
columns.AuditDeptId: in.AuditDeptId,
columns.AuditorUserId: in.AuditorUserId,
columns.AuditResult: in.AuditResult,
columns.AuditOpinion: in.AuditOpinion,
columns.AuditTime: in.AuditTime,
columns.TenantId: tenantId,
columns.CreatedDept: deptId,
columns.CreatedBy: userId,
columns.CreatedAt: createdAt,
}).Insert(); err != nil {
return gerror.Wrap(err, "新增财务审核记录失败,请稍后重试!")
}
return nil
}
@@ -0,0 +1,439 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/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"
"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"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
"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/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sCommonTMessageLog struct{}
func NewCommonTMessageLog() *sCommonTMessageLog {
return &sCommonTMessageLog{}
}
func init() {
service.RegisterCommonTMessageLog(NewCommonTMessageLog())
}
// Model 消息日志ORM模型
func (s *sCommonTMessageLog) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TMessageLog.Ctx(ctx), option...)
}
// List 获取消息日志列表
func (s *sCommonTMessageLog) List(ctx context.Context, in *model.TMessageLogListParam) (list []*model.TMessageLogListModel, totalCount int, err error) {
mod := dao.TMessageLog.Ctx(ctx)
deptId := contexts.GetDeptId(ctx)
// 字段过滤
mod = mod.Fields(model.TMessageLogListModel{})
g.Log().Info(ctx, "查询用户ID:", contexts.GetUserId(ctx))
if in.MessageType != nil {
mod = mod.Where(dao.TMessageLog.Columns().MessageType, in.MessageType)
}
if title := strings.TrimSpace(in.MessageTitle); title != "" {
mod = mod.WhereLike(dao.TMessageLog.Columns().MessageTitle, "%"+title+"%")
}
// 查询发送时间范围
if in.SendTimeStart != nil {
mod = mod.WhereGTE(dao.TMessageLog.Columns().SendTime, in.SendTimeStart)
}
if in.SendTimeEnd != nil {
mod = mod.WhereLTE(dao.TMessageLog.Columns().SendTime, in.SendTimeEnd.EndOfDay())
}
// 兼容旧的单日发送时间查询
if in.SendTime != nil {
mod = mod.Where(fmt.Sprintf("DATE(%s) = ?", dao.TMessageLog.Columns().SendTime), in.SendTime.Format("Y-m-d"))
}
// 查询用户ID(JSON字段,使用JSON_CONTAINS查询)
userId := contexts.GetUserId(ctx)
var sysUser *entity.SysUser
dao.SysUser.Ctx(ctx).WherePri(userId).Scan(&sysUser)
userType := sysUser.UserType
// 1. 查找顶级部门(ParentId = 0)
var topDept entity.SysDept
err = dao.SysDept.Ctx(ctx).
Where(dao.SysDept.Columns().ParentId, 0).
Scan(&topDept)
if err != nil {
return
}
// 检查顶级部门是否存在
if topDept.DeptId > 0 && deptId != topDept.DeptId {
mod = mod.Where(dao.TMessageLog.Columns().IsRecall, 0)
mod = mod.Where(fmt.Sprintf("JSON_CONTAINS(%s, CAST(? AS JSON))", dao.TMessageLog.Columns().UserIds), userId)
if userType == "sys_user" {
mod = mod.Where(dao.TMessageLog.Columns().SendUserType, 1)
} else if userType == "app_user" {
mod = mod.Where(dao.TMessageLog.Columns().SendUserType, 2)
}
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TMessageLog.Columns().MessageLogId)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取消息日志列表失败,请稍后重试!")
return
}
return
}
// Export 导出消息日志(异步:提交任务后到下载中心取文件)
func (s *sCommonTMessageLog) Export(ctx context.Context, in *model.TMessageLogListParam) (err error) {
_, 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 修改/新增消息日志
func (s *sCommonTMessageLog) Edit(ctx context.Context, in *model.TMessageLogEditParam) (err error) {
var (
pushUserIds []uint64
pushPayload g.Map
)
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.MessageLogId > 0 {
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TMessageLogUpdateFields{}).
WherePri(in.MessageLogId).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改消息日志失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
in.SendTime = gtime.Now()
result, err := tx.Ctx(ctx).Model(dao.TMessageLog.Table()).
Fields(model.TMessageLogInsertFields{}).
Data(in).Insert()
if err != nil {
err = gerror.Wrap(err, "新增消息日志失败,请稍后重试!")
return
}
// 获取插入的消息日志ID
messageLogId, err := result.LastInsertId()
if err != nil {
err = gerror.Wrap(err, "获取消息日志ID失败")
return
}
pushPayload = g.Map{
"messageLogId": messageLogId,
"messageType": gconv.Uint(in.MessageType),
"sendUserType": gconv.Uint(in.SendUserType),
"messageTitle": gconv.String(in.MessageTitle),
"messageContent": gconv.String(in.MessageContent),
"messageLink": gconv.String(in.MessageLink),
"sendTime": in.SendTime,
"isRecall": gconv.Uint(in.IsRecall),
"isRead": 0,
}
optionalUserMessageColumns := s.userMessageOptionalColumns(ctx)
// 根据 user_ids 往用户消息表插入记录
if in.UserIds != nil && len(in.UserIds.Interfaces()) > 0 {
userIds := in.UserIds.Interfaces()
for _, userId := range userIds {
userIdInt := gconv.Int64(userId)
if userIdInt <= 0 {
continue
}
userMessageData := g.Map{
dao.TUserMessage.Columns().MessageLogId: messageLogId,
dao.TUserMessage.Columns().UserId: userIdInt,
dao.TUserMessage.Columns().TenantId: tenantId,
dao.TUserMessage.Columns().CreatedDept: createdDept,
dao.TUserMessage.Columns().CreatedBy: createdBy,
dao.TUserMessage.Columns().CreatedAt: in.CreatedAt,
}
if _, ok := optionalUserMessageColumns["user_rank"]; ok {
userMessageData["user_rank"] = 0
}
if _, ok := optionalUserMessageColumns["title"]; ok {
userMessageData["title"] = messageLogStringValue(in.MessageTitle)
}
if _, ok := optionalUserMessageColumns["content"]; ok {
userMessageData["content"] = messageLogStringValue(in.MessageContent)
}
if _, ok := optionalUserMessageColumns["link"]; ok {
userMessageData["link"] = messageLogStringValue(in.MessageLink)
}
_, err = tx.Ctx(ctx).Model(dao.TUserMessage.Table()).Data(userMessageData).Insert()
if err != nil {
err = gerror.Wrap(err, "新增用户消息失败,请稍后重试!")
return
}
pushUserIds = append(pushUserIds, uint64(userIdInt))
}
}
return
})
if err != nil {
return
}
for _, userId := range pushUserIds {
g.Log().Info(ctx, "推送消息到用户 %v", userId)
websocket.SendToUser(userId, &websocket.WResponse{
Event: "message:new",
Data: pushPayload,
})
}
return
}
func (s *sCommonTMessageLog) userMessageOptionalColumns(ctx context.Context) map[string]struct{} {
result, err := g.DB().GetAll(ctx, "SHOW COLUMNS FROM "+dao.TUserMessage.Table())
if err != nil {
return map[string]struct{}{}
}
columns := make(map[string]struct{}, len(result))
for _, row := range result {
field := row["Field"].String()
if field != "" {
columns[field] = struct{}{}
}
}
return columns
}
func messageLogStringValue(value *string) string {
if value == nil {
return ""
}
return *value
}
// Delete 删除消息日志
func (s *sCommonTMessageLog) Delete(ctx context.Context, in *model.TMessageLogDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.MessageLogId).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除消息日志失败,请稍后重试!")
return
}
return
}
// View 获取消息日志指定信息,并标记已读
func (s *sCommonTMessageLog) View(ctx context.Context, in *model.TMessageLogViewParam) (res *model.TMessageLogViewModel, err error) {
if err = dao.TMessageLog.Ctx(ctx).WherePri(in.MessageLogId).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取消息日志信息,请稍后重试!")
return
}
// 标记用户消息为已读
userId := contexts.GetUserId(ctx)
now := gtime.Now()
_, err = dao.TUserMessage.Ctx(ctx).Where(g.Map{
dao.TUserMessage.Columns().MessageLogId: in.MessageLogId,
dao.TUserMessage.Columns().UserId: userId,
dao.TUserMessage.Columns().IsRead: 0,
}).Data(g.Map{
dao.TUserMessage.Columns().IsRead: 1,
dao.TUserMessage.Columns().UpdatedBy: userId,
dao.TUserMessage.Columns().UpdatedAt: now,
}).Update()
if err != nil {
// 已读标记失败不影响主业务,仅记录日志
g.Log().Warning(ctx, "标记消息已读失败: %v", err)
err = nil
}
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
}
@@ -0,0 +1,264 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/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"
"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"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
"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/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sCommonTNotifyLog struct{}
func NewCommonTNotifyLog() *sCommonTNotifyLog {
return &sCommonTNotifyLog{}
}
func init() {
service.RegisterCommonTNotifyLog(NewCommonTNotifyLog())
}
// Model 通知日志ORM模型
func (s *sCommonTNotifyLog) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TNotifyLog.Ctx(ctx), option...)
}
// List 获取通知日志列表
func (s *sCommonTNotifyLog) List(ctx context.Context, in *model.TNotifyLogListParam) (list []*model.TNotifyLogListModel, totalCount int, err error) {
mod := dao.TNotifyLog.Ctx(ctx)
// 字段过滤
mod = mod.Fields(model.TNotifyLogListModel{})
if title := strings.TrimSpace(in.NotifyTitle); title != "" {
mod = mod.WhereLike(dao.TNotifyLog.Columns().NotifyTitle, "%"+title+"%")
}
// 查询发送用户类型
if in.SendUserType != nil && *in.SendUserType > 0 {
mod = mod.Where(dao.TNotifyLog.Columns().SendUserType, *in.SendUserType)
}
// 查询发送时间范围
if in.SendTimeStart != nil {
mod = mod.WhereGTE(dao.TNotifyLog.Columns().SendTime, in.SendTimeStart)
}
if in.SendTimeEnd != nil {
mod = mod.WhereLTE(dao.TNotifyLog.Columns().SendTime, in.SendTimeEnd.EndOfDay())
}
// 兼容旧的单日发送时间查询
if in.SendTime != nil {
mod = mod.Where(fmt.Sprintf("DATE(%s) = ?", dao.TNotifyLog.Columns().SendTime), in.SendTime.Format("Y-m-d"))
}
userId := contexts.GetUserId(ctx)
var sysUser *entity.SysUser
dao.SysUser.Ctx(ctx).WherePri(userId).Scan(&sysUser)
userType := sysUser.UserType
// 1. 查找顶级部门(ParentId = 0)
var topDept entity.SysDept
err = dao.SysDept.Ctx(ctx).
Where(dao.SysDept.Columns().ParentId, 0).
Scan(&topDept)
if err != nil {
return
}
// 获取当前用户部门ID
deptId := contexts.GetDeptId(ctx)
// 只有非顶级用户才根据用户类型过滤
if deptId != topDept.DeptId {
if userType == "sys_user" {
mod = mod.Where(dao.TNotifyLog.Columns().SendUserType, 1)
} else if userType == "app_user" {
mod = mod.Where(dao.TNotifyLog.Columns().SendUserType, 2)
}
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TNotifyLog.Columns().NotifyId)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取通知日志列表失败,请稍后重试!")
return
}
return
}
// Export 导出通知日志(异步:提交任务后到下载中心取文件)
func (s *sCommonTNotifyLog) Export(ctx context.Context, in *model.TNotifyLogListParam) (err error) {
_, 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 修改/新增通知日志
func (s *sCommonTNotifyLog) Edit(ctx context.Context, in *model.TNotifyLogEditParam) (err error) {
var (
pushPayload g.Map
pushUserIds []uint64
)
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.NotifyId > 0 {
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TNotifyLogUpdateFields{}).
WherePri(in.NotifyId).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改通知日志失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
in.SendTime = gtime.Now()
result, err := s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TNotifyLogInsertFields{}).
Data(in).Insert()
if err != nil {
err = gerror.Wrap(err, "新增通知日志失败,请稍后重试!")
return
}
notifyId, err := result.LastInsertId()
if err != nil {
err = gerror.Wrap(err, "获取通知ID失败")
return
}
pushPayload = g.Map{
"notifyId": notifyId,
"notifyTitle": gconv.String(in.NotifyTitle),
"notifyContent": gconv.String(in.NotifyContent),
"sendUserType": gconv.Int(in.SendUserType),
"sendTime": in.SendTime,
}
var userType string
if in.SendUserType != nil {
switch *in.SendUserType {
case 1:
userType = "sys_user"
case 2:
userType = "app_user"
}
}
if userType != "" {
var users []*entity.SysUser
if err = tx.Ctx(ctx).Model(dao.SysUser.Table()).
Fields(dao.SysUser.Columns().UserId).
Where(dao.SysUser.Columns().TenantId, tenantId).
Where(dao.SysUser.Columns().UserType, userType).
Where(dao.SysUser.Columns().Status, "0").
Scan(&users); err != nil {
err = gerror.Wrap(err, "获取通知推送用户失败")
return
}
for _, user := range users {
if user.UserId > 0 {
pushUserIds = append(pushUserIds, uint64(user.UserId))
}
}
}
return
})
if err != nil {
return
}
for _, userId := range pushUserIds {
g.Log().Info(ctx, "推送通知到用户 %v", userId)
websocket.SendToUser(userId, &websocket.WResponse{
Event: "notify:new",
Data: pushPayload,
})
}
return
}
// Delete 删除通知日志
func (s *sCommonTNotifyLog) Delete(ctx context.Context, in *model.TNotifyLogDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.NotifyId).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除通知日志失败,请稍后重试!")
return
}
return
}
// View 获取通知日志指定信息
func (s *sCommonTNotifyLog) View(ctx context.Context, in *model.TNotifyLogViewParam) (res *model.TNotifyLogViewModel, err error) {
if err = dao.TNotifyLog.Ctx(ctx).WherePri(in.NotifyId).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取通知日志信息,请稍后重试!")
return
}
return
}
@@ -0,0 +1,695 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package common
import (
"context"
"database/sql"
"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/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"
"strconv"
"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 sCommonProfitAccountMonthBill struct{}
func NewCommonProfitAccountMonthBill() *sCommonProfitAccountMonthBill {
return &sCommonProfitAccountMonthBill{}
}
func init() {
service.RegisterCommonProfitAccountMonthBill(NewCommonProfitAccountMonthBill())
}
// Model 盈利账户月账单表ORM模型
func (s *sCommonProfitAccountMonthBill) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TProfitAccountMonthBill.Ctx(ctx), option...)
}
// List 获取盈利账户月账单表列表
func (s *sCommonProfitAccountMonthBill) List(ctx context.Context, in *model.TProfitAccountMonthBillListParam) (list []*model.TProfitAccountMonthBillListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(model.TProfitAccountMonthBillListModel{})
// 查询账单ID
if in.Id > 0 {
mod = mod.Where(dao.TProfitAccountMonthBill.Columns().Id, in.Id)
}
// 查询账单状态:1正常,2已调整,3已作废
if in.BillStatus > 0 {
mod = mod.Where(dao.TProfitAccountMonthBill.Columns().BillStatus, in.BillStatus)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TProfitAccountMonthBill.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TProfitAccountMonthBill.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取盈利账户月账单表列表失败,请稍后重试!")
return
}
return
}
// Export 导出盈利账户月账单表
func (s *sCommonProfitAccountMonthBill) Export(ctx context.Context, in *model.TProfitAccountMonthBillListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TProfitAccountMonthBillExportModel{})
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.TProfitAccountMonthBillExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增盈利账户月账单表
func (s *sCommonProfitAccountMonthBill) Edit(ctx context.Context, in *model.TProfitAccountMonthBillEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TProfitAccountMonthBillUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改盈利账户月账单表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TProfitAccountMonthBillInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增盈利账户月账单表失败,请稍后重试!")
}
return
})
}
// Delete 删除盈利账户月账单表
func (s *sCommonProfitAccountMonthBill) Delete(ctx context.Context, in *model.TProfitAccountMonthBillDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除盈利账户月账单表失败,请稍后重试!")
return
}
return
}
// View 获取盈利账户月账单表指定信息
func (s *sCommonProfitAccountMonthBill) View(ctx context.Context, in *model.TProfitAccountMonthBillViewParam) (res *model.TProfitAccountMonthBillViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取盈利账户月账单表信息,请稍后重试!")
return
}
return
}
// SettleProfitAccountMonthBill 按可提现变动明细统计并写入盈利账户月账单。
func (s *sCommonProfitAccountMonthBill) SettleProfitAccountMonthBill(ctx context.Context, in *model.TProfitAccountMonthBillSettleParam) (res *model.TProfitAccountMonthBillSettleResult, err error) {
if in == nil {
in = &model.TProfitAccountMonthBillSettleParam{}
}
if err = in.Filter(ctx); err != nil {
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
}
_, err = time.Parse("2006-01", in.AccountPeriod)
if err != nil {
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
}
now := gtime.Now()
userId := contexts.GetUserId(ctx)
tenantId := contexts.GetTenantId(ctx)
changeColumns := dao.TFinWithdrawableChange.Columns()
billColumns := dao.TProfitAccountMonthBill.Columns()
type changeRow struct {
Id int64 `orm:"id"`
DeptId int64 `orm:"dept_id"`
TenantId string `orm:"tenant_id"`
CreatedDept int64 `orm:"created_dept"`
ChangeType int `orm:"change_type"`
IncomeExpenseType int `orm:"income_expense_type"`
ChangeAmount float64 `orm:"change_amount"`
BeforeBalance float64 `orm:"before_balance"`
CreatedAt *gtime.Time `orm:"created_at"`
}
var rows []*changeRow
mod := handler.Model(dao.TFinWithdrawableChange.Ctx(ctx), &handler.Option{
FilterTenant: false,
FilterAuth: false,
}).
Fields(
changeColumns.Id,
changeColumns.DeptId,
changeColumns.TenantId,
changeColumns.CreatedDept,
changeColumns.ChangeType,
changeColumns.IncomeExpenseType,
changeColumns.ChangeAmount,
changeColumns.BeforeBalance,
changeColumns.CreatedAt,
).
Where(changeColumns.AccountPeriod, in.AccountPeriod)
if tenantId != "" {
mod = mod.Where(changeColumns.TenantId, tenantId)
}
if err = mod.OrderAsc(changeColumns.TenantId).OrderAsc(changeColumns.DeptId).OrderAsc(changeColumns.CreatedAt).OrderAsc(changeColumns.Id).Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "查询可提现变动明细失败,请稍后重试!")
}
type billGroup struct {
DeptId int64
TenantId string
CreatedDept int64
AccountPeriod string
OpeningBalance float64
CommissionIncome float64
BalanceTransferInIncome float64
WithdrawalExpense float64
WithdrawableTransferOutExpense float64
ProfitCreditRepayExpense float64
TotalIncome float64
TotalExpense float64
IncomeCount int
ExpenseCount int
DetailCount int
}
groups := make(map[string]*billGroup)
keys := make([]string, 0, 16)
for _, row := range rows {
if row == nil || row.DeptId <= 0 {
continue
}
switch row.ChangeType {
case 0, 1, 2, 3, 4:
default:
continue
}
key := fmt.Sprintf("%s:%d", row.TenantId, row.DeptId)
group := groups[key]
if group == nil {
group = &billGroup{
DeptId: row.DeptId,
TenantId: row.TenantId,
CreatedDept: row.CreatedDept,
AccountPeriod: in.AccountPeriod,
OpeningBalance: row.BeforeBalance,
}
groups[key] = group
keys = append(keys, key)
}
group.DetailCount++
switch row.ChangeType {
case 0:
group.CommissionIncome += row.ChangeAmount
case 1:
group.WithdrawalExpense += row.ChangeAmount
case 2:
group.WithdrawableTransferOutExpense += row.ChangeAmount
case 3:
group.BalanceTransferInIncome += row.ChangeAmount
case 4:
group.ProfitCreditRepayExpense += row.ChangeAmount
}
if row.IncomeExpenseType == 0 {
group.TotalIncome += row.ChangeAmount
group.IncomeCount++
} else if row.IncomeExpenseType == 1 {
group.TotalExpense += row.ChangeAmount
group.ExpenseCount++
}
}
res = &model.TProfitAccountMonthBillSettleResult{
AccountPeriod: in.AccountPeriod,
}
if len(groups) == 0 {
return res, nil
}
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
for _, key := range keys {
group := groups[key]
if group == nil {
continue
}
totalIncome := group.TotalIncome
totalExpense := group.TotalExpense
closingBalance := group.OpeningBalance + totalIncome - totalExpense
group.TotalIncome = totalIncome
group.TotalExpense = totalExpense
lookup := tx.Ctx(ctx).Model(dao.TProfitAccountMonthBill.Table()).
Fields(billColumns.Id).
Where(billColumns.DeptId, group.DeptId).
Where(billColumns.AccountPeriod, group.AccountPeriod)
if group.TenantId != "" {
lookup = lookup.Where(billColumns.TenantId, group.TenantId)
}
var existing struct {
Id int64 `orm:"id"`
}
if err := lookup.LockUpdate().Scan(&existing); err != nil && err != sql.ErrNoRows {
return gerror.Wrap(err, "查询盈利账户月账单失败,请稍后重试!")
}
tenantValue := group.TenantId
if tenantValue == "" {
tenantValue = tenantId
}
if tenantValue == "" {
return gerror.New("租户ID不能为空")
}
createdDeptValue := group.CreatedDept
if createdDeptValue == 0 {
createdDeptValue = contexts.GetDeptId(ctx)
}
if createdDeptValue == 0 {
return gerror.New("创建部门不能为空")
}
data := g.Map{
billColumns.DeptId: group.DeptId,
billColumns.AccountPeriod: group.AccountPeriod,
billColumns.OpeningBalance: group.OpeningBalance,
billColumns.CommissionIncome: group.CommissionIncome,
billColumns.BalanceTransferInIncome: group.BalanceTransferInIncome,
billColumns.WithdrawalExpense: group.WithdrawalExpense,
billColumns.WithdrawableTransferOutExpense: group.WithdrawableTransferOutExpense,
billColumns.ProfitCreditRepayExpense: group.ProfitCreditRepayExpense,
billColumns.TotalIncome: totalIncome,
billColumns.TotalExpense: totalExpense,
billColumns.ClosingBalance: closingBalance,
billColumns.IncomeCount: group.IncomeCount,
billColumns.ExpenseCount: group.ExpenseCount,
billColumns.BillStatus: 1,
billColumns.SettleTime: now,
billColumns.SettleBy: userId,
billColumns.Remark: "根据可提现变动明细统计生成",
billColumns.TenantId: tenantValue,
billColumns.UpdatedBy: userId,
billColumns.UpdatedAt: now,
}
if existing.Id > 0 {
if _, err := tx.Ctx(ctx).Model(dao.TProfitAccountMonthBill.Table()).
WherePri(existing.Id).
Data(data).
Update(); err != nil {
return gerror.Wrap(err, "更新盈利账户月账单失败,请稍后重试!")
}
res.UpdateCount++
} else {
data[billColumns.CreatedDept] = createdDeptValue
data[billColumns.CreatedBy] = userId
data[billColumns.CreatedAt] = now
if _, err := tx.Ctx(ctx).Model(dao.TProfitAccountMonthBill.Table()).
Data(data).
Insert(); err != nil {
return gerror.Wrap(err, "新增盈利账户月账单失败,请稍后重试!")
}
res.InsertCount++
}
res.DeptCount++
res.DetailCount += group.DetailCount
res.TotalIncome += totalIncome
res.TotalExpense += totalExpense
}
return nil
})
if err != nil {
return nil, err
}
return res, nil
}
// 获取月账单盈利账户汇总信息
func (s *sCommonProfitAccountMonthBill) GetProfitBillSummary(ctx context.Context, month string) (res *model.TProfitBillModel, err error) {
// 计算上月账期(基于当前时间)
// now := time.Now()
// lastMonthTime := now.AddDate(0, -1, 0)
// lastMonthPeriod := lastMonthTime.Format("2006-01")
var bill *model.TProfitAccountMonthBillViewModel
if err = dao.TProfitAccountMonthBill.Ctx(ctx).
Where(dao.TProfitAccountMonthBill.Columns().AccountPeriod, month).
Where(dao.TProfitAccountMonthBill.Columns().DeptId, contexts.GetDeptId(ctx)).
Scan(&bill); err != nil {
err = gerror.Wrap(err, "查询上月账单失败,请稍后重试!")
return
}
if bill == nil {
return
}
res = &model.TProfitBillModel{}
res.TotalIncome = bill.TotalIncome
res.TotalExpense = bill.TotalExpense
return
}
// GetCoreFinancialData 获取核心财务指标
func (s *sCommonProfitAccountMonthBill) GetCoreFinancialData(ctx context.Context, in *model.TCommissionOverviewParam) (res *model.TCoreFinancialDataModel, err error) {
res = &model.TCoreFinancialDataModel{}
deptId := contexts.GetDeptId(ctx)
// 解析账期参数
var startPeriod, endPeriod string
if in.Type == 1 { // 年
startPeriod = in.Year + "-01"
endPeriod = in.Year + "-12"
} else if in.Type == 2 { // 月
startPeriod = in.Month
endPeriod = in.Month
}
// 1. 获取总收入:统计 t_profit_account_month_bill 的 total_income
type totalIncomeRow struct {
TotalIncome float64 `orm:"total_income"`
}
var totalIncomeResult *totalIncomeRow
if err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Where(dao.TProfitAccountMonthBill.Columns().DeptId, deptId).
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, startPeriod).
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, endPeriod).
Fields("SUM(" + dao.TProfitAccountMonthBill.Columns().TotalIncome + ") as total_income").
Scan(&totalIncomeResult); err != nil {
err = gerror.Wrap(err, "查询总收入失败,请稍后重试!")
return
}
if totalIncomeResult != nil {
res.TotalIncome = totalIncomeResult.TotalIncome
}
// 2. 获取顶级利润:查询 t_fin_commission_detail 中顶级部门的 commission_income
// 顶级部门ID通常为1或当前部门的顶级父部门
type topProfitRow struct {
CommissionIncome float64 `orm:"commission_income"`
}
var topProfitResult *topProfitRow
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_income").
Scan(&topProfitResult); err != nil {
err = gerror.Wrap(err, "查询顶级利润失败,请稍后重试!")
return
}
if topProfitResult != nil {
res.TopLevelProfit = topProfitResult.CommissionIncome
}
// 3. 获取待审核提现:从 t_profit_account_month_bill 查询 withdrawable_transfer_out_expense
type pendingWithdrawRow struct {
WithdrawableTransferOutExpense float64 `orm:"withdrawable_transfer_out_expense"`
}
var pendingWithdrawResult *pendingWithdrawRow
if err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Where(dao.TProfitAccountMonthBill.Columns().DeptId, deptId).
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, startPeriod).
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, endPeriod).
Fields("SUM(" + dao.TProfitAccountMonthBill.Columns().WithdrawableTransferOutExpense + ") as withdrawable_transfer_out_expense").
Scan(&pendingWithdrawResult); err != nil {
err = gerror.Wrap(err, "查询待审核提现失败,请稍后重试!")
return
}
if pendingWithdrawResult != nil {
res.PendingWithdrawAmount = pendingWithdrawResult.WithdrawableTransferOutExpense
}
// 4. 获取已提现金额:统计 t_profit_account_month_bill 的 balance_transfer_in_income
type withdrawedRow struct {
WithdrawalExpense float64 `orm:"withdrawal_expense"`
}
var withdrawedResult *withdrawedRow
if err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Where(dao.TProfitAccountMonthBill.Columns().DeptId, deptId).
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, startPeriod).
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, endPeriod).
Fields("SUM(" + dao.TProfitAccountMonthBill.Columns().WithdrawalExpense + ") as withdrawal_expense").
Scan(&withdrawedResult); err != nil {
err = gerror.Wrap(err, "查询已提现金额失败,请稍后重试!")
return
}
if withdrawedResult != nil {
res.WithdrawedAmount = withdrawedResult.WithdrawalExpense
}
return
}
// GetFinancialTrend 获取财务趋势折线图
// 指标类型:1总收入,2顶级利润,3待审核提现,4已提现金额
// 月份视图:按天查询 t_fin_withdrawable_change
// 年份视图:按月查询 t_profit_account_month_bill
func (s *sCommonProfitAccountMonthBill) GetFinancialTrend(ctx context.Context, in *model.TFinancialTrendParam) (res *model.TFinancialTrendModel, err error) {
res = &model.TFinancialTrendModel{}
// 默认查询所有指标
indicators := in.Indicators
if len(indicators) == 0 {
indicators = []int{1, 2, 3, 4}
}
// 指标名称映射
indicatorNames := map[int]string{
1: "总收入",
2: "顶级利润",
3: "待审核提现",
4: "已提现金额",
}
if in.Type == 1 {
// 年视图:按月查询 t_profit_account_month_bill
year := in.Year
months := []string{"01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"}
res.Labels = make([]string, 0, 12)
for _, m := range months {
res.Labels = append(res.Labels, year+"-"+m)
}
type monthBillRow struct {
AccountPeriod string `orm:"account_period"`
TotalIncome float64 `orm:"total_income"`
CommissionIncome float64 `orm:"commission_income"`
BalanceTransferInIncome float64 `orm:"balance_transfer_in_income"`
WithdrawalExpense float64 `orm:"withdrawal_expense"`
}
var bills []*monthBillRow
if err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Where(dao.TProfitAccountMonthBill.Columns().DeptId, contexts.GetDeptId(ctx)).
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, year+"-01").
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, year+"-12").
Fields(
dao.TProfitAccountMonthBill.Columns().AccountPeriod,
dao.TProfitAccountMonthBill.Columns().TotalIncome,
dao.TProfitAccountMonthBill.Columns().CommissionIncome,
dao.TProfitAccountMonthBill.Columns().BalanceTransferInIncome,
dao.TProfitAccountMonthBill.Columns().WithdrawalExpense,
).
Scan(&bills); err != nil {
return nil, gerror.Wrap(err, "查询财务趋势数据失败,请稍后重试!")
}
// 构建月份映射
billMap := make(map[string]*monthBillRow)
for _, bill := range bills {
billMap[bill.AccountPeriod] = bill
}
// 构建每个指标的数据数组
res.Series = make([]model.TFinancialTrendSeries, 0, len(indicators))
for _, ind := range indicators {
series := model.TFinancialTrendSeries{
Name: indicatorNames[ind],
Type: ind,
Values: make([]float64, 0, 12),
}
for _, period := range res.Labels {
var val float64
if bill, ok := billMap[period]; ok {
switch ind {
case 1:
val = bill.TotalIncome
case 2:
val = bill.CommissionIncome
case 3:
val = bill.BalanceTransferInIncome
case 4:
val = bill.WithdrawalExpense
}
}
series.Values = append(series.Values, val)
}
res.Series = append(res.Series, series)
}
} else if in.Type == 2 {
// 月视图:按天查询 t_fin_withdrawable_change
month := in.Month
t := gtime.NewFromStr(month + "-01")
daysInMonth := t.EndOfMonth().Day()
res.Labels = make([]string, 0, daysInMonth)
for day := 1; day <= daysInMonth; day++ {
res.Labels = append(res.Labels, month+"-"+strconv.Itoa(day))
}
// 解析月份获取开始和结束日期
startDate := month + "-01"
endDate := month + "-" + strconv.Itoa(daysInMonth)
type dailyRow struct {
Date string `orm:"date"`
IncomeAmt float64 `orm:"income_amt"`
Expense1Amt float64 `orm:"expense1_amt"`
Expense3Amt float64 `orm:"expense3_amt"`
}
var dailyResults []*dailyRow
if err = handler.Model(dao.TFinWithdrawableChange.Ctx(ctx)).
Where(dao.TFinWithdrawableChange.Columns().DeptId, contexts.GetDeptId(ctx)).
WhereGTE(dao.TFinWithdrawableChange.Columns().CreatedAt, startDate).
WhereLTE(dao.TFinWithdrawableChange.Columns().CreatedAt, endDate).
Fields(`
DATE_FORMAT(` + dao.TFinWithdrawableChange.Columns().CreatedAt + `, '%Y-%m-%d') as date,
SUM(CASE WHEN ` + dao.TFinWithdrawableChange.Columns().IncomeExpenseType + `=0 THEN ` + dao.TFinWithdrawableChange.Columns().ChangeAmount + ` ELSE 0 END) as income_amt,
SUM(CASE WHEN ` + dao.TFinWithdrawableChange.Columns().IncomeExpenseType + `=1 THEN ` + dao.TFinWithdrawableChange.Columns().ChangeAmount + ` ELSE 0 END) as expense1_amt,
SUM(CASE WHEN ` + dao.TFinWithdrawableChange.Columns().IncomeExpenseType + `=3 THEN ` + dao.TFinWithdrawableChange.Columns().ChangeAmount + ` ELSE 0 END) as expense3_amt
`).
Group("DATE_FORMAT(" + dao.TFinWithdrawableChange.Columns().CreatedAt + ", '%Y-%m-%d')").
Order("date ASC").
Scan(&dailyResults); err != nil {
return nil, gerror.Wrap(err, "查询日趋势数据失败,请稍后重试!")
}
// 构建日期映射
dailyMap := make(map[string]*dailyRow)
for _, row := range dailyResults {
dailyMap[row.Date] = row
}
// 查询顶级利润(从分润表)
type commissionRow struct {
CommissionAmount float64 `orm:"commission_amount"`
}
var commissionResult *commissionRow
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, contexts.GetDeptId(ctx)).
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, month).
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, month).
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount").
Scan(&commissionResult); err != nil {
return nil, gerror.Wrap(err, "查询顶级利润失败,请稍后重试!")
}
topProfit := 0.0
if commissionResult != nil {
topProfit = commissionResult.CommissionAmount
}
// 构建每个指标的数据数组
res.Series = make([]model.TFinancialTrendSeries, 0, len(indicators))
for _, ind := range indicators {
series := model.TFinancialTrendSeries{
Name: indicatorNames[ind],
Type: ind,
Values: make([]float64, 0, daysInMonth),
}
for _, date := range res.Labels {
var val float64
switch ind {
case 1: // 总收入
if row, ok := dailyMap[date]; ok {
val = row.IncomeAmt
}
case 2: // 顶级利润
val = topProfit
case 3: // 待审核提现
if row, ok := dailyMap[date]; ok {
val = row.Expense3Amt
}
case 4: // 已提现金额
if row, ok := dailyMap[date]; ok {
val = row.Expense1Amt
}
}
series.Values = append(series.Values, val)
}
res.Series = append(res.Series, series)
}
}
return
}
@@ -0,0 +1,307 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/websocket"
"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/model/do"
"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"
"unicode/utf8"
"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/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sCommonTWoCommunication struct{}
func NewCommonTWoCommunication() *sCommonTWoCommunication {
return &sCommonTWoCommunication{}
}
func init() {
service.RegisterCommonTWoCommunication(NewCommonTWoCommunication())
}
func (s *sCommonTWoCommunication) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TWoCommunication.Ctx(ctx), option...)
}
func (s *sCommonTWoCommunication) List(ctx context.Context, in *model.TWoCommunicationListParam) (list []*model.TWoCommunicationListModel, totalCount int, err error) {
if in == nil || strings.TrimSpace(in.WoNo) == "" {
return nil, 0, gerror.New("请指定工单编号")
}
if _, err = getWorkOrder(ctx, in.WoNo); err != nil {
return nil, 0, err
}
mod := s.Model(ctx, &handler.Option{
FilterTenant: true,
FilterAuth: false,
})
mod = mod.Fields(model.TWoCommunicationListModel{})
if in.WoNo != "" {
mod = mod.Where(dao.TWoCommunication.Columns().WoNo, in.WoNo)
}
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
mod = mod.WhereBetween(dao.TWoCommunication.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
mod = mod.Page(in.Page, in.PageSize)
mod = mod.OrderDesc(dao.TWoCommunication.Columns().Id)
mod = mod.Hook(hook.MemberSummary)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "get work order communication list failed")
return
}
return
}
func (s *sCommonTWoCommunication) Export(ctx context.Context, in *model.TWoCommunicationListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
tags, err := convert.GetEntityDescTags(model.TWoCommunicationExportModel{})
if err != nil {
return
}
var (
fileName = "export-work-order-communication-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("total %v rows, %v pages, page %v, current %v rows", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TWoCommunicationExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
func (s *sCommonTWoCommunication) Edit(ctx context.Context, in *model.TWoCommunicationEditParam) (err error) {
if err = xgorm.IsUnique(ctx, &dao.TWoCommunication, g.Map{dao.TWoCommunication.Columns().CommNo: in.CommNo}, "communication number exists", in.Id); err != nil {
return
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TWoCommunicationUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "update work order communication failed")
}
return
}
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TWoCommunicationInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "create work order communication failed")
}
return
})
}
// CreateInitialMessage 创建工单时同步生成首条沟通消息。
func (s *sCommonTWoCommunication) CreateInitialMessage(ctx context.Context, tx gdb.TX, in *model.TWoCommunicationInitialMessageParam) error {
if in == nil {
return gerror.New("invalid request")
}
if in.WoNo == "" {
return gerror.New("work order number cannot be empty")
}
if in.Content == "" {
return nil
}
now := in.CreatedAt
if now == nil {
now = gtime.Now()
}
columns := dao.TWoCommunication.Columns()
data := g.Map{
columns.WoNo: in.WoNo,
columns.CommType: in.CommType,
columns.SenderType: in.SenderType,
columns.SenderDeptId: in.SenderDeptId,
columns.SenderUserId: in.SenderUserId,
columns.SenderName: in.SenderName,
columns.Content: in.Content,
columns.CommTime: now,
columns.TenantId: in.TenantId,
columns.CreatedDept: in.CreatedDept,
columns.CreatedBy: in.CreatedBy,
columns.CreatedAt: now,
columns.UpdatedBy: in.CreatedBy,
columns.UpdatedAt: now,
}
if len(in.AttachmentUrls) > 0 {
data[columns.AttachmentUrls] = gjson.New(in.AttachmentUrls)
}
if _, err := tx.Ctx(ctx).Model(dao.TWoCommunication.Table()).Data(data).Insert(); err != nil {
return gerror.Wrap(err, "create initial work order communication failed")
}
return nil
}
func (s *sCommonTWoCommunication) Message(ctx context.Context, in *model.TWoMainMessageParam) (err error) {
if in == nil {
return gerror.New("invalid request")
}
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
current, err := getWorkOrder(ctx, in.WoNo)
if err != nil {
return err
}
tenantId := contexts.GetTenantId(ctx)
createdDept := contexts.GetDeptId(ctx)
createdBy := contexts.GetUserId(ctx)
senderName := contexts.GetUserName(ctx)
// 处理文件上传
if in.File != nil {
ossResult, err := service.SysOss().Upload(ctx, &model.SysOssUploadParam{
File: in.File,
FileType: "file",
NewFileType: 0,
})
if err != nil {
return gerror.Wrap(err, "file upload failed")
}
if ossResult != nil && ossResult.Url != "" {
in.AttachmentUrls = append(in.AttachmentUrls, ossResult.Url)
}
}
// 写入前核对终态;发送人由当前登录用户确定。
if current.CurrentStatus == 4 || current.CurrentStatus == 5 {
return gerror.New("已完结或已取消的工单不能回复")
}
content := strings.TrimSpace(in.Content)
for _, attachment := range in.AttachmentUrls {
var file *entity.SysOss
columns := dao.SysOss.Columns()
if err = dao.SysOss.Ctx(ctx).Where(columns.TenantId, tenantId).Where(columns.CreatedBy, createdBy).Where(columns.Url, attachment).Scan(&file); err != nil {
return gerror.Wrap(err, "校验工单附件失败")
}
if file == nil || file.OssId == 0 || file.FileSize > 10*1024*1024 {
return gerror.New("附件不存在、不属于当前用户或超过10MB")
}
}
if content == "" && len(in.AttachmentUrls) == 0 {
return gerror.New("请输入内容或上传附件")
}
if utf8.RuneCountInString(content) > 2000 || len(in.AttachmentUrls) > 6 {
return gerror.New("内容最多2000字,附件最多6个")
}
isCreator := current.CreatorUserId == createdBy
commType := 0
if isCreator {
commType = 1
}
now := gtime.Now()
communication := do.TWoCommunication{WoNo: current.WoNo, CommType: commType, SenderDeptId: createdDept, SenderUserId: createdBy, SenderName: senderName, Content: content, TenantId: tenantId, CreatedDept: createdDept, CreatedBy: createdBy, UpdatedBy: createdBy}
communication.CommNo = fmt.Sprintf("comm%d", gtime.TimestampNano())
communication.WoId = current.Id
communication.CommTime = now
if isCreator {
communication.SenderType = current.CreatorType
} else {
communication.SenderType = 2
}
if len(in.AttachmentUrls) > 0 {
communication.AttachmentUrls = gjson.New(in.AttachmentUrls)
}
if _, err = dao.TWoCommunication.Ctx(ctx).TX(tx).Data(communication).Insert(); err != nil {
return gerror.Wrap(err, "保存工单回复失败")
}
update := do.TWoMain{UpdatedBy: createdBy}
// 创建人在待确认阶段继续反馈视为未解决;客服补充回复不重新打开工单。
if isCreator && current.CurrentStatus == workOrderStatusResolved {
update.CurrentStatus = workOrderStatusProcessing
if err = service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{WoNo: current.WoNo, OperationType: workOrderOperationProcess, BeforeStatus: workOrderStatusResolved, AfterStatus: workOrderStatusProcessing}); err != nil {
return err
}
}
if !isCreator && current.FirstResponseTime == nil {
update.FirstResponseTime = now
}
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().Id, current.Id).Where(dao.TWoMain.Columns().TenantId, tenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).Data(update).Update(); err != nil {
return gerror.Wrap(err, "更新工单失败")
}
if !isCreator {
if err = saveWorkOrderNotice(ctx, tx, &current, createdBy, "工单收到回复", content); err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
wsTag := "ws:tag:wo:communication:" + in.WoNo
g.Log().Infof(ctx, "work order communication websocket broadcast tag:%s count:%d", wsTag, websocket.GetTagCount(wsTag))
websocket.SendToTag(wsTag, &websocket.WResponse{
Event: "ws:event:wo:communication:message",
Data: g.Map{
"woNo": in.WoNo,
},
})
return nil
}
func (s *sCommonTWoCommunication) Delete(ctx context.Context, in *model.TWoCommunicationDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "delete work order communication failed")
return
}
return
}
func (s *sCommonTWoCommunication) View(ctx context.Context, in *model.TWoCommunicationViewParam) (res *model.TWoCommunicationViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "get work order communication failed")
return
}
return
}
@@ -0,0 +1,601 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/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/rabbitmq"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/websocket"
"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/model/do"
"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"
"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"
)
const (
workOrderAutoCloseDelay = 72 * time.Hour
workOrderStatusAssigned = 1 //已分派
workOrderStatusProcessing = 2 //处理中
workOrderStatusResolved = 3 //已解决
workOrderStatusClosed = 4 //已关单
workOrderStatusCanceled = 5 //已取消
workOrderOperationCreate = 0 //创建
workOrderOperationAssign = 1 //分派
workOrderOperationProcess = 2 //处理
workOrderOperationFinish = 3 //完成
workOrderOperationClose = 4 //关单
workOrderOperationCancel = 5 //取消
)
type sCommonTWoMain struct{}
func NewCommonTWoMain() *sCommonTWoMain {
return &sCommonTWoMain{}
}
func init() {
service.RegisterCommonTWoMain(NewCommonTWoMain())
}
func getWorkOrder(ctx context.Context, woNo string) (current entity.TWoMain, err error) {
if contexts.GetUserId(ctx) <= 0 || strings.TrimSpace(woNo) == "" {
return current, gerror.New("请登录并指定工单编号")
}
query := dao.TWoMain.Ctx(ctx).Where(dao.TWoMain.Columns().TenantId, contexts.GetTenantId(ctx)).Where(dao.TWoMain.Columns().WoNo, strings.TrimSpace(woNo))
if g.DB().GetConfig().Type != "sqlite" {
query = query.LockUpdate()
}
if err = query.Scan(&current); err != nil {
return current, gerror.Wrap(err, "query work order failed")
}
if current.WoNo == "" {
return current, gerror.New("work order not found")
}
if !contexts.IsSuperAdmin(ctx) && current.CreatorUserId != contexts.GetUserId(ctx) && current.HandlerDeptId != contexts.GetDeptId(ctx) && current.HandlerUserId != contexts.GetUserId(ctx) {
return current, gerror.New("无权访问该工单")
}
return current, nil
}
func (s *sCommonTWoMain) notifyWorkOrderChanged(ctx context.Context, woNo string) {
if woNo == "" {
return
}
websocket.SendToTag("ws:tag:wo:communication:"+woNo, &websocket.WResponse{
Event: "ws:event:wo:communication:transfer",
Data: g.Map{
"woNo": woNo,
},
})
}
// Model returns the work order main ORM model
func (s *sCommonTWoMain) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TWoMain.Ctx(ctx), option...)
}
// List returns work order main records.
func (s *sCommonTWoMain) List(ctx context.Context, in *model.TWoMainListParam) (list []*model.TWoMainListModel, totalCount int, err error) {
if contexts.GetUserId(ctx) <= 0 {
return nil, 0, gerror.New("请先登录")
}
mod := s.Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false})
mod = mod.Fields(model.TWoMainListModel{})
if in.Id > 0 {
mod = mod.Where(dao.TWoMain.Columns().Id, in.Id)
}
if in.CurrentStatus > 0 {
mod = mod.Where(dao.TWoMain.Columns().CurrentStatus, in.CurrentStatus)
}
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
mod = mod.WhereBetween(dao.TWoMain.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
if in.WoNo != "" {
mod = mod.Where(dao.TWoMain.Columns().WoNo, in.WoNo)
}
if in.WoType > 0 {
mod = mod.Where(dao.TWoMain.Columns().WoType, in.WoType)
}
if !contexts.IsSuperAdmin(ctx) {
columns := dao.TWoMain.Columns()
mod = mod.Where("("+columns.CreatorUserId+" = ? OR "+columns.HandlerUserId+" = ? OR "+columns.HandlerDeptId+" = ?)", contexts.GetUserId(ctx), contexts.GetUserId(ctx), contexts.GetDeptId(ctx))
}
mod = mod.Page(in.Page, in.PageSize)
mod = mod.OrderDesc(dao.TWoMain.Columns().Id)
mod = mod.Hook(hook.MemberSummary)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "get work order list failed")
return
}
return
}
func (s *sCommonTWoMain) Export(ctx context.Context, in *model.TWoMainListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
tags, err := convert.GetEntityDescTags(model.TWoMainExportModel{})
if err != nil {
return
}
var (
fileName = "export-work-order-main-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("total %v rows, %v pages, page %v, current %v rows", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TWoMainExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// 添加工单
func (s *sCommonTWoMain) Edit(ctx context.Context, in *model.TWoMainEditParam) (err error) {
if in.Id > 0 && in.WoNo != nil {
err = xgorm.IsUnique(ctx, &dao.TWoMain, g.Map{dao.TWoMain.Columns().WoNo: in.WoNo}, "work order number exists", in.Id)
}
if err != nil {
return
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
WherePri(in.Id).
Fields(model.TWoMainUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "update work order failed")
}
return
}
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
woNo := fmt.Sprintf("gd%d", gtime.TimestampMilli())
in.WoNo = &woNo
in.CreatorDeptId = &createdDept
in.CreatorUserId = &createdBy
creatorName := contexts.GetUserName(ctx)
in.CreatorName = &creatorName
currentStatus := workOrderStatusProcessing
in.CurrentStatus = &currentStatus
// 查询当前部门信息
var currentDept entity.SysDept
err = dao.SysDept.Ctx(ctx).
Where(dao.SysDept.Columns().DeptId, createdDept).
Scan(&currentDept)
if err != nil {
return err
}
// 分派到上级部门
var handlerDeptId int64
var handlerUserId int64
if currentDept.ParentId != 0 && currentDept.ParentId != 100 {
// 如果有上级部门,分派到上级部门
handlerDeptId = currentDept.ParentId
var parentDept entity.SysDept
err = dao.SysDept.Ctx(ctx).
Where(dao.SysDept.Columns().DeptId, handlerDeptId).
Scan(&parentDept)
if err != nil {
return err
}
handlerUserId = gconv.Int64(parentDept.Leader)
} else {
// 如果没有上级部门,分派到当前部门
handlerDeptId = createdDept
handlerUserId = gconv.Int64(currentDept.Leader)
}
in.HandlerDeptId = &handlerDeptId
in.HandlerUserId = &handlerUserId
if _, err = tx.Ctx(ctx).Model(dao.TWoMain.Table()).
Fields(model.TWoMainInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "create work order failed")
return err
}
content := ""
if in.Content != nil {
content = *in.Content
}
attachmentUrls := append([]string{}, in.AttachmentUrls...)
if len(attachmentUrls) == 0 && in.Attachment != nil {
attachment := strings.TrimSpace(*in.Attachment)
if attachment != "" {
attachmentUrls = append(attachmentUrls, attachment)
}
}
creatorType := 0
if in.CreatorType != nil {
creatorType = *in.CreatorType
}
if err = service.CommonTWoCommunication().CreateInitialMessage(ctx, tx, &model.TWoCommunicationInitialMessageParam{
WoNo: *in.WoNo,
CommType: 1,
SenderType: creatorType,
SenderDeptId: createdDept,
SenderUserId: createdBy,
SenderName: creatorName,
Content: content,
AttachmentUrls: attachmentUrls,
TenantId: tenantId,
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: in.CreatedAt,
}); err != nil {
return err
}
return service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
WoNo: *in.WoNo,
OperationType: workOrderOperationCreate,
AfterStatus: currentStatus,
})
})
}
func (s *sCommonTWoMain) Message(ctx context.Context, in *model.TWoMainMessageParam) (err error) {
return service.CommonTWoCommunication().Message(ctx, in)
}
// 转单:转给顶级部门(ParentId=0)的负责人
func (s *sCommonTWoMain) Transfer(ctx context.Context, in *model.TWoMainTransferParam) (err error) {
if in == nil {
return gerror.New("invalid request")
}
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
current, err := getWorkOrder(ctx, in.WoNo)
if err != nil {
return err
}
if current.CurrentStatus != workOrderStatusAssigned && current.CurrentStatus != workOrderStatusProcessing {
return gerror.New("工单状态不允许此操作")
}
if current.CreatorUserId == contexts.GetUserId(ctx) && !contexts.IsSuperAdmin(ctx) {
return gerror.New("请由处理人员执行此操作")
}
updatedBy := contexts.GetUserId(ctx)
// 1. 查找顶级部门(ParentId = 0)
var topDept entity.SysDept
err = dao.SysDept.Ctx(ctx).
Where(dao.SysDept.Columns().ParentId, 0).
Where(dao.SysDept.Columns().TenantId, current.TenantId).
OrderAsc(dao.SysDept.Columns().OrderNum).OrderAsc(dao.SysDept.Columns().DeptId).
Scan(&topDept)
if err != nil {
return gerror.Wrap(err, "查询顶级部门失败")
}
if topDept.DeptId == 0 {
return gerror.New("未找到顶级部门")
}
// 3. 更新工单,转给顶级部门及其负责人
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().TenantId, current.TenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).
Where(dao.TWoMain.Columns().WoNo, in.WoNo).
Data(do.TWoMain{
AssignDeptId: topDept.DeptId,
AssignUserId: topDept.Leader,
HandlerDeptId: topDept.DeptId,
HandlerUserId: topDept.Leader,
CurrentStatus: workOrderStatusProcessing,
UpdatedBy: updatedBy,
}).Update(); err != nil {
return gerror.Wrap(err, "转单失败")
}
// 4. 记录操作日志
operationType := workOrderOperationAssign
beforeStatus := current.CurrentStatus
afterStatus := workOrderStatusProcessing
return service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
WoNo: in.WoNo,
OperationType: operationType,
OperationDeptId: topDept.DeptId,
OperationUserId: topDept.Leader,
TargetDeptId: topDept.DeptId,
BeforeStatus: beforeStatus,
AfterStatus: afterStatus,
})
})
if err != nil {
return err
}
s.notifyWorkOrderChanged(ctx, in.WoNo)
return nil
}
// 解决
func (s *sCommonTWoMain) Resolve(ctx context.Context, in *model.TWoMainResolveParam) (err error) {
if in == nil {
return gerror.New("invalid request")
}
var resolvedAt *gtime.Time
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
current, err := getWorkOrder(ctx, in.WoNo)
if err != nil {
return err
}
if current.CurrentStatus != workOrderStatusAssigned && current.CurrentStatus != workOrderStatusProcessing {
return gerror.New("工单状态不允许此操作")
}
if current.CreatorUserId == contexts.GetUserId(ctx) && !contexts.IsSuperAdmin(ctx) {
return gerror.New("请由处理人员执行此操作")
}
updatedBy := contexts.GetUserId(ctx)
creatorDept := contexts.GetDeptId(ctx)
now := gtime.Now()
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().TenantId, current.TenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).
Where(dao.TWoMain.Columns().WoNo, in.WoNo).
Data(do.TWoMain{
CurrentStatus: workOrderStatusResolved,
ResolveTime: now,
HandlerDeptId: creatorDept,
HandlerUserId: updatedBy,
UpdatedBy: updatedBy,
}).Update(); err != nil {
return gerror.Wrap(err, "operation failed")
}
if err = service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
WoNo: in.WoNo,
OperationType: workOrderOperationFinish,
BeforeStatus: current.CurrentStatus,
AfterStatus: workOrderStatusResolved,
}); err != nil {
return err
}
if err = saveWorkOrderNotice(ctx, tx, &current, updatedBy, "工单待确认解决", "问题已标记解决,请在72小时内确认或反馈未解决。"); err != nil {
return err
}
resolvedAt = now
return nil
})
if err != nil {
return err
}
if resolvedAt != nil {
s.publishAutoClose(ctx, in.WoNo, resolvedAt)
}
return nil
}
func (s *sCommonTWoMain) publishAutoClose(ctx context.Context, woNo string, resolvedAt *gtime.Time) {
if !rabbitmq.Enabled(ctx) {
g.Log().Errorf(ctx, "work order auto close skipped, rabbitmq disabled, woNo:%s", woNo)
return
}
message := model.WorkOrderAutoCloseMessage{
WoNo: woNo,
ResolveTimeTs: resolvedAt.Timestamp(),
}
if err := rabbitmq.PublishDelayed(ctx, consts.QueueWorkOrderAutoCloseDelay, consts.QueueWorkOrderAutoClose, workOrderAutoCloseDelay, message); err != nil {
g.Log().Errorf(ctx, "publish work order auto close failed, woNo:%s, err:%+v", woNo, err)
return
}
g.Log().Infof(ctx, "work order auto close published, woNo:%s, delay:%s", woNo, workOrderAutoCloseDelay)
}
func (s *sCommonTWoMain) AutoCloseResolved(ctx context.Context, message model.WorkOrderAutoCloseMessage) error {
if message.WoNo == "" {
return nil
}
g.Log().Infof(ctx, "work order auto close received, woNo:%s", message.WoNo)
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
current := entity.TWoMain{}
if err := tx.Ctx(ctx).Model(dao.TWoMain.Table()).
Where(dao.TWoMain.Columns().WoNo, message.WoNo).
Scan(&current); err != nil {
return gerror.Wrap(err, "query work order failed")
}
if current.WoNo == "" || current.CurrentStatus != workOrderStatusResolved {
return nil
}
if current.ResolveTime == nil || current.ResolveTime.Add(72*time.Hour).After(gtime.Now()) {
return nil
}
if message.ResolveTimeTs > 0 && (current.ResolveTime == nil || current.ResolveTime.Timestamp() != message.ResolveTimeTs) {
g.Log().Infof(ctx, "work order auto close skipped, resolve time changed, woNo:%s, messageResolveTime:%d", message.WoNo, message.ResolveTimeTs)
return nil
}
operatorID := int64(0)
now := gtime.Now()
result, err := tx.Ctx(ctx).Model(dao.TWoMain.Table()).
Where(dao.TWoMain.Columns().Id, current.Id).
Where(dao.TWoMain.Columns().TenantId, current.TenantId).
Where(dao.TWoMain.Columns().ResolveTime, current.ResolveTime).
Where(dao.TWoMain.Columns().CurrentStatus, workOrderStatusResolved).
Data(do.TWoMain{
CurrentStatus: workOrderStatusClosed,
CloseTime: now,
UpdatedBy: operatorID,
}).Update()
if err != nil {
return gerror.Wrap(err, "auto close work order failed")
}
affected, err := result.RowsAffected()
if err != nil || affected == 0 {
return err
}
_, err = tx.Ctx(ctx).Model(dao.TWoRecord.Table()).Data(do.TWoRecord{
WoNo: message.WoNo,
OperationType: workOrderOperationClose,
OperationDeptId: current.HandlerDeptId,
OperationUserId: operatorID,
BeforeStatus: workOrderStatusResolved,
AfterStatus: workOrderStatusClosed,
TenantId: current.TenantId,
CreatedDept: current.CreatedDept,
CreatedBy: operatorID,
}).Insert()
if err != nil {
return gerror.Wrap(err, "保存自动完结记录失败")
}
return saveWorkOrderNotice(ctx, tx, &current, 0, "工单已自动完结", "问题标记解决超过72小时,工单已自动完结。")
})
}
// 关单
func (s *sCommonTWoMain) Close(ctx context.Context, in *model.TWoMainCloseParam) (err error) {
if in == nil {
return gerror.New("invalid request")
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
current, err := getWorkOrder(ctx, in.WoNo)
if err != nil {
return err
}
if current.CurrentStatus != workOrderStatusResolved {
return gerror.New("工单状态不允许此操作")
}
updatedBy := contexts.GetUserId(ctx)
now := gtime.Now()
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().TenantId, current.TenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).
Where(dao.TWoMain.Columns().WoNo, in.WoNo).
Data(do.TWoMain{
CurrentStatus: workOrderStatusClosed,
CloseTime: now,
UpdatedBy: updatedBy,
}).Update(); err != nil {
return gerror.Wrap(err, "operation failed")
}
return service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
WoNo: in.WoNo,
OperationType: workOrderOperationClose,
BeforeStatus: current.CurrentStatus,
AfterStatus: workOrderStatusClosed,
})
})
}
// 取消
func (s *sCommonTWoMain) Cancel(ctx context.Context, in *model.TWoMainCancelParam) (err error) {
if in == nil {
return gerror.New("invalid request")
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
current, err := getWorkOrder(ctx, in.WoNo)
if err != nil {
return err
}
if current.CurrentStatus != workOrderStatusAssigned && current.CurrentStatus != workOrderStatusProcessing {
return gerror.New("工单状态不允许此操作")
}
if current.CreatorUserId != contexts.GetUserId(ctx) && !contexts.IsSuperAdmin(ctx) {
return gerror.New("只有创建人可以取消工单")
}
updatedBy := contexts.GetUserId(ctx)
now := gtime.Now()
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().TenantId, current.TenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).
Where(dao.TWoMain.Columns().WoNo, in.WoNo).
Data(do.TWoMain{
CurrentStatus: workOrderStatusCanceled,
CloseTime: now,
UpdatedBy: updatedBy,
}).Update(); err != nil {
return gerror.Wrap(err, "operation failed")
}
return service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
WoNo: in.WoNo,
OperationType: workOrderOperationCancel,
BeforeStatus: current.CurrentStatus,
AfterStatus: workOrderStatusCanceled,
})
})
}
// 评价
func (s *sCommonTWoMain) Evaluate(ctx context.Context, in *model.TWoMainEvaluateParam) error {
if in == nil || in.SatisfactionLevel < 1 || in.SatisfactionLevel > 5 {
return gerror.New("请选择1至5星评价")
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
current, err := getWorkOrder(ctx, in.WoNo)
if err != nil {
return err
}
userID := contexts.GetUserId(ctx)
if current.CreatorUserId != userID || current.CurrentStatus != workOrderStatusClosed || current.EvalTime != nil || current.SatisfactionLevel != 0 {
return gerror.New("只有创建人可以对已完结工单评价一次")
}
result, err := dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().Id, current.Id).Where(dao.TWoMain.Columns().TenantId, current.TenantId).WhereNull(dao.TWoMain.Columns().EvalTime).Data(do.TWoMain{SatisfactionLevel: in.SatisfactionLevel, EvalContent: in.EvalContent, EvalTime: gtime.Now(), UpdatedBy: userID}).Update()
if err != nil {
return gerror.Wrap(err, "保存评价失败")
}
count, err := result.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return gerror.New("工单已评价,请刷新")
}
return nil
})
}
func (s *sCommonTWoMain) Delete(ctx context.Context, in *model.TWoMainDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "get work order list failed")
return
}
return
}
// View returns a work order main record.
func (s *sCommonTWoMain) View(ctx context.Context, in *model.TWoMainViewParam) (res *model.TWoMainViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "get work order list failed")
return
}
return
}
@@ -0,0 +1,232 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package common
import (
"context"
"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"
"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/gtime"
)
type sCommonTWoRecord struct{}
func NewCommonTWoRecord() *sCommonTWoRecord {
return &sCommonTWoRecord{}
}
func init() {
service.RegisterCommonTWoRecord(NewCommonTWoRecord())
}
// Model 工单记录表ORM模型
func (s *sCommonTWoRecord) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TWoRecord.Ctx(ctx), option...)
}
// List 获取工单记录表列表
func (s *sCommonTWoRecord) List(ctx context.Context, in *model.TWoRecordListParam) (list []*model.TWoRecordListModel, totalCount int, err error) {
mod := s.Model(ctx, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
mod = mod.Fields(model.TWoRecordListModel{})
// 直接使用 dao 创建 model,避免自动 handler 对关联表添加软删除条件
mod = s.Model(ctx)
// 手动添加租户过滤
tenantId := contexts.GetTenantId(ctx)
if tenantId != "" {
mod = mod.Where(dao.TWoRecord.Columns().TenantId, tenantId)
}
cardTable := dao.TWoRecord.Table()
// 字段过滤
mod = mod.FieldsPrefix(cardTable, model.TWoRecordListModel{})
mod = mod.Fields("op_dept.dept_name AS operation_dept_name")
mod = mod.Fields("op_user.user_name AS operation_user_name")
mod = mod.Fields("t_dept.dept_name AS target_dept_name")
mod = mod.Fields("t_user.user_name AS target_user_name")
// 左关联部门表获取操作机构名称
mod = mod.LeftJoin("sys_dept AS op_dept ON op_dept.dept_id = " + cardTable + ".operation_dept_id")
// 左关联用户表获取操作用户名称
mod = mod.LeftJoin("sys_user AS op_user ON op_user.user_id = " + cardTable + ".operation_user_id")
// 左关联部门表获取目标部门名称
mod = mod.LeftJoin("sys_dept AS t_dept ON t_dept.dept_id = " + cardTable + ".target_dept_id")
// 左关联用户表获取目标用户名称
mod = mod.LeftJoin("sys_user AS t_user ON t_user.user_id = " + cardTable + ".target_user_id")
// 查询主键ID
if in.Id > 0 {
mod = mod.Where(dao.TWoRecord.Columns().Id, in.Id)
}
// 查询变更前状态
if in.BeforeStatus > 0 {
mod = mod.Where(dao.TWoRecord.Columns().BeforeStatus, in.BeforeStatus)
}
// 查询变更后状态
if in.AfterStatus > 0 {
mod = mod.Where(dao.TWoRecord.Columns().AfterStatus, in.AfterStatus)
}
// 查询工单编号
if in.WoNo != "" {
mod = mod.Where(dao.TWoRecord.Columns().WoNo, in.WoNo)
}
// 查询操作类型
if in.OperationType > 0 {
mod = mod.Where(dao.TWoRecord.Columns().OperationType, in.OperationType)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TWoRecord.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TWoRecord.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取工单记录表列表失败,请稍后重试!")
return
}
return
}
// Export 导出工单记录(异步:提交任务后到下载中心取文件)
func (s *sCommonTWoRecord) Export(ctx context.Context, in *model.TWoRecordListParam) (err error) {
_, 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 修改/新增工单记录表
func (s *sCommonTWoRecord) Edit(ctx context.Context, in *model.TWoRecordEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TWoRecordUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改工单记录表失败,请稍后重试!")
}
return
}
// 新增 创建工单后 新增工单记录
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TWoRecordInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增工单记录表失败,请稍后重试!")
}
return
})
}
// 添加工单记录表
func (s *sCommonTWoRecord) AddOperation(ctx context.Context, in *model.TWoRecordAddOperationParam) (err error) {
operationDeptId := contexts.GetDeptId(ctx)
operationUserId := contexts.GetUserId(ctx)
param := &model.TWoRecordEditParam{
WoNo: &in.WoNo,
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
}
return s.Edit(ctx, param)
}
func (s *sCommonTWoRecord) Delete(ctx context.Context, in *model.TWoRecordDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除工单记录表失败,请稍后重试!")
return
}
return
}
// View 获取工单记录表指定信息
func (s *sCommonTWoRecord) View(ctx context.Context, in *model.TWoRecordViewParam) (res *model.TWoRecordViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取工单记录表信息,请稍后重试!")
return
}
return
}
@@ -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)
})
}
@@ -0,0 +1,26 @@
package common
import (
"context"
"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"
)
// saveWorkOrderNotice 与工单操作使用同一事务,通知失败时回滚,避免遗漏。
func saveWorkOrderNotice(ctx context.Context, tx gdb.TX, current *entity.TWoMain, operator int64, title, content string) error {
if current.CreatorUserId <= 0 || current.CreatorUserId == operator {
return nil
}
id, err := dao.TMessageLog.Ctx(ctx).TX(tx).Data(do.TMessageLog{MessageType: 1, SendUserType: current.CreatorType, UserIds: gjson.New([]int64{current.CreatorUserId}), MessageTitle: title, MessageContent: content, MessageLink: "/pages/business/views/workOrderDetail/index?woNo=" + url.QueryEscape(current.WoNo), SendTime: gtime.Now(), IsRecall: 0, TenantId: current.TenantId, CreatedDept: current.CreatorDeptId, CreatedBy: operator}).InsertAndGetId()
if err != nil {
return gerror.Wrap(err, "保存工单通知失败")
}
_, err = dao.TUserMessage.Ctx(ctx).TX(tx).Data(do.TUserMessage{MessageLogId: id, UserId: current.CreatorUserId, IsRead: 0, TenantId: current.TenantId, CreatedDept: current.CreatorDeptId, CreatedBy: operator}).Insert()
return gerror.Wrap(err, "保存工单用户通知失败")
}
@@ -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)
}
})
}
}