59eb34976a
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 已说明。
455 lines
14 KiB
Go
455 lines
14 KiB
Go
package iot
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"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/batchimport"
|
|
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
|
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/outbound"
|
|
"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/iot_excelize"
|
|
|
|
"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/net/ghttp"
|
|
"github.com/gogf/gf/v2/util/gconv"
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
type sIotTSimCardOutbound struct{}
|
|
|
|
const outboundFinanceAccountStatusNormal = 0
|
|
|
|
func NewIotTSimCardOutbound() *sIotTSimCardOutbound {
|
|
return &sIotTSimCardOutbound{}
|
|
}
|
|
|
|
func init() {
|
|
service.RegisterIotTSimCardOutbound(NewIotTSimCardOutbound())
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) DownloadExcelTemplate(ctx context.Context) error {
|
|
f := excelize.NewFile()
|
|
defer func() { _ = f.Close() }()
|
|
|
|
headers := outbound.NewProcessor(outbound.PreviewOptions{}).Config().TemplateHeaders
|
|
if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil {
|
|
return err
|
|
}
|
|
if err := iot_excelize.ApplyHeaderColumnsTextStyle(f); err != nil {
|
|
return err
|
|
}
|
|
|
|
return iot_excelize.WriteHTTP(ctx, f, "sim_card_outbound_template")
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) CustomerBalance(ctx context.Context, in *model.TSimCardOutboundCustomerBalanceParam) (*model.TSimCardOutboundCustomerBalanceRes, error) {
|
|
if in == nil || in.CustomerAgentId <= 0 {
|
|
return nil, gerror.New("客户不能为空")
|
|
}
|
|
operatorDeptId := contexts.GetDeptId(ctx)
|
|
tenantId := contexts.GetTenantId(ctx)
|
|
if operatorDeptId <= 0 || tenantId == "" {
|
|
return nil, gerror.New("当前用户未绑定组织或租户")
|
|
}
|
|
|
|
customer := &entity.SysDept{}
|
|
if err := dao.SysDept.Ctx(ctx).
|
|
Fields(dao.SysDept.Columns().DeptId, dao.SysDept.Columns().DeptName, dao.SysDept.Columns().ParentId).
|
|
Where(dao.SysDept.Columns().DeptId, in.CustomerAgentId).
|
|
Where(dao.SysDept.Columns().TenantId, tenantId).
|
|
Scan(customer); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return nil, gerror.Wrap(err, "查询客户信息失败")
|
|
}
|
|
if customer.DeptId == 0 || (customer.DeptId != operatorDeptId && customer.ParentId != operatorDeptId) {
|
|
return nil, gerror.New("客户不存在或不在当前操作范围内")
|
|
}
|
|
|
|
res := &model.TSimCardOutboundCustomerBalanceRes{
|
|
CustomerAgentId: customer.DeptId,
|
|
CustomerName: customer.DeptName,
|
|
AccountExists: false,
|
|
StatusText: "暂无余额账户",
|
|
}
|
|
account := &entity.TAgentBalance{}
|
|
if err := dao.TAgentBalance.Ctx(ctx).
|
|
Where(dao.TAgentBalance.Columns().DeptId, customer.DeptId).
|
|
Where(dao.TAgentBalance.Columns().TenantId, tenantId).
|
|
Scan(account); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return nil, gerror.Wrap(err, "查询账户余额失败")
|
|
}
|
|
if account.Id == 0 {
|
|
return res, nil
|
|
}
|
|
res.AccountExists = true
|
|
res.Balance = account.Balance
|
|
res.Status = account.Status
|
|
if account.Status == outboundFinanceAccountStatusNormal {
|
|
res.StatusText = "正常"
|
|
} else {
|
|
res.StatusText = "禁用"
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) validateBalanceBeforeSubmit(ctx context.Context, in *model.TSimCardOutboundSubmitParam) error {
|
|
if in == nil || in.OfflineContractCompleted {
|
|
return nil
|
|
}
|
|
amountText := strings.TrimSpace(in.Amount)
|
|
if amountText == "" {
|
|
return gerror.New("订单金额不能为空")
|
|
}
|
|
amount, err := strconv.ParseFloat(amountText, 64)
|
|
if err != nil || amount <= 0 {
|
|
return gerror.New("订单金额必须大于0")
|
|
}
|
|
balance, err := s.CustomerBalance(ctx, &model.TSimCardOutboundCustomerBalanceParam{
|
|
CustomerAgentId: in.CustomerAgentId,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if balance == nil || !balance.AccountExists {
|
|
return gerror.New("客户暂无余额账户,请先充值")
|
|
}
|
|
if balance.Status != outboundFinanceAccountStatusNormal {
|
|
return gerror.New("客户余额账户状态异常,不能出库")
|
|
}
|
|
if balance.Balance+0.0001 < amount {
|
|
return gerror.Newf("客户余额不足,需支付%.2f元,当前余额%.2f元", amount, balance.Balance)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) SubmitExcel(ctx context.Context, in *model.TSimCardOutboundExcelSubmitParam) (*model.TSimCardOutboundExcelSubmitRes, error) {
|
|
if in == nil {
|
|
in = &model.TSimCardOutboundExcelSubmitParam{}
|
|
}
|
|
|
|
file, err := s.getUploadFile(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := outbound.SubmitExcelAsync(ctx, file, in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return (*model.TSimCardOutboundExcelSubmitRes)(outboundSubmitResult(result, consts.BatchTypeOutbound)), nil
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) SubmitRange(ctx context.Context, in *model.TSimCardOutboundRangeSubmitParam) (*model.TSimCardOutboundRangeSubmitRes, error) {
|
|
if in == nil {
|
|
in = &model.TSimCardOutboundRangeSubmitParam{}
|
|
}
|
|
normalizeOutboundSubmitParam(&in.TSimCardOutboundSubmitParam)
|
|
if err := s.validateBalanceBeforeSubmit(ctx, &in.TSimCardOutboundSubmitParam); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rows, err := outbound.BuildRangeRows(ctx, in.RangeCodeType, in.StartIccid, in.EndIccid, in.StartMsisdn, in.EndMsisdn, in.StartImsi, in.EndImsi)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := outbound.SubmitRowsAsync(ctx, in, rows, consts.BatchSourceTypeRange, "", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return (*model.TSimCardOutboundRangeSubmitRes)(outboundSubmitResult(result, consts.BatchTypeOutbound)), nil
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) SubmitInput(ctx context.Context, in *model.TSimCardOutboundInputSubmitParam) (*model.TSimCardOutboundInputSubmitRes, error) {
|
|
if in == nil {
|
|
in = &model.TSimCardOutboundInputSubmitParam{}
|
|
}
|
|
normalizeOutboundSubmitParam(&in.TSimCardOutboundSubmitParam)
|
|
if err := s.validateBalanceBeforeSubmit(ctx, &in.TSimCardOutboundSubmitParam); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rows, err := outbound.ParseInputRows(ctx, in.Input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := outbound.SubmitRowsAsync(ctx, in, rows, consts.BatchSourceTypeBatchInput, "", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return (*model.TSimCardOutboundInputSubmitRes)(outboundSubmitResult(result, consts.BatchTypeOutbound)), nil
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) ListInboundBatches(ctx context.Context, in *model.TSimCardOutboundInboundBatchListParam) (*model.TSimCardOutboundInboundBatchListRes, error) {
|
|
if in == nil {
|
|
in = &model.TSimCardOutboundInboundBatchListParam{}
|
|
}
|
|
normalizePage(&in.Page, &in.PageSize)
|
|
|
|
base := outboundStockBaseModel(ctx)
|
|
columns := dao.TSimCardStock.Columns()
|
|
if batchNo := strings.TrimSpace(in.BatchNo); batchNo != "" {
|
|
base = base.WhereLike(columns.BatchNo, "%"+batchNo+"%")
|
|
}
|
|
base = applyStockBusinessFilters(ctx, base, stockBusinessFilter{
|
|
ChanName: in.ChanName,
|
|
CardType: in.CardType,
|
|
UpPkgKeyword: in.UpPkgKeyword,
|
|
AgreementExpiryMonths: in.AgreementExpiryMonths,
|
|
})
|
|
|
|
totalValue, err := base.Clone().Fields("COUNT(DISTINCT " + columns.BatchNo + ")").Value()
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "查询可出库批次列表失败")
|
|
}
|
|
total := gconv.Int(totalValue)
|
|
|
|
var items []*model.TSimCardOutboundInboundBatchListItem
|
|
err = base.Fields(
|
|
columns.BatchNo,
|
|
"MIN("+columns.ChanKey+") AS chan_key",
|
|
"COUNT(1) AS stock_count",
|
|
"MIN("+columns.CreatedAt+") AS first_created_at",
|
|
"MAX("+columns.CreatedAt+") AS last_created_at",
|
|
).
|
|
Group(columns.BatchNo).
|
|
OrderDesc("last_created_at").
|
|
Page(in.Page, in.PageSize).
|
|
Scan(&items)
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "查询可出库批次列表失败")
|
|
}
|
|
if err = enrichOutboundBatchBusinessFields(ctx, items); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &model.TSimCardOutboundInboundBatchListRes{
|
|
Items: items,
|
|
Total: total,
|
|
}, nil
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) ListInboundBatchCards(ctx context.Context, in *model.TSimCardOutboundInboundBatchCardsParam) (*model.TSimCardOutboundInboundBatchCardsRes, error) {
|
|
if in == nil {
|
|
in = &model.TSimCardOutboundInboundBatchCardsParam{}
|
|
}
|
|
normalizePage(&in.Page, &in.PageSize)
|
|
|
|
batchNo := strings.TrimSpace(in.BatchNo)
|
|
if batchNo == "" {
|
|
return nil, gerror.New("批次号不能为空")
|
|
}
|
|
|
|
mod := outboundStockBaseModel(ctx).Where(dao.TSimCardStock.Columns().BatchNo, batchNo)
|
|
mod = filterOutboundStockCards(mod, in.Iccid, in.Msisdn, in.Imsi)
|
|
|
|
var items []*model.TSimCardOutboundInboundBatchCardItem
|
|
total, err := mod.Clone().Count()
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "查询可出库批次卡列表失败")
|
|
}
|
|
err = mod.Fields(outboundBatchCardSelectFields()).
|
|
OrderAsc(dao.TSimCardStock.Columns().Id).
|
|
Page(in.Page, in.PageSize).
|
|
Scan(&items)
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "查询可出库批次卡列表失败")
|
|
}
|
|
if err = enrichOutboundBatchCardBusinessFields(ctx, items); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &model.TSimCardOutboundInboundBatchCardsRes{
|
|
Items: items,
|
|
Total: total,
|
|
}, nil
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) SubmitBatch(ctx context.Context, in *model.TSimCardOutboundBatchSubmitParam) (*model.TSimCardOutboundBatchSubmitRes, error) {
|
|
if in == nil {
|
|
in = &model.TSimCardOutboundBatchSubmitParam{}
|
|
}
|
|
normalizeOutboundSubmitParam(&in.TSimCardOutboundSubmitParam)
|
|
if err := s.validateBalanceBeforeSubmit(ctx, &in.TSimCardOutboundSubmitParam); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
batchNo := strings.TrimSpace(in.BatchNo)
|
|
if batchNo == "" {
|
|
return nil, gerror.New("批次号不能为空")
|
|
}
|
|
|
|
rows, err := s.buildBatchOutboundRows(ctx, batchNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := outbound.SubmitRowsAsync(ctx, in, rows, consts.BatchSourceTypeBatchSelect, batchNo, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return (*model.TSimCardOutboundBatchSubmitRes)(outboundSubmitResult(result, consts.BatchTypeOutbound)), nil
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) getUploadFile(ctx context.Context) (*ghttp.UploadFile, error) {
|
|
r := g.RequestFromCtx(ctx)
|
|
if r == nil {
|
|
return nil, gerror.New("当前上下文不是HTTP请求")
|
|
}
|
|
file := r.GetUploadFile("file")
|
|
if file == nil {
|
|
return nil, gerror.New("请选择上传文件")
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func (s *sIotTSimCardOutbound) buildBatchOutboundRows(ctx context.Context, batchNo string) ([]batchimport.Row, error) {
|
|
mod := outboundStockBaseModel(ctx).Where(dao.TSimCardStock.Columns().BatchNo, batchNo)
|
|
|
|
var cards []*model.TSimCardOutboundInboundBatchCardItem
|
|
err := mod.Fields(outboundBatchCardSelectFields()).
|
|
OrderAsc(dao.TSimCardStock.Columns().Id).
|
|
Scan(&cards)
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "查询可出库批次卡列表失败")
|
|
}
|
|
if len(cards) == 0 {
|
|
return nil, gerror.New("当前批次没有可出库库存卡")
|
|
}
|
|
|
|
rows := make([]batchimport.Row, 0, len(cards))
|
|
for index, card := range cards {
|
|
rows = append(rows, &outbound.Row{
|
|
RowNo: index + 1,
|
|
Iccid: strings.TrimSpace(card.Iccid),
|
|
Msisdn: strings.TrimSpace(card.Msisdn),
|
|
Imsi: strings.TrimSpace(card.Imsi),
|
|
SourceBatchNo: batchNo,
|
|
})
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func outboundStockBaseModel(ctx context.Context) *gdb.Model {
|
|
columns := dao.TSimCardStock.Columns()
|
|
return dao.TSimCardStock.Ctx(ctx).
|
|
Where(columns.TenantId, contexts.GetTenantId(ctx)).
|
|
Where(columns.CreatedDept, contexts.GetDeptId(ctx)).
|
|
Where(columns.BatchNo+" <> ?", "")
|
|
}
|
|
|
|
func filterOutboundStockCards(mod *gdb.Model, iccid, msisdn, imsi string) *gdb.Model {
|
|
columns := dao.TSimCardStock.Columns()
|
|
if value := strings.TrimSpace(iccid); value != "" {
|
|
mod = mod.WhereLike(columns.Iccid, "%"+value+"%")
|
|
}
|
|
if value := strings.TrimSpace(msisdn); value != "" {
|
|
mod = mod.WhereLike(columns.Msisdn, "%"+value+"%")
|
|
}
|
|
if value := strings.TrimSpace(imsi); value != "" {
|
|
mod = mod.WhereLike(columns.Imsi, "%"+value+"%")
|
|
}
|
|
return mod
|
|
}
|
|
|
|
func outboundBatchCardSelectFields() []string {
|
|
columns := dao.TSimCardStock.Columns()
|
|
return []string{
|
|
columns.Iccid,
|
|
columns.Msisdn,
|
|
columns.Imsi,
|
|
columns.Imei,
|
|
columns.ChanKey,
|
|
columns.BatchNo,
|
|
columns.CreatedAt,
|
|
}
|
|
}
|
|
|
|
func outboundSubmitResult(result *batchimport.Result, batchType int) *model.TSimCardOutboundSubmitRes {
|
|
res := &model.TSimCardOutboundSubmitRes{
|
|
BatchType: batchType,
|
|
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
|
Status: "pending",
|
|
}
|
|
if result != nil {
|
|
res.BatchNo = result.BatchNo
|
|
res.Total = result.TotalRows
|
|
res.Quantity = result.TotalRows
|
|
res.ValidCount = result.TotalRows
|
|
res.FailCount = 0
|
|
res.ResultDownloadId = result.ResultDownloadId
|
|
res.ResultFileOssId = result.ResultFileOssId
|
|
}
|
|
return res
|
|
}
|
|
|
|
func normalizeOutboundSubmitParam(in *model.TSimCardOutboundSubmitParam) {
|
|
if in == nil {
|
|
return
|
|
}
|
|
if in.DurationMonths <= 0 && in.DurationYears > 0 {
|
|
in.DurationMonths = in.DurationYears*12 + in.DurationMonthPart
|
|
}
|
|
if in.DurationMonths <= 0 && in.DurationMonthPart > 0 {
|
|
in.DurationMonths = in.DurationMonthPart
|
|
}
|
|
if in.DurationYears <= 0 && in.DurationMonthPart <= 0 && in.DurationMonths > 0 {
|
|
in.DurationYears = in.DurationMonths / 12
|
|
in.DurationMonthPart = in.DurationMonths % 12
|
|
}
|
|
if in.GiftDurationMonths <= 0 && in.GiftDurationYears > 0 {
|
|
in.GiftDurationMonths = in.GiftDurationYears*12 + in.GiftDurationMonthPart
|
|
}
|
|
if in.GiftDurationMonths <= 0 && in.GiftDurationMonthPart > 0 {
|
|
in.GiftDurationMonths = in.GiftDurationMonthPart
|
|
}
|
|
if in.GiftDurationYears <= 0 && in.GiftDurationMonthPart <= 0 && in.GiftDurationMonths > 0 {
|
|
in.GiftDurationYears = in.GiftDurationMonths / 12
|
|
in.GiftDurationMonthPart = in.GiftDurationMonths % 12
|
|
}
|
|
disableOutboundPeriodOptions(in)
|
|
if strings.TrimSpace(in.Remark) != "" {
|
|
in.BuyerNote = strings.TrimSpace(in.Remark)
|
|
in.AdminNote = strings.TrimSpace(in.Remark)
|
|
}
|
|
}
|
|
|
|
func disableOutboundPeriodOptions(in *model.TSimCardOutboundSubmitParam) {
|
|
if in == nil {
|
|
return
|
|
}
|
|
// 1 表示否。出库不再暴露测试期、沉默期配置,统一按无测试期/无沉默期保存。
|
|
in.HasTestPeriod = 1
|
|
in.TestPeriodDays = 0
|
|
in.TestPeriodLatestExpiryDate = nil
|
|
in.HasSilentPeriod = 1
|
|
in.SilentPeriodDays = 0
|
|
in.SilentPeriodLatestExpiryDate = nil
|
|
}
|
|
|
|
func normalizePage(page *int, pageSize *int) {
|
|
if page != nil && *page <= 0 {
|
|
*page = 1
|
|
}
|
|
if pageSize != nil && *pageSize <= 0 {
|
|
*pageSize = 20
|
|
}
|
|
if pageSize != nil && *pageSize > 2000 {
|
|
*pageSize = 2000
|
|
}
|
|
}
|