chore: 初始开源发布

iotlcm-admin-svr:管理后台服务。

- Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件)
- 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、
  MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位;
  私网 IP → 文档地址段;库名 xiujie_admin → iotlcm
- 真实手机号/邮箱 → 合法 11 位编造号 / example.com
- 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、
  私有依赖 go get 行、明文 registry 登录口令
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- Dockerfile 改用公共基础镜像
- 拼错的 RADEME.md → README.md,README 重写
- 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用
- 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes

数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
This commit is contained in:
MiFaZhan
2026-10-23 14:46:05 +08:00
commit 59eb34976a
1132 changed files with 138197 additions and 0 deletions
@@ -0,0 +1,637 @@
package common
import (
"context"
"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/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/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...)
}
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 {
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,
StartTime: batch.StartTime,
EndTime: batch.EndTime,
}
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
}
if resultFile, err := s.getResultFile(ctx, batch.BatchNo, batch.ResultOssId, batch.BatchInfo); err != nil {
return nil, err
} else {
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 = mod.Page(in.Page, in.PageSize).OrderAsc(columns.RowNo)
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
}
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{}
if err := s.downloadModel(ctx).
Where(dao.TResourceBatchDownload.Columns().BatchNo, batchNo).
OrderDesc(dao.TResourceBatchDownload.Columns().Id).
Limit(1).
Scan(download); err != nil {
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)
if ossId > 0 {
oss := &entity.SysOss{}
if err := s.ossModel(ctx).
Where(dao.SysOss.Columns().OssId, ossId).
Scan(oss); err != nil {
return nil, gerror.Wrap(err, "获取结果文件OSS信息失败,请稍后重试!")
}
if oss.OssId > 0 {
if ossUrl := batchExecutionLogFileURL(ctx, oss.Url, oss.Path); ossUrl != "" {
url = ossUrl
}
}
}
return &model.BatchExecutionLogResultFileModel{
DownloadId: download.Id,
OssId: ossId,
Url: url,
DownloadStatus: resultDownloadStatus(download),
}, nil
}
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
}
+667
View File
@@ -0,0 +1,667 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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/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.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 = visibleBatchTypeModel(mod, columns.BatchType, requestedBatchType)
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{
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,
}
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 = mod.Page(query.Page, query.PageSize).OrderAsc(columns.RowNo)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取批次明细列表失败,请稍后重试")
}
return
}
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
}
// 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,24 @@
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
}
@@ -0,0 +1,837 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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,250 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sCommonTBalanceDetail struct{}
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) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TBalanceDetailExportModel{})
if err != nil {
return
}
var (
fileName = "导出余额明细表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TBalanceDetailExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// 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 <li@xiujie.cn>
// @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 <li@xiujie.cn>
// @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 <li@xiujie.cn>
// @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 <li@xiujie.cn>
// @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,289 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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 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) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TFinWithdrawableChangeExportModel{})
if err != nil {
return
}
var (
fileName = "导出可提现变动明细表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TFinWithdrawableChangeExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// 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,263 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sCommonTFinanceApply struct{}
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) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TFinanceApplyExportModel{})
if err != nil {
return
}
var (
fileName = "导出财务申请表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TFinanceApplyExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// 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,212 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sCommonTFinanceAuditRecord struct{}
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) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TFinanceAuditRecordExportModel{})
if err != nil {
return
}
var (
fileName = "导出财务审核记录表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TFinanceAuditRecordExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// 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,323 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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/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"
"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 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) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TMessageLogExportModel{})
if err != nil {
return
}
var (
fileName = "导出消息日志-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TMessageLogExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// 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
}
@@ -0,0 +1,264 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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/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"
"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 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) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TNotifyLogExportModel{})
if err != nil {
return
}
var (
fileName = "导出通知日志-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []*model.TNotifyLogExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// 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 <li@xiujie.cn>
// @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 <li@xiujie.cn>
// @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
}
+600
View File
@@ -0,0 +1,600 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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,
})
})
}
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
}
+238
View File
@@ -0,0 +1,238 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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 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) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TWoRecordExportModel{})
if err != nil {
return
}
var (
fileName = "导出工单记录表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TWoRecordExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// 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,
}
if in.TargetDeptId > 0 {
param.TargetDeptId = &in.TargetDeptId
param.TargetUserId = &in.TargetUserId
}
if in.BeforeStatus > 0 {
param.BeforeStatus = &in.BeforeStatus
}
if in.AfterStatus > 0 {
param.AfterStatus = &in.AfterStatus
}
return s.Edit(ctx, param)
}
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,26 @@
package common
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
"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, "保存工单用户通知失败")
}