chore: 并入 iotlcm-worker-svr 历史到 worker-svr/(原提交 96f0f4e)
This commit is contained in:
@@ -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(¤t); 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, ¤t)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user