chore: 初始开源发布

iotlcm-app:小程序端 + 用户端服务。

- app/:uni-app 小程序前端(基于 unibest,MIT)
  - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID
  - 内网后端地址 → admin.iotlcm.example.com
  - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明)
- server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0)
  - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件)
  - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化
  - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位)
  - 依赖号码尾部的断言同步更新
  - 补 server/LICENSE(Apache-2.0)
- 移除内部资产:CI 触发器
- 补根 README,LF 行尾 + .gitattributes

已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
MiFaZhan
2026-10-26 14:01:25 +08:00
commit 2ed5dfa6a1
1013 changed files with 138466 additions and 0 deletions
@@ -0,0 +1,468 @@
package app
import (
"context"
"fmt"
"os"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility/hash"
"github.com/gogf/gf/v2/crypto/gmd5"
"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"
"github.com/xuri/excelize/v2"
)
type appAllocateBatchLogFiles struct {
ImportOssId int64
ImportFileName string
ResultOssId int64
DownloadId int64
}
func (s *sAppCard) ensureAppAllocateBatchLogFiles(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appAllocateBatchContext, existingInfo *gjson.Json, batchNo string, sourceBatchNo string, remark string, now *gtime.Time) (*appAllocateBatchLogFiles, error) {
batchNo = strings.TrimSpace(batchNo)
if batchNo == "" {
return nil, gerror.New("划拨批次号不能为空")
}
if user == nil {
return nil, gerror.New("当前用户不存在")
}
if batch == nil || batch.Target == nil {
return nil, gerror.New("划拨批次日志文件信息不完整")
}
files := appAllocateBatchLogFilesFromInfo(existingInfo)
existingDownload, err := appAllocateBatchResultDownload(ctx, tx, batchNo)
if err != nil {
return nil, err
}
if existingDownload != nil {
files.ResultOssId = existingDownload.ResultOssId
files.DownloadId = existingDownload.DownloadId
}
if files.ImportOssId <= 0 {
importContent, err := appBuildAllocateBatchImportExcel(batch, sourceBatchNo, remark)
if err != nil {
return nil, gerror.Wrap(err, "生成划拨批次源文件失败")
}
importUpload, importOssId, err := appSaveAllocateBatchOss(ctx, tx, user, user.DeptId, importContent, batchNo+"-import.xlsx", consts.SimCardAllocateSubDirImportExcel, now)
if err != nil {
return nil, gerror.Wrap(err, "保存划拨批次源文件失败")
}
files.ImportOssId = importOssId
files.ImportFileName = strings.TrimSpace(importUpload.Name)
}
if strings.TrimSpace(files.ImportFileName) == "" {
files.ImportFileName = appAllocateBatchImportFileName(ctx, tx, files.ImportOssId, batchNo+"-import.xlsx")
}
if existingDownload != nil {
return files, nil
}
resultContent, err := appBuildAllocateBatchResultExcel(batch, files, batchNo, sourceBatchNo, now, user.UserId, 0)
if err != nil {
return nil, gerror.Wrap(err, "生成划拨批次结果文件失败")
}
resultUpload, resultOssId, err := appSaveAllocateBatchOss(ctx, tx, user, user.DeptId, resultContent, batchNo+"-result.xlsx", consts.SimCardAllocateSubDirResultExcel, now)
if err != nil {
return nil, gerror.Wrap(err, "保存划拨批次结果文件失败")
}
files.ResultOssId = resultOssId
if err = appFillAllocateResultOssId(ctx, tx, resultOssId, resultUpload.Path, user.UserId, now); err != nil {
return nil, err
}
refreshedResult, err := appLoadAllocateOssUpload(ctx, tx, resultOssId, resultUpload)
if err != nil {
return nil, err
}
quantity := len(batch.Cards)
downloadId, err := dao.TResourceBatchDownload.Ctx(ctx).TX(tx).Data(do.TResourceBatchDownload{
BatchNo: batchNo,
TaskName: consts.ResourceBatchDownloadTaskSimCardAllocate,
ResourceType: consts.ResourceTypeExcel,
ExportScene: consts.ResourceExportSceneAllocate,
TotalCount: quantity,
SuccessCount: quantity,
FailCount: 0,
DownloadStatus: consts.DownloadStatusGenerated,
OssId: resultOssId,
FileName: refreshedResult.Name,
FileExt: refreshedResult.Ext,
FileUrl: refreshedResult.FullPath,
FilePath: refreshedResult.Path,
FileSize: refreshedResult.Size,
FileMd5: refreshedResult.Md5,
DownloadCount: 0,
MaxDownloadCount: 0,
DeptId: user.DeptId,
StartTime: now,
FinishTime: now,
TenantId: user.TenantId,
CreatedDept: user.DeptId,
CreatedBy: user.UserId,
CreatedAt: now,
UpdatedBy: user.UserId,
UpdatedAt: now,
}).InsertAndGetId()
if err != nil {
return nil, gerror.Wrap(err, "写入划拨批次结果下载记录失败")
}
files.DownloadId = downloadId
return files, nil
}
func appAllocateBatchLogFilesFromInfo(info *gjson.Json) *appAllocateBatchLogFiles {
files := &appAllocateBatchLogFiles{}
if info == nil {
return files
}
files.ImportOssId = info.Get("importFileOssId").Int64()
files.ResultOssId = info.Get("resultFileOssId").Int64()
files.DownloadId = info.Get("resultDownloadId").Int64()
return files
}
func appAllocateBatchResultDownload(ctx context.Context, tx gdb.TX, batchNo string) (*appAllocateBatchLogFiles, error) {
var downloads []*entity.TResourceBatchDownload
columns := dao.TResourceBatchDownload.Columns()
err := dao.TResourceBatchDownload.Ctx(ctx).TX(tx).
Fields(columns.Id, columns.OssId).
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
OrderDesc(columns.Id).
Limit(1).
Scan(&downloads)
if err != nil {
return nil, gerror.Wrap(err, "查询划拨批次结果下载记录失败")
}
if len(downloads) == 0 || downloads[0] == nil || downloads[0].Id == 0 {
return nil, nil
}
return &appAllocateBatchLogFiles{
DownloadId: downloads[0].Id,
ResultOssId: downloads[0].OssId,
}, nil
}
func appAllocateBatchInfoWithLogFiles(info *gjson.Json, files *appAllocateBatchLogFiles) *gjson.Json {
data := map[string]any{}
if info != nil {
for key, value := range info.Map() {
data[key] = value
}
}
if files != nil {
if files.ImportOssId > 0 {
data["importFileOssId"] = files.ImportOssId
}
if files.ResultOssId > 0 {
data["resultFileOssId"] = files.ResultOssId
}
if files.DownloadId > 0 {
data["resultDownloadId"] = files.DownloadId
}
}
return gjson.New(data)
}
func appBuildAllocateBatchImportExcel(batch *appAllocateBatchContext, sourceBatchNo string, remark string) ([]byte, error) {
f := excelize.NewFile()
defer f.Close()
sheet := "Sheet1"
headers := []string{"ICCID", "MSISDN", "IMSI", "sourceBatchNo", "sourceDeptId", "targetDeptId", "targetDeptName", "remark"}
if err := f.SetSheetRow(sheet, "A1", &headers); err != nil {
return nil, err
}
if err := appApplyHeaderColumnsTextStyle(f, sheet); err != nil {
return nil, err
}
for index, item := range batch.Cards {
row := []interface{}{
strings.TrimSpace(item.Card.Iccid),
strings.TrimSpace(item.Card.Msisdn),
strings.TrimSpace(item.Card.Imsi),
strings.TrimSpace(sourceBatchNo),
item.SourceDeptId,
batch.Target.DeptId,
strings.TrimSpace(batch.Target.DeptName),
strings.TrimSpace(remark),
}
cell, err := excelize.CoordinatesToCellName(1, index+2)
if err != nil {
return nil, err
}
if err = f.SetSheetRow(sheet, cell, &row); err != nil {
return nil, err
}
}
_ = f.SetColWidth(sheet, "A", "A", 24)
_ = f.SetColWidth(sheet, "B", "C", 18)
_ = f.SetColWidth(sheet, "D", "H", 18)
return appExcelBytes(f)
}
func appBuildAllocateBatchResultExcel(batch *appAllocateBatchContext, files *appAllocateBatchLogFiles, batchNo string, sourceBatchNo string, now *gtime.Time, userId int64, resultOssId int64) ([]byte, error) {
f := excelize.NewFile()
defer f.Close()
leftStyleID, err := f.NewStyle(&excelize.Style{
Alignment: &excelize.Alignment{
Horizontal: "left",
Vertical: "center",
},
})
if err != nil {
return nil, err
}
resultSheet := "导入结果"
if err = f.SetSheetName("Sheet1", resultSheet); err != nil {
return nil, err
}
headers := []string{"源文件行号", "item_id", "处理状态", "异常原因"}
if err = f.SetSheetRow(resultSheet, "A1", &headers); err != nil {
return nil, err
}
for index, item := range batch.Cards {
row := []interface{}{
item.RowNo,
strings.TrimSpace(item.Card.Iccid),
"成功",
"",
}
cell, err := excelize.CoordinatesToCellName(1, index+2)
if err != nil {
return nil, err
}
if err = f.SetSheetRow(resultSheet, cell, &row); err != nil {
return nil, err
}
}
resultEndCell, err := excelize.CoordinatesToCellName(4, len(batch.Cards)+1)
if err != nil {
return nil, err
}
if err = f.SetCellStyle(resultSheet, "A1", resultEndCell, leftStyleID); err != nil {
return nil, err
}
infoSheet := "批次信息"
if _, err = f.NewSheet(infoSheet); err != nil {
return nil, err
}
infoHeaders := []string{"字段", "值"}
if err = f.SetSheetRow(infoSheet, "A1", &infoHeaders); err != nil {
return nil, err
}
infoRows := appAllocateBatchResultInfoRows(batch, files, batchNo, sourceBatchNo, now, userId, resultOssId)
for index, item := range infoRows {
row := []interface{}{item[0], item[1]}
cell, err := excelize.CoordinatesToCellName(1, index+2)
if err != nil {
return nil, err
}
if err = f.SetSheetRow(infoSheet, cell, &row); err != nil {
return nil, err
}
}
infoEndCell, err := excelize.CoordinatesToCellName(2, len(infoRows)+1)
if err != nil {
return nil, err
}
if err = f.SetCellStyle(infoSheet, "A1", infoEndCell, leftStyleID); err != nil {
return nil, err
}
_ = f.SetColWidth(resultSheet, "A", "A", 14)
_ = f.SetColWidth(resultSheet, "B", "B", 24)
_ = f.SetColWidth(resultSheet, "C", "C", 12)
_ = f.SetColWidth(resultSheet, "D", "D", 40)
_ = f.SetColWidth(infoSheet, "A", "B", 24)
f.SetActiveSheet(0)
return appExcelBytes(f)
}
func appAllocateBatchResultInfoRows(batch *appAllocateBatchContext, files *appAllocateBatchLogFiles, batchNo string, sourceBatchNo string, now *gtime.Time, userId int64, resultOssId int64) [][2]string {
importFileName := ""
importOssId := ""
if files != nil {
importFileName = strings.TrimSpace(files.ImportFileName)
if files.ImportOssId > 0 {
importOssId = fmt.Sprintf("%d", files.ImportOssId)
}
}
if importFileName == "" {
importFileName = strings.TrimSpace(batchNo) + "-import.xlsx"
}
resultFileOssId := ""
if resultOssId > 0 {
resultFileOssId = fmt.Sprintf("%d", resultOssId)
}
return [][2]string{
{"批次号", strings.TrimSpace(batchNo)},
{"批次类型", "卡划拨"},
{"处理结果", "成功"},
{"总行数", fmt.Sprintf("%d", len(batch.Cards))},
{"校验通过行数", fmt.Sprintf("%d", len(batch.Cards))},
{"失败行数", "0"},
{"实际写入行数", fmt.Sprintf("%d", len(batch.Cards))},
{"源批次号", strings.TrimSpace(sourceBatchNo)},
{"目标机构", fmt.Sprintf("%d %s", batch.Target.DeptId, strings.TrimSpace(batch.Target.DeptName))},
{"原始文件名", importFileName},
{"原始文件OSS ID", importOssId},
{"结果文件OSS ID", resultFileOssId},
{"操作人", fmt.Sprintf("%d", userId)},
{"开始时间", appRenewFormatTime(now)},
{"结束时间", appRenewFormatTime(now)},
{"备注", "导入成功"},
}
}
func appSaveAllocateBatchOss(ctx context.Context, tx gdb.TX, user *entity.SysUser, deptId int64, content []byte, fileName string, subDirName string, now *gtime.Time) (model.UploadResponse, int64, error) {
upload, err := service.SysOss().SaveContentLocal(ctx, content, consts.SysUploadNewFileTypeUserFile, fileName, subDirName, 0)
if err != nil {
return upload, 0, err
}
ossId, err := dao.SysOss.Ctx(ctx).TX(tx).Data(do.SysOss{
TenantId: user.TenantId,
FileName: upload.Name,
OriginalName: upload.OriginalName,
FileSuffix: upload.Ext,
Path: upload.Path,
Url: upload.FullPath,
CreatedDept: deptId,
CreatedAt: now,
CreatedBy: user.UserId,
UpdatedAt: now,
UpdatedBy: user.UserId,
Service: "local",
Md5: upload.Md5,
FileSize: upload.Size,
FileCrc16: upload.Crc16,
FileSum: upload.Sum16,
}).InsertAndGetId()
if err != nil {
return upload, 0, gerror.Wrap(err, "写入划拨批次OSS记录失败")
}
return upload, ossId, nil
}
func appFillAllocateResultOssId(ctx context.Context, tx gdb.TX, ossId int64, filePath string, userId int64, now *gtime.Time) error {
if ossId <= 0 || strings.TrimSpace(filePath) == "" {
return gerror.New("划拨结果文件OSS信息不完整")
}
f, err := excelize.OpenFile(filePath)
if err != nil {
return gerror.Wrap(err, "打开划拨结果文件失败")
}
defer f.Close()
rows, err := f.GetRows("批次信息")
if err != nil {
return gerror.Wrap(err, "读取划拨结果批次信息失败")
}
found := false
for index, row := range rows {
if len(row) == 0 || strings.TrimSpace(row[0]) != "结果文件OSS ID" {
continue
}
cell, err := excelize.CoordinatesToCellName(2, index+1)
if err != nil {
return err
}
if err = f.SetCellStr("批次信息", cell, fmt.Sprintf("%d", ossId)); err != nil {
return err
}
found = true
break
}
if !found {
return gerror.New("划拨结果文件缺少结果文件OSS ID字段")
}
if err = f.SaveAs(filePath); err != nil {
return gerror.Wrap(err, "回写划拨结果文件OSS ID失败")
}
return appRefreshAllocateOssFileInfo(ctx, tx, ossId, filePath, userId, now)
}
func appRefreshAllocateOssFileInfo(ctx context.Context, tx gdb.TX, ossId int64, filePath string, userId int64, now *gtime.Time) error {
fileInfo, err := os.Stat(filePath)
if err != nil {
return gerror.Wrap(err, "读取划拨结果文件信息失败")
}
md5, _ := gmd5.EncryptFile(filePath)
crc16 := hash.Crc16ChecksumFile(filePath)
sum16 := hash.Sum16ChecksumFile(filePath)
_, err = dao.SysOss.Ctx(ctx).TX(tx).
Where(dao.SysOss.Columns().OssId, ossId).
Data(do.SysOss{
Md5: md5,
FileSize: fileInfo.Size(),
FileCrc16: crc16,
FileSum: sum16,
UpdatedAt: now,
UpdatedBy: userId,
}).
Update()
if err != nil {
return gerror.Wrap(err, "更新划拨结果文件信息失败")
}
return nil
}
func appLoadAllocateOssUpload(ctx context.Context, tx gdb.TX, ossId int64, fallback model.UploadResponse) (model.UploadResponse, error) {
if ossId <= 0 {
return fallback, nil
}
var oss *entity.SysOss
err := dao.SysOss.Ctx(ctx).TX(tx).
Where(dao.SysOss.Columns().OssId, ossId).
Limit(1).
Scan(&oss)
if err != nil {
return fallback, gerror.Wrap(err, "读取划拨结果文件OSS记录失败")
}
if oss == nil || oss.OssId == 0 {
return fallback, nil
}
fallback.Name = oss.FileName
fallback.OriginalName = oss.OriginalName
fallback.Ext = oss.FileSuffix
fallback.Path = oss.Path
fallback.FullPath = oss.Url
fallback.Size = int64(oss.FileSize)
fallback.Md5 = oss.Md5
return fallback, nil
}
func appAllocateBatchImportFileName(ctx context.Context, tx gdb.TX, ossId int64, sourceName string) string {
sourceName = strings.TrimSpace(sourceName)
if ossId <= 0 {
return sourceName
}
var oss *entity.SysOss
if err := dao.SysOss.Ctx(ctx).TX(tx).
Fields(dao.SysOss.Columns().FileName).
Where(dao.SysOss.Columns().OssId, ossId).
Limit(1).
Scan(&oss); err != nil {
return sourceName
}
if oss != nil && strings.TrimSpace(oss.FileName) != "" {
return strings.TrimSpace(oss.FileName)
}
return sourceName
}