Files
iotlcm/server/internal/logic/common/t_consume_account_month_bill.go
T
MiFaZhan 59eb34976a 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 已说明。
2026-10-23 14:48:30 +08:00

459 lines
15 KiB
Go

// 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
}