chore: 并入 iotlcm-worker-svr 历史到 worker-svr/(原提交 96f0f4e)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:03 +08:00
422 changed files with 80993 additions and 0 deletions
@@ -0,0 +1,25 @@
package workorder
import (
"context"
"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"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
"net/url"
)
func saveAutoCloseNotice(ctx context.Context, tx gdb.TX, current *entity.TWoMain) error {
if current.CreatorUserId <= 0 {
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: "工单已自动完结", MessageContent: "工单解决后72小时未收到未解决反馈,现已自动完结。您可以查看记录并评价服务。", MessageLink: "/pages/business/views/workOrderDetail/index?woNo=" + url.QueryEscape(current.WoNo), SendTime: gtime.Now(), IsRecall: 0, TenantId: current.TenantId, CreatedDept: current.CreatorDeptId, CreatedBy: 0}).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: 0}).Insert()
return gerror.Wrap(err, "保存自动完结用户通知失败")
}
@@ -0,0 +1,105 @@
package workorder
import (
"context"
"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/gtime"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
)
const (
statusResolved = 3
statusClosed = 4
operationClose = 4
)
type Service struct{}
func New() *Service {
return &Service{}
}
func (s *Service) 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 != statusResolved {
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, statusResolved).
Data(do.TWoMain{
CurrentStatus: statusClosed,
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: operationClose,
OperationDeptId: current.HandlerDeptId,
OperationUserId: operatorID,
BeforeStatus: statusResolved,
AfterStatus: statusClosed,
TenantId: current.TenantId,
CreatedDept: current.CreatedDept,
CreatedBy: operatorID,
}).Insert()
if err != nil {
return gerror.Wrap(err, "保存自动完结记录失败")
}
return saveAutoCloseNotice(ctx, tx, &current)
})
}
func (s *Service) CloseOverdue(ctx context.Context) error {
var rows []*entity.TWoMain
if err := dao.TWoMain.Ctx(ctx).Where(dao.TWoMain.Columns().CurrentStatus, statusResolved).WhereLTE(dao.TWoMain.Columns().ResolveTime, gtime.Now().Add(-72*time.Hour)).OrderAsc(dao.TWoMain.Columns().ResolveTime).Limit(100).Scan(&rows); err != nil {
return gerror.Wrap(err, "查询到期工单失败")
}
for _, row := range rows {
if row == nil || row.ResolveTime == nil {
continue
}
if err := s.AutoCloseResolved(ctx, model.WorkOrderAutoCloseMessage{WoNo: row.WoNo, ResolveTimeTs: row.ResolveTime.Timestamp()}); err != nil {
g.Log().Errorf(ctx, "工单补偿关闭失败 woNo:%s err:%v", row.WoNo, err)
}
}
return nil
}
@@ -0,0 +1,244 @@
package workorder
import (
"context"
"io"
"os"
"path/filepath"
"testing"
"time"
_ "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"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
)
func TestMain(m *testing.M) {
g.Log().SetWriter(io.Discard)
code := m.Run()
_ = os.RemoveAll("resource")
os.Exit(code)
}
func TestAutoCloseResolved(t *testing.T) {
ctx := context.Background()
if err := gdb.SetConfig(gdb.Config{
"default": gdb.ConfigGroup{
{
Type: "sqlite",
Name: filepath.Join(t.TempDir(), "workorder.db"),
},
},
}); err != nil {
t.Fatalf("configure test database: %v", err)
}
defer g.DB().Close(ctx)
createWorkOrderTables(t, ctx)
service := New()
t.Run("resolved order is closed once", func(t *testing.T) {
insertWorkOrder(t, ctx, "WO-RESOLVED", statusResolved)
message := model.WorkOrderAutoCloseMessage{WoNo: "WO-RESOLVED"}
if err := service.AutoCloseResolved(ctx, message); err != nil {
t.Fatalf("AutoCloseResolved returned error: %v", err)
}
if status := workOrderStatus(t, ctx, message.WoNo); status != statusClosed {
t.Fatalf("expected closed status, got %d", status)
}
if count := workOrderRecordCount(t, ctx, message.WoNo); count != 1 {
t.Fatalf("expected one close record, got %d", count)
}
if err := service.AutoCloseResolved(ctx, message); err != nil {
t.Fatalf("duplicate AutoCloseResolved returned error: %v", err)
}
if count := workOrderRecordCount(t, ctx, message.WoNo); count != 1 {
t.Fatalf("duplicate message added records, got %d", count)
}
})
t.Run("non-resolved order is ignored", func(t *testing.T) {
insertWorkOrder(t, ctx, "WO-PROCESSING", 2)
if err := service.AutoCloseResolved(ctx, model.WorkOrderAutoCloseMessage{WoNo: "WO-PROCESSING"}); err != nil {
t.Fatalf("AutoCloseResolved returned error: %v", err)
}
if status := workOrderStatus(t, ctx, "WO-PROCESSING"); status != 2 {
t.Fatalf("non-resolved order changed status to %d", status)
}
if count := workOrderRecordCount(t, ctx, "WO-PROCESSING"); count != 0 {
t.Fatalf("non-resolved order added %d records", count)
}
})
t.Run("stale resolve message is ignored", func(t *testing.T) {
const woNo = "WO-STALE-MESSAGE"
insertWorkOrder(t, ctx, woNo, statusResolved)
resolveTime := gtime.Now().Add(-73 * time.Hour)
if _, err := g.DB().Model("t_wo_main").Ctx(ctx).Where("wo_no", woNo).Data(g.Map{"resolve_time": resolveTime}).Update(); err != nil {
t.Fatalf("set resolve time: %v", err)
}
if err := service.AutoCloseResolved(ctx, model.WorkOrderAutoCloseMessage{WoNo: woNo, ResolveTimeTs: resolveTime.Timestamp() - 1}); err != nil {
t.Fatalf("AutoCloseResolved returned error: %v", err)
}
if status := workOrderStatus(t, ctx, woNo); status != statusResolved {
t.Fatalf("stale message changed status to %d", status)
}
if count := workOrderRecordCount(t, ctx, woNo); count != 0 {
t.Fatalf("stale message added %d records", count)
}
})
t.Run("record failure rolls back status update", func(t *testing.T) {
for _, statement := range []string{
`CREATE TABLE t_message_log (message_log_id INTEGER PRIMARY KEY AUTOINCREMENT, message_type INTEGER, send_user_type INTEGER, user_ids TEXT, message_title TEXT, message_content TEXT, message_link TEXT, send_time DATETIME, is_recall INTEGER, tenant_id TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_at DATETIME)`,
`CREATE TABLE t_user_message (message_id INTEGER PRIMARY KEY AUTOINCREMENT, message_log_id INTEGER, user_id INTEGER, is_read INTEGER, tenant_id TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_at DATETIME)`,
} {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
insertWorkOrder(t, ctx, "WO-NOTICE", statusResolved)
if _, err := g.DB().Exec(ctx, `UPDATE t_wo_main SET creator_user_id = 88, creator_dept_id = 30, creator_type = 1 WHERE wo_no = ?`, "WO-NOTICE"); err != nil {
t.Fatal(err)
}
for attempt := 0; attempt < 2; attempt++ {
if err := service.AutoCloseResolved(ctx, model.WorkOrderAutoCloseMessage{WoNo: "WO-NOTICE"}); err != nil {
t.Fatal(err)
}
}
count, err := g.DB().Model("t_user_message").Ctx(ctx).Where("user_id", 88).Where("tenant_id", "tenant").Count()
if err != nil || count != 1 {
t.Fatalf("expected one tenant-scoped notice, count=%d err=%v", count, err)
}
insertWorkOrder(t, ctx, "WO-NOTICE-ROLLBACK", statusResolved)
if _, err := g.DB().Exec(ctx, `UPDATE t_wo_main SET creator_user_id = 88 WHERE wo_no = ?`, "WO-NOTICE-ROLLBACK"); err != nil {
t.Fatal(err)
}
if _, err := g.DB().Exec(ctx, `DROP TABLE t_user_message`); err != nil {
t.Fatal(err)
}
if err := service.AutoCloseResolved(ctx, model.WorkOrderAutoCloseMessage{WoNo: "WO-NOTICE-ROLLBACK"}); err == nil {
t.Fatal("expected notice failure")
}
if status := workOrderStatus(t, ctx, "WO-NOTICE-ROLLBACK"); status != statusResolved {
t.Fatalf("notice failure did not roll back: %d", status)
}
if _, err := g.DB().Exec(ctx, `UPDATE t_wo_main SET creator_user_id = 0 WHERE wo_no = ?`, "WO-NOTICE-ROLLBACK"); err != nil {
t.Fatal(err)
}
for _, name := range []string{"WO-NO-TIME", "WO-EARLY"} {
insertWorkOrder(t, ctx, name, statusResolved)
var resolveTime *gtime.Time
if name == "WO-EARLY" {
resolveTime = gtime.Now()
}
if _, err := g.DB().Exec(ctx, `UPDATE t_wo_main SET resolve_time = ? WHERE wo_no = ?`, resolveTime, name); err != nil {
t.Fatal(err)
}
if err := service.AutoCloseResolved(ctx, model.WorkOrderAutoCloseMessage{WoNo: name}); err != nil {
t.Fatal(err)
}
if status := workOrderStatus(t, ctx, name); status != statusResolved {
t.Fatalf("premature close: %s status=%d", name, status)
}
}
insertWorkOrder(t, ctx, "WO-SCAN", statusResolved)
if err := service.CloseOverdue(ctx); err != nil {
t.Fatal(err)
}
if status := workOrderStatus(t, ctx, "WO-SCAN"); status != statusClosed {
t.Fatalf("overdue scan did not close order: %d", status)
}
insertWorkOrder(t, ctx, "WO-ROLLBACK", statusResolved)
if _, err := g.DB().Exec(ctx, `DROP TABLE t_wo_record`); err != nil {
t.Fatalf("drop record table: %v", err)
}
if err := service.AutoCloseResolved(ctx, model.WorkOrderAutoCloseMessage{WoNo: "WO-ROLLBACK"}); err == nil {
t.Fatal("expected record insert error")
}
if status := workOrderStatus(t, ctx, "WO-ROLLBACK"); status != statusResolved {
t.Fatalf("transaction did not roll back, status=%d", status)
}
})
}
func createWorkOrderTables(t *testing.T, ctx context.Context) {
t.Helper()
statements := []string{
`CREATE TABLE t_wo_main (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wo_no TEXT NOT NULL UNIQUE,
creator_user_id INTEGER,
creator_dept_id INTEGER,
creator_type INTEGER,
current_status INTEGER NOT NULL,
resolve_time DATETIME,
handler_dept_id INTEGER,
handler_user_id INTEGER,
tenant_id TEXT,
created_dept INTEGER,
created_by INTEGER,
updated_by INTEGER,
close_time DATETIME,
created_at DATETIME,
updated_at DATETIME
)`,
`CREATE TABLE t_wo_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wo_no TEXT NOT NULL,
operation_type INTEGER,
operation_dept_id INTEGER,
operation_user_id INTEGER,
before_status INTEGER,
after_status INTEGER,
tenant_id TEXT,
created_dept INTEGER,
created_by INTEGER,
created_at DATETIME,
updated_at DATETIME
)`,
}
for _, statement := range statements {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatalf("create test table: %v", err)
}
}
}
func insertWorkOrder(t *testing.T, ctx context.Context, woNo string, status int) {
t.Helper()
_, err := g.DB().Exec(ctx, `
INSERT INTO t_wo_main (
wo_no, current_status, handler_dept_id, handler_user_id,
tenant_id, created_dept, created_by, updated_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, woNo, status, 10, 20, "tenant", 30, 40, 50)
if err != nil {
t.Fatalf("insert work order: %v", err)
}
if _, err := g.DB().Exec(ctx, `UPDATE t_wo_main SET resolve_time = ? WHERE wo_no = ?`, gtime.Now().Add(-73*time.Hour), woNo); err != nil {
t.Fatal(err)
}
}
func workOrderStatus(t *testing.T, ctx context.Context, woNo string) int {
t.Helper()
value, err := g.DB().Model("t_wo_main").Ctx(ctx).Where("wo_no", woNo).Value("current_status")
if err != nil {
t.Fatalf("query work order status: %v", err)
}
return value.Int()
}
func workOrderRecordCount(t *testing.T, ctx context.Context, woNo string) int {
t.Helper()
count, err := g.DB().Model("t_wo_record").Ctx(ctx).Where("wo_no", woNo).Count()
if err != nil {
t.Fatalf("count work order records: %v", err)
}
return count
}