sync: 对齐小程序 v1.0.2 生产代码并脱敏

This commit is contained in:
MiFaZhan
2026-10-29 15:15:12 +08:00
parent a7abd476f0
commit 5fcf74deb0
423 changed files with 15001 additions and 6382 deletions
@@ -0,0 +1,144 @@
package allocatedispatch
import (
"context"
"strings"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"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/library/batchmetadata"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/rabbitmq"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
)
type message struct {
BatchNo string `json:"batchNo"`
BatchType int `json:"batchType"`
}
func Start(ctx context.Context) context.CancelFunc {
ctx, cancel := context.WithCancel(ctx)
if !g.Cfg().MustGet(ctx, "workerBatchAsync.enabled", false).Bool() || !g.Cfg().MustGet(ctx, "workerBatchAsync.allocateRecovery.enabled", true).Bool() {
return cancel
}
interval := g.Cfg().MustGet(ctx, "workerBatchAsync.allocateRecovery.intervalSeconds", 60).Int()
if interval < 30 {
interval = 30
}
go func() {
ticker := time.NewTicker(time.Duration(interval) * time.Second)
defer ticker.Stop()
var cursor int64
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
next, err := recoverPage(ctx, cursor, publish)
if err != nil {
g.Log().Errorf(ctx, "划拨任务补投失败: %+v", err)
} else {
cursor = next
}
}
}
}()
return cancel
}
func due(batch *entity.BatchMain, now time.Time, processingDelay time.Duration) bool {
if batch == nil || batch.BatchType != consts.BatchTypeAllocate || batch.SourceChannel != batchmetadata.MiniProgram {
return false
}
delay := time.Minute
switch batch.BatchAsyncStatus {
case consts.BatchAsyncStatusPending:
case consts.BatchAsyncStatusProcessing:
delay = processingDelay
default:
return false
}
timestamp := batch.UpdatedAt
if batch.BatchAsyncStatus == consts.BatchAsyncStatusProcessing && batch.StartTime != nil && !batch.StartTime.IsZero() {
timestamp = batch.StartTime
}
if timestamp == nil || timestamp.IsZero() {
timestamp = batch.CreatedAt
}
if timestamp == nil || timestamp.IsZero() {
return false
}
return !timestamp.Time.Add(delay).After(now)
}
func recoverPage(ctx context.Context, cursor int64, send func(context.Context, string, string) error) (int64, error) {
if ctx.Err() != nil {
return cursor, ctx.Err()
}
columns := dao.BatchMain.Columns()
var parents []*entity.BatchMain
err := dao.BatchMain.Ctx(ctx).Where(columns.Id+" > ?", cursor).
Where(columns.BatchType, consts.BatchTypeAllocate).Where(columns.SourceChannel, batchmetadata.MiniProgram).
Where("COALESCE(source_batch_no, '') = ''").
WhereIn(columns.BatchAsyncStatus, []int{consts.BatchAsyncStatusPending, consts.BatchAsyncStatusProcessing}).
OrderAsc(columns.Id).Limit(50).Scan(&parents)
if err != nil {
return cursor, err
}
if len(parents) == 0 {
return 0, nil
}
minutes := g.Cfg().MustGet(ctx, "workerBatchAsync.allocateRecovery.processingRetryMinutes", 30).Int()
if minutes < 30 {
minutes = 30
}
delay := time.Duration(minutes) * time.Minute
queue := strings.TrimSpace(g.Cfg().MustGet(ctx, "workerBatchAsync.queueName", "iotlcm.batch.execute").String())
if queue == "" {
queue = "iotlcm.batch.execute"
}
for _, parent := range parents {
if ctx.Err() != nil {
return cursor, ctx.Err()
}
cursor = parent.Id
if parent.BatchInfo == nil || parent.BatchInfo.Get("jobRole").String() != "allocate_parent" || !due(parent, time.Now(), delay) {
continue
}
var children []*entity.BatchMain
err = dao.BatchMain.Ctx(ctx).Where(columns.SourceBatchNo, parent.BatchNo).
Where(columns.BatchType, consts.BatchTypeAllocate).Where(columns.TenantId, parent.TenantId).
Where(columns.CreatedBy, parent.CreatedBy).Where(columns.SourceChannel, batchmetadata.MiniProgram).Scan(&children)
if err != nil {
return cursor, err
}
for _, child := range children {
if child.BatchInfo == nil || child.BatchInfo.Get("jobRole").String() != "allocate_chunk" || !due(child, time.Now(), delay) {
continue
}
if err := send(ctx, queue, child.BatchNo); err != nil {
g.Log().Errorf(ctx, "划拨子任务补投失败 batchNo:%s err:%+v", child.BatchNo, err)
}
}
if err := send(ctx, queue, parent.BatchNo); err != nil {
g.Log().Errorf(ctx, "划拨父任务补投失败 batchNo:%s err:%+v", parent.BatchNo, err)
}
}
if len(parents) < 50 {
return 0, nil
}
return cursor, nil
}
func publish(ctx context.Context, queue, batchNo string) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
err := rabbitmq.Publish(ctx, queue, message{BatchNo: batchNo, BatchType: consts.BatchTypeAllocate})
if err == nil {
g.Log().Infof(ctx, "划拨持久任务补投 batchNo:%s time:%s", batchNo, gtime.Now())
}
return err
}
@@ -0,0 +1,77 @@
package allocatedispatch
import (
"context"
"errors"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"testing"
"time"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
)
func TestDue(t *testing.T) {
now := time.Now()
for _, test := range []struct {
status int
age time.Duration
due bool
}{{1, 59 * time.Second, false}, {1, 2 * time.Minute, true}, {2, 29 * time.Minute, false}, {2, 31 * time.Minute, true}, {3, time.Hour, false}, {4, time.Hour, false}, {5, time.Hour, false}} {
batch := &entity.BatchMain{BatchType: 3, SourceChannel: "mini_program", BatchAsyncStatus: test.status, UpdatedAt: gtime.New(now.Add(-test.age))}
if due(batch, now, 30*time.Minute) != test.due {
t.Fatal(test)
}
}
if due(&entity.BatchMain{BatchType: 3, SourceChannel: "console", BatchAsyncStatus: 1, UpdatedAt: gtime.New(now.Add(-time.Hour))}, now, 30*time.Minute) {
t.Fatal("foreign channel replayed")
}
if due(&entity.BatchMain{BatchType: 3, SourceChannel: "mini_program", BatchAsyncStatus: 1}, now, 30*time.Minute) {
t.Fatal("missing time replayed")
}
}
func TestRecoveryPersistsFailureAndIsolatesParents(t *testing.T) {
ctx := context.Background()
if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = g.DB().Close(ctx) })
for _, sql := range []string{
`CREATE TABLE batch_main (id INTEGER PRIMARY KEY,batch_no TEXT,batch_type INTEGER,source_batch_no TEXT,source_channel TEXT,batch_async_status INTEGER,tenant_id TEXT,created_by INTEGER,batch_info TEXT,created_at TEXT,updated_at TEXT,start_time TEXT)`,
`INSERT INTO batch_main VALUES (1,'PARENT',3,'','mini_program',1,'A',7,'{"jobRole":"allocate_parent"}','2020-01-01','2020-01-01',NULL), (2,'CHILD',3,'PARENT','mini_program',1,'A',7,'{"jobRole":"allocate_chunk"}','2020-01-01','2020-01-01',NULL), (3,'FOREIGN',3,'PARENT','mini_program',1,'B',7,'{"jobRole":"allocate_chunk"}','2020-01-01','2020-01-01',NULL), (4,'DONE',3,'','mini_program',3,'A',7,'{"jobRole":"allocate_parent"}','2020-01-01','2020-01-01',NULL), (5,'OTHER',101,'','mini_program',1,'A',7,'{}','2020-01-01','2020-01-01',NULL), (6,'CONSOLE',3,'','console',1,'A',7,'{"jobRole":"allocate_parent"}','2020-01-01','2020-01-01',NULL)`,
} {
if _, err := g.DB().Exec(ctx, sql); err != nil {
t.Fatal(err)
}
}
for _, fail := range []bool{true, false} {
var calls []string
cursor, err := recoverPage(ctx, 0, func(ctx context.Context, queue, batchNo string) error {
calls = append(calls, batchNo)
if queue != "iotlcm.batch.execute" {
t.Fatal(queue)
}
if fail {
return errors.New("queue offline")
}
return nil
})
if err != nil || cursor != 0 || len(calls) != 2 || calls[0] != "CHILD" || calls[1] != "PARENT" {
t.Fatal(calls, cursor, err)
}
}
if _, err := g.DB().Exec(ctx, `UPDATE batch_main SET batch_async_status=3 WHERE batch_no='PARENT'`); err != nil {
t.Fatal(err)
}
_, err := recoverPage(ctx, 0, func(context.Context, string, string) error { t.Fatal("terminal parent replayed"); return nil })
if err != nil {
t.Fatal(err)
}
cancelled, cancel := context.WithCancel(ctx)
cancel()
if _, err := recoverPage(cancelled, 0, func(context.Context, string, string) error { t.Fatal("cancelled replay"); return nil }); err == nil {
t.Fatal("cancel ignored")
}
}
+6 -1
View File
@@ -19,6 +19,7 @@ type (
SendSms(ctx context.Context, param *model.AppSendSmsParam) (res *model.AppSendSmsModel, err error)
PhoneRegister(ctx context.Context, param *model.AppPhoneRegisterParam) (res *model.AppAuthTokenModel, err error)
PhoneLogin(ctx context.Context, param *model.AppPhoneLoginParam) (res *model.AppAuthTokenModel, err error)
PhoneSmsLogin(ctx context.Context, param *model.AppPhoneSmsLoginParam) (res *model.AppAuthTokenModel, err error)
WechatLogin(ctx context.Context, param *model.AppWechatLoginParam) (res *model.AppWechatLoginModel, err error)
WechatBindPhone(ctx context.Context, param *model.AppWechatBindPhoneParam) (res *model.AppWechatBindPhoneModel, err error)
BindWechatOpenIdByLoginCode(ctx context.Context, user *entity.SysUser, wxCode string) error
@@ -79,7 +80,10 @@ type (
RecognizeCard(ctx context.Context) (res *model.AppCardRecognitionModel, err error)
}
IAppUser interface {
CustomerList(ctx context.Context, param *model.AppCustomerListParam) (items []*model.AppCustomerModel, total int, err error)
WarningPreference(ctx context.Context) (*model.AppWarningPreferenceModel, error)
SaveWarningPreference(ctx context.Context, param *model.AppWarningPreferenceParam) (*model.AppWarningPreferenceModel, error)
UnbindWechat(ctx context.Context) (*model.AppWechatBindModel, error)
CustomerList(ctx context.Context, param *model.AppCustomerListParam) (items []*model.AppCustomerModel, total int, summary *model.AppCustomerSummary, err error)
CustomerDetail(ctx context.Context, param *model.AppCustomerDetailParam) (*model.AppCustomerDetailModel, error)
Info(ctx context.Context) (res *model.AppUserInfoModel, err error)
UpdateInfo(ctx context.Context, param *model.AppUpdateUserInfoParam) (res *model.AppUserInfoModel, err error)
@@ -103,6 +107,7 @@ type (
ConfigBatchNotify(ctx context.Context, param *model.AppWarningConfigBatchNotifyParam) (res *model.AppWarningConfigBatchNotifyModel, err error)
}
IAppWorkOrder interface {
Action(ctx context.Context, param *model.AppWorkOrderActionParam) error
Upload(ctx context.Context) (output *model.SysOssUploadModel, err error)
List(ctx context.Context, param *model.AppWorkOrderListParam) (items []*model.AppWorkOrderModel, total int, err error)
Detail(ctx context.Context, param *model.AppWorkOrderDetailParam) (res *model.AppWorkOrderModel, err error)
@@ -0,0 +1,303 @@
package executionlog
import (
"context"
"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/util/gconv"
"regexp"
"slices"
"strings"
"unicode/utf8"
"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/library/allocationunits"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchmetadata"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
)
// 办理结果只允许发起人查看;通用批次日志沿用控制台的数据范围。
type ownerOnlyKey struct{}
func scope(ctx context.Context) (*gdb.Model, error) {
if contexts.GetUserId(ctx) <= 0 || strings.TrimSpace(contexts.GetTenantId(ctx)) == "" {
return nil, gerror.New("请先登录")
}
return scopedModel(ctx, dao.BatchMain.Ctx(ctx))
}
func scopedModel(ctx context.Context, mod *gdb.Model) (*gdb.Model, error) {
user, tenant := contexts.GetUserId(ctx), strings.TrimSpace(contexts.GetTenantId(ctx))
if user <= 0 || tenant == "" {
return nil, gerror.New("请先登录")
}
mod = mod.Where("tenant_id", tenant)
if ownerOnly, _ := ctx.Value(ownerOnlyKey{}).(bool); ownerOnly {
return mod.Where("created_by", user), nil
}
if contexts.IsSuperAdmin(ctx) {
return mod, nil
}
// 菜单范围由认证中间件读取当前数据库后写入,优先于登录时的角色快照。
roleId := gconv.Int64(contexts.GetDataValue(ctx, "currentRoleId"))
dataScope := gconv.String(contexts.GetDataValue(ctx, "currentRoleDataScope"))
if roleId > 0 && dataScope != "" && len(contexts.GetRoles(ctx)) == 0 {
identity := *contexts.GetUser(ctx)
identity.Roles = []model.Role{{RoleId: roleId, DataScope: dataScope}}
copied := *contexts.Get(ctx)
copied.User = &identity
ctx = contexts.Set(ctx, &copied)
mod = mod.Ctx(ctx)
}
if len(contexts.GetRoles(ctx)) == 0 {
return mod.Where("1 = 0"), nil
}
validScopes := []string{
consts.SysRoleDataScopeAll, consts.SysRoleDataScopeCustom,
consts.SysRoleDataScopeDept, consts.SysRoleDataScopeDeptAndBelow,
consts.SysRoleDataScopePersonal, consts.SysRoleDataScopeDeptAndBelowOrPersonal,
consts.SysRoleDataScopeOrgAndNextLevel, consts.SysRoleDataScopeOrgNextLevel,
}
if roleId > 0 && dataScope != "" {
if !slices.Contains(validScopes, dataScope) {
return mod.Where("1 = 0"), nil
}
} else {
for _, role := range contexts.GetRoles(ctx) {
if !slices.Contains(validScopes, role.DataScope) {
return mod.Where("1 = 0"), nil
}
}
}
// 先生成创建部门/创建人的过滤条件,再选择返回字段,避免 DTO 缺少权限字段导致漏过滤。
return handler.FilterAuth(mod.Fields("created_dept", "created_by")), nil
}
func dbError(ctx context.Context, err error) error {
g.Log().Error(ctx, "execution log query failed", err)
return gerror.New("执行日志查询失败,请稍后重试")
}
func paging(page, size int) (int, int) {
if page < 1 {
page = 1
}
if size < 1 {
size = 20
}
if size > 100 {
size = 100
}
return page, size
}
func normalize(item *model.AppExecutionLogItem) {
item.SourceChannel = batchmetadata.Channel(item.SourceChannel)
if !batchmetadata.ValidMode(item.ExecutionMode) {
item.ExecutionMode = batchmetadata.Unknown
}
item.FailReason = safeReason(item.FailReason)
}
// Do not expose internal network addresses, credentials or stack traces to mobile clients.
var publicErrorCode = regexp.MustCompile("^[A-Za-z0-9_.-]{1,64}$")
func safeReason(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
lower := strings.ToLower(value)
for _, marker := range []string{"token", "secret", "password", "authorization", "api_key", "apikey", "http:", "https:", "rpc", "sql", "tcp", "panic", "goroutine", "stack", "/src/", "/app/", "dial ", "127.0.", "192." + "168.", "10.", "172.", "sk-", "mysql", "redis"} {
if strings.Contains(lower, marker) {
return "执行失败,请凭批次号联系客服核查"
}
}
if strings.ContainsAny(value, "\r\n") || utf8.RuneCountInString(value) > 180 {
return "执行失败,请凭批次号联系客服核查"
}
return value
}
func List(ctx context.Context, in *model.AppExecutionLogListParam) ([]*model.AppExecutionLogItem, int, error) {
mod, err := scope(ctx)
if err != nil {
return nil, 0, err
}
if in == nil {
in = &model.AppExecutionLogListParam{}
}
c := dao.BatchMain.Columns()
mod = mod.Fields(model.AppExecutionLogItem{})
if n := strings.TrimSpace(in.BatchNo); n != "" {
mod = mod.Where(c.BatchNo, n)
}
if in.SourceChannel != "" {
mod = mod.Where(c.SourceChannel, in.SourceChannel)
}
if in.ExecutionMode != nil {
mod = mod.Where(c.ExecutionMode, *in.ExecutionMode)
}
if in.SourceType != nil {
mod = mod.Where(c.SourceType, *in.SourceType)
}
if in.BatchAsyncStatus != nil {
mod = mod.Where(c.BatchAsyncStatus, *in.BatchAsyncStatus)
}
// Split allocation children are execution details, not a second user submission.
// Allocation children are execution details of the parent submission. The
// source_batch_no relation is authoritative; batch_info is only descriptive.
mod = mod.Where("NOT (batch_type = ? AND COALESCE(source_batch_no, '') <> '' AND source_batch_no <> batch_no)", consts.BatchTypeAllocate)
page, size := paging(in.Page, in.PageSize)
items := make([]*model.AppExecutionLogItem, 0)
total := 0
if err = mod.Page(page, size).OrderDesc(c.Id).ScanAndCount(&items, &total, false); err != nil {
return nil, 0, dbError(ctx, err)
}
unitFields := []*allocationunits.Fields{}
for _, item := range items {
normalize(item)
if item.BatchType == consts.BatchTypeAllocate {
item.Fields = allocationunits.Resolve(item.UnitInfo, item.StoredSourceDeptId, item.StoredTargetDeptId)
unitFields = append(unitFields, &item.Fields)
}
}
if err := allocationunits.Fill(ctx, contexts.GetTenantId(ctx), unitFields); err != nil {
return nil, 0, dbError(ctx, err)
}
return items, total, nil
}
func ensureAllocateParent(ctx context.Context, batchNo string) error {
mod, err := scope(ctx)
if err != nil {
return err
}
columns := dao.BatchMain.Columns()
count, err := mod.Where(columns.BatchNo, strings.TrimSpace(batchNo)).Where("(batch_type = ? OR (batch_type = ? AND JSON_EXTRACT(batch_info, '$.targetBatchType') = ?))", consts.BatchTypeAllocate, consts.BatchTypePrepare, consts.BatchTypeAllocate).Where("COALESCE(source_batch_no, '') = ''").Count()
if err != nil {
return dbError(ctx, err)
}
if count != 1 {
return gerror.New("划拨批次不存在或无权查看")
}
return nil
}
func AllocateSummary(ctx context.Context, batchNo string) (*model.AppExecutionLogItem, error) {
ctx = context.WithValue(ctx, ownerOnlyKey{}, true)
if err := ensureAllocateParent(ctx, batchNo); err != nil {
return nil, err
}
return Summary(ctx, batchNo)
}
func AllocateDetails(ctx context.Context, in *model.AppExecutionLogDetailParam) ([]*model.AppExecutionLogDetail, int, error) {
ctx = context.WithValue(ctx, ownerOnlyKey{}, true)
if in == nil {
return nil, 0, gerror.New("批次号不能为空")
}
if err := ensureAllocateParent(ctx, in.BatchNo); err != nil {
return nil, 0, err
}
return Details(ctx, in)
}
func Summary(ctx context.Context, batchNo string) (*model.AppExecutionLogItem, error) {
mod, err := scope(ctx)
if err != nil {
return nil, err
}
if strings.TrimSpace(batchNo) == "" {
return nil, gerror.New("批次号不能为空")
}
var item *model.AppExecutionLogItem
if err = mod.Fields(model.AppExecutionLogItem{}).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
Where("NOT (batch_type = ? AND COALESCE(source_batch_no, '') <> '' AND source_batch_no <> batch_no)", consts.BatchTypeAllocate).
Scan(&item); err != nil {
return nil, dbError(ctx, err)
}
if item == nil {
return nil, gerror.New("执行日志不存在或无权查看")
}
normalize(item)
if item.BatchType == consts.BatchTypeAllocate {
item.Fields = allocationunits.Resolve(item.UnitInfo, item.StoredSourceDeptId, item.StoredTargetDeptId)
if err := allocationunits.Fill(ctx, contexts.GetTenantId(ctx), []*allocationunits.Fields{&item.Fields}); err != nil {
return nil, dbError(ctx, err)
}
}
return item, nil
}
func Details(ctx context.Context, in *model.AppExecutionLogDetailParam) ([]*model.AppExecutionLogDetail, int, error) {
if in == nil {
return nil, 0, gerror.New("批次号不能为空")
}
parent, err := Summary(ctx, in.BatchNo)
if err != nil {
return nil, 0, err
}
c := dao.BatchDetail.Columns()
mod, err := scopedModel(ctx, dao.BatchDetail.Ctx(ctx))
if err != nil {
return nil, 0, err
}
mod = mod.Fields(model.AppExecutionLogDetail{})
if parent.BatchType == consts.BatchTypeAllocate {
// 先按当前菜单的数据范围解析子批次,再用批次号索引查询明细。
var children []struct {
BatchNo string `orm:"batch_no"`
}
mainColumns := dao.BatchMain.Columns()
childrenMod, err := scope(ctx)
if err != nil {
return nil, 0, err
}
if err := childrenMod.Fields(mainColumns.BatchNo).
Where(mainColumns.SourceBatchNo, in.BatchNo).Where(mainColumns.BatchType, parent.BatchType).
Where(mainColumns.BatchNo+" <> ?", in.BatchNo).Scan(&children); err != nil {
return nil, 0, dbError(ctx, err)
}
batchNos := []string{in.BatchNo}
for _, child := range children {
batchNos = append(batchNos, child.BatchNo)
}
mod = mod.WhereIn(c.BatchNo, batchNos)
} else {
mod = mod.Where(c.BatchNo, in.BatchNo)
}
if in.DetailStatus != nil {
mod = mod.Where(c.DetailStatus, *in.DetailStatus)
}
page, size := paging(in.Page, in.PageSize)
items := make([]*model.AppExecutionLogDetail, 0)
total := 0
if err = mod.OrderAsc(c.Id).Page(page, size).ScanAndCount(&items, &total, false); err != nil {
return nil, 0, dbError(ctx, err)
}
unitFields := []*allocationunits.Fields{}
for index, item := range items {
if parent.BatchType == consts.BatchTypeAllocate {
item.Fields = allocationunits.Resolve(item.UnitInfo, parent.SourceDeptId, parent.TargetDeptId)
if item.SourceDeptName == "" && item.SourceDeptId == parent.SourceDeptId {
item.SourceDeptName = parent.SourceDeptName
}
if item.TargetDeptName == "" && item.TargetDeptId == parent.TargetDeptId {
item.TargetDeptName = parent.TargetDeptName
}
unitFields = append(unitFields, &item.Fields)
}
item.RowNo = (page-1)*size + index + 1
item.ErrorReason = safeReason(item.ErrorReason)
if item.ErrorCode != "" && !publicErrorCode.MatchString(item.ErrorCode) {
item.ErrorCode = "EXECUTION_FAILED"
}
}
if err := allocationunits.Fill(ctx, contexts.GetTenantId(ctx), unitFields); err != nil {
return nil, 0, dbError(ctx, err)
}
return items, total, nil
}
@@ -0,0 +1,251 @@
package executionlog
import (
"context"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
)
func TestPrincipalIsolation(t *testing.T) {
ctx := context.Background()
if _, _, err := List(ctx, nil); err == nil {
t.Fatal("guest allowed")
}
if _, err := Summary(ctx, "mine"); err == nil {
t.Fatal("guest summary allowed")
}
if _, _, err := Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: "mine"}); err == nil {
t.Fatal("guest details allowed")
}
if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = g.DB().Close(ctx) })
for _, sql := range []string{
"CREATE TABLE batch_main (id INTEGER PRIMARY KEY, batch_no TEXT, batch_type INTEGER, source_batch_no TEXT, source_channel TEXT, execution_mode INTEGER, source_type INTEGER, batch_async_status INTEGER, total_count INTEGER, run_result INTEGER, fail_reason TEXT, created_at TEXT, start_time TEXT, end_time TEXT, tenant_id TEXT, created_by INTEGER, created_dept INTEGER DEFAULT 10, target_dept_id INTEGER DEFAULT 0, batch_info TEXT)",
"CREATE TABLE batch_detail (id INTEGER PRIMARY KEY, batch_no TEXT, row_no INTEGER, item_id TEXT, detail_status INTEGER, error_code TEXT, error_reason TEXT, tenant_id TEXT, created_by INTEGER, created_dept INTEGER DEFAULT 10, snapshot TEXT)",
"INSERT INTO batch_main(id,batch_no,batch_type,tenant_id,created_by,source_channel,execution_mode,source_type) VALUES (1,'mine',101,'A',7,'mini_program',1,0),(2,'other-user',101,'A',8,'console',2,3),(3,'other-tenant',101,'B',7,'console',2,3)",
"INSERT INTO batch_detail(id,batch_no,row_no,item_id,detail_status,error_code,error_reason,tenant_id,created_by) VALUES(1,'mine',1,'card1',3,'ERR','套餐已过期','A',7),(2,'mine',2,'hidden',3,'ERR','hidden','B',7)",
} {
if _, err := g.DB().Exec(ctx, sql); err != nil {
t.Fatal(err)
}
}
ctx = contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 7, DeptId: 10, TenantId: "A", Roles: []model.Role{{RoleId: 82, DataScope: consts.SysRoleDataScopePersonal}}}}})
list, total, err := List(ctx, nil)
if err != nil || total != 1 || len(list) != 1 || list[0].BatchNo != "mine" {
t.Fatalf("list=%+v total=%d err=%v", list, total, err)
}
for _, no := range []string{"other-user", "other-tenant", "missing"} {
if _, err := Summary(ctx, no); err == nil {
t.Fatalf("summary exposed %s", no)
}
if _, _, err := Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: no}); err == nil {
t.Fatalf("details exposed %s", no)
}
}
details, total, err := Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: "mine"})
if err != nil || total != 1 || len(details) != 1 || details[0].ItemId != "card1" {
t.Fatalf("details=%+v total=%d err=%v", details, total, err)
}
source := 0
list, total, err = List(ctx, &model.AppExecutionLogListParam{SourceType: &source, SourceChannel: "mini_program"})
if err != nil || total != 1 || len(list) != 1 {
t.Fatal("zero filter failed", err, total)
}
source = 3
_, total, err = List(ctx, &model.AppExecutionLogListParam{SourceType: &source})
if err != nil || total != 0 {
t.Fatal("foreign records leaked through filter", err, total)
}
t.Run("控制台数据范围", func(t *testing.T) {
for _, sql := range []string{
"CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, parent_id INTEGER, dept_type INTEGER, ancestors TEXT,tenant_id TEXT,dept_key TEXT,dept_name TEXT,deleted_at TEXT)",
"CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)",
"INSERT INTO sys_dept(dept_id,parent_id,dept_type,ancestors) VALUES(10,0,1,',0,'),(20,10,1,',0,10,'),(30,20,1,',0,10,20,'),(40,0,1,',0,')",
"UPDATE sys_dept SET tenant_id='A',dept_name='部门',dept_key='key'",
"INSERT INTO sys_role_dept VALUES('A',82,20),('B',82,40)",
"UPDATE batch_main SET created_dept=20 WHERE batch_no='other-user'",
"INSERT INTO batch_main(id,batch_no,batch_type,tenant_id,created_by,created_dept) VALUES(20,'grandchild',101,'A',9,30),(21,'outside',101,'A',10,40)",
"INSERT INTO batch_detail(id,batch_no,row_no,item_id,detail_status,tenant_id,created_by,created_dept) VALUES(50,'other-user',1,'other-card',2,'A',8,20)",
} {
if _, err := g.DB().Exec(ctx, sql); err != nil {
t.Fatal(err)
}
}
t.Cleanup(func() {
_, _ = g.DB().Exec(ctx, "DELETE FROM batch_main WHERE id IN (20,21)")
_, _ = g.DB().Exec(ctx, "DELETE FROM batch_detail WHERE id=50")
})
cases := []struct {
name, roleScope, menuScope string
dept, role int64
count int
super bool
}{
{name: "仅本人", roleScope: "5", dept: 10, role: 82, count: 1},
{name: "本部门", roleScope: "3", dept: 10, role: 82, count: 1},
{name: "本部门及下级", roleScope: "4", dept: 10, role: 82, count: 3},
{name: "自定义部门", roleScope: "2", dept: 10, role: 82, count: 1},
{name: "全部仍隔离租户", roleScope: "1", dept: 10, role: 82, count: 4},
{name: "部门及下级或本人", roleScope: "6", dept: 40, role: 82, count: 2},
{name: "本组织及下一级", roleScope: "7", dept: 10, role: 82, count: 2},
{name: "本组织下一级", roleScope: "8", dept: 10, role: 82, count: 1},
{name: "无下一级", roleScope: "8", dept: 40, role: 82, count: 0},
{name: "自定义空集合", roleScope: "2", dept: 10, role: 99, count: 0},
{name: "菜单收窄角色", roleScope: "1", menuScope: "5", dept: 10, role: 82, count: 1},
{name: "菜单放宽角色", roleScope: "5", menuScope: "1", dept: 10, role: 82, count: 4},
{name: "无角色", dept: 10, count: 0},
{name: "未知范围", roleScope: "unknown", dept: 10, role: 82, count: 0},
{name: "超管仍隔离租户", dept: 10, role: consts.SuperAdminRoleId, count: 4, super: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
roles := []model.Role{}
if tc.role > 0 {
roles = append(roles, model.Role{RoleId: tc.role, DataScope: tc.roleScope})
}
testCtx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 7, DeptId: tc.dept, TenantId: "A", Roles: roles}}})
if tc.menuScope != "" {
contexts.SetDataValue(testCtx, "currentRoleId", "82")
contexts.SetDataValue(testCtx, "currentRoleDataScope", tc.menuScope)
}
list, total, err := List(testCtx, nil)
if err != nil || total != tc.count {
t.Fatalf("total=%d want=%d err=%v", total, tc.count, err)
}
for _, item := range list {
if _, err := Summary(testCtx, item.BatchNo); err != nil {
t.Fatal("列表与概览范围不一致", err)
}
}
if _, err := Summary(testCtx, "other-tenant"); err == nil {
t.Fatal("泄露其他租户")
}
if _, _, err := Details(testCtx, &model.AppExecutionLogDetailParam{BatchNo: "other-tenant"}); err == nil {
t.Fatal("泄露其他租户明细")
}
visibleOther := false
for _, item := range list {
if item.BatchNo == "other-user" {
visibleOther = true
}
}
rows, total, err := Details(testCtx, &model.AppExecutionLogDetailParam{BatchNo: "other-user"})
if visibleOther {
if err != nil || total != 1 || rows[0].ItemId != "other-card" {
t.Fatalf("可见部门明细缺失: %v %d %v", rows, total, err)
}
} else if err == nil {
t.Fatal("不可见批次明细被放行")
}
})
}
})
for _, sql := range []string{
`INSERT INTO batch_main(id,batch_no,batch_type,source_batch_no,batch_info,tenant_id,created_by) VALUES (4,'PARENT',3,'','{"jobRole":"allocate_parent"}','A',7),(5,'CHILD',3,'PARENT','{"jobRole":"allocate_chunk"}','A',7),(6,'FOREIGN-CHILD',3,'PARENT','{"jobRole":"allocate_chunk"}','A',8),(7,'RELATED',3,'PARENT','{}','A',7)`,
"INSERT INTO batch_detail(id,batch_no,row_no,item_id,detail_status,error_code,error_reason,tenant_id,created_by) VALUES(3,'PARENT',1,'parent-card',2,'','','A',7),(4,'CHILD',1,'mine-child',2,'','','A',7),(5,'FOREIGN-CHILD',1,'foreign-child',2,'','','A',8),(6,'RELATED',1,'related-child',2,'','','A',7)",
} {
if _, err := g.DB().Exec(ctx, sql); err != nil {
t.Fatal(err)
}
}
for _, sql := range []string{
`UPDATE batch_main SET target_dept_id=20,batch_info='{"jobRole":"allocate_parent","sourceDeptName":"提交时划拨单位","targetDeptName":"提交时接收单位"}' WHERE batch_no='PARENT'`,
`UPDATE batch_detail SET snapshot='{"sourceDeptId":10,"targetDeptId":20,"sourceDeptName":"明细划拨单位","targetDeptName":"明细接收单位"}' WHERE id=4`,
} {
if _, err := g.DB().Exec(ctx, sql); err != nil {
t.Fatal(err)
}
}
summary, err := Summary(ctx, "PARENT")
if err != nil || summary.SourceDeptName != "提交时划拨单位" || summary.TargetDeptName != "提交时接收单位" {
t.Fatal("概览未优先使用历史名称", summary, err)
}
details, total, err = Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: "PARENT"})
if err != nil || total != 3 || len(details) != 3 {
t.Fatalf("child isolation: %+v %d %v", details, total, err)
}
seen := map[string]bool{}
for _, detail := range details {
if detail.ItemId == "mine-child" && (detail.SourceDeptName != "明细划拨单位" || detail.TargetDeptName != "明细接收单位") {
t.Fatal("明细快照名称被覆盖", detail)
}
if detail.ItemId != "mine-child" && detail.TargetDeptName != "提交时接收单位" {
t.Fatal("明细未继承批次历史接收单位", detail)
}
seen[detail.ItemId] = true
}
for _, itemID := range []string{"parent-card", "mine-child", "related-child"} {
if !seen[itemID] {
t.Fatalf("parent details missing %s: %+v", itemID, details)
}
}
list, total, err = List(ctx, nil)
if err != nil || total != 2 {
t.Fatalf("children appeared as submissions: %+v %d %v", list, total, err)
}
if _, err := AllocateSummary(ctx, "PARENT"); err != nil {
t.Fatal("allocate result rejected", err)
}
if _, total, err := AllocateDetails(ctx, &model.AppExecutionLogDetailParam{BatchNo: "PARENT"}); err != nil || total != 3 {
t.Fatal("allocate details rejected", total, err)
}
allCtx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 8, DeptId: 20, TenantId: "A", Roles: []model.Role{{RoleId: 82, DataScope: consts.SysRoleDataScopeAll}}}}})
if _, err := Summary(allCtx, "PARENT"); err != nil {
t.Fatal("全部范围不能查看他人批次", err)
}
if _, err := AllocateSummary(allCtx, "PARENT"); err == nil {
t.Fatal("办理恢复接口泄露他人任务")
}
if _, _, err := AllocateDetails(allCtx, &model.AppExecutionLogDetailParam{BatchNo: "PARENT"}); err == nil {
t.Fatal("办理明细接口泄露他人任务")
}
for _, no := range []string{"mine", "CHILD", "RELATED", "other-user", "other-tenant", "missing"} {
if _, err := AllocateSummary(ctx, no); err == nil {
t.Fatalf("allocate summary exposed %s", no)
}
if _, _, err := AllocateDetails(ctx, &model.AppExecutionLogDetailParam{BatchNo: no}); err == nil {
t.Fatalf("allocate details exposed %s", no)
}
}
for _, no := range []string{"CHILD", "RELATED"} {
if _, err := Summary(ctx, no); err == nil {
t.Fatalf("child exposed as a standalone submission: %s", no)
}
}
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_main(id,batch_no,batch_type,source_batch_no,tenant_id,created_by) VALUES (8,'SELF',3,'SELF','A',7)"); err != nil {
t.Fatal(err)
}
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(id,batch_no,row_no,item_id,detail_status,error_code,error_reason,tenant_id,created_by) VALUES(7,'SELF',1,'self-card',2,'','','A',7)"); err != nil {
t.Fatal(err)
}
if _, err := Summary(ctx, "SELF"); err != nil {
t.Fatal("self-referencing allocation hidden", err)
}
details, total, err = Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: "SELF"})
if err != nil || total != 1 || len(details) != 1 || details[0].ItemId != "self-card" {
t.Fatalf("self allocation details lost: %+v %d %v", details, total, err)
}
}
func TestSafeReason(t *testing.T) {
for _, s := range []string{"token=abc", "dial tcp 192.0.2.10", "secret=abc", "panic\nstack", "https://internal"} {
if safeReason(s) == s {
t.Fatalf("sensitive reason exposed: %s", s)
}
}
if safeReason("套餐已过期") != "套餐已过期" {
t.Fatal("business explanation lost")
}
p, n := paging(0, 5000)
if p != 1 || n != 100 {
t.Fatal(p, n)
}
}