chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type agentBase struct{}
|
||||
|
||||
func newAgentBase() *agentBase {
|
||||
return &agentBase{}
|
||||
}
|
||||
|
||||
func (s *agentBase) deptModel(ctx context.Context) *gdb.Model {
|
||||
return service.SysDept().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false})
|
||||
}
|
||||
|
||||
func (s *agentBase) roleModel(ctx context.Context) *gdb.Model {
|
||||
return service.SysRole().ModelQuery(ctx, &handler.Option{FilterTenant: true, FilterAuth: false})
|
||||
}
|
||||
|
||||
func (s *agentBase) userModel(ctx context.Context) *gdb.Model {
|
||||
return service.SysUser().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false})
|
||||
}
|
||||
|
||||
// scopeDeptIds 获取当前登录组织本级及下一级组织的部门ID列表
|
||||
func (s *agentBase) scopeDeptIds(ctx context.Context) (deptIds []int64, err error) {
|
||||
currentDeptId := contexts.GetDeptId(ctx)
|
||||
if currentDeptId == 0 {
|
||||
return nil, gerror.New("当前用户未绑定组织")
|
||||
}
|
||||
deptIds = []int64{currentDeptId}
|
||||
var children []*entity.SysDept
|
||||
err = service.SysDept().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false}).
|
||||
Where(dao.SysDept.Columns().ParentId, currentDeptId).
|
||||
Order(dao.SysDept.Columns().OrderNum, "ASC").
|
||||
Scan(&children)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, dept := range children {
|
||||
deptIds = append(deptIds, dept.DeptId)
|
||||
}
|
||||
slices.Sort(deptIds)
|
||||
return slices.Compact(deptIds), nil
|
||||
}
|
||||
|
||||
// ensureDeptInScope 确保部门在当前机构的权限范围内
|
||||
func (s *agentBase) ensureDeptInScope(ctx context.Context, deptId int64) error {
|
||||
deptIds, err := s.scopeDeptIds(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains(deptIds, deptId) {
|
||||
return gerror.New("只能操作本级及下一级组织")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureRoleInScope 确保角色在当前机构的权限范围内
|
||||
func (s *agentBase) ensureRoleInScope(ctx context.Context, roleId int64) error {
|
||||
if roleId == 0 {
|
||||
return nil
|
||||
}
|
||||
deptIds, err := s.scopeDeptIds(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, err := s.roleModel(ctx).
|
||||
Where(dao.SysRole.Columns().RoleId, roleId).
|
||||
WhereIn(dao.SysRole.Columns().DeptId, deptIds).
|
||||
Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("只能绑定本级及下一级组织的角色")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureDeptRoleUnchangedForSelf 确保当前机构的权限未被修改
|
||||
func (s *agentBase) ensureDeptRoleUnchangedForSelf(ctx context.Context, deptId, deptRole int64) error {
|
||||
currentDeptId := contexts.GetDeptId(ctx)
|
||||
if deptId == 0 || deptId != currentDeptId {
|
||||
return nil
|
||||
}
|
||||
var dept *entity.SysDept
|
||||
err := s.deptModel(ctx).
|
||||
Where(dao.SysDept.Columns().DeptId, currentDeptId).
|
||||
Scan(&dept)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
return gerror.New("组织不存在")
|
||||
}
|
||||
if dept.DeptRole != deptRole {
|
||||
return gerror.New("不能修改本级组织的权限")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// currentDeptDefaultRoleId 获取当前机构的默认角色ID
|
||||
func (s *agentBase) currentDeptDefaultRoleId(ctx context.Context) (int64, error) {
|
||||
currentDeptId := contexts.GetDeptId(ctx)
|
||||
if currentDeptId == 0 {
|
||||
return 0, gerror.New("当前用户未绑定组织")
|
||||
}
|
||||
var dept *entity.SysDept
|
||||
err := s.deptModel(ctx).
|
||||
Where(dao.SysDept.Columns().DeptId, currentDeptId).
|
||||
Scan(&dept)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
return 0, gerror.New("组织不存在")
|
||||
}
|
||||
return dept.DeptRole, nil
|
||||
}
|
||||
|
||||
// ensureRoleNotCurrentDeptDefault 确保角色不是当前机构的默认角色
|
||||
func (s *agentBase) ensureRoleNotCurrentDeptDefault(ctx context.Context, roleId int64) error {
|
||||
if roleId == 0 {
|
||||
return nil
|
||||
}
|
||||
defaultRoleId, err := s.currentDeptDefaultRoleId(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if defaultRoleId != 0 && roleId == defaultRoleId {
|
||||
return gerror.New("不能修改本级机构绑定的默认角色权限")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type paginationDeptService struct{ service.ISysDept }
|
||||
|
||||
func (paginationDeptService) Model(ctx context.Context, _ ...*handler.Option) *gdb.Model {
|
||||
return g.DB().Model("sys_dept").Ctx(ctx).Handler(handler.FilterTenant)
|
||||
}
|
||||
|
||||
type paginationUserService struct{ service.ISysUser }
|
||||
|
||||
func (paginationUserService) Model(ctx context.Context, _ ...*handler.Option) *gdb.Model {
|
||||
return g.DB().Model("sys_user").Ctx(ctx).Handler(handler.FilterTenant)
|
||||
}
|
||||
|
||||
func TestEnterpriseCustomerDatabasePaginationAndDefaultUsers(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{TenantId: "000000", DeptId: 1}}})
|
||||
service.RegisterSysDept(paginationDeptService{})
|
||||
service.RegisterSysUser(paginationUserService{})
|
||||
t.Cleanup(func() {
|
||||
service.RegisterSysDept(nil)
|
||||
service.RegisterSysUser(nil)
|
||||
})
|
||||
statements := []string{
|
||||
"ALTER TABLE sys_dept ADD COLUMN dept_name TEXT",
|
||||
"CREATE TABLE sys_user (user_id INTEGER PRIMARY KEY, tenant_id TEXT, dept_id INTEGER, user_name TEXT, nick_name TEXT, deleted_at DATETIME)",
|
||||
"INSERT INTO sys_dept (dept_id,tenant_id,parent_id,dept_type,dept_role,dept_name) VALUES (2,'000000',1,1,12,'企业A'),(3,'000000',1,1,13,'企业B'),(4,'000000',1,1,14,'企业C'),(5,'000000',2,1,15,'孙级企业'),(6,'other',1,1,16,'其他租户'),(7,'000000',1,0,17,'内部部门'),(8,'000000',1,1,18,'已删除企业'),(9,'000000',1,1,0,'无账号企业')",
|
||||
"UPDATE sys_dept SET deleted_at=CURRENT_TIMESTAMP WHERE dept_id=8",
|
||||
"INSERT INTO sys_user VALUES (20,'000000',2,'fallback-a','回退A',NULL),(21,'000000',2,'admin-a','管理员A',NULL),(30,'000000',3,'deleted-admin','已删除',CURRENT_TIMESTAMP),(31,'000000',3,'fallback-b','回退B',NULL),(40,'000000',4,'fallback-c','回退C',NULL),(50,'other',4,'other-user','跨租户',NULL)",
|
||||
"INSERT INTO sys_user_role (tenant_id,user_id,role_id) VALUES ('000000',21,12),('000000',30,13),('other',50,14)",
|
||||
}
|
||||
for _, statement := range statements {
|
||||
mustExecAgentRoleSyncTest(t, ctx, statement)
|
||||
}
|
||||
subject := NewAgentDept()
|
||||
params := &model.AgentDeptListParam{Page: 1, PageSize: 2}
|
||||
list, total, err := subject.DeptList(ctx, params)
|
||||
if err != nil || total != 4 || len(list) != 2 {
|
||||
t.Fatalf("page 1: total=%d list=%+v err=%v", total, list, err)
|
||||
}
|
||||
if list[0].DeptId != 2 || list[0].UserName != "admin-a" || list[1].DeptId != 3 || list[1].UserName != "fallback-b" {
|
||||
t.Fatalf("unexpected account selection: %+v %+v", list[0], list[1])
|
||||
}
|
||||
params.Page = 2
|
||||
list, total, err = subject.DeptList(ctx, params)
|
||||
if err != nil || total != 4 || len(list) != 2 || list[0].DeptId != 4 || list[0].UserName != "fallback-c" || list[1].DeptId != 9 || list[1].UserName != "" {
|
||||
t.Fatalf("page 2: total=%d list=%+v err=%v", total, list, err)
|
||||
}
|
||||
params.Page = 3
|
||||
list, total, err = subject.DeptList(ctx, params)
|
||||
if err != nil || total != 4 || len(list) != 0 || list == nil {
|
||||
t.Fatalf("out of range: total=%d list=%+v err=%v", total, list, err)
|
||||
}
|
||||
list, total, err = subject.DeptList(ctx, &model.AgentDeptListParam{DeptName: "企业A", Status: "0"})
|
||||
if err != nil || total != 1 || len(list) != 1 || list[0].DeptId != 2 {
|
||||
t.Fatalf("filtered list: total=%d list=%+v err=%v", total, list, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
_ "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/gcfg"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/memorydb"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/system"
|
||||
)
|
||||
|
||||
// 复现生产形态:模板角色已配置(role 18 @ dept 100,is_pass=1),
|
||||
// 一级代理的默认角色是创建代理时复制下来的(is_pass=0)。
|
||||
// 二级代理(dept 1862, parent 128)要再创建下级时,必须能从本机构默认角色拿到权限。
|
||||
func TestPassRoleByDeptWorksForChildAgentWithoutTemplateFlag(t *testing.T) {
|
||||
setupPassRoleTestDB(t)
|
||||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||||
ID: 1,
|
||||
TenantId: "000000",
|
||||
DeptId: 128, // 一级代理,要继续创建二级代理
|
||||
}}})
|
||||
|
||||
// 三级结构:平台 100(模板 18,is_pass=1)→ 一级代理 128(默认角色 2321,is_pass=0)→ 二级代理 1862(默认角色 1779,is_pass=0)
|
||||
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES
|
||||
(100,'000000',0,1,'0',1),
|
||||
(128,'000000',100,1,'0',2321),
|
||||
(1862,'000000',128,1,'0',1779)`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
|
||||
(18,'000000',100,'代理商','dl','0','4',1,100),
|
||||
(2321,'000000',128,'代理商','dl','0','4',0,128),
|
||||
(1779,'000000',1862,'代理商','dl','0','4',0,100)`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES
|
||||
('000000',18,100,'0'),('000000',2321,100,'0'),('000000',1779,100,'0')`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
|
||||
|
||||
service := NewAgentDept()
|
||||
role, menus, err := service.currentDeptPassRole(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("一级代理创建下级时应能取到本机构默认角色权限,实际报错: %v", err)
|
||||
}
|
||||
if role == nil || role.RoleId != 2321 {
|
||||
t.Fatalf("应以本机构默认角色 2321 作为下发来源,实际: %+v", role)
|
||||
}
|
||||
if len(menus) != 1 || menus[0].MenuId != 100 {
|
||||
t.Fatalf("应复制本机构默认角色的菜单,实际: %+v", menus)
|
||||
}
|
||||
}
|
||||
|
||||
func setupPassRoleTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
service.RegisterSysDept(system.NewSysDept())
|
||||
service.RegisterSysRole(system.NewSysRole())
|
||||
if err := gdb.SetConfig(gdb.Config{
|
||||
"default": gdb.ConfigGroup{
|
||||
{Type: "sqlite", Link: "sqlite::@file(:memory:)", Debug: false},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("configure sqlite failed: %v", err)
|
||||
}
|
||||
// 走真实部署形态:内存库指向主库(config.yaml 里 link 为空时回落主库)
|
||||
if err := g.Cfg().GetAdapter().(*gcfg.AdapterFile).Set("database.memorydb.link", ""); err != nil {
|
||||
t.Fatalf("override memorydb link failed: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
t.Cleanup(func() { _ = g.DB().Close(ctx) })
|
||||
mustExec(t, ctx, `CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, parent_id INTEGER, dept_name TEXT, dept_type INTEGER, status TEXT DEFAULT '0', dept_role INTEGER, deleted_at DATETIME)`)
|
||||
mustExec(t, ctx, `CREATE TABLE sys_config (config_id INTEGER PRIMARY KEY, tenant_id TEXT, config_name TEXT, config_key TEXT, config_value TEXT, config_type TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, remark TEXT)`)
|
||||
mustExec(t, ctx, `CREATE TABLE sys_role (
|
||||
role_id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT, dept_id INTEGER, role_name TEXT, role_key TEXT, role_sort INTEGER, data_scope TEXT,
|
||||
menu_check_strictly INTEGER, dept_check_strictly INTEGER, status TEXT, is_pass INTEGER, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, deleted_by INTEGER, deleted_at DATETIME, remark TEXT)`)
|
||||
mustExec(t, ctx, `CREATE TABLE sys_role_menu (tenant_id TEXT, role_id INTEGER, menu_id INTEGER, data_scope TEXT)`)
|
||||
mustExec(t, ctx, `CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)`)
|
||||
mustExec(t, ctx, `CREATE TABLE sys_user_role (tenant_id TEXT DEFAULT '000000', user_id INTEGER, role_id INTEGER)`)
|
||||
_ = consts.SysRoleDataScopeDeptAndBelow
|
||||
}
|
||||
|
||||
func mustExec(t *testing.T, ctx context.Context, sql string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx, sql); err != nil {
|
||||
t.Fatalf("execute sql failed: %v\nsql=%s", err, sql)
|
||||
}
|
||||
}
|
||||
|
||||
// 平台自身(dept 100 = 模板所属机构)仍以模板角色为下发来源。
|
||||
func TestPassRoleByDeptTemplateOwnerUsesTemplateRole(t *testing.T) {
|
||||
setupPassRoleTestDB(t)
|
||||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||||
ID: 1, TenantId: "000000", DeptId: 100,
|
||||
}}})
|
||||
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (100,'000000',0,1,'0',1)`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
|
||||
(1,'000000',100,'超级管理员','admin','0','1',0,100),
|
||||
(18,'000000',100,'代理商','dl','0','4',1,100)`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',18,100,'0')`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
|
||||
|
||||
role, menus, err := NewAgentDept().currentDeptPassRole(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("平台创建一级代理应成功,实际: %v", err)
|
||||
}
|
||||
if role.RoleId != 18 {
|
||||
t.Fatalf("平台下发来源应为模板角色 18,实际: %d", role.RoleId)
|
||||
}
|
||||
if len(menus) != 1 || menus[0].MenuId != 100 {
|
||||
t.Fatalf("应复制模板菜单,实际: %+v", menus)
|
||||
}
|
||||
}
|
||||
|
||||
// 默认角色缺失/停用/归属不符时必须明确报错,不能静默 0 权限。
|
||||
func TestPassRoleByDeptRejectsBrokenDefaultRole(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
sql []string
|
||||
}{
|
||||
{
|
||||
name: "默认角色不存在",
|
||||
sql: []string{
|
||||
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',9999)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "默认角色已停用",
|
||||
sql: []string{
|
||||
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',2321)`,
|
||||
`INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (2321,'000000',128,'代理商','dl','1','4',0,128)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "默认角色归属其它机构",
|
||||
sql: []string{
|
||||
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',2321)`,
|
||||
`INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (2321,'000000',129,'代理商','dl','0','4',0,100)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "本机构未绑定默认角色",
|
||||
sql: []string{
|
||||
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',0)`,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
setupPassRoleTestDB(t)
|
||||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||||
ID: 1, TenantId: "000000", DeptId: 128,
|
||||
}}})
|
||||
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (100,'000000',0,1,'0',1)`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (18,'000000',100,'代理商','dl','0','4',1,100)`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',18,100,'0')`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
|
||||
for _, sql := range tc.sql {
|
||||
mustExec(t, ctx, sql)
|
||||
}
|
||||
if _, _, err := NewAgentDept().currentDeptPassRole(ctx); err == nil {
|
||||
t.Fatal("默认角色异常时必须报错,不能静默下发空权限")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 未配置平台模板时,保留原 is_pass=1 唯一角色的下发流程。
|
||||
func TestPassRoleByDeptKeepsLegacyFlowWithoutTemplate(t *testing.T) {
|
||||
setupPassRoleTestDB(t)
|
||||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||||
ID: 1, TenantId: "000000", DeptId: 128,
|
||||
}}})
|
||||
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',2321)`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (2321,'000000',128,'代理商','dl','0','4',1,128)`)
|
||||
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',2321,100,'0')`)
|
||||
|
||||
role, menus, err := NewAgentDept().currentDeptPassRole(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("未配模板时应走原有 is_pass 流程,实际: %v", err)
|
||||
}
|
||||
if role.RoleId != 2321 || len(menus) != 1 {
|
||||
t.Fatalf("旧流程结果不符: role=%+v menus=%+v", role, menus)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
// agentExportColumns 只允许列表字段,列标题和映射由服务端确定。
|
||||
func agentExportColumns(raw string, defaults []model.ExportColumn) ([]model.ExportColumn, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil, gerror.New("请选择导出列")
|
||||
}
|
||||
var requested []model.ExportColumn
|
||||
if err := json.Unmarshal([]byte(raw), &requested); err != nil {
|
||||
return nil, gerror.New("导出列格式错误")
|
||||
}
|
||||
allowed := make(map[string]model.ExportColumn, len(defaults))
|
||||
for _, col := range defaults {
|
||||
allowed[col.Field] = col
|
||||
}
|
||||
result := make([]model.ExportColumn, 0, len(requested))
|
||||
seen := make(map[string]bool)
|
||||
for _, col := range requested {
|
||||
safe, ok := allowed[col.Field]
|
||||
if !ok {
|
||||
return nil, gerror.New("包含不允许导出的字段")
|
||||
}
|
||||
if !seen[col.Field] {
|
||||
result = append(result, safe)
|
||||
seen[col.Field] = true
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, gerror.New("请选择导出列")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *sAgentUser) UserExport(ctx context.Context, in *model.AgentUserListParam) error {
|
||||
columns, err := agentExportColumns(in.ExportColumnsJson, []model.ExportColumn{
|
||||
{Field: "userName", Label: "用户名称"}, {Field: "nickName", Label: "用户昵称"},
|
||||
{Field: "roles", Label: "角色"}, {Field: "phonenumber", Label: "联系电话"},
|
||||
{Field: "email", Label: "邮箱"}, {Field: "status", Label: "状态"}, {Field: "createdAt", Label: "创建时间"},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "agentUser", BizName: "用户管理", PermCode: "cpm:agent:user:export", QueryParam: in, Columns: columns,
|
||||
RowFields: agentExportFields(columns),
|
||||
ToRow: func(value interface{}) []interface{} {
|
||||
row := value.(map[string]interface{})
|
||||
values := make([]interface{}, 0, len(columns))
|
||||
for _, column := range columns {
|
||||
values = append(values, row[column.Field])
|
||||
}
|
||||
return values
|
||||
},
|
||||
Tags: func() ([]string, error) {
|
||||
labels := make([]string, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
labels = append(labels, col.Label)
|
||||
}
|
||||
return labels, nil
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) ([]interface{}, int, error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, total, err := s.UserList(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows := make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
roles := make([]string, 0, len(item.Roles))
|
||||
for _, role := range item.Roles {
|
||||
if role != nil && role.RoleName != "" {
|
||||
roles = append(roles, role.RoleName)
|
||||
}
|
||||
}
|
||||
roleNames := strings.Join(roles, "、")
|
||||
if roleNames == "" {
|
||||
roleNames = "-"
|
||||
}
|
||||
createdAt := ""
|
||||
if item.CreatedAt != nil {
|
||||
createdAt = item.CreatedAt.Format("Y-m-d H:i:s")
|
||||
}
|
||||
rows = append(rows, map[string]interface{}{"userName": item.UserName, "nickName": item.NickName, "roles": roleNames, "phonenumber": item.Phonenumber, "email": item.Email, "status": agentExportStatus(item.Status), "createdAt": createdAt})
|
||||
}
|
||||
return rows, total, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sAgentDept) DeptExport(ctx context.Context, in *model.AgentDeptListParam) error {
|
||||
columns, err := agentExportColumns(in.ExportColumnsJson, []model.ExportColumn{
|
||||
{Field: "deptName", Label: "企业名称"}, {Field: "userName", Label: "登录账号"}, {Field: "nickName", Label: "负责人"},
|
||||
{Field: "phone", Label: "联系电话"}, {Field: "email", Label: "联系人邮箱"}, {Field: "areaCode", Label: "组织所在地"},
|
||||
{Field: "address", Label: "详细地址"}, {Field: "createdAt", Label: "创建时间"}, {Field: "status", Label: "状态"},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "agentDept", BizName: "企业客户", PermCode: "cpm:agent:dept:export", QueryParam: in, Columns: columns,
|
||||
RowFields: agentExportFields(columns),
|
||||
ToRow: func(value interface{}) []interface{} {
|
||||
row := value.(map[string]interface{})
|
||||
values := make([]interface{}, 0, len(columns))
|
||||
for _, column := range columns {
|
||||
values = append(values, row[column.Field])
|
||||
}
|
||||
return values
|
||||
},
|
||||
Tags: func() ([]string, error) {
|
||||
labels := make([]string, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
labels = append(labels, col.Label)
|
||||
}
|
||||
return labels, nil
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) ([]interface{}, int, error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, total, err := s.DeptList(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows := make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
createdAt := ""
|
||||
if item.CreatedAt != nil {
|
||||
createdAt = item.CreatedAt.Format("Y-m-d H:i:s")
|
||||
}
|
||||
rows = append(rows, map[string]interface{}{"deptName": item.DeptName, "userName": item.UserName, "nickName": item.NickName, "phone": item.Phone, "email": item.Email, "areaCode": item.AreaCode, "address": item.Address, "createdAt": createdAt, "status": agentExportStatus(item.Status)})
|
||||
}
|
||||
return rows, total, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func agentExportFields(columns []model.ExportColumn) []string {
|
||||
fields := make([]string, 0, len(columns))
|
||||
for _, column := range columns {
|
||||
fields = append(fields, column.Field)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func agentExportStatus(status string) string {
|
||||
if status == "0" {
|
||||
return "正常"
|
||||
}
|
||||
if status == "1" {
|
||||
return "停用"
|
||||
}
|
||||
return "未知"
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"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/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"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"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sAgentMenu struct {
|
||||
*agentBase
|
||||
}
|
||||
|
||||
func NewAgentMenu() *sAgentMenu {
|
||||
return &sAgentMenu{
|
||||
agentBase: newAgentBase(),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAgentMenu(NewAgentMenu())
|
||||
}
|
||||
|
||||
// AllowedMenuIdsByDeptRole 获取当前机构允许的菜单ID
|
||||
func (s *sAgentMenu) AllowedMenuIdsByDeptRole(ctx context.Context, deptId int64) (menuIds []int64, err error) {
|
||||
if err = s.ensureDeptInScope(ctx, deptId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var dept *entity.SysDept
|
||||
err = service.SysDept().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false}).
|
||||
Where(dao.SysDept.Columns().DeptId, deptId).
|
||||
Scan(&dept)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dept == nil || dept.DeptRole == 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
if dept.DeptRole == consts.SuperAdminRoleId {
|
||||
menuIds, err = s.allMenuIds(ctx)
|
||||
} else {
|
||||
var roleMenus []*entity.SysRoleMenu
|
||||
roleMenus, err = service.SysRole().GetRoleListMenu(ctx, []int64{dept.DeptRole})
|
||||
for _, item := range roleMenus {
|
||||
menuIds = append(menuIds, item.MenuId)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slices.Sort(menuIds)
|
||||
menuIds = slices.Compact(menuIds)
|
||||
if !contexts.IsSuperAdmin(ctx) {
|
||||
return rolepolicy.RestrictAssignableMenus(ctx, dept.TenantId, dept.DeptId, contexts.GetUserId(ctx), menuIds)
|
||||
}
|
||||
return rolepolicy.RestrictMenus(ctx, dept.TenantId, dept.DeptId, menuIds)
|
||||
}
|
||||
|
||||
func (s *sAgentMenu) allMenuIds(ctx context.Context) (menuIds []int64, err error) {
|
||||
var menus []*entity.SysMenu
|
||||
err = service.SysMenu().Model(ctx).
|
||||
Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
|
||||
Order(dao.SysMenu.Columns().OrderNum, "ASC").
|
||||
Scan(&menus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range menus {
|
||||
menuIds = append(menuIds, item.MenuId)
|
||||
}
|
||||
slices.Sort(menuIds)
|
||||
return slices.Compact(menuIds), nil
|
||||
}
|
||||
|
||||
func (s *sAgentMenu) getRoleById(ctx context.Context, roleId int64) (role *entity.SysRole, err error) {
|
||||
err = service.SysRole().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false}).
|
||||
Where(dao.SysRole.Columns().RoleId, roleId).
|
||||
Scan(&role)
|
||||
return role, err
|
||||
}
|
||||
|
||||
func (s *sAgentMenu) AllowedMenuTree(ctx context.Context, deptId int64) (tree []*model.SysMenuTreeModel, menuIds []int64, err error) {
|
||||
deptId = contexts.GetDeptId(ctx)
|
||||
menuIds, err = s.AllowedMenuIdsByDeptRole(ctx, deptId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(menuIds) == 0 && !contexts.IsSuperAdmin(ctx) {
|
||||
return []*model.SysMenuTreeModel{}, []int64{}, nil
|
||||
}
|
||||
visibleMenuIds, err := rolepolicy.WithMenuAncestors(ctx, menuIds)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mod := service.SysMenu().Model(ctx)
|
||||
if !contexts.IsSuperAdmin(ctx) {
|
||||
mod = mod.WhereIn(dao.SysMenu.Columns().MenuId, visibleMenuIds)
|
||||
}
|
||||
var menus []*entity.SysMenu
|
||||
err = mod.Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
|
||||
Order(dao.SysMenu.Columns().OrderNum, "ASC").
|
||||
Scan(&menus)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
tree = s.buildMenuList(menus)
|
||||
activeMenuIds := make([]int64, 0, len(menus))
|
||||
for _, menu := range menus {
|
||||
activeMenuIds = append(activeMenuIds, menu.MenuId)
|
||||
}
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
menuIds = activeMenuIds
|
||||
} else {
|
||||
menuIds = rolepolicy.Intersect(menuIds, activeMenuIds)
|
||||
}
|
||||
return tree, menuIds, nil
|
||||
}
|
||||
|
||||
func (s *sAgentMenu) buildMenuList(menus []*entity.SysMenu) (list []*model.SysMenuTreeModel) {
|
||||
for _, menu := range menus {
|
||||
list = append(list, &model.SysMenuTreeModel{
|
||||
Title: menu.MenuName,
|
||||
Key: gconv.String(menu.MenuId),
|
||||
ParentId: menu.ParentId,
|
||||
MenuId: menu.MenuId,
|
||||
MenuName: menu.MenuName,
|
||||
MenuType: menu.MenuType,
|
||||
Perms: menu.Perms,
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (s *sAgentMenu) buildMenuTree(parentId int64, menus []*entity.SysMenu) (tree []*model.SysMenuTreeModel) {
|
||||
for _, menu := range menus {
|
||||
if menu.ParentId != parentId {
|
||||
continue
|
||||
}
|
||||
item := &model.SysMenuTreeModel{
|
||||
Title: menu.MenuName,
|
||||
Key: gconv.String(menu.MenuId),
|
||||
ParentId: menu.ParentId,
|
||||
MenuId: menu.MenuId,
|
||||
MenuName: menu.MenuName,
|
||||
MenuType: menu.MenuType,
|
||||
Perms: menu.Perms,
|
||||
}
|
||||
item.Children = s.buildMenuTree(menu.MenuId, menus)
|
||||
tree = append(tree, item)
|
||||
}
|
||||
if parentId == 0 && len(tree) == 0 {
|
||||
for _, menu := range menus {
|
||||
item := &model.SysMenuTreeModel{
|
||||
Title: menu.MenuName,
|
||||
Key: gconv.String(menu.MenuId),
|
||||
ParentId: menu.ParentId,
|
||||
MenuId: menu.MenuId,
|
||||
MenuName: menu.MenuName,
|
||||
MenuType: menu.MenuType,
|
||||
Perms: menu.Perms,
|
||||
}
|
||||
item.Children = s.buildMenuTree(menu.MenuId, menus)
|
||||
tree = append(tree, item)
|
||||
}
|
||||
}
|
||||
return tree
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolesync"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type sAgentRole struct {
|
||||
*sAgentMenu
|
||||
}
|
||||
|
||||
// NewAgentRole 创建代理商角色管理服务实例。
|
||||
func NewAgentRole() *sAgentRole {
|
||||
return &sAgentRole{
|
||||
sAgentMenu: &sAgentMenu{
|
||||
agentBase: newAgentBase(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAgentRole(NewAgentRole())
|
||||
}
|
||||
|
||||
// RoleList 查询当前登录组织本级角色列表。
|
||||
func (s *sAgentRole) RoleList(ctx context.Context, in *model.AgentRoleListParam) (list []*model.AgentRoleListModel, total int, err error) {
|
||||
currentDeptId := contexts.GetDeptId(ctx)
|
||||
if currentDeptId == 0 {
|
||||
return nil, 0, gerror.New("当前用户未绑定组织")
|
||||
}
|
||||
if err = service.AgentDept().EnsureCurrentDeptDefaultRole(ctx); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
m := s.roleModel(ctx).Where(dao.SysRole.Columns().DeptId, currentDeptId)
|
||||
if in.RoleName != "" {
|
||||
m = m.WhereLike(dao.SysRole.Columns().RoleName, "%"+in.RoleName+"%")
|
||||
}
|
||||
if in.RoleKey != "" {
|
||||
m = m.WhereLike(dao.SysRole.Columns().RoleKey, "%"+in.RoleKey+"%")
|
||||
}
|
||||
if in.Status != "" {
|
||||
m = m.Where(dao.SysRole.Columns().Status, in.Status)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
begin, end := gtime.NewFromStr(in.CreatedAt[0]), gtime.NewFromStr(in.CreatedAt[1])
|
||||
m = m.WhereBetween(dao.SysRole.Columns().CreatedAt, begin, end.EndOfDay())
|
||||
}
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = m.Order(dao.SysRole.Columns().RoleId, "ASC").
|
||||
Scan(&list)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defaultRoleId, err := s.currentDeptDefaultRoleId(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, role := range list {
|
||||
role.IsDefaultRole = defaultRoleId != 0 && role.RoleId == defaultRoleId
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// RoleView 查询角色详情,并返回当前登录组织可分配的菜单树。
|
||||
func (s *sAgentRole) RoleView(ctx context.Context, roleId int64) (res *model.AgentRoleViewModel, err error) {
|
||||
role, err := service.SysRole().View(ctx, &model.SysRoleViewParam{RoleId: roleId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if role == nil || role.RoleId == 0 {
|
||||
return nil, gerror.New("角色不存在")
|
||||
}
|
||||
if role.DeptId != contexts.GetDeptId(ctx) {
|
||||
return nil, gerror.New("只能操作本级角色")
|
||||
}
|
||||
tree, allowed, err := s.AllowedMenuTree(ctx, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isDefaultRole, err := s.isCurrentDeptDefaultRole(ctx, role.RoleId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.AgentRoleViewModel{
|
||||
SysRoleViewModel: *role,
|
||||
AllowedMenuIds: allowed,
|
||||
MenuTree: tree,
|
||||
IsDefaultRole: isDefaultRole,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RoleSave 保存代理商角色,按角色ID区分新增和修改流程。
|
||||
func (s *sAgentRole) RoleSave(ctx context.Context, in *model.AgentRoleSaveParam) (roleId int64, err error) {
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
var saveErr error
|
||||
roleId, saveErr = s.saveTemplateRole(rolesync.DeferRecording(ctx), in)
|
||||
if saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := rolesync.EnqueueRole(ctx, roleId, contexts.GetUserId(ctx)); err != nil {
|
||||
return roleId, err
|
||||
}
|
||||
return roleId, nil
|
||||
}
|
||||
|
||||
func (s *sAgentRole) saveTemplateRole(ctx context.Context, in *model.AgentRoleSaveParam) (roleId int64, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0, gerror.New("登录已过期")
|
||||
}
|
||||
if in.DeptId == 0 {
|
||||
in.DeptId = user.DeptId
|
||||
}
|
||||
if in.DeptId != user.DeptId {
|
||||
return 0, gerror.New("只能维护本级角色")
|
||||
}
|
||||
if err = s.ensureRoleNotCurrentDeptDefault(ctx, in.RoleId); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if in.RoleId > 0 {
|
||||
if err = s.ensureRoleNotInherited(ctx, in.RoleId); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if in.Status == "" {
|
||||
in.Status = "0"
|
||||
}
|
||||
if in.DataScope == "" {
|
||||
in.DataScope = consts.SysRoleDataScopeDeptAndBelow
|
||||
}
|
||||
if !contexts.IsSuperAdmin(ctx) {
|
||||
allowed, err := rolepolicy.NormalizeAssignableMenus(ctx, user.TenantId, in.DeptId, user.ID, in.MenuIds)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
in.MenuIds = allowed
|
||||
switch in.DataScope {
|
||||
case consts.SysRoleDataScopeDept, consts.SysRoleDataScopeDeptAndBelow, consts.SysRoleDataScopePersonal, consts.SysRoleDataScopeDeptAndBelowOrPersonal, consts.SysRoleDataScopeOrgAndNextLevel, consts.SysRoleDataScopeOrgNextLevel:
|
||||
default:
|
||||
return 0, gerror.New("代理角色数据权限不能超出本机构及下级范围")
|
||||
}
|
||||
}
|
||||
template, templateErr := rolepolicy.TemplateForRoles(ctx, in.RoleId)
|
||||
if templateErr != nil {
|
||||
return 0, templateErr
|
||||
}
|
||||
if in.IsPass != 0 && in.IsPass != 1 {
|
||||
return 0, gerror.New("传递标记无效")
|
||||
}
|
||||
if template != nil {
|
||||
in.DataScope = consts.SysRoleDataScopeDeptAndBelow
|
||||
}
|
||||
legacyPass := in.IsPass == 1
|
||||
if legacyPass {
|
||||
configured, err := dao.SysConfig.Ctx(ctx).Master().Where("tenant_id", consts.DefaultSystemTenantCode).Where("config_key", rolepolicy.TemplateConfigKey).Count()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
legacyPass = configured == 0
|
||||
}
|
||||
if legacyPass {
|
||||
in.DataScope = consts.SysRoleDataScopeDeptAndBelow
|
||||
var dept *entity.SysDept
|
||||
deptModel := s.deptModel(ctx).Where(dao.SysDept.Columns().DeptId, in.DeptId)
|
||||
if dao.SysDept.DB().GetConfig().Type != "sqlite" {
|
||||
deptModel = deptModel.LockUpdate()
|
||||
}
|
||||
if err := deptModel.Scan(&dept); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if dept == nil {
|
||||
return 0, gerror.New("当前机构不存在")
|
||||
}
|
||||
count, err := s.roleModel(ctx).
|
||||
Where(dao.SysRole.Columns().DeptId, in.DeptId).
|
||||
Where("is_pass", 1).
|
||||
WhereNot(dao.SysRole.Columns().RoleId, in.RoleId).
|
||||
Count()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if count > 0 {
|
||||
return 0, gerror.New("每个机构只能配置一个下级管理员角色")
|
||||
}
|
||||
}
|
||||
|
||||
if in.RoleId == 0 {
|
||||
return s.addRole(ctx, in)
|
||||
}
|
||||
return s.editRole(ctx, in)
|
||||
}
|
||||
|
||||
// addRole 新增代理商角色,并同步角色的数据权限配置。
|
||||
func (s *sAgentRole) addRole(ctx context.Context, in *model.AgentRoleSaveParam) (int64, error) {
|
||||
deptId := in.DeptId
|
||||
role, err := service.SysRole().Add(ctx, &model.SysRoleAddParam{
|
||||
RoleName: in.RoleName,
|
||||
RoleKey: in.RoleKey,
|
||||
RoleSort: in.RoleSort,
|
||||
DataScope: in.DataScope,
|
||||
MenuCheckStrictly: in.MenuCheckStrictly,
|
||||
Status: in.Status,
|
||||
Remark: in.Remark,
|
||||
MenuIds: in.MenuIds,
|
||||
DeptId: &deptId,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if role == nil {
|
||||
return 0, nil
|
||||
}
|
||||
if _, err = dao.SysRole.Ctx(ctx).
|
||||
Where(dao.SysRole.Columns().RoleId, role.RoleId).
|
||||
Data(do.SysRole{IsPass: in.IsPass}).Update(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = s.updateRoleDataScope(ctx, role.RoleId, in); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return role.RoleId, nil
|
||||
}
|
||||
|
||||
// editRole 修改代理商角色,并禁止在代理商页面移动角色所属组织。
|
||||
func (s *sAgentRole) editRole(ctx context.Context, in *model.AgentRoleSaveParam) (int64, error) {
|
||||
current, err := s.RoleView(ctx, in.RoleId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if current.DeptId != in.DeptId {
|
||||
return 0, gerror.New("不能在代理商页面移动角色所属组织")
|
||||
}
|
||||
deptId := in.DeptId
|
||||
roleName := in.RoleName
|
||||
roleKey := in.RoleKey
|
||||
roleSort := in.RoleSort
|
||||
menuCheckStrictly := in.MenuCheckStrictly
|
||||
status := in.Status
|
||||
remark := in.Remark
|
||||
role, err := service.SysRole().Edit(ctx, &model.SysRoleEditParam{
|
||||
RoleId: in.RoleId,
|
||||
RoleName: &roleName,
|
||||
RoleKey: &roleKey,
|
||||
RoleSort: &roleSort,
|
||||
MenuCheckStrictly: &menuCheckStrictly,
|
||||
Status: &status,
|
||||
Remark: &remark,
|
||||
MenuIds: in.MenuIds,
|
||||
DeptId: &deptId,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = s.updateRoleDataScope(ctx, in.RoleId, in); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err = dao.SysRole.Ctx(ctx).
|
||||
Where(dao.SysRole.Columns().RoleId, in.RoleId).
|
||||
Data(do.SysRole{IsPass: in.IsPass}).Update(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if role == nil {
|
||||
return in.RoleId, nil
|
||||
}
|
||||
return role.RoleId, nil
|
||||
}
|
||||
|
||||
// updateRoleDataScope 更新角色数据权限和组织树勾选配置。
|
||||
func (s *sAgentRole) updateRoleDataScope(ctx context.Context, roleId int64, in *model.AgentRoleSaveParam) error {
|
||||
deptCheckStrictly := in.DeptCheckStrictly
|
||||
return service.SysRole().EditRoleDataScope(ctx, &model.SysRoleDataScopeEditParam{
|
||||
RoleId: roleId,
|
||||
DataScope: in.DataScope,
|
||||
DeptCheckStrictly: &deptCheckStrictly,
|
||||
DeptIds: []int64{in.DeptId},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sAgentRole) isCurrentDeptDefaultRole(ctx context.Context, roleId int64) (bool, error) {
|
||||
defaultRoleId, err := s.currentDeptDefaultRoleId(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return defaultRoleId != 0 && roleId == defaultRoleId, nil
|
||||
}
|
||||
|
||||
func (s *sAgentRole) ensureRoleInCurrentDept(ctx context.Context, roleId int64) error {
|
||||
currentDeptId := contexts.GetDeptId(ctx)
|
||||
if currentDeptId == 0 {
|
||||
return gerror.New("当前用户未绑定组织")
|
||||
}
|
||||
count, err := s.roleModel(ctx).
|
||||
Where(dao.SysRole.Columns().RoleId, roleId).
|
||||
Where(dao.SysRole.Columns().DeptId, currentDeptId).
|
||||
Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("只能操作本级角色")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAgentRole) ensureRoleNotInherited(ctx context.Context, roleId int64) error {
|
||||
if roleId <= 0 {
|
||||
return nil
|
||||
}
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
return nil
|
||||
}
|
||||
count, err := s.inheritedRoleModel(ctx).
|
||||
Where("r.role_id", roleId).
|
||||
Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("上级下发角色由上级角色统一维护,当前组织只能查看")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAgentRole) ensureRoleNoUsers(ctx context.Context, roleId int64) error {
|
||||
count, err := dao.SysUserRole.Ctx(ctx).
|
||||
Where(dao.SysUserRole.Columns().RoleId, roleId).
|
||||
Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("该角色已绑定用户,不能删除")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RoleDelete 删除代理商角色,删除前校验本级机构默认角色不可被删除。
|
||||
func (s *sAgentRole) RoleDelete(ctx context.Context, roleIds []int64) (err error) {
|
||||
for _, roleId := range roleIds {
|
||||
if err = s.ensureRoleInCurrentDept(ctx, roleId); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.ensureRoleNotCurrentDeptDefault(ctx, roleId); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.ensureRoleNotInherited(ctx, roleId); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.ensureRoleNoUsers(ctx, roleId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = service.SysRole().Delete(ctx, &model.SysRoleDeleteParam{RoleIds: roleIds})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sAgentRole) inheritedRoleModel(ctx context.Context) *gdb.Model {
|
||||
m := g.Model(dao.SysRole.Table()+" r").
|
||||
LeftJoin(dao.SysDept.Table()+" d", "d.dept_id = r.dept_id AND d.tenant_id = r.tenant_id").
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("d.deleted_at IS NULL").
|
||||
Where("r.created_dept > 0").
|
||||
Where("r.dept_id <> r.created_dept").
|
||||
Where("d.parent_id = r.created_dept")
|
||||
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
|
||||
m = m.Where("r.tenant_id", tenantId)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
_ "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/gcfg"
|
||||
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/memorydb"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/system"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
)
|
||||
|
||||
// 复现生产形态:dept 100 同时存在模板角色 18(is_pass=1)和普通角色 79(运营人员,is_pass=0)。
|
||||
// 给 79 勾权限保存时,旧逻辑强制把 in.IsPass 置 1 再查重,必然报
|
||||
// 「每个机构只能配置一个下级管理员角色」,导致普通角色永远改不了。
|
||||
func TestSaveTemplateRoleAllowsOrdinaryRoleWithTemplateConfigured(t *testing.T) {
|
||||
setupRoleSaveTestDB(t)
|
||||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||||
ID: 1,
|
||||
TenantId: "000000",
|
||||
DeptId: 100,
|
||||
Roles: []model.Role{{RoleId: 1}},
|
||||
}}})
|
||||
// 模板已配置为 18,机构 100 已有 is_pass=1 的 18
|
||||
mustExecRole(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (100,'000000',0,1,'0',1)`)
|
||||
mustExecRole(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
|
||||
(1,'000000',100,'超级管理员','admin','0','1',0,100),
|
||||
(18,'000000',100,'代理商','dl','0','4',1,100),
|
||||
(79,'000000',100,'运营人员','yunying','0','4',0,100)`)
|
||||
mustExecRole(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',18,100,'0')`)
|
||||
mustExecRole(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
|
||||
|
||||
// 编辑普通角色 79:勾一个菜单
|
||||
_, err := NewAgentRole().saveTemplateRole(ctx, &model.AgentRoleSaveParam{
|
||||
RoleId: 79,
|
||||
DeptId: 100,
|
||||
RoleName: "运营人员",
|
||||
RoleKey: "yunying",
|
||||
Status: "0",
|
||||
DataScope: "4",
|
||||
MenuIds: []int64{100},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("已配置模板时普通角色必须能保存,实际报错: %v", err)
|
||||
}
|
||||
|
||||
// 关键断言:79 不能被悄悄改成 is_pass=1
|
||||
var isPass int
|
||||
v, qErr := g.DB().GetValue(ctx, `SELECT is_pass FROM sys_role WHERE role_id = 79`)
|
||||
if qErr != nil {
|
||||
t.Fatalf("query is_pass failed: %v", qErr)
|
||||
}
|
||||
isPass = v.Int()
|
||||
if isPass != 0 {
|
||||
t.Fatalf("普通角色 79 的 is_pass 被改成了 %d,应保持 0", isPass)
|
||||
}
|
||||
// 模板 18 的 is_pass 也不能被动到
|
||||
v18, _ := g.DB().GetValue(ctx, `SELECT is_pass FROM sys_role WHERE role_id = 18`)
|
||||
if v18.Int() != 1 {
|
||||
t.Fatalf("模板角色 18 的 is_pass 变成了 %d,应保持 1", v18.Int())
|
||||
}
|
||||
}
|
||||
|
||||
// 未配置平台模板时,原有「本级唯一 is_pass=1 角色」限制必须保留。
|
||||
func TestSaveTemplateRoleKeepsLegacyUniquenessWithoutTemplate(t *testing.T) {
|
||||
setupRoleSaveTestDB(t)
|
||||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||||
ID: 1, TenantId: "000000", DeptId: 200,
|
||||
}}})
|
||||
mustExecRole(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (200,'000000',0,1,'0',1)`)
|
||||
mustExecRole(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
|
||||
(1,'000000',200,'超级管理员','admin','0','1',0,200),
|
||||
(90,'000000',200,'已有的下发角色','dl','0','4',1,200)`)
|
||||
// 没有 sys_config 模板参数 → 走 legacy 分支
|
||||
_, err := NewAgentRole().saveTemplateRole(ctx, &model.AgentRoleSaveParam{
|
||||
RoleId: 0,
|
||||
DeptId: 200,
|
||||
RoleName: "第二个下发角色",
|
||||
RoleKey: "dl2",
|
||||
Status: "0",
|
||||
DataScope: "4",
|
||||
MenuIds: []int64{},
|
||||
IsPass: 1,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("未配置模板时,第二个 is_pass=1 角色必须被拒绝")
|
||||
}
|
||||
if err.Error() != "每个机构只能配置一个下级管理员角色" {
|
||||
t.Fatalf("错误信息不符,实际: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupRoleSaveTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
if err := gdb.SetConfig(gdb.Config{
|
||||
"default": gdb.ConfigGroup{
|
||||
{Type: "sqlite", Link: "sqlite::@file(:memory:)", Debug: false},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("configure sqlite failed: %v", err)
|
||||
}
|
||||
_ = g.Cfg().GetAdapter().(*gcfg.AdapterFile).Set("database.memorydb.link", "")
|
||||
ctx := context.Background()
|
||||
t.Cleanup(func() { _ = g.DB().Close(ctx) })
|
||||
mustExecRole(t, ctx, `CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, parent_id INTEGER, dept_type INTEGER, status TEXT DEFAULT '0', dept_role INTEGER, order_num INTEGER DEFAULT 0, ancestors TEXT, dept_name TEXT, deleted_at DATETIME)`)
|
||||
mustExecRole(t, ctx, `CREATE TABLE sys_config (config_id INTEGER PRIMARY KEY, tenant_id TEXT, config_name TEXT, config_key TEXT, config_value TEXT, config_type TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, remark TEXT)`)
|
||||
mustExecRole(t, ctx, `CREATE TABLE sys_role (
|
||||
role_id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT, dept_id INTEGER, role_name TEXT, role_key TEXT, role_sort INTEGER, data_scope TEXT,
|
||||
menu_check_strictly INTEGER, dept_check_strictly INTEGER, status TEXT, is_pass INTEGER, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, deleted_by INTEGER, deleted_at DATETIME, remark TEXT)`)
|
||||
mustExecRole(t, ctx, `CREATE TABLE sys_role_menu (tenant_id TEXT, role_id INTEGER, menu_id INTEGER, data_scope TEXT)`)
|
||||
mustExecRole(t, ctx, `CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)`)
|
||||
mustExecRole(t, ctx, `CREATE TABLE sys_user_role (tenant_id TEXT DEFAULT '000000', user_id INTEGER, role_id INTEGER)`)
|
||||
mustExecRole(t, ctx, `CREATE TABLE sys_menu (menu_id INTEGER PRIMARY KEY, menu_name TEXT, parent_id INTEGER, level INTEGER, tree TEXT, order_num INTEGER DEFAULT 0, path TEXT, component TEXT, query_param TEXT, is_frame INTEGER, is_cache INTEGER, menu_type TEXT, visible TEXT, status TEXT DEFAULT '0', perms TEXT, icon TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, remark TEXT)`)
|
||||
mustExecRole(t, ctx, `INSERT INTO sys_menu (menu_id, menu_name, parent_id, status) VALUES (100,'卡片信息',0,'0')`)
|
||||
}
|
||||
|
||||
func mustExecRole(t *testing.T, ctx context.Context, sql string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx, sql); err != nil {
|
||||
t.Fatalf("execute sql failed: %v\nsql=%s", err, sql)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestEnsureRoleNotInheritedAllowsSuperAdmin(t *testing.T) {
|
||||
ctx := contexts.Set(context.Background(), &model.Context{
|
||||
User: &model.Identity{BaseClaims: model.BaseClaims{
|
||||
TenantId: "000000",
|
||||
Roles: []model.Role{{RoleId: consts.SuperAdminRoleId}},
|
||||
}},
|
||||
})
|
||||
|
||||
if err := NewAgentRole().ensureRoleNotInherited(ctx, 100); err != nil {
|
||||
t.Fatalf("super admin should edit inherited roles, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentSuperAdminChecksLiveRoleAssignment(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := context.Background()
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,status) VALUES (1,'000000',1,'0')")
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_user_role (tenant_id,user_id,role_id) VALUES ('000000',99,1)")
|
||||
isAdmin, err := rolepolicy.CurrentSuperAdmin(ctx, 99)
|
||||
if err != nil || !isAdmin {
|
||||
t.Fatalf("expected active administrator: %v %v", isAdmin, err)
|
||||
}
|
||||
mustExecAgentRoleSyncTest(t, ctx, "DELETE FROM sys_user_role WHERE user_id=99")
|
||||
isAdmin, err = rolepolicy.CurrentSuperAdmin(ctx, 99)
|
||||
if err != nil || isAdmin {
|
||||
t.Fatalf("revoked administrator must lose access: %v %v", isAdmin, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoActiveRolesCannotReadFilteredData(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 99, TenantId: "000000", DeptId: 2}}})
|
||||
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,status) VALUES (21,'000000',2,'0')")
|
||||
count, err := g.DB().Model("sys_role").Ctx(ctx).Handler(handler.FilterAuth).Count()
|
||||
if err != nil || count != 0 {
|
||||
t.Fatalf("user without roles must not read filtered data: %d %v", count, err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupAgentRoleSyncTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
if err := gdb.SetConfig(gdb.Config{
|
||||
"default": gdb.ConfigGroup{
|
||||
{Type: "sqlite", Link: "sqlite::@file(:memory:)", Debug: false},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("configure sqlite failed: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
t.Cleanup(func() { _ = g.DB().Close(ctx) })
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, parent_id INTEGER, dept_type INTEGER, status TEXT DEFAULT '0', dept_role INTEGER, deleted_at DATETIME)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_config (config_id INTEGER PRIMARY KEY, tenant_id TEXT, config_name TEXT, config_key TEXT, config_value TEXT, config_type TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, remark TEXT)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_role (
|
||||
role_id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT, dept_id INTEGER, role_name TEXT, role_key TEXT, role_sort INTEGER, data_scope TEXT,
|
||||
menu_check_strictly INTEGER, dept_check_strictly INTEGER, status TEXT, is_pass INTEGER, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, deleted_by INTEGER, deleted_at DATETIME, remark TEXT)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_role_menu (tenant_id TEXT, role_id INTEGER, menu_id INTEGER, data_scope TEXT)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)`)
|
||||
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_user_role (tenant_id TEXT DEFAULT '000000', user_id INTEGER, role_id INTEGER)`)
|
||||
}
|
||||
|
||||
func mustExecAgentRoleSyncTest(t *testing.T, ctx context.Context, sql string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx, sql); err != nil {
|
||||
t.Fatalf("execute sql failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertAgentRoleSyncCount(t *testing.T, ctx context.Context, sql string, expected int) {
|
||||
t.Helper()
|
||||
count, err := g.DB().GetValue(ctx, sql)
|
||||
if err != nil {
|
||||
t.Fatalf("query count failed: %v", err)
|
||||
}
|
||||
if count.Int() != expected {
|
||||
t.Fatalf("count = %d, want %d, sql=%s", count.Int(), expected, sql)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
type sAgentScope struct {
|
||||
*agentBase
|
||||
}
|
||||
|
||||
func NewAgentScope() *sAgentScope {
|
||||
return &sAgentScope{
|
||||
agentBase: newAgentBase(),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAgentScope(NewAgentScope())
|
||||
}
|
||||
|
||||
func (s *sAgentScope) ScopeDeptIds(ctx context.Context) (deptIds []int64, err error) {
|
||||
return s.scopeDeptIds(ctx)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
type templateRoleService struct{ service.ISysRole }
|
||||
|
||||
func (templateRoleService) ModelQuery(ctx context.Context, _ ...*handler.Option) *gdb.Model {
|
||||
return g.DB().Model("sys_role").Ctx(ctx).Handler(handler.FilterTenant)
|
||||
}
|
||||
|
||||
func TestConfiguredTemplateUsesInheritedDefaultAtEveryLevel(t *testing.T) {
|
||||
setupAgentRoleSyncTestDB(t)
|
||||
service.RegisterSysDept(paginationDeptService{})
|
||||
service.RegisterSysRole(templateRoleService{})
|
||||
t.Cleanup(func() {
|
||||
service.RegisterSysDept(nil)
|
||||
service.RegisterSysRole(nil)
|
||||
})
|
||||
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 99, TenantId: "000000", DeptId: 2}}})
|
||||
for _, statement := range []string{
|
||||
"ALTER TABLE sys_dept ADD COLUMN dept_name TEXT",
|
||||
"INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',0,1,0),(2,'000000',1,1,21),(3,'000000',2,1,31)",
|
||||
"INSERT INTO sys_role(role_id,tenant_id,dept_id,status,is_pass,created_dept,data_scope) VALUES(10,'000000',1,'0',0,1,'4'),(21,'000000',2,'0',0,1,'4'),(31,'000000',3,'0',0,2,'4')",
|
||||
"INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES('000000','agent.role.template.role_id','10')",
|
||||
"INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',10,100,'0'),('000000',10,200,'0'),('000000',21,100,'0'),('000000',21,300,'0'),('000000',31,100,'0')",
|
||||
} {
|
||||
mustExecAgentRoleSyncTest(t, ctx, statement)
|
||||
}
|
||||
for _, deptID := range []int64{1, 2, 3} {
|
||||
source, menus, err := NewAgentDept().passRoleByDept(ctx, deptID)
|
||||
if err != nil || source == nil {
|
||||
t.Fatalf("source for department %d: %+v %v", deptID, source, err)
|
||||
}
|
||||
expected := map[int64][]int64{1: {100, 200}, 2: {100, 300}, 3: {100}}[deptID]
|
||||
got := make([]int64, 0, len(menus))
|
||||
for _, menu := range menus {
|
||||
got = append(got, menu.MenuId)
|
||||
}
|
||||
slices.Sort(got)
|
||||
if !slices.Equal(got, expected) {
|
||||
t.Fatalf("department %d must copy its current default role including platform expansion: %v, want %v", deptID, got, expected)
|
||||
}
|
||||
}
|
||||
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_dept SET dept_role=0 WHERE dept_id=2")
|
||||
if _, _, err := NewAgentDept().passRoleByDept(ctx, 2); err == nil {
|
||||
t.Fatal("missing default role must not fall back to full platform permissions")
|
||||
}
|
||||
if err := NewAgentDept().EnsureCurrentDeptDefaultRole(ctx); err == nil {
|
||||
t.Fatal("read request must report missing default role")
|
||||
}
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role", 3)
|
||||
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_user_role", 0)
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/contexts"
|
||||
"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"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sAgentUser struct {
|
||||
*agentBase
|
||||
}
|
||||
|
||||
func NewAgentUser() *sAgentUser {
|
||||
return &sAgentUser{
|
||||
agentBase: newAgentBase(),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAgentUser(NewAgentUser())
|
||||
}
|
||||
|
||||
// UserList 查询用户列表
|
||||
func (s *sAgentUser) UserList(ctx context.Context, in *model.AgentUserListParam) (list []*model.AgentUserListModel, total int, err error) {
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
if deptId == 0 {
|
||||
return nil, 0, gerror.New("当前用户未绑定组织")
|
||||
}
|
||||
if in.Page <= 0 {
|
||||
in.Page = 1
|
||||
}
|
||||
if in.PageSize <= 0 {
|
||||
in.PageSize = 20
|
||||
}
|
||||
m := s.userModel(ctx).Where(dao.SysUser.Columns().DeptId, deptId)
|
||||
if in.UserName != "" {
|
||||
m = m.WhereLike(dao.SysUser.Columns().UserName, "%"+in.UserName+"%")
|
||||
}
|
||||
if in.NickName != "" {
|
||||
m = m.WhereLike(dao.SysUser.Columns().NickName, "%"+in.NickName+"%")
|
||||
}
|
||||
if in.Email != "" {
|
||||
m = m.WhereLike(dao.SysUser.Columns().Email, "%"+in.Email+"%")
|
||||
}
|
||||
if in.Phonenumber != "" {
|
||||
m = m.WhereLike(dao.SysUser.Columns().Phonenumber, "%"+in.Phonenumber+"%")
|
||||
}
|
||||
if in.Status != "" {
|
||||
m = m.Where(dao.SysUser.Columns().Status, in.Status)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
begin, end := gtime.NewFromStr(in.CreatedAt[0]), gtime.NewFromStr(in.CreatedAt[1])
|
||||
m = m.WhereBetween(dao.SysUser.Columns().CreatedAt, begin, end.EndOfDay())
|
||||
}
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var sysList []*model.SysUserListModel
|
||||
err = m.Order(dao.SysUser.Columns().UserId, "ASC").WithAll().Page(in.Page, in.PageSize).Scan(&sysList)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err = s.fillRolesAndDefaultFlag(ctx, sysList); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err = gconv.Structs(sysList, &list); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// UserView 查看用户详情
|
||||
func (s *sAgentUser) UserView(ctx context.Context, userId int64) (res *model.AgentUserViewModel, err error) {
|
||||
sysUser, err := service.SysUser().GetUserById(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sysUser == nil || sysUser.UserId == 0 {
|
||||
return nil, gerror.New("用户不存在")
|
||||
}
|
||||
if sysUser.DeptId != contexts.GetDeptId(ctx) {
|
||||
return nil, gerror.New("只能操作本级用户")
|
||||
}
|
||||
res = &model.AgentUserViewModel{}
|
||||
if err = gconv.Struct(sysUser, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.IsDefaultUser, err = s.isDefaultUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// filterRoleIdsByDept 过滤角色ID,只保留本级及下一级角色
|
||||
func (s *sAgentUser) filterRoleIdsByDept(ctx context.Context, deptId int64, roleIds []int64) (res []int64, err error) {
|
||||
if len(roleIds) == 0 {
|
||||
return roleIds, nil
|
||||
}
|
||||
var roles []*entity.SysRole
|
||||
err = s.roleModel(ctx).
|
||||
WhereIn(dao.SysRole.Columns().RoleId, roleIds).
|
||||
Where(dao.SysRole.Columns().DeptId, deptId).
|
||||
Where("is_pass", 0).
|
||||
Scan(&roles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, role := range roles {
|
||||
res = append(res, role.RoleId)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// fillRolesAndDefaultFlag 填充用户角色和默认标志
|
||||
func (s *sAgentUser) fillRolesAndDefaultFlag(ctx context.Context, list []*model.SysUserListModel) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
userIds := make([]int64, 0, len(list))
|
||||
deptIds := make([]int64, 0, len(list))
|
||||
userById := make(map[int64]*model.SysUserListModel, len(list))
|
||||
for _, user := range list {
|
||||
userIds = append(userIds, user.UserId)
|
||||
deptIds = append(deptIds, user.DeptId)
|
||||
userById[user.UserId] = user
|
||||
}
|
||||
|
||||
var userRoles []*entity.SysUserRole
|
||||
err := dao.SysUserRole.Ctx(ctx).
|
||||
WhereIn(dao.SysUserRole.Columns().UserId, userIds).
|
||||
Scan(&userRoles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
roleIds := make([]int64, 0, len(userRoles))
|
||||
userRoleMap := make(map[int64][]int64, len(userRoles))
|
||||
for _, userRole := range userRoles {
|
||||
roleIds = append(roleIds, userRole.RoleId)
|
||||
userRoleMap[userRole.UserId] = append(userRoleMap[userRole.UserId], userRole.RoleId)
|
||||
}
|
||||
if len(roleIds) > 0 {
|
||||
var roles []*entity.SysRole
|
||||
if err = s.roleModel(ctx).
|
||||
WhereIn(dao.SysRole.Columns().RoleId, roleIds).
|
||||
Scan(&roles); err != nil {
|
||||
return err
|
||||
}
|
||||
roleMap := make(map[int64]*entity.SysRole, len(roles))
|
||||
for _, role := range roles {
|
||||
roleMap[role.RoleId] = role
|
||||
}
|
||||
for _, userRole := range userRoles {
|
||||
role := roleMap[userRole.RoleId]
|
||||
user := userById[userRole.UserId]
|
||||
if role == nil || user == nil {
|
||||
continue
|
||||
}
|
||||
user.Roles = append(user.Roles, &model.SysRoleMiniModel{
|
||||
RoleId: role.RoleId,
|
||||
RoleName: role.RoleName,
|
||||
DataScope: role.DataScope,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var depts []*entity.SysDept
|
||||
if err = s.deptModel(ctx).
|
||||
WhereIn(dao.SysDept.Columns().DeptId, deptIds).
|
||||
Scan(&depts); err != nil {
|
||||
return err
|
||||
}
|
||||
deptRoleMap := make(map[int64]int64, len(depts))
|
||||
for _, dept := range depts {
|
||||
if dept.DeptRole > 0 {
|
||||
deptRoleMap[dept.DeptId] = dept.DeptRole
|
||||
}
|
||||
}
|
||||
for _, user := range list {
|
||||
deptRoleId := deptRoleMap[user.DeptId]
|
||||
if deptRoleId == 0 {
|
||||
continue
|
||||
}
|
||||
for _, roleId := range userRoleMap[user.UserId] {
|
||||
if roleId == deptRoleId {
|
||||
user.IsDefaultUser = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDefaultUser 判断用户是否为默认用户
|
||||
func (s *sAgentUser) isDefaultUser(ctx context.Context, userId int64) (bool, error) {
|
||||
var user *entity.SysUser
|
||||
if err := s.userModel(ctx).
|
||||
Where(dao.SysUser.Columns().UserId, userId).
|
||||
Scan(&user); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if user == nil || user.UserId == 0 {
|
||||
return false, gerror.New("用户不存在")
|
||||
}
|
||||
var dept *entity.SysDept
|
||||
if err := s.deptModel(ctx).
|
||||
Where(dao.SysDept.Columns().DeptId, user.DeptId).
|
||||
Scan(&dept); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if dept == nil || dept.DeptRole == 0 {
|
||||
return false, nil
|
||||
}
|
||||
count, err := dao.SysUserRole.Ctx(ctx).
|
||||
Where(dao.SysUserRole.Columns().UserId, userId).
|
||||
Where(dao.SysUserRole.Columns().RoleId, dept.DeptRole).
|
||||
Count()
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// UserSave 新增/修改用户
|
||||
func (s *sAgentUser) UserSave(ctx context.Context, in *model.SysUserAddModel) (res *model.AgentUserViewModel, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil, gerror.New("登录已过期")
|
||||
}
|
||||
in.TenantId = user.TenantId
|
||||
in.DeptId = user.DeptId
|
||||
in.UserType = consts.SysUserTypeSys
|
||||
if in.DeptId == 0 {
|
||||
return nil, gerror.New("当前用户未绑定组织")
|
||||
}
|
||||
if len(in.RoleIds) == 0 {
|
||||
return nil, gerror.New("请选择角色")
|
||||
}
|
||||
in.RoleIds, err = s.filterRoleIdsByDept(ctx, in.DeptId, in.RoleIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(in.RoleIds) == 0 {
|
||||
return nil, gerror.New("请选择本级角色")
|
||||
}
|
||||
// 验证角色的DeptId和用户的deptId一致
|
||||
if err = s.ensureRoleDeptInScope(ctx, in.DeptId, in.RoleIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sysUser, err := service.SysUser().AddUser(ctx, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = &model.AgentUserViewModel{}
|
||||
if err = gconv.Struct(sysUser, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.IsDefaultUser, err = s.isDefaultUser(ctx, res.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UserUpdate 更新用户
|
||||
func (s *sAgentUser) UserUpdate(ctx context.Context, in *model.SysUserUpdateModel) (err error) {
|
||||
current, err := s.UserView(ctx, in.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentDeptId := contexts.GetDeptId(ctx)
|
||||
if currentDeptId == 0 {
|
||||
return gerror.New("当前用户未绑定组织")
|
||||
}
|
||||
if current.DeptId != currentDeptId {
|
||||
return gerror.New("只能修改本级用户")
|
||||
}
|
||||
in.DeptId = ¤t.DeptId
|
||||
if len(in.RoleIds) == 0 {
|
||||
return gerror.New("请选择角色")
|
||||
}
|
||||
in.RoleIds, err = s.filterRoleIdsByDept(ctx, *in.DeptId, in.RoleIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(in.RoleIds) == 0 {
|
||||
return gerror.New("请选择本级角色")
|
||||
}
|
||||
userType := consts.SysUserTypeSys
|
||||
in.UserType = &userType
|
||||
// 验证角色的DeptId和用户的deptId一致
|
||||
if err = s.ensureRoleDeptInScope(ctx, *in.DeptId, in.RoleIds); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.SysUser().UpdateUser(ctx, in)
|
||||
}
|
||||
|
||||
// UserDelete 删除用户
|
||||
func (s *sAgentUser) UserDelete(ctx context.Context, userIds []int64) (err error) {
|
||||
for _, userId := range userIds {
|
||||
if _, err = s.UserView(ctx, userId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return service.SysUser().DeleteUser(ctx, userIds)
|
||||
}
|
||||
|
||||
// UserResetPassword 重置用户密码
|
||||
func (s *sAgentUser) UserResetPassword(ctx context.Context, userId int64, password string) (err error) {
|
||||
if _, err = s.UserView(ctx, userId); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.SysUser().ResetPassword(ctx, userId, password)
|
||||
}
|
||||
|
||||
// 验证角色部门和用户部门是否一致
|
||||
func (s *sAgentUser) ensureRoleDeptInScope(ctx context.Context, deptId int64, roleIds []int64) (err error) {
|
||||
// 验证角色的DeptId和用户的deptId一致
|
||||
var roles []*entity.SysRole
|
||||
err = s.roleModel(ctx).
|
||||
WhereIn(dao.SysRole.Columns().RoleId, roleIds).
|
||||
Scan(&roles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, role := range roles {
|
||||
if role.DeptId != deptId {
|
||||
return gerror.New("角色部门与用户部门不一致")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Package iot
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sAgentEnterprise struct{}
|
||||
|
||||
func NewAgentEnterprise() *sAgentEnterprise {
|
||||
return &sAgentEnterprise{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAgentEnterprise(NewAgentEnterprise())
|
||||
}
|
||||
|
||||
// Model 企业客户单位信息ORM模型
|
||||
func (s *sAgentEnterprise) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TAgentEnterprise.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取企业客户单位信息列表
|
||||
func (s *sAgentEnterprise) List(ctx context.Context, in *model.TAgentEnterpriseListParam) (list []*model.TAgentEnterpriseListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.TAgentEnterpriseListModel{})
|
||||
|
||||
// 查询主键ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TAgentEnterprise.Columns().Id, in.Id)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TAgentEnterprise.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TAgentEnterprise.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取企业客户单位信息列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出企业客户单位信息
|
||||
func (s *sAgentEnterprise) Export(ctx context.Context, in *model.TAgentEnterpriseListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TAgentEnterpriseExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出企业客户单位信息-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TAgentEnterpriseExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增企业客户单位信息
|
||||
func (s *sAgentEnterprise) Edit(ctx context.Context, in *model.TAgentEnterpriseEditParam) (err error) {
|
||||
// 验证'LegalPersonIdCard'唯一
|
||||
if err = xgorm.IsUnique(ctx, &dao.TAgentEnterprise, g.Map{dao.TAgentEnterprise.Columns().LegalPersonIdCard: in.LegalPersonIdCard}, "法人身份证号已存在", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &userId
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TAgentEnterpriseUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改企业客户单位信息失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &deptId
|
||||
userId := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &userId
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TAgentEnterpriseInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增企业客户单位信息失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除企业客户单位信息
|
||||
func (s *sAgentEnterprise) Delete(ctx context.Context, in *model.TAgentEnterpriseDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).Where(dao.TAgentEnterprise.Columns().DeptId, in.DeptId).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除企业客户单位信息失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取企业客户单位信息指定信息
|
||||
func (s *sAgentEnterprise) View(ctx context.Context, in *model.TAgentEnterpriseViewParam) (res *model.TAgentEnterpriseViewModel, err error) {
|
||||
if err = s.Model(ctx).Where(dao.TAgentEnterprise.Columns().DeptId, in.DeptId).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取企业客户单位信息信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Package iot
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type sAgentTApi struct{}
|
||||
|
||||
func NewAgentTApi() *sAgentTApi {
|
||||
return &sAgentTApi{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAgentTApi(NewAgentTApi())
|
||||
}
|
||||
|
||||
// Model API集成ORM模型
|
||||
func (s *sAgentTApi) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TApi.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取API集成列表
|
||||
func (s *sAgentTApi) List(ctx context.Context, in *model.TApiListParam) (list []*model.TApiListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
tApiTable := dao.TApi.Table()
|
||||
sysDeptTable := dao.SysDept.Table()
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.FieldsPrefix(tApiTable, model.TApiListModel{})
|
||||
mod = mod.Fields(
|
||||
sysDeptTable+"."+dao.SysDept.Columns().DeptKey+" AS deptKey",
|
||||
sysDeptTable+"."+dao.SysDept.Columns().DeptName+" AS deptName",
|
||||
)
|
||||
mod = mod.LeftJoin(
|
||||
sysDeptTable,
|
||||
sysDeptTable+"."+dao.SysDept.Columns().DeptId+"="+tApiTable+"."+dao.TApi.Columns().DeptId,
|
||||
)
|
||||
|
||||
// 查询APPKey
|
||||
if in.AppKey != "" {
|
||||
mod = mod.Where(dao.TApi.Columns().AppKey+" like ?", "%"+in.AppKey+"%")
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TApi.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取API集成列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出API集成(异步:提交任务后到下载中心取文件)
|
||||
func (s *sAgentTApi) Export(ctx context.Context, in *model.TApiListParam) (err error) {
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tApi",
|
||||
BizName: "API集成",
|
||||
PermCode: "cpm:agent:tApi:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TApiListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增API集成
|
||||
func (s *sAgentTApi) Edit(ctx context.Context, in *model.TApiEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
in.UpdatedBy = &userId
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TApiUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改API集成失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &deptId
|
||||
userId := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &userId
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TApiInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增API集成失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除API集成
|
||||
func (s *sAgentTApi) Delete(ctx context.Context, in *model.TApiDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除API集成失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取API集成指定信息
|
||||
func (s *sAgentTApi) View(ctx context.Context, in *model.TApiViewParam) (res *model.TApiViewModel, err error) {
|
||||
if err = s.Model(ctx).Where(dao.TApi.Columns().AppKey, in.AppKey).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取API集成信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
|
||||
"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"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sAgentTApiGroup struct{}
|
||||
|
||||
// NewAgentTApiGroup 创建开放接口权限组后台服务实例。
|
||||
func NewAgentTApiGroup() *sAgentTApiGroup {
|
||||
return &sAgentTApiGroup{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAgentTApiGroup(NewAgentTApiGroup())
|
||||
}
|
||||
|
||||
// Model 返回开放接口权限组 ORM 模型,默认启用租户和数据权限过滤。
|
||||
func (s *sAgentTApiGroup) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TApiGroup.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 分页查询开放接口权限组列表。
|
||||
func (s *sAgentTApiGroup) List(ctx context.Context, in *model.TApiGroupListParam) (list []*model.TApiGroupListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx).Fields(model.TApiGroupListModel{})
|
||||
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TApiGroup.Columns().Id, in.Id)
|
||||
}
|
||||
if in.Status > 0 {
|
||||
mod = mod.Where(dao.TApiGroup.Columns().Status, in.Status)
|
||||
}
|
||||
if in.GroupName != "" {
|
||||
mod = mod.Where(dao.TApiGroup.Columns().GroupName+" LIKE ?", "%"+in.GroupName+"%")
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
mod = mod.WhereBetween(dao.TApiGroup.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
mod = mod.Page(in.Page, in.PageSize).
|
||||
OrderDesc(dao.TApiGroup.Columns().Id).
|
||||
Hook(hook.MemberSummary)
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取开放接口权限组列表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出开放接口权限组(异步:提交任务后到下载中心取文件)
|
||||
func (s *sAgentTApiGroup) Export(ctx context.Context, in *model.TApiGroupListParam) (err error) {
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tApiGroup",
|
||||
BizName: "开放接口权限组",
|
||||
PermCode: "cpm:agent:tApiGroup:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TApiGroupListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 新增或修改开放接口权限组,并同步维护权限组与接口资源的关联。
|
||||
func (s *sAgentTApiGroup) Edit(ctx context.Context, in *model.TApiGroupEditParam) (err error) {
|
||||
if in.GroupCode != nil {
|
||||
if err = xgorm.IsUnique(ctx, &dao.TApiGroup, g.Map{dao.TApiGroup.Columns().GroupCode: *in.GroupCode}, "权限组编码已存在", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
groupId := in.Id
|
||||
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TApiGroupUpdateFields{}).
|
||||
WherePri(in.Id).
|
||||
Data(in).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "修改开放接口权限组失败,请稍后重试!")
|
||||
}
|
||||
return s.replaceGroupResources(ctx, tx, groupId, in.ApiIds)
|
||||
}
|
||||
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
groupId, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TApiGroupInsertFields{}).
|
||||
Data(in).
|
||||
InsertAndGetId()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "新增开放接口权限组失败,请稍后重试!")
|
||||
}
|
||||
return s.replaceGroupResources(ctx, tx, groupId, in.ApiIds)
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除开放接口权限组,并删除该权限组下的资源关联。
|
||||
func (s *sAgentTApiGroup) Delete(ctx context.Context, in *model.TApiGroupDeleteParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
groupIds := gconv.Int64s(in.Id)
|
||||
if len(groupIds) > 0 {
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TApiGroupResource.Table()).
|
||||
WhereIn(dao.TApiGroupResource.Columns().GroupId, groupIds).
|
||||
Delete(); err != nil {
|
||||
return gerror.Wrap(err, "删除开放接口权限组资源关联失败,请稍后重试!")
|
||||
}
|
||||
}
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
return gerror.Wrap(err, "删除开放接口权限组失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// View returns the API group detail with bound resource IDs and resource list.
|
||||
func (s *sAgentTApiGroup) View(ctx context.Context, in *model.TApiGroupViewParam) (res *model.TApiGroupViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
return nil, gerror.Wrap(err, "get open api group failed")
|
||||
}
|
||||
if res == nil {
|
||||
return
|
||||
}
|
||||
apiIds, err := dao.TApiGroupResource.Ctx(ctx).
|
||||
Fields(dao.TApiGroupResource.Columns().ApiId).
|
||||
Where(dao.TApiGroupResource.Columns().GroupId, in.Id).
|
||||
Array()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "get open api group resource relations failed")
|
||||
}
|
||||
res.ApiIds = gconv.Int64s(apiIds)
|
||||
res.Apis = []*model.TApiResourceListModel{}
|
||||
if len(res.ApiIds) > 0 {
|
||||
if err = handler.Model(dao.TApiResource.Ctx(ctx), &handler.Option{FilterTenant: true, FilterAuth: false}).
|
||||
Fields(model.TApiResourceListModel{}).
|
||||
WhereIn(dao.TApiResource.Columns().Id, res.ApiIds).
|
||||
OrderAsc(dao.TApiResource.Columns().Id).
|
||||
Hook(hook.MemberSummary).
|
||||
Scan(&res.Apis); err != nil {
|
||||
return nil, gerror.Wrap(err, "get open api group resources failed")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新开放接口权限组启停状态。
|
||||
func (s *sAgentTApiGroup) Status(ctx context.Context, in *model.TApiGroupStatusParam) (err error) {
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
|
||||
dao.TApiGroup.Columns().Status: in.Status,
|
||||
dao.TApiGroup.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
||||
dao.TApiGroup.Columns().UpdatedAt: gtime.Now(),
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新开放接口权限组状态失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// replaceGroupResources 替换权限组绑定的接口资源,先清空旧关联再批量写入新关联。
|
||||
func (s *sAgentTApiGroup) replaceGroupResources(ctx context.Context, tx gdb.TX, groupId int64, apiIds []int64) (err error) {
|
||||
if groupId <= 0 {
|
||||
return gerror.New("权限组ID不能为空")
|
||||
}
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TApiGroupResource.Table()).
|
||||
Where(dao.TApiGroupResource.Columns().GroupId, groupId).
|
||||
Delete(); err != nil {
|
||||
return gerror.Wrap(err, "删除开放接口权限组资源关联失败,请稍后重试!")
|
||||
}
|
||||
|
||||
seen := make(map[int64]struct{}, len(apiIds))
|
||||
data := g.List{}
|
||||
for _, apiId := range apiIds {
|
||||
if apiId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[apiId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[apiId] = struct{}{}
|
||||
data = append(data, g.Map{
|
||||
dao.TApiGroupResource.Columns().GroupId: groupId,
|
||||
dao.TApiGroupResource.Columns().ApiId: apiId,
|
||||
})
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TApiGroupResource.Table()).Data(data).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "保存开放接口权限组资源关联失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
|
||||
"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"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sAgentTApiResource struct{}
|
||||
|
||||
// NewAgentTApiResource 创建开放接口资源后台服务实例。
|
||||
func NewAgentTApiResource() *sAgentTApiResource {
|
||||
return &sAgentTApiResource{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAgentTApiResource(NewAgentTApiResource())
|
||||
}
|
||||
|
||||
// Model 返回开放接口资源 ORM 模型,默认启用租户和数据权限过滤。
|
||||
func (s *sAgentTApiResource) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TApiResource.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 分页查询开放接口资源列表。
|
||||
func (s *sAgentTApiResource) List(ctx context.Context, in *model.TApiResourceListParam) (list []*model.TApiResourceListModel, totalCount int, err error) {
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
if deptId <= 0 {
|
||||
return []*model.TApiResourceListModel{}, 0, nil
|
||||
}
|
||||
|
||||
mod := s.Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false}).
|
||||
Fields(model.TApiResourceListModel{})
|
||||
|
||||
isTop, dept, err := service.AgentDept().IsTopDept(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if dept != nil && (dept.DeptId == 100 || dept.ParentId == 100) {
|
||||
isTop = true
|
||||
}
|
||||
|
||||
if !isTop {
|
||||
var apiInfo struct {
|
||||
GroupId int64 `orm:"group_id"`
|
||||
}
|
||||
if err = dao.TApi.Ctx(ctx).
|
||||
Fields(dao.TApi.Columns().GroupId).
|
||||
Where(dao.TApi.Columns().DeptId, deptId).
|
||||
Where(dao.TApi.Columns().TenantId, contexts.GetTenantId(ctx)).
|
||||
OrderDesc(dao.TApi.Columns().Id).
|
||||
Scan(&apiInfo); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "get current dept open api group failed")
|
||||
}
|
||||
if apiInfo.GroupId <= 0 {
|
||||
return []*model.TApiResourceListModel{}, 0, nil
|
||||
}
|
||||
|
||||
apiIds, err := dao.TApiGroupResource.Ctx(ctx).
|
||||
Fields(dao.TApiGroupResource.Columns().ApiId).
|
||||
Where(dao.TApiGroupResource.Columns().GroupId, apiInfo.GroupId).
|
||||
Array()
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "get open api group resources failed")
|
||||
}
|
||||
if len(apiIds) == 0 {
|
||||
return []*model.TApiResourceListModel{}, 0, nil
|
||||
}
|
||||
mod = mod.WhereIn(dao.TApiResource.Columns().Id, gconv.Int64s(apiIds))
|
||||
}
|
||||
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TApiResource.Columns().Id, in.Id)
|
||||
}
|
||||
if in.Status > 0 {
|
||||
mod = mod.Where(dao.TApiResource.Columns().Status, in.Status)
|
||||
}
|
||||
if in.ApiName != "" {
|
||||
mod = mod.Where(dao.TApiResource.Columns().ApiName+" LIKE ?", "%"+in.ApiName+"%")
|
||||
}
|
||||
if in.Version != "" {
|
||||
mod = mod.Where(dao.TApiResource.Columns().Version+" LIKE ?", "%"+in.Version+"%")
|
||||
}
|
||||
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
mod = mod.WhereBetween(dao.TApiResource.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
mod = mod.Page(in.Page, in.PageSize).
|
||||
OrderDesc(dao.TApiResource.Columns().Id).
|
||||
Hook(hook.MemberSummary)
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取开放接口资源列表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出开放接口资源(异步:提交任务后到下载中心取文件)
|
||||
func (s *sAgentTApiResource) Export(ctx context.Context, in *model.TApiResourceListParam) (err error) {
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tApiResource",
|
||||
BizName: "开放接口资源",
|
||||
PermCode: "cpm:agent:tApiResource:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TApiResourceListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 新增或修改开放接口资源。
|
||||
func (s *sAgentTApiResource) Edit(ctx context.Context, in *model.TApiResourceEditParam) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if in.Status == nil {
|
||||
status := 0
|
||||
in.Status = &status
|
||||
}
|
||||
if in.Method != nil {
|
||||
method := *in.Method
|
||||
in.Method = &method
|
||||
}
|
||||
|
||||
if in.ApiCode != nil && gstr.Trim(*in.ApiCode) != "" {
|
||||
if err = xgorm.IsUnique(ctx, &dao.TApiResource, g.Map{dao.TApiResource.Columns().ApiCode: gstr.Trim(*in.ApiCode)}, "接口权限标识已存在", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TApiResourceUpdateFields{}).
|
||||
WherePri(in.Id).
|
||||
Data(in).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "修改开放接口资源失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TApiResourceInsertFields{}).
|
||||
Data(in).
|
||||
Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增开放接口资源失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除开放接口资源,并清理权限组资源关联。
|
||||
func (s *sAgentTApiResource) Delete(ctx context.Context, in *model.TApiResourceDeleteParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
apiIds := gconv.Int64s(in.Id)
|
||||
if len(apiIds) > 0 {
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TApiGroupResource.Table()).
|
||||
WhereIn(dao.TApiGroupResource.Columns().ApiId, apiIds).
|
||||
Delete(); err != nil {
|
||||
return gerror.Wrap(err, "删除开放接口权限组资源关联失败,请稍后重试!")
|
||||
}
|
||||
}
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
return gerror.Wrap(err, "删除开放接口资源失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// View 查询开放接口资源详情。
|
||||
func (s *sAgentTApiResource) View(ctx context.Context, in *model.TApiResourceViewParam) (res *model.TApiResourceViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取开放接口资源信息失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新开放接口资源启停状态。
|
||||
func (s *sAgentTApiResource) Status(ctx context.Context, in *model.TApiResourceStatusParam) (err error) {
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
|
||||
dao.TApiResource.Columns().Status: in.Status,
|
||||
dao.TApiResource.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
||||
dao.TApiResource.Columns().UpdatedAt: gtime.Now(),
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新开放接口资源状态失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Package iot
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sAgentTCustWithdrawAccount struct{}
|
||||
|
||||
func NewAgentTCustWithdrawAccount() *sAgentTCustWithdrawAccount {
|
||||
return &sAgentTCustWithdrawAccount{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAgentTCustWithdrawAccount(NewAgentTCustWithdrawAccount())
|
||||
}
|
||||
|
||||
// Model 客户提现账户表ORM模型
|
||||
func (s *sAgentTCustWithdrawAccount) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TCustWithdrawAccount.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取客户提现账户表列表
|
||||
func (s *sAgentTCustWithdrawAccount) List(ctx context.Context, in *model.TCustWithdrawAccountListParam) (list []*model.TCustWithdrawAccountListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.TCustWithdrawAccountListModel{})
|
||||
|
||||
// 查询主键ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TCustWithdrawAccount.Columns().Id, in.Id)
|
||||
}
|
||||
|
||||
// 查询状态:0-禁用 1-正常
|
||||
if in.Status > 0 {
|
||||
mod = mod.Where(dao.TCustWithdrawAccount.Columns().Status, in.Status)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TCustWithdrawAccount.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TCustWithdrawAccount.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取客户提现账户表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出客户提现账户表
|
||||
func (s *sAgentTCustWithdrawAccount) Export(ctx context.Context, in *model.TCustWithdrawAccountListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TCustWithdrawAccountExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出客户提现账户表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TCustWithdrawAccountExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增客户提现账户表
|
||||
func (s *sAgentTCustWithdrawAccount) Edit(ctx context.Context, in *model.TCustWithdrawAccountEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &userId
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TCustWithdrawAccountUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改客户提现账户表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &deptId
|
||||
userId := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &userId
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TCustWithdrawAccountInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增客户提现账户表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除客户提现账户表
|
||||
func (s *sAgentTCustWithdrawAccount) Delete(ctx context.Context, in *model.TCustWithdrawAccountDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除客户提现账户表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取客户提现账户表指定信息
|
||||
func (s *sAgentTCustWithdrawAccount) View(ctx context.Context, in *model.TCustWithdrawAccountViewParam) (res *model.TCustWithdrawAccountViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取客户提现账户表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新客户提现账户表状态
|
||||
func (s *sAgentTCustWithdrawAccount) Status(ctx context.Context, in *model.TCustWithdrawAccountStatusParam) (err error) {
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
|
||||
dao.TCustWithdrawAccount.Columns().Status: in.Status,
|
||||
dao.TCustWithdrawAccount.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
||||
}).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "更新客户提现账户表状态失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package cardflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
)
|
||||
|
||||
// MainPackageFlowExpr returns the card's base package flow. When the current
|
||||
// package table is already joined, pass its table alias to avoid another subquery.
|
||||
func MainPackageFlowExpr(cardAlias, currentPackageAlias string) string {
|
||||
cardColumns := dao.TSimCardInfo.Columns()
|
||||
packageColumns := dao.TSimFlowPackageInfo.Columns()
|
||||
cardTotalFlow := column(cardAlias, cardColumns.PackageTotalFlowMb)
|
||||
|
||||
currentPackageAlias = strings.TrimSpace(currentPackageAlias)
|
||||
if currentPackageAlias != "" {
|
||||
return fmt.Sprintf(
|
||||
"COALESCE(NULLIF(%s, 0), %s, 0)",
|
||||
column(currentPackageAlias, packageColumns.FlowMb),
|
||||
cardTotalFlow,
|
||||
)
|
||||
}
|
||||
|
||||
packageTable := dao.TSimFlowPackageInfo.Table()
|
||||
packageAlias := "card_base_pkg"
|
||||
return fmt.Sprintf(
|
||||
"COALESCE(NULLIF((SELECT %s.%s FROM %s AS %s WHERE %s.%s = %s AND %s.%s = %s LIMIT 1), 0), %s, 0)",
|
||||
packageAlias,
|
||||
packageColumns.FlowMb,
|
||||
packageTable,
|
||||
packageAlias,
|
||||
packageAlias,
|
||||
packageColumns.PackageCode,
|
||||
column(cardAlias, cardColumns.CurrentPackageCode),
|
||||
packageAlias,
|
||||
packageColumns.TenantId,
|
||||
column(cardAlias, cardColumns.TenantId),
|
||||
cardTotalFlow,
|
||||
)
|
||||
}
|
||||
|
||||
// ActiveCardTopupFlowExpr returns active, unexpired single-card add-package flow.
|
||||
func ActiveCardTopupFlowExpr(cardAlias string) string {
|
||||
cardColumns := dao.TSimCardInfo.Columns()
|
||||
bindTable := dao.TSimCardBindPkg.Table()
|
||||
bindColumns := dao.TSimCardBindPkg.Columns()
|
||||
packageTable := dao.TSimFlowPackageInfo.Table()
|
||||
packageColumns := dao.TSimFlowPackageInfo.Columns()
|
||||
bindAlias := "card_topup_bind"
|
||||
packageAlias := "card_topup_pkg"
|
||||
|
||||
return fmt.Sprintf(
|
||||
"IFNULL((SELECT SUM(IFNULL(%s.%s, 0)) FROM %s AS %s LEFT JOIN %s AS %s ON %s.%s = %s.%s AND %s.%s = %s.%s WHERE %s.%s = %s AND %s.%s = %s AND %s.%s = %d AND %s.%s = %d AND (%s.%s IS NULL OR %s.%s <= NOW()) AND (%s.%s IS NULL OR %s.%s > NOW())), 0)",
|
||||
packageAlias,
|
||||
packageColumns.FlowMb,
|
||||
bindTable,
|
||||
bindAlias,
|
||||
packageTable,
|
||||
packageAlias,
|
||||
bindAlias,
|
||||
bindColumns.PackageCode,
|
||||
packageAlias,
|
||||
packageColumns.PackageCode,
|
||||
bindAlias,
|
||||
bindColumns.TenantId,
|
||||
packageAlias,
|
||||
packageColumns.TenantId,
|
||||
bindAlias,
|
||||
bindColumns.Iccid,
|
||||
column(cardAlias, cardColumns.Iccid),
|
||||
bindAlias,
|
||||
bindColumns.TenantId,
|
||||
column(cardAlias, cardColumns.TenantId),
|
||||
bindAlias,
|
||||
bindColumns.PackageType,
|
||||
consts.PackageTypeCardAddPackage,
|
||||
bindAlias,
|
||||
bindColumns.EffectiveStatus,
|
||||
consts.EffectiveStatusEffective,
|
||||
bindAlias,
|
||||
bindColumns.EffectiveTime,
|
||||
bindAlias,
|
||||
bindColumns.EffectiveTime,
|
||||
bindAlias,
|
||||
bindColumns.ExpectedExpireTime,
|
||||
bindAlias,
|
||||
bindColumns.ExpectedExpireTime,
|
||||
)
|
||||
}
|
||||
|
||||
func TotalFlowExpr(cardAlias, currentPackageAlias string) string {
|
||||
return fmt.Sprintf(
|
||||
"(%s + %s)",
|
||||
MainPackageFlowExpr(cardAlias, currentPackageAlias),
|
||||
ActiveCardTopupFlowExpr(cardAlias),
|
||||
)
|
||||
}
|
||||
|
||||
func RemainFlowExpr(cardAlias, currentPackageAlias string) string {
|
||||
return fmt.Sprintf(
|
||||
"GREATEST(%s - IFNULL(%s, 0), 0)",
|
||||
TotalFlowExpr(cardAlias, currentPackageAlias),
|
||||
column(cardAlias, dao.TSimCardInfo.Columns().UsedFlowMb),
|
||||
)
|
||||
}
|
||||
|
||||
func ExhaustedCondition(cardAlias, currentPackageAlias string) string {
|
||||
return fmt.Sprintf(
|
||||
"%s <= IFNULL(%s, 0)",
|
||||
TotalFlowExpr(cardAlias, currentPackageAlias),
|
||||
column(cardAlias, dao.TSimCardInfo.Columns().UsedFlowMb),
|
||||
)
|
||||
}
|
||||
|
||||
func column(alias, name string) string {
|
||||
alias = strings.TrimSpace(alias)
|
||||
if alias == "" {
|
||||
return name
|
||||
}
|
||||
return alias + "." + name
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestBatchCardsFilterStatus(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
"ALTER TABLE batch_main ADD target_dept_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD dept_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD updated_by INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD batch_async_status INTEGER DEFAULT 3",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, batchType := range []int{consts.BatchTypeInbound, consts.BatchTypeOutbound, consts.BatchTypeAllocate} {
|
||||
batchNo := fmt.Sprintf("STATUS-%d", batchType)
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_main(batch_no,batch_type,batch_info) VALUES(?,?,'{}')", batchNo, batchType); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, status := range []int{0, 1, 2, 3, 4, 5} {
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(batch_no,batch_type,detail_status,row_no,item_id,error_reason,snapshot) VALUES(?,?,?,?,?,?,'{}')", batchNo, batchType, status, status+1, fmt.Sprintf("card-%d", status), fmt.Sprintf("reason-%d", status)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, status := range []int{0, 1, 2, 3, 4, 5} {
|
||||
rows, total, err := NewCommonBatchMain().ListCards(ctx, &model.BatchCardListParam{BatchNo: batchNo, DetailStatus: &status, PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil || total != 1 || len(rows) != 1 {
|
||||
t.Fatalf("type=%d status=%d total=%d err=%v", batchType, status, total, err)
|
||||
}
|
||||
if rows[0].DetailStatus != status || rows[0].ErrorReason != fmt.Sprintf("reason-%d", status) {
|
||||
t.Fatalf("incorrect detail: %+v", rows[0])
|
||||
}
|
||||
}
|
||||
for page, wantStatus := range []int{3, 0, 1, 2, 4, 5} {
|
||||
rows, total, err := NewCommonBatchMain().ListCards(ctx, &model.BatchCardListParam{BatchNo: batchNo, PageInfo: request.PageInfo{Page: page + 1, PageSize: 1}})
|
||||
if err != nil || total != 6 || len(rows) != 1 || rows[0].DetailStatus != wantStatus {
|
||||
t.Fatalf("type=%d page=%d status=%d total=%d err=%v", batchType, page+1, wantStatus, total, err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(batch_no,batch_type,detail_status,row_no,item_id,error_reason,snapshot) VALUES(?,?,?,?,?,?,'{}')", batchNo, batchType, consts.BatchDetailStatusFailed, 0, "cascade", "本批次存在失败明细,整批未执行"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, total, err := NewCommonBatchMain().ListCards(ctx, &model.BatchCardListParam{BatchNo: batchNo, PageInfo: request.PageInfo{Page: 1, PageSize: 1}})
|
||||
if err != nil || total != 7 || len(rows) != 1 || rows[0].Iccid != "card-3" {
|
||||
t.Fatalf("type=%d: real failure should precede legacy cascade: rows=%+v total=%d err=%v", batchType, rows, total, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,764 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"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/allocationunits"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"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"
|
||||
|
||||
"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/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sCommonBatchExecutionLog struct{}
|
||||
|
||||
type batchExecutionLogSummaryBatch struct {
|
||||
CreatedDept int64 `orm:"created_dept"`
|
||||
TargetDeptId int64 `orm:"target_dept_id"`
|
||||
SourceChannel string `orm:"source_channel"`
|
||||
ExecutionMode int `orm:"execution_mode"`
|
||||
Id int64 `orm:"id"`
|
||||
BatchNo string `orm:"batch_no"`
|
||||
BatchType int `orm:"batch_type"`
|
||||
SourceBatchNo string `orm:"source_batch_no"`
|
||||
SourceType *int `orm:"source_type"`
|
||||
BatchAsyncStatus int `orm:"batch_async_status"`
|
||||
RunResult int `orm:"run_result"`
|
||||
FailReason string `orm:"fail_reason"`
|
||||
TotalCount int `orm:"total_count"`
|
||||
SourceOssId int64 `orm:"source_oss_id"`
|
||||
ResultOssId int64 `orm:"result_oss_id"`
|
||||
BatchInfo *gjson.Json `orm:"batch_info"`
|
||||
StartTime *gtime.Time `orm:"start_time"`
|
||||
EndTime *gtime.Time `orm:"end_time"`
|
||||
}
|
||||
|
||||
func NewCommonBatchExecutionLog() *sCommonBatchExecutionLog {
|
||||
return &sCommonBatchExecutionLog{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonBatchExecutionLog(NewCommonBatchExecutionLog())
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) batchModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.BatchMain.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) detailModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.BatchDetail.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) downloadModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TResourceBatchDownload.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) ossModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.SysOss.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// exportTaskModel 导出任务表;导出批次的结果文件只登记在这里。
|
||||
func (s *sCommonBatchExecutionLog) exportTaskModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TExportTask.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) List(ctx context.Context, in *model.BatchExecutionLogListParam) (list []*model.BatchExecutionLogListModel, totalCount int, err error) {
|
||||
mod := s.batchModel(ctx).
|
||||
Fields(model.BatchExecutionLogListModel{})
|
||||
|
||||
if in.BatchNo != "" {
|
||||
mod = mod.Where(dao.BatchMain.Columns().BatchNo+" LIKE ?", "%"+in.BatchNo+"%")
|
||||
}
|
||||
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
mod = mod.Where(
|
||||
"("+dao.BatchMain.Columns().BatchNo+" LIKE ? OR "+dao.BatchMain.Columns().OrderSn+" LIKE ? OR "+dao.BatchMain.Columns().FailReason+" LIKE ?)",
|
||||
like,
|
||||
like,
|
||||
like,
|
||||
)
|
||||
}
|
||||
if in.BatchAsyncStatus != nil {
|
||||
mod = mod.Where(dao.BatchMain.Columns().BatchAsyncStatus, *in.BatchAsyncStatus)
|
||||
}
|
||||
if in.SourceType != nil {
|
||||
mod = mod.Where(dao.BatchMain.Columns().SourceType, *in.SourceType)
|
||||
}
|
||||
if in.SourceChannel != "" {
|
||||
mod = mod.Where(dao.BatchMain.Columns().SourceChannel, in.SourceChannel)
|
||||
}
|
||||
if in.ExecutionMode != nil {
|
||||
mod = mod.Where(dao.BatchMain.Columns().ExecutionMode, *in.ExecutionMode)
|
||||
}
|
||||
mod = mod.Where(
|
||||
fmt.Sprintf("NOT (%s = ? AND COALESCE(%s, '') <> '' AND %s <> %s)", dao.BatchMain.Columns().BatchType, dao.BatchMain.Columns().SourceBatchNo, dao.BatchMain.Columns().SourceBatchNo, dao.BatchMain.Columns().BatchNo),
|
||||
consts.BatchTypeAllocate,
|
||||
)
|
||||
mod = executionLogBatchTypeModel(mod, dao.BatchMain.Columns().BatchType, dao.BatchMain.Columns().BatchInfo, in.BatchType)
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
mod = mod.WhereBetween(dao.BatchMain.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
mod = mod.Page(in.Page, in.PageSize).
|
||||
OrderDesc(dao.BatchMain.Columns().Id).
|
||||
Hook(hook.MemberSummary)
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取执行日志列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
fields := []*allocationunits.Fields{}
|
||||
for _, item := range list {
|
||||
item.UnknownCount = batchExecutionLogUnknownCount(item.UnitInfo)
|
||||
item.RestartPhase, item.RestartResumeAt = batchRestartPhase(item.UnitInfo)
|
||||
if item.BatchType == consts.BatchTypeAllocate {
|
||||
item.Fields = allocationunits.Resolve(item.UnitInfo, item.CreatedDept, item.StoredTargetDeptId)
|
||||
fields = append(fields, &item.Fields)
|
||||
}
|
||||
}
|
||||
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), fields); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) Summary(ctx context.Context, in *model.BatchExecutionLogDetailParam) (res *model.BatchExecutionLogDetailModel, err error) {
|
||||
if in == nil || in.BatchNo == "" {
|
||||
return nil, gerror.New("批次号不能为空")
|
||||
}
|
||||
|
||||
batch := &batchExecutionLogSummaryBatch{}
|
||||
mod := executionLogBatchTypeModel(
|
||||
s.batchModel(ctx).Where(dao.BatchMain.Columns().BatchNo, in.BatchNo),
|
||||
dao.BatchMain.Columns().BatchType,
|
||||
dao.BatchMain.Columns().BatchInfo,
|
||||
nil,
|
||||
)
|
||||
if err = mod.Scan(batch); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取执行日志详情失败,请稍后重试!")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, gerror.New("执行日志不存在")
|
||||
}
|
||||
if isAllocateChildBatch(batch.BatchType, batch.BatchNo, batch.SourceBatchNo) {
|
||||
return nil, gerror.New("执行日志不存在")
|
||||
}
|
||||
|
||||
res = &model.BatchExecutionLogDetailModel{
|
||||
SourceChannel: batch.SourceChannel,
|
||||
ExecutionMode: batch.ExecutionMode,
|
||||
Id: batch.Id,
|
||||
BatchNo: batch.BatchNo,
|
||||
BatchType: batch.BatchType,
|
||||
SourceType: batch.SourceType,
|
||||
BatchAsyncStatus: batch.BatchAsyncStatus,
|
||||
RunResult: batch.RunResult,
|
||||
FailReason: batch.FailReason,
|
||||
TotalCount: batch.TotalCount,
|
||||
UnknownCount: batchExecutionLogUnknownCount(batch.BatchInfo),
|
||||
StartTime: batch.StartTime,
|
||||
EndTime: batch.EndTime,
|
||||
}
|
||||
res.RestartPhase, res.RestartResumeAt = batchRestartPhase(batch.BatchInfo)
|
||||
|
||||
if batch.BatchType == consts.BatchTypeAllocate {
|
||||
res.Fields = allocationunits.Resolve(batch.BatchInfo, batch.CreatedDept, batch.TargetDeptId)
|
||||
if err := allocationunits.Fill(ctx, contexts.GetTenantId(ctx), []*allocationunits.Fields{&res.Fields}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if sourceFile, err := s.getSourceFile(ctx, batch.SourceOssId, batch.BatchInfo); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
res.SourceFile = sourceFile
|
||||
}
|
||||
|
||||
// 导出任务的成品文件落在 t_export_task,不写 t_resource_batch_download,须分开取。
|
||||
var resultFile *model.BatchExecutionLogResultFileModel
|
||||
if batch.BatchType == consts.BatchTypeExport {
|
||||
resultFile, err = s.getExportResultFile(ctx, batch.BatchNo, batch.ResultOssId)
|
||||
} else {
|
||||
resultFile, err = s.getResultFile(ctx, batch.BatchNo, batch.ResultOssId, batch.BatchInfo)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.ResultFile = resultFile
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) DetailList(ctx context.Context, in *model.BatchDetailListParam) (list []*model.BatchDetailListModel, totalCount int, err error) {
|
||||
if in == nil || in.BatchNo == "" {
|
||||
return nil, 0, gerror.New("批次号不能为空")
|
||||
}
|
||||
|
||||
batch, err := s.getDetailBatch(ctx, in.BatchNo, in.BatchType)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
columns := dao.BatchDetail.Columns()
|
||||
detailBatchType := batch.BatchType
|
||||
mod := s.detailModel(ctx).
|
||||
Fields(
|
||||
columns.Id,
|
||||
columns.BatchNo,
|
||||
columns.BatchType,
|
||||
columns.DetailStatus,
|
||||
columns.RowNo,
|
||||
columns.ItemId,
|
||||
columns.ErrorReason,
|
||||
columns.Snapshot,
|
||||
columns.TenantId,
|
||||
columns.CreatedDept,
|
||||
columns.CreatedBy,
|
||||
).
|
||||
Where(columns.BatchType, detailBatchType)
|
||||
if isAllocateParentBatch(batch) {
|
||||
childBatches := s.batchModel(ctx).
|
||||
Fields(dao.BatchMain.Columns().BatchNo).
|
||||
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeAllocate).
|
||||
Where(dao.BatchMain.Columns().SourceBatchNo, batch.BatchNo).
|
||||
Where(dao.BatchMain.Columns().SourceBatchNo + " <> " + dao.BatchMain.Columns().BatchNo)
|
||||
mod = mod.Where("("+columns.BatchNo+" = ? OR "+columns.BatchNo+" IN (?))", batch.BatchNo, childBatches)
|
||||
} else {
|
||||
mod = mod.Where(columns.BatchNo, batch.BatchNo)
|
||||
}
|
||||
|
||||
if in.DetailStatus != nil {
|
||||
mod = mod.Where(columns.DetailStatus, *in.DetailStatus)
|
||||
}
|
||||
if in.Iccid != "" {
|
||||
mod = mod.Where(columns.ItemId+" LIKE ?", "%"+in.Iccid+"%")
|
||||
}
|
||||
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
mod = mod.Where("("+columns.ItemId+" LIKE ? OR "+columns.ErrorReason+" LIKE ?)", like, like)
|
||||
}
|
||||
|
||||
var details []*entity.BatchDetail
|
||||
mod = orderBatchCards(mod.Page(in.Page, in.PageSize), batch.BatchType)
|
||||
if err = mod.ScanAndCount(&details, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取批次执行明细列表失败,请稍后重试")
|
||||
}
|
||||
|
||||
list = make([]*model.BatchDetailListModel, 0, len(details))
|
||||
displayRowStart := 0
|
||||
if in.Page > 0 && in.PageSize > 0 {
|
||||
displayRowStart = (in.Page - 1) * in.PageSize
|
||||
}
|
||||
for index, detail := range details {
|
||||
iccid, msisdn, imsi := batchExecutionLogDetailSnapshotCards(detail)
|
||||
item := &model.BatchDetailListModel{
|
||||
RowNo: displayRowStart + index + 1,
|
||||
Iccid: iccid,
|
||||
Msisdn: msisdn,
|
||||
Imsi: imsi,
|
||||
Imei: batchExecutionLogSnapshotValue(detail, "IMEI", "imei", "rawFields.IMEI"),
|
||||
ChanKey: batchExecutionLogSnapshotValue(detail, "chanKey", "chan_key", "rawFields.chanKey", "rawFields.chan_key", "rawFields.通道编号", "rawFields.通道编码", "rawFields.通道编号*"),
|
||||
CardType: batchExecutionLogSnapshotValue(detail, "cardType", "rawFields.cardType", "rawFields.卡类型"),
|
||||
UpPkgCode: batchExecutionLogSnapshotValue(detail, "upPkgCode", "rawFields.upPkgCode", "rawFields.上游套餐编码", "rawFields.上游套餐"),
|
||||
SuppliersCode: batchExecutionLogSnapshotValue(detail, "suppliersCode", "rawFields.suppliersCode", "rawFields.供应商编码", "rawFields.供应商"),
|
||||
UpstreamActivationTime: batchExecutionLogSnapshotValue(detail, "upstreamActivationTime", "rawFields.upstreamActivationTime", "rawFields.上游激活时间", "rawFields.上游给出激活时间"),
|
||||
InsertToPlugEnabled: batchExecutionLogSnapshotValue(detail, "insertToPlugEnabled", "rawFields.insertToPlugEnabled", "rawFields.是否贴片转插拔"),
|
||||
CarrierTrafficCutoffEnabled: batchExecutionLogSnapshotValue(detail, "carrierTrafficCutoffEnabled", "rawFields.carrierTrafficCutoffEnabled", "rawFields.是否运营商达量断网"),
|
||||
AgreementExpiryDate: batchExecutionLogSnapshotValue(detail, "agreementExpiryDate", "rawFields.agreementExpiryDate", "rawFields.协议到期时间", "rawFields.协议期到期时间"),
|
||||
HasTestPeriod: batchExecutionLogSnapshotValue(detail, "hasTestPeriod", "rawFields.hasTestPeriod", "rawFields.是否有测试期"),
|
||||
TestPeriodDays: batchExecutionLogSnapshotValue(detail, "testPeriodDays", "rawFields.testPeriodDays", "rawFields.测试期天数"),
|
||||
TestPeriodLatestExpiryDate: batchExecutionLogSnapshotValue(detail, "testPeriodLatestExpiryDate", "rawFields.testPeriodLatestExpiryDate", "rawFields.测试期最晚过期日期"),
|
||||
HasSilentPeriod: batchExecutionLogSnapshotValue(detail, "hasSilentPeriod", "rawFields.hasSilentPeriod", "rawFields.是否有沉默期"),
|
||||
SilentPeriodDays: batchExecutionLogSnapshotValue(detail, "silentPeriodDays", "rawFields.silentPeriodDays", "rawFields.沉默期天数"),
|
||||
SilentPeriodLatestExpiryDate: batchExecutionLogSnapshotValue(detail, "silentPeriodLatestExpiryDate", "rawFields.silentPeriodLatestExpiryDate", "rawFields.沉默期最晚过期日期"),
|
||||
DetailStatus: detail.DetailStatus,
|
||||
}
|
||||
if batch.BatchType == consts.BatchTypeAllocate {
|
||||
parentUnits := allocationunits.Resolve(batch.BatchInfo, batch.CreatedDept, batch.TargetDeptId)
|
||||
item.Fields = allocationunits.Resolve(detail.Snapshot, parentUnits.SourceDeptId, parentUnits.TargetDeptId)
|
||||
if item.SourceDeptName == "" && item.SourceDeptId == parentUnits.SourceDeptId {
|
||||
item.SourceDeptName = parentUnits.SourceDeptName
|
||||
}
|
||||
if item.TargetDeptName == "" && item.TargetDeptId == parentUnits.TargetDeptId {
|
||||
item.TargetDeptName = parentUnits.TargetDeptName
|
||||
}
|
||||
}
|
||||
|
||||
if detail.ErrorReason != "" {
|
||||
item.Error = &model.BatchDetailErrorModel{
|
||||
Reason: detail.ErrorReason,
|
||||
}
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
fields := []*allocationunits.Fields{}
|
||||
for _, item := range list {
|
||||
if batch.BatchType == consts.BatchTypeAllocate {
|
||||
fields = append(fields, &item.Fields)
|
||||
}
|
||||
}
|
||||
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), fields); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// dataReviewQueryMessage 通知 Worker 对既有 operationId 做一次只读补查。
|
||||
type dataReviewQueryMessage struct {
|
||||
BatchNo string `json:"batchNo"`
|
||||
RequestedBy string `json:"requestedBy,omitempty"`
|
||||
}
|
||||
|
||||
var publishDataReviewQuery = rabbitmq.Publish
|
||||
|
||||
// SyncReview 手动同步待核查批次的上游执行结果。
|
||||
// 只重查既有受理号(jobId),不重发办理请求、不清零自动补查计数;
|
||||
// 上游仍无结论的明细继续保持待核查。
|
||||
func (s *sCommonBatchExecutionLog) SyncReview(ctx context.Context, in *model.BatchExecutionLogSyncReviewParam) (res *model.BatchExecutionLogSyncReviewModel, err error) {
|
||||
if in == nil {
|
||||
in = &model.BatchExecutionLogSyncReviewParam{}
|
||||
}
|
||||
batchNo := strings.TrimSpace(in.BatchNo)
|
||||
reason := strings.TrimSpace(in.Reason)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("批次编号不能为空")
|
||||
}
|
||||
|
||||
columns := dao.BatchMain.Columns()
|
||||
batch := &entity.BatchMain{}
|
||||
// 沿用执行日志列表的可见性口径(部门权限过滤)。
|
||||
if err = s.batchModel(ctx).
|
||||
Fields(columns.Id, columns.BatchNo, columns.BatchType, columns.BatchAsyncStatus, columns.RunResult, columns.BatchInfo).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Scan(batch); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询批次信息失败")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, gerror.New("批次不存在或无权限")
|
||||
}
|
||||
if batch.BatchAsyncStatus != consts.BatchAsyncStatusReview {
|
||||
return nil, gerror.New("仅待核查批次支持手动同步上游结果")
|
||||
}
|
||||
unknownCount := batchExecutionLogUnknownCount(batch.BatchInfo)
|
||||
if unknownCount <= 0 {
|
||||
return nil, gerror.New("该批次没有待核查明细")
|
||||
}
|
||||
|
||||
message := dataReviewQueryMessage{BatchNo: batch.BatchNo, RequestedBy: contexts.GetUserName(ctx)}
|
||||
if err = publishDataReviewQuery(ctx, consts.QueueDataReviewQuery, message); err != nil {
|
||||
g.Log().Errorf(ctx, "手动同步上游结果消息发布失败, batchNo=%s, tenantId=%s, userId=%d, reason=%s, err=%+v",
|
||||
batch.BatchNo, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason, err)
|
||||
return nil, gerror.Wrap(err, "发布上游结果同步消息失败")
|
||||
}
|
||||
g.Log().Infof(ctx, "手动同步上游结果消息已发布, batchNo=%s, batchType=%d, unknownCount=%d, tenantId=%s, userId=%d, reason=%s",
|
||||
batch.BatchNo, batch.BatchType, unknownCount, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason)
|
||||
|
||||
return &model.BatchExecutionLogSyncReviewModel{
|
||||
BatchNo: batch.BatchNo,
|
||||
BatchType: batch.BatchType,
|
||||
BatchAsyncStatus: batch.BatchAsyncStatus,
|
||||
UnknownCount: unknownCount,
|
||||
Queued: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) getDetailBatch(ctx context.Context, batchNo string, batchType *int) (*entity.BatchMain, error) {
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("批次号不能为空")
|
||||
}
|
||||
|
||||
columns := dao.BatchMain.Columns()
|
||||
batch := &entity.BatchMain{}
|
||||
mod := s.batchModel(ctx).
|
||||
Fields(
|
||||
columns.Id,
|
||||
columns.BatchNo,
|
||||
columns.BatchType,
|
||||
columns.SourceType,
|
||||
columns.SourceBatchNo,
|
||||
columns.BatchInfo,
|
||||
columns.CreatedDept, columns.TargetDeptId,
|
||||
).
|
||||
Where(columns.BatchNo, batchNo)
|
||||
mod = executionLogBatchTypeModel(mod, columns.BatchType, columns.BatchInfo, batchType)
|
||||
if err := mod.Scan(batch); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取批次信息失败,请稍后重试")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, gerror.New("批次不存在")
|
||||
}
|
||||
if isAllocateChildBatch(batch.BatchType, batch.BatchNo, batch.SourceBatchNo) {
|
||||
return nil, gerror.New("批次不存在")
|
||||
}
|
||||
if !executionLogBatchTypeMatches(batch.BatchType, batch.BatchInfo, batchType) {
|
||||
return nil, gerror.New("批次类型不匹配")
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
func executionLogBatchTypeModel(mod *gdb.Model, batchTypeColumn, batchInfoColumn string, requested *int) *gdb.Model {
|
||||
if requested == nil {
|
||||
return mod
|
||||
}
|
||||
if *requested == consts.BatchTypePrepare {
|
||||
return mod.Where(batchTypeColumn, consts.BatchTypePrepare)
|
||||
}
|
||||
if *requested == consts.BatchTypeAllocate {
|
||||
return mod.Where(
|
||||
fmt.Sprintf("(%s = ? OR (%s = ? AND JSON_UNQUOTE(JSON_EXTRACT(%s, '$.targetBatchType')) = ?))", batchTypeColumn, batchTypeColumn, batchInfoColumn),
|
||||
consts.BatchTypeAllocate,
|
||||
consts.BatchTypePrepare,
|
||||
strconv.Itoa(*requested),
|
||||
)
|
||||
}
|
||||
return mod.Where(
|
||||
fmt.Sprintf("(%s = ? OR (%s = ? AND JSON_UNQUOTE(JSON_EXTRACT(%s, '$.targetBatchType')) = ?))", batchTypeColumn, batchTypeColumn, batchInfoColumn),
|
||||
*requested,
|
||||
consts.BatchTypePrepare,
|
||||
strconv.Itoa(*requested),
|
||||
)
|
||||
}
|
||||
|
||||
func executionLogBatchTypeMatches(actual int, info *gjson.Json, requested *int) bool {
|
||||
if requested == nil {
|
||||
return true
|
||||
}
|
||||
if actual == *requested {
|
||||
return true
|
||||
}
|
||||
return actual == consts.BatchTypePrepare && info != nil && info.Get("targetBatchType").Int() == *requested
|
||||
}
|
||||
|
||||
func isAllocateParentBatch(batch *entity.BatchMain) bool {
|
||||
if batch == nil || batch.BatchType != consts.BatchTypeAllocate || isAllocateChildBatch(batch.BatchType, batch.BatchNo, batch.SourceBatchNo) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isAllocateChildBatch(batchType int, batchNo, sourceBatchNo string) bool {
|
||||
return batchType == consts.BatchTypeAllocate &&
|
||||
strings.TrimSpace(sourceBatchNo) != "" &&
|
||||
strings.TrimSpace(sourceBatchNo) != strings.TrimSpace(batchNo)
|
||||
}
|
||||
|
||||
func batchExecutionLogDetailSnapshotCards(detail *entity.BatchDetail) (iccid, msisdn, imsi string) {
|
||||
if detail == nil {
|
||||
return
|
||||
}
|
||||
if detail.Snapshot != nil {
|
||||
iccid = strings.TrimSpace(detail.Snapshot.Get("ICCID").String())
|
||||
msisdn = strings.TrimSpace(detail.Snapshot.Get("MSISDN").String())
|
||||
imsi = strings.TrimSpace(detail.Snapshot.Get("IMSI").String())
|
||||
if iccid == "" {
|
||||
iccid = strings.TrimSpace(detail.Snapshot.Get("iccid").String())
|
||||
}
|
||||
if msisdn == "" {
|
||||
msisdn = strings.TrimSpace(detail.Snapshot.Get("msisdn").String())
|
||||
}
|
||||
if imsi == "" {
|
||||
imsi = strings.TrimSpace(detail.Snapshot.Get("imsi").String())
|
||||
}
|
||||
}
|
||||
if iccid == "" {
|
||||
iccid = detail.ItemId
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func batchExecutionLogSnapshotValue(detail *entity.BatchDetail, paths ...string) string {
|
||||
if detail == nil || detail.Snapshot == nil {
|
||||
return ""
|
||||
}
|
||||
for _, path := range paths {
|
||||
value := strings.TrimSpace(detail.Snapshot.Get(path).String())
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) getSourceFile(ctx context.Context, sourceOssId int64, batchInfo *gjson.Json) (*model.BatchExecutionLogFileModel, error) {
|
||||
ossId := sourceOssId
|
||||
if ossId == 0 {
|
||||
ossId = legacySourceOssId(batchInfo)
|
||||
}
|
||||
if ossId == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
oss := &entity.SysOss{}
|
||||
if err := s.ossModel(ctx).
|
||||
Where(dao.SysOss.Columns().OssId, ossId).
|
||||
Scan(oss); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取原始文件信息失败,请稍后重试!")
|
||||
}
|
||||
if oss.OssId == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &model.BatchExecutionLogFileModel{
|
||||
OssId: oss.OssId,
|
||||
Url: batchExecutionLogFileURL(ctx, oss.Url, oss.Path),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sCommonBatchExecutionLog) getResultFile(ctx context.Context, batchNo string, resultOssId int64, batchInfo *gjson.Json) (*model.BatchExecutionLogResultFileModel, error) {
|
||||
download := &entity.TResourceBatchDownload{}
|
||||
// 出库/划拨/导出等批次不写 t_resource_batch_download,查不到记录按「无结果文件」处理,
|
||||
// 不能当异常抛给页面(否则详情弹窗整体报 sql: no rows in result set)。
|
||||
if err := s.downloadModel(ctx).
|
||||
Where(dao.TResourceBatchDownload.Columns().BatchNo, batchNo).
|
||||
OrderDesc(dao.TResourceBatchDownload.Columns().Id).
|
||||
Limit(1).
|
||||
Scan(download); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(err, "获取结果文件信息失败,请稍后重试!")
|
||||
}
|
||||
ossId := resultOssId
|
||||
if ossId == 0 && download.OssId > 0 {
|
||||
ossId = download.OssId
|
||||
}
|
||||
if ossId == 0 {
|
||||
ossId = legacyResultOssId(batchInfo)
|
||||
}
|
||||
if download.Id == 0 && ossId == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
url := batchExecutionLogFileURL(ctx, download.FileUrl, download.FilePath)
|
||||
originalName := ""
|
||||
if ossId > 0 {
|
||||
oss := &entity.SysOss{}
|
||||
if scanErr := s.ossModel(ctx).
|
||||
Where(dao.SysOss.Columns().OssId, ossId).
|
||||
Scan(oss); scanErr != nil && !errors.Is(scanErr, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(scanErr, "获取结果文件OSS信息失败,请稍后重试!")
|
||||
}
|
||||
if oss.OssId > 0 {
|
||||
originalName = oss.OriginalName
|
||||
if ossUrl := batchExecutionLogFileURL(ctx, oss.Url, oss.Path); ossUrl != "" {
|
||||
url = ossUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &model.BatchExecutionLogResultFileModel{
|
||||
DownloadId: download.Id,
|
||||
OssId: ossId,
|
||||
Url: url,
|
||||
OriginalName: originalName,
|
||||
DownloadStatus: resultDownloadStatus(download),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getExportResultFile 只暴露任务 ID;文件必须经下载中心重新校验本人、权限和有效期。
|
||||
func (s *sCommonBatchExecutionLog) getExportResultFile(ctx context.Context, taskNo string, _ int64) (*model.BatchExecutionLogResultFileModel, error) {
|
||||
task := &entity.TExportTask{}
|
||||
if err := s.exportTaskModel(ctx).
|
||||
Where(dao.TExportTask.Columns().TaskNo, taskNo).
|
||||
Where(dao.TExportTask.Columns().OwnerUserId, contexts.GetUserId(ctx)).
|
||||
Scan(task); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, gerror.Wrap(err, "获取结果文件信息失败,请稍后重试!")
|
||||
}
|
||||
if task.Id <= 0 || task.DeletedTime != nil || task.Status != consts.DownloadStatusGenerated || task.OssId <= 0 ||
|
||||
(task.ExpireTime != nil && task.ExpireTime.Before(gtime.Now())) || !hasExportPermission(ctx, task.PermCode) {
|
||||
return nil, nil
|
||||
}
|
||||
return &model.BatchExecutionLogResultFileModel{
|
||||
ExportTaskId: task.Id,
|
||||
OriginalName: task.FileName,
|
||||
DownloadStatus: consts.DownloadStatusGenerated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func batchExecutionLogUnknownCount(batchInfo *gjson.Json) int {
|
||||
if batchInfo == nil {
|
||||
return 0
|
||||
}
|
||||
return batchInfo.Get("apnUnknownCount").Int()
|
||||
}
|
||||
|
||||
// batchRestartPhase 从 batch_info 取出重启阶段与计划复机时间。
|
||||
// 重启批次停机完成后要等待若干分钟再复机,这段时间主状态仍是处理中,
|
||||
// 明细却已落终态;前端据此显示「等待复机」,避免误以为卡住或已办结。
|
||||
func batchRestartPhase(batchInfo *gjson.Json) (phase, resumeAt string) {
|
||||
if batchInfo == nil {
|
||||
return "", ""
|
||||
}
|
||||
return strings.TrimSpace(batchInfo.Get("restart.phase").String()),
|
||||
strings.TrimSpace(batchInfo.Get("restart.resumeAt").String())
|
||||
}
|
||||
|
||||
func legacySourceOssId(batchInfo *gjson.Json) int64 {
|
||||
if batchInfo == nil {
|
||||
return 0
|
||||
}
|
||||
for _, path := range []string{"importFileOssId", "source.importFileOssId", "source.inputFileOssId"} {
|
||||
if ossId := batchInfo.Get(path).Int64(); ossId > 0 {
|
||||
return ossId
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func legacyResultOssId(batchInfo *gjson.Json) int64 {
|
||||
if batchInfo == nil {
|
||||
return 0
|
||||
}
|
||||
return batchInfo.Get("resultFileOssId").Int64()
|
||||
}
|
||||
|
||||
func resultDownloadStatus(download *entity.TResourceBatchDownload) int {
|
||||
if download != nil && download.Id > 0 {
|
||||
return download.DownloadStatus
|
||||
}
|
||||
return consts.DownloadStatusGenerated
|
||||
}
|
||||
|
||||
func batchExecutionLogFileURL(ctx context.Context, storedURL string, storedPath string) string {
|
||||
if url := normalizeBatchLocalUploadURL(ctx, storedURL); url != "" {
|
||||
return url
|
||||
}
|
||||
if url := normalizeBatchLocalUploadURL(ctx, storedPath); url != "" {
|
||||
return url
|
||||
}
|
||||
return strings.TrimSpace(storedURL)
|
||||
}
|
||||
|
||||
func normalizeBatchLocalUploadURL(ctx context.Context, value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
lower := strings.ToLower(value)
|
||||
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
|
||||
return value
|
||||
}
|
||||
normalized := filepath.ToSlash(filepath.Clean(value))
|
||||
if normalized == "/upload" || strings.HasPrefix(normalized, "/upload/") {
|
||||
return normalized
|
||||
}
|
||||
|
||||
if configType, err := service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssUrlPath); err == nil && configType != nil {
|
||||
configValue := strings.TrimSpace(configType.ConfigValue)
|
||||
if configValue != "" {
|
||||
if strings.Contains(configValue, "|") {
|
||||
split := strings.SplitN(configValue, "|", 2)
|
||||
from := strings.TrimSpace(filepath.ToSlash(split[0]))
|
||||
to := strings.TrimSpace(filepath.ToSlash(split[1]))
|
||||
if suffix, ok := batchLocalUploadMappedSuffix(normalized, from); ok {
|
||||
return batchJoinUploadURL(to, suffix)
|
||||
}
|
||||
} else if suffix := batchLocalUploadRelativePath(normalized); suffix != "" {
|
||||
return batchJoinUploadURL(configValue, suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if suffix := batchLocalUploadRelativePath(normalized); suffix != "" {
|
||||
return batchJoinUploadURL("/upload", suffix)
|
||||
}
|
||||
if strings.HasPrefix(normalized, "/") {
|
||||
return normalized
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func batchLocalUploadMappedSuffix(filePath string, from string) (string, bool) {
|
||||
from = strings.Trim(strings.TrimSpace(filepath.ToSlash(from)), "/")
|
||||
if from == "" {
|
||||
return "", false
|
||||
}
|
||||
normalized := strings.TrimSpace(filepath.ToSlash(filePath))
|
||||
marker := "/" + from + "/"
|
||||
if idx := strings.Index(normalized, marker); idx >= 0 {
|
||||
return strings.TrimPrefix(normalized[idx+len(marker):], "/"), true
|
||||
}
|
||||
prefix := from + "/"
|
||||
if strings.HasPrefix(normalized, prefix) {
|
||||
return strings.TrimPrefix(normalized[len(prefix):], "/"), true
|
||||
}
|
||||
if strings.Trim(normalized, "/") == from {
|
||||
return "", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func batchLocalUploadRelativePath(filePath string) string {
|
||||
normalized := filepath.ToSlash(filepath.Clean(strings.TrimSpace(filePath)))
|
||||
for _, marker := range []string{
|
||||
"resource/upload/",
|
||||
"/resource/upload/",
|
||||
"/app/resource/upload/",
|
||||
"/app/upload/",
|
||||
"upload/",
|
||||
"/upload/",
|
||||
} {
|
||||
if idx := strings.Index(normalized, marker); idx >= 0 {
|
||||
return strings.TrimPrefix(normalized[idx+len(marker):], "/")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func batchJoinUploadURL(base string, suffix string) string {
|
||||
base = strings.TrimRight(strings.TrimSpace(filepath.ToSlash(base)), "/")
|
||||
suffix = strings.TrimLeft(strings.TrimSpace(filepath.ToSlash(suffix)), "/")
|
||||
if base == "" {
|
||||
base = "/upload"
|
||||
}
|
||||
if suffix == "" {
|
||||
return base
|
||||
}
|
||||
return base + "/" + suffix
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestBatchExecutionLogDetailListPreservesUnknownStatusAndReason(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "DATA-REVIEW", consts.BatchTypeDataStart, consts.BatchSourceTypeBatchInput, 1, "test-review-card")
|
||||
const reason = "上游已受理,补查超时,执行结果待核查"
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE batch_detail SET detail_status = ?, error_reason = ? WHERE batch_no = ?", consts.BatchDetailStatusUnknown, reason, "DATA-REVIEW"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status := consts.BatchDetailStatusUnknown
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
BatchNo: "DATA-REVIEW", DetailStatus: &status,
|
||||
})
|
||||
if err != nil || total != 1 || len(list) != 1 {
|
||||
t.Fatalf("读取待核查明细失败:total=%d,明细数=%d,错误:%v", total, len(list), err)
|
||||
}
|
||||
if list[0].DetailStatus != consts.BatchDetailStatusUnknown || list[0].Error == nil || list[0].Error.Reason != reason {
|
||||
t.Fatalf("待核查状态或具体原因未保留:%+v", list[0])
|
||||
}
|
||||
status = consts.BatchDetailStatusFailed
|
||||
list, total, err = NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
BatchNo: "DATA-REVIEW", DetailStatus: &status,
|
||||
})
|
||||
if err != nil || total != 0 || len(list) != 0 {
|
||||
t.Fatalf("待核查明细被计入失败筛选:total=%d,明细数=%d,错误:%v", total, len(list), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogListPreservesReviewStatusAndResult(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE batch_main ADD batch_async_status INTEGER NOT NULL DEFAULT 0"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_main(batch_no, batch_type, batch_async_status, run_result) VALUES (?, ?, ?, ?), (?, ?, ?, ?)",
|
||||
"DATA-REVIEW", consts.BatchTypeDataStart, consts.BatchAsyncStatusReview, consts.BatchRunResultReview,
|
||||
"DATA-FAILED", consts.BatchTypeDataStop, consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status := consts.BatchAsyncStatusReview
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{
|
||||
BatchAsyncStatus: &status, PageInfo: request.PageInfo{Page: 1, PageSize: 20},
|
||||
})
|
||||
if err != nil || total != 1 || len(list) != 1 {
|
||||
t.Fatalf("读取待核查批次失败:total=%d,批次数=%d,错误:%v", total, len(list), err)
|
||||
}
|
||||
if list[0].BatchNo != "DATA-REVIEW" || list[0].BatchAsyncStatus != consts.BatchAsyncStatusReview || list[0].RunResult != consts.BatchRunResultReview {
|
||||
t.Fatalf("待核查父状态或运行结果未保留:%+v", list[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogUnknownCountFromBatchInfo(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
`ALTER TABLE batch_main ADD COLUMN target_dept_id INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE batch_main ADD COLUMN batch_async_status INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE batch_main ADD COLUMN fail_reason TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE batch_main ADD COLUMN source_oss_id INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE batch_main ADD COLUMN result_oss_id INTEGER NOT NULL DEFAULT 0`,
|
||||
`CREATE TABLE t_resource_batch_download (id INTEGER PRIMARY KEY, batch_no TEXT)`,
|
||||
`INSERT INTO t_resource_batch_download(id, batch_no) VALUES (1, 'DATA-REVIEW'), (2, 'DATA-LEGACY')`,
|
||||
`INSERT INTO batch_main(batch_no, batch_type, batch_info, total_count) VALUES ('DATA-REVIEW', 109, '{"apnUnknownCount":2}', 3), ('DATA-LEGACY', 109, '{}', 1)`,
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil || total != 2 || len(list) != 2 {
|
||||
t.Fatalf("读取未知计数列表失败: total=%d len=%d err=%v", total, len(list), err)
|
||||
}
|
||||
counts := map[string]int{}
|
||||
for _, item := range list {
|
||||
counts[item.BatchNo] = item.UnknownCount
|
||||
}
|
||||
if counts["DATA-REVIEW"] != 2 || counts["DATA-LEGACY"] != 0 {
|
||||
t.Fatalf("列表待核查数错误: %+v", counts)
|
||||
}
|
||||
for batchNo, want := range map[string]int{"DATA-REVIEW": 2, "DATA-LEGACY": 0} {
|
||||
summary, err := NewCommonBatchExecutionLog().Summary(ctx, &model.BatchExecutionLogDetailParam{BatchNo: batchNo})
|
||||
if err != nil || summary == nil || summary.UnknownCount != want {
|
||||
t.Fatalf("概要 %s 待核查数错误: summary=%+v err=%v", batchNo, summary, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogResultFilePreservesOriginalName(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
`CREATE TABLE t_resource_batch_download (id INTEGER PRIMARY KEY, batch_no TEXT, oss_id INTEGER, file_url TEXT, file_path TEXT)`,
|
||||
`CREATE TABLE sys_oss (oss_id INTEGER PRIMARY KEY, original_name TEXT, url TEXT, path TEXT)`,
|
||||
`INSERT INTO t_resource_batch_download(id, batch_no, oss_id, file_url, file_path) VALUES (1, 'DATA-REVIEW', 1, '/upload/DATA-REVIEW-result-nonce.xlsx', ''), (2, 'DATA-LEGACY', 2, '/upload/legacy.xlsx', '')`,
|
||||
`INSERT INTO sys_oss(oss_id, original_name, url, path) VALUES (1, 'DATA-REVIEW-result.xlsx', '/upload/DATA-REVIEW-result-nonce.xlsx', ''), (2, '', '/upload/legacy.xlsx', '')`,
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for batchNo, want := range map[string]string{"DATA-REVIEW": "DATA-REVIEW-result.xlsx", "DATA-LEGACY": ""} {
|
||||
result, err := NewCommonBatchExecutionLog().getResultFile(ctx, batchNo, 0, nil)
|
||||
if err != nil || result == nil || result.OriginalName != want {
|
||||
t.Fatalf("原始下载名未透传: batch=%s result=%+v err=%v", batchNo, result, err)
|
||||
}
|
||||
encoded, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var response map[string]any
|
||||
if err := json.Unmarshal(encoded, &response); err != nil || response["originalName"] != want {
|
||||
t.Fatalf("响应JSON丢失原始下载名: %s err=%v", encoded, err)
|
||||
}
|
||||
if batchNo == "DATA-REVIEW" && result.Url != "/upload/DATA-REVIEW-result-nonce.xlsx" {
|
||||
t.Fatalf("原始下载名影响了物理文件定位: %s", result.Url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogListReturnsTotalCount(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(
|
||||
batch_no, batch_type, source_type, total_count, run_result,
|
||||
created_dept, created_by, tenant_id
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, "OUT-1", 2, 0, 7, 2, 0, 0, ""); err != nil {
|
||||
t.Fatalf("insert batch: %v", err)
|
||||
}
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{
|
||||
PageInfo: request.PageInfo{Page: 1, PageSize: 20},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list execution logs: %v", err)
|
||||
}
|
||||
if total != 1 {
|
||||
t.Fatalf("total rows = %d, want 1", total)
|
||||
}
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("list length = %d, want 1", len(list))
|
||||
}
|
||||
if list[0].TotalCount != 7 {
|
||||
t.Fatalf("totalCount = %d, want 7", list[0].TotalCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocateDetailInheritsHistoricalUnits(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
`ALTER TABLE batch_main ADD target_dept_id INTEGER DEFAULT 0`,
|
||||
`INSERT INTO batch_main(batch_no,batch_type,batch_info,created_dept,target_dept_id) VALUES('ALC-HISTORY',3,'{"sourceDeptId":10,"targetDeptId":20,"sourceDeptName":"历史划拨单位","targetDeptName":"历史接收单位"}',10,20)`,
|
||||
`INSERT INTO batch_detail(batch_no,batch_type,item_id,snapshot) VALUES('ALC-HISTORY',3,'card','{}')`,
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
rows, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ALC-HISTORY"})
|
||||
if err != nil || total != 1 || len(rows) != 1 {
|
||||
t.Fatal("读取历史明细失败", total, err)
|
||||
}
|
||||
if rows[0].SourceDeptName != "历史划拨单位" || rows[0].TargetDeptName != "历史接收单位" {
|
||||
t.Fatal("历史明细未优先继承批次快照", rows[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListNormalizesFileRowNo(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "INB-1", consts.BatchTypeInbound, consts.BatchSourceTypeFile, 2, "89860000000000000001")
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
BatchNo: "INB-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("detail list: %v", err)
|
||||
}
|
||||
if total != 1 {
|
||||
t.Fatalf("total rows = %d, want 1", total)
|
||||
}
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("list length = %d, want 1", len(list))
|
||||
}
|
||||
if list[0].RowNo != 1 {
|
||||
t.Fatalf("file batch rowNo = %d, want 1", list[0].RowNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListNormalizesManualRowNo(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "INB-2", consts.BatchTypeInbound, consts.BatchSourceTypeBatchInput, 2, "89860000000000000002")
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
BatchNo: "INB-2",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("detail list: %v", err)
|
||||
}
|
||||
if total != 1 {
|
||||
t.Fatalf("total rows = %d, want 1", total)
|
||||
}
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("list length = %d, want 1", len(list))
|
||||
}
|
||||
if list[0].RowNo != 1 {
|
||||
t.Fatalf("manual batch rowNo = %d, want 1", list[0].RowNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListNumbersPages(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "ROWS", consts.BatchTypeInbound, consts.BatchSourceTypeBatchSelect, 2, "first")
|
||||
_, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(batch_no, batch_type, detail_status, row_no, item_id) VALUES (?, ?, ?, ?, ?)", "ROWS", consts.BatchTypeInbound, consts.BatchDetailStatusFailed, 9, "second")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ROWS", PageInfo: request.PageInfo{Page: 2, PageSize: 1}})
|
||||
if err != nil || total != 2 || len(list) != 1 || list[0].RowNo != 2 || list[0].Iccid != "first" {
|
||||
t.Fatalf("paged rows: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
list, total, err = NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ROWS", PageInfo: request.PageInfo{Page: 1, PageSize: 1}})
|
||||
if err != nil || total != 2 || len(list) != 1 || list[0].Iccid != "second" {
|
||||
t.Fatalf("failed card not first: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
failed := consts.BatchDetailStatusFailed
|
||||
list, total, err = NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ROWS", DetailStatus: &failed, PageInfo: request.PageInfo{Page: 1, PageSize: 1}})
|
||||
if err != nil || total != 1 || len(list) != 1 || list[0].RowNo != 1 {
|
||||
t.Fatalf("filtered rows: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListPrioritizesRealFailureOverCascade(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "PRIORITY", consts.BatchTypeAllocate, consts.BatchSourceTypeBatchInput, 1, "cascade")
|
||||
if _, err := g.DB().Exec(ctx, "UPDATE batch_detail SET detail_status = ?, error_reason = ? WHERE batch_no = ?", consts.BatchDetailStatusFailed, "本批次存在失败明细,整批未执行", "PRIORITY"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(batch_no,batch_type,detail_status,row_no,item_id,error_reason) VALUES(?,?,?,?,?,?)", "PRIORITY", consts.BatchTypeAllocate, consts.BatchDetailStatusFailed, 2, "actual-error", "ICCID不在当前操作员授权范围内"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for page, want := range []string{"actual-error", "cascade"} {
|
||||
rows, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "PRIORITY", PageInfo: request.PageInfo{Page: page + 1, PageSize: 1}})
|
||||
if err != nil || total != 2 || len(rows) != 1 || rows[0].Iccid != want {
|
||||
t.Fatalf("page=%d want=%s rows=%+v total=%d err=%v", page+1, want, rows, total, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupBatchExecutionLogListDB(t *testing.T) context.Context {
|
||||
t.Helper()
|
||||
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)
|
||||
})
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
CREATE TABLE batch_main (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL,
|
||||
batch_type INTEGER NOT NULL,
|
||||
source_batch_no TEXT NOT NULL DEFAULT '',
|
||||
source_type INTEGER,
|
||||
source_channel TEXT NOT NULL DEFAULT 'legacy', execution_mode INTEGER NOT NULL DEFAULT 0,
|
||||
batch_info TEXT,
|
||||
total_count INTEGER NOT NULL DEFAULT 0,
|
||||
run_result INTEGER NOT NULL DEFAULT 0,
|
||||
start_time TEXT,
|
||||
end_time TEXT,
|
||||
created_dept INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT,
|
||||
tenant_id TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
CREATE TABLE batch_detail (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL,
|
||||
batch_type INTEGER NOT NULL,
|
||||
detail_status INTEGER NOT NULL DEFAULT 0,
|
||||
row_no INTEGER NOT NULL DEFAULT 0,
|
||||
item_id TEXT NOT NULL DEFAULT '',
|
||||
error_reason TEXT NOT NULL DEFAULT '',
|
||||
snapshot TEXT,
|
||||
tenant_id TEXT NOT NULL DEFAULT '',
|
||||
created_dept INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogListHidesAllocateChildBatches(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(
|
||||
batch_no, batch_type, source_batch_no, source_type, batch_info, total_count,
|
||||
run_result, created_dept, created_by, tenant_id
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
"ALC-PARENT", consts.BatchTypeAllocate, "", consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_parent"}`, 2, consts.BatchRunResultRunning, 0, 0, "",
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, "ALC-PARENT", consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_chunk"}`, 2, consts.BatchRunResultRunning, 0, 0, "",
|
||||
); err != nil {
|
||||
t.Fatalf("insert allocate batches: %v", err)
|
||||
}
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{
|
||||
PageInfo: request.PageInfo{Page: 1, PageSize: 20},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list execution logs: %v", err)
|
||||
}
|
||||
if total != 1 || len(list) != 1 {
|
||||
t.Fatalf("allocate child batch was not hidden: total=%d list=%d", total, len(list))
|
||||
}
|
||||
if list[0].BatchNo != "ALC-PARENT" {
|
||||
t.Fatalf("visible batch = %q, want parent", list[0].BatchNo)
|
||||
}
|
||||
if _, err := NewCommonBatchExecutionLog().Summary(ctx, &model.BatchExecutionLogDetailParam{BatchNo: "ALC-CHILD"}); err == nil {
|
||||
t.Fatal("allocate child was exposed by summary")
|
||||
}
|
||||
if _, _, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ALC-CHILD"}); err == nil {
|
||||
t.Fatal("allocate child was exposed by detail list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListShowsAllocateChildFailureReason(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(batch_no, batch_type, source_batch_no, source_type, batch_info, total_count, run_result, created_dept, created_by, tenant_id)
|
||||
VALUES (?, ?, '', ?, ?, 2, ?, 0, 0, ''), (?, ?, ?, ?, ?, 2, ?, 0, 0, '')
|
||||
`, "ALC-PARENT", consts.BatchTypeAllocate, consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_parent"}`, consts.BatchRunResultFailed,
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, "ALC-PARENT", consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_chunk"}`, consts.BatchRunResultFailed); err != nil {
|
||||
t.Fatalf("insert allocate batches: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_detail(batch_no, batch_type, detail_status, row_no, item_id, error_reason, snapshot, tenant_id, created_dept, created_by)
|
||||
VALUES (?, ?, ?, 1, '89860000000000000001', ?, '{}', '', 0, 0), (?, ?, ?, 2, '89860000000000000002', '', '{}', '', 0, 0)
|
||||
`, "ALC-CHILD", consts.BatchTypeAllocate, consts.BatchDetailStatusFailed, "套餐已过期,不能划拨",
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, consts.BatchDetailStatusSuccess); err != nil {
|
||||
t.Fatalf("insert allocate details: %v", err)
|
||||
}
|
||||
failed := consts.BatchDetailStatusFailed
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{
|
||||
PageInfo: request.PageInfo{Page: 1, PageSize: 20},
|
||||
BatchNo: "ALC-PARENT",
|
||||
DetailStatus: &failed,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list parent failure details: %v", err)
|
||||
}
|
||||
if total != 1 || len(list) != 1 {
|
||||
t.Fatalf("failed detail count = %d, list length = %d, want 1", total, len(list))
|
||||
}
|
||||
if list[0].Iccid != "89860000000000000001" || list[0].Error == nil || list[0].Error.Reason != "套餐已过期,不能划拨" {
|
||||
t.Fatalf("unexpected failure detail: %+v", list[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExecutionLogDetailListAggregatesAllAllocateChildren(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(batch_no, batch_type, source_batch_no, source_type, batch_info, total_count, run_result, created_dept, created_by, tenant_id)
|
||||
VALUES (?, ?, '', ?, ?, 1, ?, 0, 0, ''), (?, ?, ?, ?, ?, 1, ?, 0, 0, ''), (?, ?, ?, ?, ?, 1, ?, 0, 0, '')
|
||||
`, "ALC-PARENT", consts.BatchTypeAllocate, consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_parent"}`, consts.BatchRunResultFailed,
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, "ALC-PARENT", consts.BatchSourceTypeBatchInput, `{"jobRole":"allocate_chunk"}`, consts.BatchRunResultFailed,
|
||||
"ALC-RELATED", consts.BatchTypeAllocate, "ALC-PARENT", consts.BatchSourceTypeBatchInput, `{}`, consts.BatchRunResultFailed); err != nil {
|
||||
t.Fatalf("insert allocate batches: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_detail(batch_no, batch_type, detail_status, row_no, item_id, error_reason, snapshot, tenant_id, created_dept, created_by)
|
||||
VALUES (?, ?, ?, 1, 'parent-card', '', '{}', '', 0, 0), (?, ?, ?, 1, 'chunk-card', '', '{}', '', 0, 0), (?, ?, ?, 1, 'related-card', '', '{}', '', 0, 0)
|
||||
`, "ALC-PARENT", consts.BatchTypeAllocate, consts.BatchDetailStatusSuccess,
|
||||
"ALC-CHILD", consts.BatchTypeAllocate, consts.BatchDetailStatusSuccess,
|
||||
"ALC-RELATED", consts.BatchTypeAllocate, consts.BatchDetailStatusSuccess); err != nil {
|
||||
t.Fatalf("insert batch details: %v", err)
|
||||
}
|
||||
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "ALC-PARENT"})
|
||||
if err != nil || total != 3 || len(list) != 3 {
|
||||
t.Fatalf("unexpected parent details: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, item := range list {
|
||||
seen[item.Iccid] = true
|
||||
}
|
||||
for _, iccid := range []string{"parent-card", "chunk-card", "related-card"} {
|
||||
if !seen[iccid] {
|
||||
t.Fatalf("parent details missing %s: %+v", iccid, list)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestBatchExecutionLogSelfReferencedParentAggregatesChildren(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(batch_no, batch_type, source_batch_no, batch_info)
|
||||
VALUES ('SELF-PARENT', 3, 'SELF-PARENT', '{}'), ('SELF-CHILD', 3, 'SELF-PARENT', '{}')
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_detail(batch_no, batch_type, detail_status, row_no, item_id, snapshot)
|
||||
VALUES ('SELF-PARENT', 3, 2, 1, 'parent-card', '{}'), ('SELF-CHILD', 3, 2, 2, 'child-card', '{}')
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
list, total, err := NewCommonBatchExecutionLog().DetailList(ctx, &model.BatchDetailListParam{BatchNo: "SELF-PARENT"})
|
||||
if err != nil || total != 2 || len(list) != 2 || list[0].Iccid != "parent-card" || list[1].Iccid != "child-card" {
|
||||
t.Fatalf("self-referenced parent aggregation: list=%+v total=%d err=%v", list, total, err)
|
||||
}
|
||||
}
|
||||
|
||||
func insertBatchExecutionLogDetail(t *testing.T, ctx context.Context, batchNo string, batchType, sourceType, rowNo int, itemID string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(
|
||||
batch_no, batch_type, source_type, total_count, run_result,
|
||||
created_dept, created_by, tenant_id
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, batchNo, batchType, sourceType, 1, 2, 0, 0, ""); err != nil {
|
||||
t.Fatalf("insert batch main: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_detail(
|
||||
batch_no, batch_type, detail_status, row_no, item_id, snapshot,
|
||||
tenant_id, created_dept, created_by
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, batchNo, batchType, consts.BatchDetailStatusSuccess, rowNo, itemID, `{"ICCID":"`+itemID+`"}`, "", 0, 0); err != nil {
|
||||
t.Fatalf("insert batch detail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 出库等批次不写 t_resource_batch_download,详情查询查不到记录时
|
||||
// 必须按「无结果文件」返回,而不是把 sql: no rows in result set 抛给页面。
|
||||
func TestBatchExecutionLogSummaryTreatsMissingDownloadAsNoResultFile(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
CREATE TABLE t_resource_batch_download (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL DEFAULT '',
|
||||
oss_id INTEGER NOT NULL DEFAULT 0,
|
||||
file_url TEXT NOT NULL DEFAULT '',
|
||||
file_path TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT,
|
||||
tenant_id TEXT NOT NULL DEFAULT '',
|
||||
created_dept INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`); err != nil {
|
||||
t.Fatalf("create download table: %v", err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `
|
||||
INSERT INTO batch_main(
|
||||
batch_no, batch_type, source_type, batch_info, total_count,
|
||||
run_result, created_dept, created_by, tenant_id
|
||||
) VALUES(?, ?, ?, ?, 0, ?, 0, 0, '')
|
||||
`, "OUT-NOFILE", 2, 0, `{}`, consts.BatchRunResultSuccess); err != nil {
|
||||
t.Fatalf("insert outbound batch: %v", err)
|
||||
}
|
||||
|
||||
res, err := NewCommonBatchExecutionLog().Summary(ctx, &model.BatchExecutionLogDetailParam{BatchNo: "OUT-NOFILE"})
|
||||
if err != nil {
|
||||
t.Fatalf("summary should not fail when result file is absent: %v", err)
|
||||
}
|
||||
if res == nil {
|
||||
t.Fatal("summary returned nil result")
|
||||
}
|
||||
if res.ResultFile != nil {
|
||||
t.Fatalf("resultFile = %v, want nil when no download record exists", res.ResultFile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,726 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/allocationunits"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"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/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonBatchMain struct{}
|
||||
|
||||
const restartPhaseWaitingResume = "waiting_resume"
|
||||
|
||||
type batchExecuteMessage struct {
|
||||
BatchNo string `json:"batchNo"`
|
||||
BatchType int `json:"batchType"`
|
||||
}
|
||||
|
||||
var publishBatchExecuteMessage = rabbitmq.Publish
|
||||
|
||||
func NewCommonBatchMain() *sCommonBatchMain {
|
||||
return &sCommonBatchMain{}
|
||||
}
|
||||
|
||||
func (s *sCommonBatchMain) getRequeueBatch(ctx context.Context, batchNo string) (*entity.BatchMain, error) {
|
||||
columns := dao.BatchMain.Columns()
|
||||
batch := &entity.BatchMain{}
|
||||
err := s.Model(ctx).
|
||||
Fields(
|
||||
columns.Id,
|
||||
columns.BatchNo,
|
||||
columns.BatchType,
|
||||
columns.BatchAsyncStatus,
|
||||
columns.RunResult,
|
||||
columns.BatchInfo,
|
||||
columns.TenantId,
|
||||
columns.DeptId,
|
||||
columns.CreatedDept,
|
||||
columns.CreatedBy,
|
||||
).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Where(columns.BatchType+" <> ?", consts.BatchTypePrepare).
|
||||
Scan(batch)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询批次信息失败")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, gerror.New("批次不存在或无权限")
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
func validateRequeueBatch(batch *entity.BatchMain) error {
|
||||
if batch == nil {
|
||||
return gerror.New("批次不存在")
|
||||
}
|
||||
switch batch.BatchAsyncStatus {
|
||||
case consts.BatchAsyncStatusPending:
|
||||
return nil
|
||||
case consts.BatchAsyncStatusProcessing:
|
||||
return validateProcessingRequeueBatch(batch)
|
||||
case consts.BatchAsyncStatusReview:
|
||||
return gerror.New("待核查批次不支持安全重投,请先核对上游执行结果")
|
||||
case consts.BatchAsyncStatusFailed, consts.BatchAsyncStatusSuccess, consts.BatchAsyncStatusPartialSuccess:
|
||||
return gerror.New("终态批次不支持安全重投")
|
||||
default:
|
||||
return gerror.Newf("当前批次状态不支持安全重投,status=%d", batch.BatchAsyncStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func validateProcessingRequeueBatch(batch *entity.BatchMain) error {
|
||||
if batch.BatchType != consts.BatchTypeRestart {
|
||||
return gerror.New("处理中批次不支持安全重投,避免重复调用运营商")
|
||||
}
|
||||
if restartPhase(batch) != restartPhaseWaitingResume {
|
||||
return gerror.New("RST 批次当前不在等待复机阶段,不能安全重投")
|
||||
}
|
||||
resumeAt := restartResumeAt(batch)
|
||||
if resumeAt == nil {
|
||||
return gerror.New("RST 批次缺少计划复机时间,不能安全重投")
|
||||
}
|
||||
if resumeAt.After(gtime.Now()) {
|
||||
return gerror.Newf("未到计划复机时间,resumeAt=%s", resumeAt.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartPhase(batch *entity.BatchMain) string {
|
||||
if batch == nil || batch.BatchInfo == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(batch.BatchInfo.Get("restart.phase").String())
|
||||
}
|
||||
|
||||
func restartResumeAt(batch *entity.BatchMain) *gtime.Time {
|
||||
if batch == nil || batch.BatchInfo == nil {
|
||||
return nil
|
||||
}
|
||||
value := strings.TrimSpace(batch.BatchInfo.Get("restart.resumeAt").String())
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
parsed := gtime.NewFromStr(value)
|
||||
if parsed == nil || parsed.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonBatchMain(NewCommonBatchMain())
|
||||
}
|
||||
|
||||
// Model 批次主表ORM模型
|
||||
func (s *sCommonBatchMain) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.BatchMain.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sCommonBatchMain) detailModel(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.BatchDetail.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取批次主表列表
|
||||
func (s *sCommonBatchMain) List(ctx context.Context, in *model.BatchMainListParam) (list []*model.BatchMainListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.BatchMainListModel{})
|
||||
|
||||
// 查询主键ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.BatchMain.Columns().Id, in.Id)
|
||||
}
|
||||
// 查询批次编号
|
||||
if in.BatchNo != "" {
|
||||
mod = mod.Where(dao.BatchMain.Columns().BatchNo+" LIKE ?", "%"+in.BatchNo+"%")
|
||||
}
|
||||
// 查询订单编号
|
||||
if in.OrderSn != "" {
|
||||
mod = mod.Where(dao.BatchMain.Columns().OrderSn+" LIKE ?", "%"+in.OrderSn+"%")
|
||||
}
|
||||
|
||||
// 查询批次类型 枚举
|
||||
mod = visibleBatchTypeModel(mod, dao.BatchMain.Columns().BatchType, in.BatchType)
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.BatchMain.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.BatchMain.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取批次主表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
units := make([]*allocationunits.Fields, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item.BatchType == consts.BatchTypeAllocate {
|
||||
fields := allocationunits.Resolve(item.BatchInfo, item.CreatedDept, item.TargetDeptId)
|
||||
units = append(units, &fields)
|
||||
}
|
||||
}
|
||||
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), units); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
unitIndex := 0
|
||||
for _, item := range list {
|
||||
if item.BatchType == consts.BatchTypeAllocate {
|
||||
fields := units[unitIndex]
|
||||
item.SourceDeptId, item.SourceDeptName = fields.SourceDeptId, fields.SourceDeptName
|
||||
item.TargetDeptId, item.TargetDeptName, item.TargetDeptMode = fields.TargetDeptId, fields.TargetDeptName, fields.TargetDeptMode
|
||||
unitIndex++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sCommonBatchMain) SuccessList(ctx context.Context, in *model.BatchMainSuccessListParam) (list []*model.BatchMainSuccessListModel, totalCount int, err error) {
|
||||
if in == nil {
|
||||
in = &model.BatchMainSuccessListParam{}
|
||||
}
|
||||
|
||||
columns := dao.BatchMain.Columns()
|
||||
mod := s.Model(ctx).
|
||||
Fields(
|
||||
columns.BatchInfo,
|
||||
columns.BatchNo,
|
||||
columns.BatchType,
|
||||
columns.StartTime+" AS batch_time",
|
||||
columns.TotalCount,
|
||||
columns.CreatedDept,
|
||||
columns.TargetDeptId,
|
||||
).
|
||||
Where(columns.RunResult, consts.BatchRunResultSuccess).
|
||||
WhereGT(columns.TotalCount, 0)
|
||||
|
||||
var requestedBatchType *int
|
||||
if in.BatchType != 0 {
|
||||
requestedBatchType = &in.BatchType
|
||||
}
|
||||
mod = successListBatchTypeModel(mod, columns.BatchType, requestedBatchType)
|
||||
|
||||
if chanKey := strings.TrimSpace(in.ChanKey); chanKey != "" {
|
||||
extract := "JSON_UNQUOTE(JSON_EXTRACT(%s, '%s'))"
|
||||
if strings.EqualFold(dao.BatchMain.DB().GetConfig().Type, "sqlite") {
|
||||
extract = "JSON_EXTRACT(%s, '%s')"
|
||||
}
|
||||
nested := fmt.Sprintf(extract, columns.BatchInfo, "$.inboundOptions.chanKey")
|
||||
legacy := fmt.Sprintf(extract, columns.BatchInfo, "$.chanKey")
|
||||
mod = mod.Where(fmt.Sprintf("CASE WHEN JSON_VALID(%s) THEN COALESCE(NULLIF(TRIM(%s), ''), TRIM(%s)) END = ?", columns.BatchInfo, nested, legacy), chanKey)
|
||||
}
|
||||
|
||||
if in.BatchNo != "" {
|
||||
mod = mod.Where(columns.BatchNo+" LIKE ?", "%"+in.BatchNo+"%")
|
||||
}
|
||||
if len(in.Time) == 2 {
|
||||
in.Time[1] = in.Time[1].EndOfDay()
|
||||
mod = mod.WhereBetween(columns.StartTime, in.Time[0], in.Time[1])
|
||||
}
|
||||
|
||||
mod = mod.Page(in.Page, in.PageSize).OrderDesc(columns.Id)
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取成功批次列表失败,请稍后重试")
|
||||
}
|
||||
if err = fillInboundBatchStockCounts(ctx, list); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
units := make([]*allocationunits.Fields, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item.BatchType == consts.BatchTypeAllocate {
|
||||
item.Fields = allocationunits.Resolve(item.BatchInfo, item.StoredSourceDeptId, item.StoredTargetDeptId)
|
||||
units = append(units, &item.Fields)
|
||||
}
|
||||
}
|
||||
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), units); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fillInboundBatchStockCounts(ctx context.Context, list []*model.BatchMainSuccessListModel) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
batchNos := make([]string, 0, len(list))
|
||||
for _, item := range list {
|
||||
if item == nil || item.BatchType != consts.BatchTypeInbound || strings.TrimSpace(item.BatchNo) == "" {
|
||||
continue
|
||||
}
|
||||
batchNos = append(batchNos, item.BatchNo)
|
||||
}
|
||||
if len(batchNos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type stockCountRow struct {
|
||||
BatchNo string `orm:"batch_no"`
|
||||
StockCount int `orm:"stock_count"`
|
||||
}
|
||||
var rows []*stockCountRow
|
||||
stockColumns := dao.TSimCardStock.Columns()
|
||||
if err := dao.TSimCardStock.Ctx(ctx).
|
||||
Fields(stockColumns.BatchNo, "COUNT(1) AS stock_count").
|
||||
Where(stockColumns.TenantId, contexts.GetTenantId(ctx)).
|
||||
WhereIn(stockColumns.BatchNo, batchNos).
|
||||
Group(stockColumns.BatchNo).
|
||||
Scan(&rows); err != nil {
|
||||
return gerror.Wrap(err, "统计入库批次在库数量失败")
|
||||
}
|
||||
|
||||
stockCountMap := make(map[string]int, len(rows))
|
||||
for _, row := range rows {
|
||||
if row != nil {
|
||||
stockCountMap[row.BatchNo] = row.StockCount
|
||||
}
|
||||
}
|
||||
for _, item := range list {
|
||||
if item != nil && item.BatchType == consts.BatchTypeInbound {
|
||||
item.StockCount = stockCountMap[item.BatchNo]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type batchDetailQuery struct {
|
||||
BatchNo string
|
||||
BatchType *int
|
||||
DetailStatus *int
|
||||
Iccid string
|
||||
Msisdn string
|
||||
CurrentStockOnly bool
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
func (s *sCommonBatchMain) ListCards(ctx context.Context, in *model.BatchCardListParam) (list []*model.BatchCardListModel, totalCount int, err error) {
|
||||
if in == nil || in.BatchNo == "" {
|
||||
return nil, 0, gerror.New("批次号不能为空")
|
||||
}
|
||||
batch, err := s.getVisibleBatch(ctx, in.BatchNo, in.BatchType)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
details, totalCount, err := s.listBatchDetails(ctx, batch, batchDetailQuery{
|
||||
DetailStatus: in.DetailStatus,
|
||||
BatchNo: in.BatchNo,
|
||||
BatchType: in.BatchType,
|
||||
Iccid: in.Iccid,
|
||||
Msisdn: in.Msisdn,
|
||||
CurrentStockOnly: in.CurrentStockOnly,
|
||||
Page: in.Page,
|
||||
PageSize: in.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
list = make([]*model.BatchCardListModel, 0, len(details))
|
||||
displayRowStart := 0
|
||||
if in.Page > 0 && in.PageSize > 0 {
|
||||
displayRowStart = (in.Page - 1) * in.PageSize
|
||||
}
|
||||
units := make([]*allocationunits.Fields, 0, len(details))
|
||||
parentUnits := allocationunits.Resolve(batch.BatchInfo, batch.CreatedDept, batch.TargetDeptId)
|
||||
for index, detail := range details {
|
||||
iccid, msisdn, imsi := batchDetailSnapshotCards(detail)
|
||||
item := &model.BatchCardListModel{
|
||||
RowNo: displayRowStart + index + 1,
|
||||
Iccid: iccid,
|
||||
Msisdn: msisdn,
|
||||
Imsi: imsi,
|
||||
DetailStatus: detail.DetailStatus,
|
||||
ErrorReason: detail.ErrorReason,
|
||||
}
|
||||
if batch.BatchType == consts.BatchTypeAllocate {
|
||||
item.Fields = allocationunits.Resolve(detail.Snapshot, parentUnits.SourceDeptId, parentUnits.TargetDeptId)
|
||||
if item.SourceDeptName == "" && item.SourceDeptId == parentUnits.SourceDeptId {
|
||||
item.SourceDeptName = parentUnits.SourceDeptName
|
||||
}
|
||||
if item.TargetDeptName == "" && item.TargetDeptId == parentUnits.TargetDeptId {
|
||||
item.TargetDeptName = parentUnits.TargetDeptName
|
||||
}
|
||||
units = append(units, &item.Fields)
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), units); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// listBatchDetails 获取批次卡清单明细。
|
||||
func (s *sCommonBatchMain) listBatchDetails(ctx context.Context, batch *entity.BatchMain, query batchDetailQuery) (list []*entity.BatchDetail, totalCount int, err error) {
|
||||
columns := dao.BatchDetail.Columns()
|
||||
mod := s.detailModel(ctx).
|
||||
Fields(
|
||||
columns.Id,
|
||||
columns.BatchNo,
|
||||
columns.BatchType,
|
||||
columns.DetailStatus,
|
||||
columns.RowNo,
|
||||
columns.ItemId,
|
||||
columns.ErrorReason,
|
||||
columns.Snapshot,
|
||||
columns.TenantId,
|
||||
columns.CreatedDept,
|
||||
columns.CreatedBy,
|
||||
).
|
||||
Where(columns.BatchType, batch.BatchType)
|
||||
if batch.BatchType == consts.BatchTypeAllocate && (batch.SourceBatchNo == "" || batch.SourceBatchNo == batch.BatchNo) {
|
||||
mainColumns := dao.BatchMain.Columns()
|
||||
childBatches := s.Model(ctx).Fields(mainColumns.BatchNo).
|
||||
Where(mainColumns.BatchType, consts.BatchTypeAllocate).
|
||||
Where(mainColumns.SourceBatchNo, batch.BatchNo).
|
||||
Where(mainColumns.SourceBatchNo + " <> " + mainColumns.BatchNo)
|
||||
mod = mod.Where("("+columns.BatchNo+" = ? OR "+columns.BatchNo+" IN (?))", batch.BatchNo, childBatches)
|
||||
} else {
|
||||
mod = mod.Where(columns.BatchNo, batch.BatchNo)
|
||||
}
|
||||
|
||||
if query.DetailStatus != nil {
|
||||
mod = mod.Where(columns.DetailStatus, *query.DetailStatus)
|
||||
}
|
||||
if query.Iccid != "" {
|
||||
like := "%" + query.Iccid + "%"
|
||||
mod = mod.Where(
|
||||
"("+columns.ItemId+" LIKE ? OR JSON_UNQUOTE(JSON_EXTRACT("+columns.Snapshot+", '$.ICCID')) LIKE ?)",
|
||||
like,
|
||||
like,
|
||||
)
|
||||
}
|
||||
if query.Msisdn != "" {
|
||||
mod = mod.Where(
|
||||
"JSON_UNQUOTE(JSON_EXTRACT("+columns.Snapshot+", '$.MSISDN')) LIKE ?",
|
||||
"%"+query.Msisdn+"%",
|
||||
)
|
||||
}
|
||||
if query.CurrentStockOnly {
|
||||
mod = onlyCurrentStockDetails(mod)
|
||||
}
|
||||
|
||||
mod = orderBatchCards(mod.Page(query.Page, query.PageSize), batch.BatchType)
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取批次明细列表失败,请稍后重试")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func orderBatchCards(mod *gdb.Model, batchType int) *gdb.Model {
|
||||
columns := dao.BatchDetail.Columns()
|
||||
if batchType == consts.BatchTypeInbound || batchType == consts.BatchTypeOutbound || batchType == consts.BatchTypeAllocate {
|
||||
order := fmt.Sprintf("CASE WHEN %s = %d AND COALESCE(%s, '') NOT LIKE '本批%%整批未执行%%' AND COALESCE(%s, '') NOT LIKE '同批%%本行未执行%%' AND COALESCE(%s, '') NOT LIKE '同批%%整批未入库%%' THEN 0 ELSE 1 END, %s ASC, %s ASC", columns.DetailStatus, consts.BatchDetailStatusFailed, columns.ErrorReason, columns.ErrorReason, columns.ErrorReason, columns.RowNo, columns.Id)
|
||||
return mod.Order(gdb.Raw(order))
|
||||
}
|
||||
return mod.OrderAsc(columns.RowNo).OrderAsc(columns.Id)
|
||||
}
|
||||
|
||||
func onlyCurrentStockDetails(mod *gdb.Model) *gdb.Model {
|
||||
detailTable := dao.BatchDetail.Table()
|
||||
stockTable := dao.TSimCardStock.Table()
|
||||
return mod.Where(fmt.Sprintf(
|
||||
"EXISTS (SELECT 1 FROM `%s` stock WHERE stock.`tenant_id` = `%s`.`tenant_id` AND stock.`batch_no` = `%s`.`batch_no` AND stock.`iccid` = COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`%s`.`snapshot`, '$.ICCID')), ''), `%s`.`item_id`))",
|
||||
stockTable,
|
||||
detailTable,
|
||||
detailTable,
|
||||
detailTable,
|
||||
detailTable,
|
||||
))
|
||||
}
|
||||
|
||||
func (s *sCommonBatchMain) getVisibleBatch(ctx context.Context, batchNo string, batchType *int) (*entity.BatchMain, error) {
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("批次号不能为空")
|
||||
}
|
||||
|
||||
columns := dao.BatchMain.Columns()
|
||||
batch := &entity.BatchMain{}
|
||||
mod := s.Model(ctx).
|
||||
Fields(
|
||||
columns.Id,
|
||||
columns.BatchNo,
|
||||
columns.BatchType,
|
||||
columns.TenantId,
|
||||
columns.DeptId,
|
||||
columns.BatchInfo,
|
||||
columns.TargetDeptId,
|
||||
columns.SourceBatchNo,
|
||||
columns.CreatedDept,
|
||||
columns.CreatedBy,
|
||||
).
|
||||
Where(columns.BatchNo, batchNo)
|
||||
mod = visibleBatchTypeModel(mod, columns.BatchType, batchType)
|
||||
if err := mod.Scan(batch); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取批次信息失败,请稍后重试")
|
||||
}
|
||||
if batch.Id == 0 {
|
||||
return nil, gerror.New("批次不存在")
|
||||
}
|
||||
if !visibleBatchTypeMatches(batch.BatchType, batchType) {
|
||||
return nil, gerror.New("批次类型不匹配")
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
func batchDetailSnapshotCards(detail *entity.BatchDetail) (iccid, msisdn, imsi string) {
|
||||
if detail == nil {
|
||||
return
|
||||
}
|
||||
if detail.Snapshot != nil {
|
||||
iccid = strings.TrimSpace(detail.Snapshot.Get("ICCID").String())
|
||||
msisdn = strings.TrimSpace(detail.Snapshot.Get("MSISDN").String())
|
||||
imsi = strings.TrimSpace(detail.Snapshot.Get("IMSI").String())
|
||||
}
|
||||
if iccid == "" {
|
||||
iccid = detail.ItemId
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ExportWarehousing 导出入库批次列表(异步:提交任务后到下载中心取文件)。
|
||||
// 数据来源与入库管理页保持一致:成功批次列表(batch_type=入库)。
|
||||
func (s *sCommonBatchMain) ExportWarehousing(ctx context.Context, in *model.BatchMainSuccessListParam) (err error) {
|
||||
if in == nil {
|
||||
in = &model.BatchMainSuccessListParam{}
|
||||
}
|
||||
// 与页面 WarehousingList 一致:固定只导入库批次,不接受调用方传其它类型
|
||||
in.BatchType = consts.BatchTypeInbound
|
||||
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "batchMainWarehousing",
|
||||
BizName: "入库管理",
|
||||
PermCode: "cpm:iot:tSimCardStock:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.BatchMainSuccessListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.SuccessList(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Export 导出批次主表
|
||||
func (s *sCommonBatchMain) Export(ctx context.Context, in *model.BatchMainListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.BatchMainExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出批次主表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.BatchMainExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增批次主表
|
||||
func (s *sCommonBatchMain) Edit(ctx context.Context, in *model.BatchMainEditParam) (err error) {
|
||||
if in == nil {
|
||||
return gerror.New("批次参数不能为空")
|
||||
}
|
||||
if in.Id <= 0 {
|
||||
if in.ExecutionMode == nil {
|
||||
return gerror.New("新建批次必须指定单卡或批量操作规模")
|
||||
}
|
||||
if err = batchmetadata.ValidateCreation(batchmetadata.Console, *in.ExecutionMode); err != nil {
|
||||
return err
|
||||
}
|
||||
if *in.ExecutionMode == batchmetadata.Single && (in.TotalCount == nil || *in.TotalCount != 1) {
|
||||
return gerror.New("单卡批次总数量必须为1")
|
||||
}
|
||||
}
|
||||
data := do.BatchMain{}
|
||||
if err = gconv.Struct(in, &data); err != nil {
|
||||
return gerror.Wrap(err, "转换批次参数失败")
|
||||
}
|
||||
data.CreatedAt = nil
|
||||
data.UpdatedAt = nil
|
||||
// 验证'BatchNo'唯一
|
||||
if err = xgorm.IsUnique(ctx, &dao.BatchMain, g.Map{dao.BatchMain.Columns().BatchNo: in.BatchNo}, "批次编号已存在", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
data.SourceChannel = nil
|
||||
data.ExecutionMode = nil
|
||||
if _, err = s.Model(ctx).TX(tx).
|
||||
Fields(model.BatchMainUpdateFields{}).
|
||||
WherePri(in.Id).Data(data).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改批次主表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
data.Id = nil
|
||||
data.TenantId = contexts.GetTenantId(ctx)
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.SourceChannel = batchmetadata.Console
|
||||
data.ExecutionMode = *in.ExecutionMode
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx).
|
||||
Fields(model.BatchMainInsertFields{}).
|
||||
Data(data).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增批次主表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除批次主表
|
||||
func (s *sCommonBatchMain) Delete(ctx context.Context, in *model.BatchMainDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除批次主表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取批次主表指定信息
|
||||
func (s *sCommonBatchMain) Requeue(ctx context.Context, in *model.BatchMainRequeueParam) (res *model.BatchMainRequeueModel, err error) {
|
||||
if in == nil {
|
||||
in = &model.BatchMainRequeueParam{}
|
||||
}
|
||||
batchNo := strings.TrimSpace(in.BatchNo)
|
||||
reason := strings.TrimSpace(in.Reason)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("批次编号不能为空")
|
||||
}
|
||||
if reason == "" {
|
||||
return nil, gerror.New("重投原因不能为空")
|
||||
}
|
||||
|
||||
batch, err := s.getRequeueBatch(ctx, batchNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = validateRequeueBatch(batch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := batchExecuteMessage{BatchNo: batch.BatchNo, BatchType: batch.BatchType}
|
||||
if err = publishBatchExecuteMessage(ctx, consts.QueueBatchExecute, message); err != nil {
|
||||
g.Log().Errorf(ctx, "batch requeue publish failed, batchNo=%s, batchType=%d, tenantId=%s, userId=%d, reason=%s, err=%+v",
|
||||
batch.BatchNo, batch.BatchType, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason, err)
|
||||
return nil, gerror.Wrap(err, "发布批次重投消息失败")
|
||||
}
|
||||
g.Log().Infof(ctx, "batch requeue published, batchNo=%s, batchType=%d, status=%d, runResult=%d, tenantId=%s, userId=%d, reason=%s",
|
||||
batch.BatchNo, batch.BatchType, batch.BatchAsyncStatus, batch.RunResult, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason)
|
||||
|
||||
return &model.BatchMainRequeueModel{
|
||||
BatchNo: batch.BatchNo,
|
||||
BatchType: batch.BatchType,
|
||||
BatchAsyncStatus: batch.BatchAsyncStatus,
|
||||
RunResult: batch.RunResult,
|
||||
Requeued: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sCommonBatchMain) View(ctx context.Context, in *model.BatchMainViewParam) (res *model.BatchMainViewModel, err error) {
|
||||
mod := visibleBatchTypeModel(
|
||||
s.Model(ctx).WherePri(in.Id),
|
||||
dao.BatchMain.Columns().BatchType,
|
||||
nil,
|
||||
)
|
||||
if err = mod.Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取批次主表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if res == nil || res.Id == 0 {
|
||||
return nil, gerror.New("批次不存在")
|
||||
}
|
||||
if res.BatchType == consts.BatchTypeAllocate {
|
||||
fields := allocationunits.Resolve(res.BatchInfo, res.CreatedDept, res.TargetDeptId)
|
||||
if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), []*allocationunits.Fields{&fields}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.SourceDeptId, res.SourceDeptName = fields.SourceDeptId, fields.SourceDeptName
|
||||
res.TargetDeptId, res.TargetDeptName, res.TargetDeptMode = fields.TargetDeptId, fields.TargetDeptName, fields.TargetDeptMode
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
)
|
||||
|
||||
func TestInboundBatchChannelFiltering(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
"ALTER TABLE batch_main ADD target_dept_id INTEGER DEFAULT 0",
|
||||
"CREATE TABLE t_sim_card_stock(batch_no TEXT,tenant_id TEXT)",
|
||||
"INSERT INTO t_sim_card_stock VALUES('A-NESTED',''),('A-NESTED','')",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
fixtures := []struct {
|
||||
name string
|
||||
info string
|
||||
batchType int
|
||||
runResult int
|
||||
}{
|
||||
{"A-NESTED", `{"inboundOptions":{"chanKey":"A"}}`, 1, consts.BatchRunResultSuccess},
|
||||
{"A-LEGACY", `{"chanKey":"A"}`, 1, consts.BatchRunResultSuccess},
|
||||
{"B-NESTED", `{"inboundOptions":{"chanKey":"B"},"chanKey":"A"}`, 1, consts.BatchRunResultSuccess},
|
||||
{"A-EMPTY-NESTED", `{"inboundOptions":{"chanKey":""},"chanKey":"A"}`, 1, consts.BatchRunResultSuccess},
|
||||
{"NO-CHANNEL", "{}", 1, consts.BatchRunResultSuccess},
|
||||
{"INVALID-JSON", "broken", 1, consts.BatchRunResultSuccess},
|
||||
{"OTHER-TYPE", `{"chanKey":"A"}`, 2, consts.BatchRunResultSuccess},
|
||||
{"NOT-SUCCESS", `{"chanKey":"A"}`, 1, consts.BatchRunResultFailed},
|
||||
}
|
||||
for _, fixture := range fixtures {
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO batch_main(batch_no,batch_type,batch_info,total_count,run_result) VALUES(?,?,?,?,?)", fixture.name, fixture.batchType, fixture.info, 2, fixture.runResult); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
channel string
|
||||
page int
|
||||
size int
|
||||
wantTotal int
|
||||
wantRows int
|
||||
}{
|
||||
{"渠道A兼容新旧格式", "A", 1, 20, 3, 3},
|
||||
{"渠道B优先使用嵌套编码", "B", 1, 20, 1, 1},
|
||||
{"清空恢复全部入库批次", "", 1, 20, 6, 6},
|
||||
{"前后空白不影响筛选", " A ", 1, 20, 3, 3},
|
||||
{"总数按渠道过滤后分页", "A", 2, 2, 3, 1},
|
||||
{"不存在的渠道返回空", "missing", 1, 20, 0, 0},
|
||||
{"特殊字符作为参数而不是SQL", "A' OR 1=1 --", 1, 20, 0, 0},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rows, total, err := NewCommonBatchMain().SuccessList(ctx, &model.BatchMainSuccessListParam{BatchType: consts.BatchTypeInbound, ChanKey: test.channel, PageInfo: request.PageInfo{Page: test.page, PageSize: test.size}})
|
||||
if err != nil || total != test.wantTotal || len(rows) != test.wantRows {
|
||||
t.Fatalf("total=%d rows=%d err=%v", total, len(rows), err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row.BatchNo == "A-NESTED" && row.StockCount != 2 {
|
||||
t.Fatalf("stock count changed: %+v", row)
|
||||
}
|
||||
if test.channel == "B" && row.BatchNo != "B-NESTED" {
|
||||
t.Fatalf("nested channel priority violated: %+v", row)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestBatchMainEditRejectsMissingCreationMetadata(t *testing.T) {
|
||||
for _, mode := range []int{batchmetadata.Unknown, -1, 3} {
|
||||
if err := NewCommonBatchMain().Edit(context.Background(), &model.BatchMainEditParam{ExecutionMode: &mode}); err == nil {
|
||||
t.Fatalf("accepted execution mode %d", mode)
|
||||
}
|
||||
}
|
||||
for _, input := range []*model.BatchMainEditParam{nil, {}} {
|
||||
if err := NewCommonBatchMain().Edit(context.Background(), input); err == nil {
|
||||
t.Fatal("accepted missing creation metadata")
|
||||
}
|
||||
}
|
||||
mode := batchmetadata.Single
|
||||
quantity := 2
|
||||
if err := NewCommonBatchMain().Edit(context.Background(), &model.BatchMainEditParam{ExecutionMode: &mode, TotalCount: &quantity}); err == nil {
|
||||
t.Fatal("accepted multiple cards as a single-card batch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchMainEditWritesTrustedCreationMetadata(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, column := range []string{"updated_by INTEGER", "updated_at TEXT"} {
|
||||
if _, err := g.DB().Exec(ctx, "ALTER TABLE batch_main ADD COLUMN "+column); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fields, err := g.Model("batch_main").Ctx(ctx).TableFields("batch_main")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fields["id"].Key = "PRI"
|
||||
if _, err := g.DB().GetCore().GetTablesWithCache(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx = contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 7, TenantId: "A"}}})
|
||||
batchType := consts.BatchTypeInbound
|
||||
quantity := 1
|
||||
claimedUser := int64(999)
|
||||
claimedTenant := "B"
|
||||
for _, mode := range []int{batchmetadata.Single, batchmetadata.Bulk} {
|
||||
batchNo := "META-EDIT-SINGLE"
|
||||
if mode == batchmetadata.Bulk {
|
||||
batchNo = "META-EDIT-BULK"
|
||||
}
|
||||
input := &model.BatchMainEditParam{BatchNo: &batchNo, BatchType: &batchType, ExecutionMode: &mode, TotalCount: &quantity, CreatedBy: &claimedUser, TenantId: &claimedTenant}
|
||||
if err := NewCommonBatchMain().Edit(ctx, input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var batch *entity.BatchMain
|
||||
if err := g.Model("batch_main").Ctx(ctx).Where("batch_no", batchNo).Scan(&batch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if batch == nil || batch.SourceChannel != batchmetadata.Console || batch.ExecutionMode != uint(mode) || batch.CreatedBy != 7 || batch.TenantId != "A" {
|
||||
t.Fatalf("unexpected batch: %+v", batch)
|
||||
}
|
||||
otherMode := 3 - mode
|
||||
if err := NewCommonBatchMain().Edit(ctx, &model.BatchMainEditParam{Id: batch.Id, BatchNo: &batchNo, ExecutionMode: &otherMode}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var updated *entity.BatchMain
|
||||
if err := g.Model("batch_main").Ctx(ctx).Where("id", batch.Id).Scan(&updated); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if updated.SourceChannel != batchmetadata.Console || updated.ExecutionMode != uint(mode) {
|
||||
t.Fatalf("editing changed attribution: %+v", updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
_ "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"
|
||||
)
|
||||
|
||||
func TestBatchMainRequeuePublishesPendingBatch(t *testing.T) {
|
||||
ctx := setupBatchMainRequeueDB(t)
|
||||
insertRequeueBatch(t, ctx, "PENDING-1", consts.BatchTypeInbound, consts.BatchAsyncStatusPending, consts.BatchRunResultPending, `{}`)
|
||||
|
||||
var published []batchExecuteMessage
|
||||
restore := replaceBatchRequeuePublisher(func(_ context.Context, queue string, data any) error {
|
||||
if queue != consts.QueueBatchExecute {
|
||||
t.Fatalf("queue = %s, want %s", queue, consts.QueueBatchExecute)
|
||||
}
|
||||
message, ok := data.(batchExecuteMessage)
|
||||
if !ok {
|
||||
t.Fatalf("message type = %T, want batchExecuteMessage", data)
|
||||
}
|
||||
published = append(published, message)
|
||||
return nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "PENDING-1", Reason: "message lost"})
|
||||
if err != nil {
|
||||
t.Fatalf("Requeue() error = %v", err)
|
||||
}
|
||||
if res == nil || !res.Requeued || res.BatchNo != "PENDING-1" || res.BatchType != consts.BatchTypeInbound {
|
||||
t.Fatalf("unexpected response: %+v", res)
|
||||
}
|
||||
if len(published) != 1 || published[0].BatchNo != "PENDING-1" || published[0].BatchType != consts.BatchTypeInbound {
|
||||
t.Fatalf("unexpected published messages: %+v", published)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchMainRequeuePublishesDueRestartWaitingResume(t *testing.T) {
|
||||
ctx := setupBatchMainRequeueDB(t)
|
||||
resumeAt := gtime.Now().Add(-time.Minute).String()
|
||||
insertRequeueBatch(t, ctx, "RST-DUE", consts.BatchTypeRestart, consts.BatchAsyncStatusProcessing, consts.BatchRunResultRunning, `{"restart":{"phase":"waiting_resume","resumeAt":"`+resumeAt+`"}}`)
|
||||
|
||||
var published []batchExecuteMessage
|
||||
restore := replaceBatchRequeuePublisher(func(_ context.Context, _ string, data any) error {
|
||||
published = append(published, data.(batchExecuteMessage))
|
||||
return nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "RST-DUE", Reason: "delayed message lost"})
|
||||
if err != nil {
|
||||
t.Fatalf("Requeue() error = %v", err)
|
||||
}
|
||||
if res == nil || res.BatchNo != "RST-DUE" || res.BatchType != consts.BatchTypeRestart {
|
||||
t.Fatalf("unexpected response: %+v", res)
|
||||
}
|
||||
if len(published) != 1 || published[0].BatchNo != "RST-DUE" || published[0].BatchType != consts.BatchTypeRestart {
|
||||
t.Fatalf("unexpected published messages: %+v", published)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchMainRequeueRejectsUnsafeStatuses(t *testing.T) {
|
||||
ctx := setupBatchMainRequeueDB(t)
|
||||
future := gtime.Now().Add(time.Hour).String()
|
||||
cases := []struct {
|
||||
name string
|
||||
batchNo string
|
||||
batchType int
|
||||
status int
|
||||
runResult int
|
||||
info string
|
||||
}{
|
||||
{name: "failed", batchNo: "FAILED-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusFailed, runResult: consts.BatchRunResultFailed, info: `{}`},
|
||||
{name: "success", batchNo: "SUCCESS-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusSuccess, runResult: consts.BatchRunResultSuccess, info: `{}`},
|
||||
{name: "partial", batchNo: "PARTIAL-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusPartialSuccess, runResult: consts.BatchRunResultPartialSuccess, info: `{}`},
|
||||
{name: "review", batchNo: "REVIEW-1", batchType: consts.BatchTypeDataStart, status: consts.BatchAsyncStatusReview, runResult: consts.BatchRunResultReview, info: `{}`},
|
||||
{name: "normal processing", batchNo: "PROCESSING-1", batchType: consts.BatchTypeInbound, status: consts.BatchAsyncStatusProcessing, runResult: consts.BatchRunResultRunning, info: `{}`},
|
||||
{name: "restart future resume", batchNo: "RST-FUTURE", batchType: consts.BatchTypeRestart, status: consts.BatchAsyncStatusProcessing, runResult: consts.BatchRunResultRunning, info: `{"restart":{"phase":"waiting_resume","resumeAt":"` + future + `"}}`},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
insertRequeueBatch(t, ctx, tc.batchNo, tc.batchType, tc.status, tc.runResult, tc.info)
|
||||
}
|
||||
|
||||
var published int
|
||||
restore := replaceBatchRequeuePublisher(func(context.Context, string, any) error {
|
||||
published++
|
||||
return nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: tc.batchNo, Reason: "operator retry"})
|
||||
if err == nil {
|
||||
t.Fatal("Requeue() error = nil, want error")
|
||||
}
|
||||
})
|
||||
}
|
||||
if published != 0 {
|
||||
t.Fatalf("published messages = %d, want 0", published)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchMainRequeueHandlesMissingBatchAndPublishFailure(t *testing.T) {
|
||||
ctx := setupBatchMainRequeueDB(t)
|
||||
insertRequeueBatch(t, ctx, "PENDING-FAIL-PUBLISH", consts.BatchTypeInbound, consts.BatchAsyncStatusPending, consts.BatchRunResultPending, `{}`)
|
||||
|
||||
restore := replaceBatchRequeuePublisher(func(context.Context, string, any) error {
|
||||
return errors.New("rabbitmq unavailable")
|
||||
})
|
||||
defer restore()
|
||||
|
||||
if _, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "PENDING-FAIL-PUBLISH", Reason: "message lost"}); err == nil {
|
||||
t.Fatal("Requeue() publish failure error = nil, want error")
|
||||
}
|
||||
if _, err := NewCommonBatchMain().Requeue(ctx, &model.BatchMainRequeueParam{BatchNo: "MISSING", Reason: "message lost"}); err == nil {
|
||||
t.Fatal("Requeue() missing batch error = nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func setupBatchMainRequeueDB(t *testing.T) context.Context {
|
||||
t.Helper()
|
||||
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)
|
||||
})
|
||||
// GoFrame caches table fields by group and table, across SQLite instances.
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, `CREATE TABLE batch_main (
|
||||
source_channel TEXT NOT NULL DEFAULT 'legacy',
|
||||
execution_mode INTEGER NOT NULL DEFAULT 0,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL,
|
||||
batch_type INTEGER NOT NULL,
|
||||
batch_async_status INTEGER NOT NULL,
|
||||
run_result INTEGER NOT NULL,
|
||||
batch_info TEXT,
|
||||
tenant_id TEXT NOT NULL DEFAULT '',
|
||||
dept_id INTEGER NOT NULL DEFAULT 0,
|
||||
created_dept INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER NOT NULL DEFAULT 0
|
||||
)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func insertRequeueBatch(t *testing.T, ctx context.Context, batchNo string, batchType, status, runResult int, batchInfo string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
`INSERT INTO batch_main(batch_no, batch_type, batch_async_status, run_result, batch_info, tenant_id) VALUES(?, ?, ?, ?, ?, '')`,
|
||||
batchNo, batchType, status, runResult, batchInfo,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceBatchRequeuePublisher(fn func(context.Context, string, any) error) func() {
|
||||
original := publishBatchExecuteMessage
|
||||
publishBatchExecuteMessage = fn
|
||||
return func() {
|
||||
publishBatchExecuteMessage = original
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBatchManagementAllocationUnits(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
for _, statement := range []string{
|
||||
"ALTER TABLE batch_main ADD target_dept_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD dept_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD updated_by INTEGER DEFAULT 0",
|
||||
"ALTER TABLE batch_main ADD batch_async_status INTEGER DEFAULT 3",
|
||||
"CREATE TABLE sys_dept(dept_id INTEGER,tenant_id TEXT,dept_key TEXT,dept_name TEXT,deleted_at TEXT)",
|
||||
"INSERT INTO sys_dept VALUES(10,'','source','当前划拨名称',NULL),(20,'','target','当前接收名称',NULL)",
|
||||
`INSERT INTO batch_main(batch_no,batch_type,batch_info,created_dept,target_dept_id,total_count,run_result) VALUES('ALC-PARENT',3,'{"jobRole":"allocate_parent","sourceDeptId":10,"targetDeptId":20,"sourceDeptName":"历史划拨单位","targetDeptName":"历史接收单位"}',10,20,1,2),('ALC-LEGACY',3,'{}',10,20,1,2),('ALC-PER-ROW',3,'{"sourceDeptId":10,"sourceDeptName":"历史划拨单位","targetDeptMode":"per_row"}',10,0,1,2)`,
|
||||
`INSERT INTO batch_main(batch_no,batch_type,source_batch_no,batch_info,created_dept,target_dept_id,total_count,run_result) VALUES('ALC-CHILD',3,'ALC-PARENT','{"jobRole":"allocate_chunk"}',10,20,1,2)`,
|
||||
`INSERT INTO batch_detail(batch_no,batch_type,item_id,snapshot,created_dept) VALUES('ALC-CHILD',3,'card','{}',10)`,
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
service := NewCommonBatchMain()
|
||||
rows, _, err := service.SuccessList(ctx, &model.BatchMainSuccessListParam{BatchType: consts.BatchTypeAllocate, PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, row := range rows {
|
||||
seen[row.BatchNo] = true
|
||||
switch row.BatchNo {
|
||||
case "ALC-PARENT":
|
||||
if row.SourceDeptName != "历史划拨单位" || row.TargetDeptName != "历史接收单位" {
|
||||
t.Fatal(row)
|
||||
}
|
||||
encoded, err := json.Marshal(row)
|
||||
if err != nil || !strings.Contains(string(encoded), `"targetDeptId":20`) || strings.Contains(string(encoded), "StoredTargetDeptId") {
|
||||
t.Fatal("接口契约错误", string(encoded), err)
|
||||
}
|
||||
case "ALC-LEGACY":
|
||||
if row.SourceDeptName != "当前划拨名称" || row.TargetDeptName != "当前接收名称" {
|
||||
t.Fatal(row)
|
||||
}
|
||||
case "ALC-PER-ROW":
|
||||
if row.TargetDeptMode != "per_row" || row.TargetDeptName != "" {
|
||||
t.Fatal(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !seen["ALC-PARENT"] || !seen["ALC-LEGACY"] || !seen["ALC-PER-ROW"] {
|
||||
t.Fatal(seen)
|
||||
}
|
||||
all, _, err := service.List(ctx, &model.BatchMainListParam{BatchNo: "ALC-PARENT", PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil || len(all) != 1 || all[0].TargetDeptName != "历史接收单位" {
|
||||
t.Fatal("主表列表单位缺失", all, err)
|
||||
}
|
||||
view, err := service.View(ctx, &model.BatchMainViewParam{Id: all[0].Id})
|
||||
if err != nil || view.SourceDeptName != "历史划拨单位" || view.TargetDeptName != "历史接收单位" {
|
||||
t.Fatal("批次详情单位缺失", view, err)
|
||||
}
|
||||
cards, total, err := service.ListCards(ctx, &model.BatchCardListParam{BatchNo: "ALC-PARENT", PageInfo: request.PageInfo{Page: 1, PageSize: 20}})
|
||||
if err != nil || total != 1 || len(cards) != 1 {
|
||||
t.Fatal("父批次未展示子批次卡清单", total, err)
|
||||
}
|
||||
if cards[0].Iccid != "card" || cards[0].SourceDeptName != "历史划拨单位" || cards[0].TargetDeptName != "历史接收单位" {
|
||||
t.Fatal(cards[0])
|
||||
}
|
||||
if _, _, err := service.ListCards(context.Background(), &model.BatchCardListParam{BatchNo: "missing"}); err == nil {
|
||||
t.Fatal("不存在批次被放行")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExecutionLogMetadataFilters(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
insertBatchExecutionLogDetail(t, ctx, "META-SINGLE", consts.BatchTypeInbound, 0, 1, "card1")
|
||||
insertBatchExecutionLogDetail(t, ctx, "META-BULK", consts.BatchTypeInbound, 3, 1, "card2")
|
||||
for _, sql := range []string{
|
||||
"UPDATE batch_main SET source_channel='mini_program',execution_mode=1 WHERE batch_no='META-SINGLE'",
|
||||
"UPDATE batch_main SET source_channel='console',execution_mode=2 WHERE batch_no='META-BULK'",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, sql); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
mode := 1
|
||||
list, total, err := NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{PageInfo: request.PageInfo{Page: 1, PageSize: 20}, SourceChannel: "mini_program", ExecutionMode: &mode})
|
||||
if err != nil || total != 1 || len(list) != 1 || list[0].BatchNo != "META-SINGLE" || list[0].SourceChannel != "mini_program" || list[0].ExecutionMode != 1 {
|
||||
t.Fatalf("%+v %d %v", list, total, err)
|
||||
}
|
||||
mode = 2
|
||||
_, total, err = NewCommonBatchExecutionLog().List(ctx, &model.BatchExecutionLogListParam{PageInfo: request.PageInfo{Page: 1, PageSize: 20}, SourceChannel: "mini_program", ExecutionMode: &mode})
|
||||
if err != nil || total != 0 {
|
||||
t.Fatal(total, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
// visibleBatchTypeModel always excludes internal prepare batches before applying
|
||||
// an optional caller-provided batch type filter.
|
||||
func visibleBatchTypeModel(mod *gdb.Model, column string, requested *int) *gdb.Model {
|
||||
mod = mod.Where(column+" <> ?", consts.BatchTypePrepare)
|
||||
if requested != nil {
|
||||
mod = mod.Where(column, *requested)
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
func visibleBatchTypeMatches(actual int, requested *int) bool {
|
||||
if actual == consts.BatchTypePrepare {
|
||||
return false
|
||||
}
|
||||
return requested == nil || actual == *requested
|
||||
}
|
||||
|
||||
// successListBatchTypeModel keeps export execution logs out of business batch selectors.
|
||||
func successListBatchTypeModel(mod *gdb.Model, column string, requested *int) *gdb.Model {
|
||||
mod = visibleBatchTypeModel(mod, column, requested)
|
||||
return mod.Where(column+" <> ?", consts.BatchTypeExport)
|
||||
}
|
||||
|
||||
func successListBatchTypeMatches(actual int, requested *int) bool {
|
||||
return actual != consts.BatchTypeExport && visibleBatchTypeMatches(actual, requested)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
)
|
||||
|
||||
func TestSuccessListBatchTypeMatches(t *testing.T) {
|
||||
prepare := consts.BatchTypePrepare
|
||||
renew := consts.BatchTypeRenew
|
||||
export := consts.BatchTypeExport
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
actual int
|
||||
requested *int
|
||||
want bool
|
||||
}{
|
||||
{name: "unfiltered business batch", actual: consts.BatchTypeChange, want: true},
|
||||
{name: "matching business filter", actual: consts.BatchTypeRenew, requested: &renew, want: true},
|
||||
{name: "different business filter", actual: consts.BatchTypeChange, requested: &renew, want: false},
|
||||
{name: "unfiltered prepare batch", actual: consts.BatchTypePrepare, want: false},
|
||||
{name: "explicit prepare filter remains hidden", actual: consts.BatchTypePrepare, requested: &prepare, want: false},
|
||||
{name: "unfiltered export batch remains hidden", actual: consts.BatchTypeExport, want: false},
|
||||
{name: "explicit export filter remains hidden", actual: consts.BatchTypeExport, requested: &export, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := successListBatchTypeMatches(tt.actual, tt.requested); got != tt.want {
|
||||
t.Fatalf("successListBatchTypeMatches(%d) = %v, want %v", tt.actual, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
"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/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"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"
|
||||
)
|
||||
|
||||
type sCommonDownloadCenter struct{}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonDownloadCenter(NewCommonDownloadCenter())
|
||||
}
|
||||
|
||||
func NewCommonDownloadCenter() *sCommonDownloadCenter {
|
||||
return &sCommonDownloadCenter{}
|
||||
}
|
||||
|
||||
// List 下载中心列表:K2 仅本人 + K1 业务权限过滤
|
||||
func (s *sCommonDownloadCenter) List(ctx context.Context, in *model.DownloadCenterListParam) (list []*model.DownloadCenterListModel, totalCount int, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil || user.ID <= 0 {
|
||||
return nil, 0, gerror.New("登录状态已失效,请重新登录")
|
||||
}
|
||||
|
||||
mod := dao.TExportTask.Ctx(ctx).
|
||||
Fields(model.DownloadCenterListModel{}).
|
||||
Where(dao.TExportTask.Columns().OwnerUserId, user.ID)
|
||||
mod = applyExportPermissionFilter(ctx, mod)
|
||||
|
||||
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
mod = mod.Where(
|
||||
"("+dao.TExportTask.Columns().TaskNo+" LIKE ? OR "+
|
||||
dao.TExportTask.Columns().FileName+" LIKE ? OR "+
|
||||
dao.TExportTask.Columns().BizName+" LIKE ?)",
|
||||
like, like, like,
|
||||
)
|
||||
}
|
||||
if in.BizKey != "" {
|
||||
mod = mod.Where(dao.TExportTask.Columns().BizKey, in.BizKey)
|
||||
}
|
||||
if statusIds := parseStatusIds(in.Status); len(statusIds) > 0 {
|
||||
mod = mod.WhereIn(dao.TExportTask.Columns().Status, statusIds)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
mod = mod.WhereBetween(dao.TExportTask.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
err = mod.Page(in.Page, in.PageSize).
|
||||
OrderDesc(dao.TExportTask.Columns().Id).
|
||||
ScanAndCount(&list, &totalCount, false)
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取导出任务列表失败,请稍后重试")
|
||||
}
|
||||
if list == nil {
|
||||
list = []*model.DownloadCenterListModel{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Recent 右上角全局入口:最近任务与进行中/已完成数量
|
||||
func (s *sCommonDownloadCenter) Recent(ctx context.Context, in *model.DownloadCenterRecentParam) (res *model.DownloadCenterRecentModel, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil || user.ID <= 0 {
|
||||
return nil, gerror.New("登录状态已失效,请重新登录")
|
||||
}
|
||||
limit := in.Limit
|
||||
if limit <= 0 || limit > 20 {
|
||||
limit = 5
|
||||
}
|
||||
|
||||
items := make([]*model.DownloadCenterListModel, 0, limit)
|
||||
mod := dao.TExportTask.Ctx(ctx).
|
||||
WhereNull(dao.TExportTask.Columns().DeletedTime).
|
||||
Fields(model.DownloadCenterListModel{}).
|
||||
Where(dao.TExportTask.Columns().OwnerUserId, user.ID)
|
||||
mod = applyExportPermissionFilter(ctx, mod)
|
||||
if err = mod.OrderDesc(dao.TExportTask.Columns().Id).Limit(limit).Scan(&items); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取最近导出任务失败")
|
||||
}
|
||||
|
||||
running, err := s.countByStatus(ctx, user.ID, []int{consts.DownloadStatusPending, consts.DownloadStatusRunning})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
finished, err := s.countByStatus(ctx, user.ID, []int{consts.DownloadStatusGenerated})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.DownloadCenterRecentModel{
|
||||
Items: items,
|
||||
RunningCount: running,
|
||||
FinishedCount: finished,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sCommonDownloadCenter) countByStatus(ctx context.Context, userId int64, status []int) (int, error) {
|
||||
mod := dao.TExportTask.Ctx(ctx).
|
||||
WhereNull(dao.TExportTask.Columns().DeletedTime).
|
||||
Where(dao.TExportTask.Columns().OwnerUserId, userId).
|
||||
WhereIn(dao.TExportTask.Columns().Status, status)
|
||||
mod = applyExportPermissionFilter(ctx, mod)
|
||||
count, err := mod.Count()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "统计导出任务数量失败")
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Download 下载导出文件:重新校验 K1+K2,然后直接写文件流
|
||||
func (s *sCommonDownloadCenter) Download(ctx context.Context, in *model.DownloadCenterTaskParam) (err error) {
|
||||
task, err := s.loadOwnedTask(ctx, in.TaskId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasExportPermission(ctx, task.PermCode) {
|
||||
return gerror.New("你的导出权限已变更,无法下载该文件")
|
||||
}
|
||||
if task.DeletedTime != nil {
|
||||
return gerror.New("文件已删除或已过期清理,请重新导出")
|
||||
}
|
||||
if task.Status != consts.DownloadStatusGenerated {
|
||||
return gerror.New("文件尚未生成完成,请稍后再试")
|
||||
}
|
||||
if task.ExpireTime == nil || !task.ExpireTime.After(gtime.Now()) {
|
||||
return gerror.New("文件已过期,请重新导出")
|
||||
}
|
||||
if task.OssId <= 0 {
|
||||
return gerror.New("文件不存在")
|
||||
}
|
||||
|
||||
oss := &entity.SysOss{}
|
||||
if err = dao.SysOss.Ctx(ctx).Where(dao.SysOss.Columns().OssId, task.OssId).Scan(oss); err != nil {
|
||||
return gerror.Wrap(err, "获取文件信息失败")
|
||||
}
|
||||
if oss.OssId <= 0 {
|
||||
return gerror.New("文件不存在或已被清理")
|
||||
}
|
||||
|
||||
return service.SysOss().DownloadLocal(ctx, oss, func() error {
|
||||
columns := dao.TExportTask.Columns()
|
||||
// 先计算有效期再递增次数,避免 MySQL 从左到右赋值影响首次下载判断。
|
||||
affected, updateErr := dao.TExportTask.Ctx(ctx).
|
||||
Data("expire_time = CASE WHEN download_count = 0 THEN LEAST(expire_time, DATE_ADD(NOW(), INTERVAL 1 HOUR)) ELSE expire_time END, last_download_time = NOW(), download_count = download_count + 1").
|
||||
Where(columns.Id, task.Id).
|
||||
WhereNull(columns.DeletedTime).
|
||||
Where(columns.OwnerUserId, task.OwnerUserId).
|
||||
Where(columns.Status, consts.DownloadStatusGenerated).
|
||||
Where(columns.OssId, oss.OssId).
|
||||
Where(columns.ExpireTime + " > NOW()").
|
||||
UpdateAndGetAffected()
|
||||
if updateErr != nil {
|
||||
return gerror.Wrap(updateErr, "更新下载有效期失败,请稍后重试")
|
||||
}
|
||||
if affected != 1 {
|
||||
return gerror.New("文件已过期或状态已变更,请刷新后重试")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Remove 物理清理文件与OSS记录,软删除并保留导出任务历史
|
||||
func (s *sCommonDownloadCenter) Remove(ctx context.Context, in *model.DownloadCenterTaskParam) (err error) {
|
||||
task, err := s.loadOwnedTask(ctx, in.TaskId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if task.Status == consts.DownloadStatusRunning {
|
||||
return gerror.New("文件正在生成中,暂时不能删除")
|
||||
}
|
||||
if !hasExportPermission(ctx, task.PermCode) {
|
||||
return gerror.New("你的导出权限已变更,无法删除该任务")
|
||||
}
|
||||
|
||||
userId := contexts.GetUserId(ctx)
|
||||
_, err = exportcenter.RemoveTaskFile(ctx, task.Id, "user", func(locked *entity.TExportTask) error {
|
||||
if locked.OwnerUserId != userId {
|
||||
return gerror.New("无权访问该导出任务")
|
||||
}
|
||||
if locked.Status == consts.DownloadStatusRunning {
|
||||
return gerror.New("任务已开始生成,暂时不能删除")
|
||||
}
|
||||
if !hasExportPermission(ctx, locked.PermCode) {
|
||||
return gerror.New("你的导出权限已变更,无法删除该任务")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "删除导出任务失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadOwnedTask 取任务并校验归属(K2:仅本人)
|
||||
func (s *sCommonDownloadCenter) loadOwnedTask(ctx context.Context, taskId int64) (*entity.TExportTask, error) {
|
||||
if taskId <= 0 {
|
||||
return nil, gerror.New("任务ID不能为空")
|
||||
}
|
||||
task := &entity.TExportTask{}
|
||||
if err := dao.TExportTask.Ctx(ctx).
|
||||
Where(dao.TExportTask.Columns().Id, taskId).
|
||||
Scan(task); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取导出任务失败")
|
||||
}
|
||||
if task.Id <= 0 {
|
||||
return nil, gerror.New("导出任务不存在")
|
||||
}
|
||||
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil || user.ID <= 0 {
|
||||
return nil, gerror.New("登录状态已失效,请重新登录")
|
||||
}
|
||||
if task.OwnerUserId != user.ID {
|
||||
return nil, gerror.New("无权访问该导出任务")
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// applyExportPermissionFilter K1:只显示「我仍持有对应导出权限」的任务
|
||||
func applyExportPermissionFilter(ctx context.Context, mod *gdb.Model) *gdb.Model {
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
return mod
|
||||
}
|
||||
codes := exportPermissionCodes(contexts.GetAccessCodeList(ctx))
|
||||
if len(codes) == 0 {
|
||||
return mod.Where("1 = 0")
|
||||
}
|
||||
|
||||
exact := make([]string, 0, len(codes))
|
||||
prefixes := make([]string, 0, len(codes))
|
||||
for _, code := range codes {
|
||||
if strings.HasSuffix(code, "*") {
|
||||
prefixes = append(prefixes, strings.TrimSuffix(code, "*"))
|
||||
continue
|
||||
}
|
||||
exact = append(exact, code)
|
||||
}
|
||||
|
||||
conditions := make([]string, 0, len(exact)+len(prefixes))
|
||||
args := make([]interface{}, 0, len(exact)+len(prefixes))
|
||||
column := dao.TExportTask.Columns().PermCode
|
||||
if len(exact) > 0 {
|
||||
conditions = append(conditions, column+" IN (?)")
|
||||
args = append(args, exact)
|
||||
}
|
||||
for _, prefix := range prefixes {
|
||||
conditions = append(conditions, column+" LIKE ?")
|
||||
args = append(args, prefix+"%")
|
||||
}
|
||||
if len(conditions) == 0 {
|
||||
return mod.Where("1 = 0")
|
||||
}
|
||||
return mod.Where("("+strings.Join(conditions, " OR ")+")", args...)
|
||||
}
|
||||
|
||||
// hasExportPermission 下载/删除时的二次校验
|
||||
func hasExportPermission(ctx context.Context, required string) bool {
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(required) == "" {
|
||||
// 提交时接口未声明权限码时,退化为只校验归属,避免历史任务全部不可用
|
||||
return true
|
||||
}
|
||||
for _, code := range contexts.GetAccessCodeList(ctx) {
|
||||
if matchExportCode(code, required) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchExportCode(pattern, code string) bool {
|
||||
if pattern == code {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(pattern, "*") {
|
||||
return strings.HasPrefix(code, strings.TrimSuffix(pattern, "*"))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// exportPermissionCodes 取「可用于判断导出任务可见性」的权限码。
|
||||
//
|
||||
// 历史实现只认 :export 结尾,但导出任务的 perm_code 是提交时声明的权限码,
|
||||
// 不同业务可能声明为 :view / :download / :list(例如卡基础信息查询、月历史用量查询
|
||||
// 把查询与导出合并成一个 :view 码)。只认 :export 会让这些任务对提交人自己也不可见。
|
||||
//
|
||||
// 放宽到常见后缀不影响安全:真正下载时 hasExportPermission 会拿任务记录的 perm_code
|
||||
// 再校验一次,用户失去权限后即使看得到记录也拿不到文件。
|
||||
func exportPermissionCodes(list []string) []string {
|
||||
suffixes := []string{":export", ":view", ":download", ":list"}
|
||||
out := make([]string, 0, len(list))
|
||||
for _, code := range list {
|
||||
if strings.HasSuffix(code, "*") {
|
||||
out = append(out, code)
|
||||
continue
|
||||
}
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(code, suffix) {
|
||||
out = append(out, code)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// parseStatusIds 解析逗号分隔的状态筛选
|
||||
func parseStatusIds(raw string) []int {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
ids := make([]int, 0, 4)
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if v, err := strconv.Atoi(part); err == nil {
|
||||
ids = append(ids, v)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
@@ -0,0 +1,837 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"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/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
"strings"
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type sCommonAgentBalance struct{}
|
||||
|
||||
func NewCommonAgentBalance() *sCommonAgentBalance {
|
||||
return &sCommonAgentBalance{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonAgentBalance(NewCommonAgentBalance())
|
||||
}
|
||||
|
||||
// Model 企业客户财务信息ORM模型
|
||||
func (s *sCommonAgentBalance) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TAgentBalance.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取企业客户财务信息列表
|
||||
func (s *sCommonAgentBalance) List(ctx context.Context, in *model.TAgentBalanceListParam) (list []*model.TAgentBalanceListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
mod = mod.Fields(model.TAgentBalanceListModel{})
|
||||
mod = mod.Where(dao.TAgentBalance.Columns().DeptId, deptId)
|
||||
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TAgentBalance.Columns().Id, in.Id)
|
||||
}
|
||||
if in.WithdrawAmountStatus > 0 {
|
||||
mod = mod.Where(dao.TAgentBalance.Columns().WithdrawAmountStatus, in.WithdrawAmountStatus)
|
||||
}
|
||||
if in.Status > 0 {
|
||||
mod = mod.Where(dao.TAgentBalance.Columns().Status, in.Status)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TAgentBalance.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
mod = mod.OrderDesc(dao.TAgentBalance.Columns().Id)
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取企业客户财务信息列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出企业客户财务信息
|
||||
func (s *sCommonAgentBalance) Export(ctx context.Context, in *model.TAgentBalanceListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := convert.GetEntityDescTags(model.TAgentBalanceExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出企业客户财务信息-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TAgentBalanceExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增企业客户财务信息
|
||||
func (s *sCommonAgentBalance) Edit(ctx context.Context, in *model.TAgentBalanceEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TAgentBalanceUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改企业客户财务信息失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TAgentBalanceInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增企业客户财务信息失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除企业客户财务信息
|
||||
func (s *sCommonAgentBalance) Delete(ctx context.Context, in *model.TAgentBalanceDeleteParam) (err error) {
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除企业客户财务信息失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取企业客户财务信息指定信息
|
||||
func (s *sCommonAgentBalance) View(ctx context.Context, in *model.TAgentBalanceViewParam) (res *model.TAgentBalanceViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取企业客户财务信息信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新企业客户财务信息状态
|
||||
func (s *sCommonAgentBalance) Status(ctx context.Context, in *model.TAgentBalanceStatusParam) (err error) {
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
|
||||
dao.TAgentBalance.Columns().Status: in.Status,
|
||||
dao.TAgentBalance.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
||||
}).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "更新企业客户财务信息状态失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAccountBalance 获取当前登录企业的账户余额信息。
|
||||
func (s *sCommonAgentBalance) GetAccountBalance(ctx context.Context, in *model.TAgentBalanceTrendParam) (res *model.TAgentBalanceChartData, err error) {
|
||||
res = &model.TAgentBalanceChartData{}
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
months, startTime, endTime, err := agentBalanceMonthRange(in.StartMonth, in.EndMonth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
balanceMap, err := s.getMonthlyLastBalance(ctx, dao.TBalanceDetail.Ctx(ctx),
|
||||
dao.TBalanceDetail.Columns().TransactionTime, dao.TBalanceDetail.Columns().BalanceAfter,
|
||||
dao.TBalanceDetail.Columns().DeptId, deptId, startTime, endTime, "余额趋势")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
withdrawableMap, err := s.getMonthlyLastBalance(ctx, dao.TFinWithdrawableChange.Ctx(ctx),
|
||||
dao.TFinWithdrawableChange.Columns().CreatedAt, dao.TFinWithdrawableChange.Columns().AfterBalance,
|
||||
dao.TFinWithdrawableChange.Columns().DeptId, deptId, startTime, endTime, "可提现金额趋势")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
creditMap, err := s.getMonthlyLastBalance(ctx, dao.TCreditDetail.Ctx(ctx),
|
||||
dao.TCreditDetail.Columns().TransactionTime, dao.TCreditDetail.Columns().CreditBalanceAfter,
|
||||
dao.TCreditDetail.Columns().DeptId, deptId, startTime, endTime, "信用额度趋势")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
balanceValues := make([]float64, 0, len(months))
|
||||
withdrawableValues := make([]float64, 0, len(months))
|
||||
creditValues := make([]float64, 0, len(months))
|
||||
for _, month := range months {
|
||||
balanceValues = append(balanceValues, balanceMap[month])
|
||||
withdrawableValues = append(withdrawableValues, withdrawableMap[month])
|
||||
creditValues = append(creditValues, creditMap[month])
|
||||
}
|
||||
|
||||
res.Labels = months
|
||||
res.Series = []*model.TAgentBalanceChartSeries{
|
||||
{Name: "余额", Values: balanceValues},
|
||||
{Name: "可提现金额", Values: withdrawableValues},
|
||||
{Name: "信用额度", Values: creditValues},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getMonthlyLastBalance 查询指定月份范围内每月最后一条记录的余额。
|
||||
func (s *sCommonAgentBalance) getMonthlyLastBalance(ctx context.Context, mod *gdb.Model, timeColumn string, balanceColumn string, deptIdColumn string, deptId interface{}, startTime, endTime time.Time, errMsg string) (map[string]float64, error) {
|
||||
type maxTimeRow struct {
|
||||
AccountPeriod string `orm:"account_period"`
|
||||
MaxTime string `orm:"max_time"`
|
||||
}
|
||||
var maxTimes []*maxTimeRow
|
||||
if err := mod.Fields("DATE_FORMAT("+timeColumn+", '%Y-%m') as account_period, MAX("+timeColumn+") as max_time").
|
||||
Where(deptIdColumn, deptId).
|
||||
WhereGTE(timeColumn, startTime).
|
||||
WhereLT(timeColumn, endTime.AddDate(0, 1, 0)).
|
||||
Group("DATE_FORMAT(" + timeColumn + ", '%Y-%m')").
|
||||
Scan(&maxTimes); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询"+errMsg+"失败,请稍后重试!")
|
||||
}
|
||||
|
||||
result := make(map[string]float64)
|
||||
if len(maxTimes) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
maxTimeList := make([]string, len(maxTimes))
|
||||
for i, mt := range maxTimes {
|
||||
maxTimeList[i] = mt.MaxTime
|
||||
}
|
||||
|
||||
type balanceRow struct {
|
||||
AccountPeriod string `orm:"account_period"`
|
||||
BalanceAfter float64 `orm:"balance_after"`
|
||||
}
|
||||
var allResults []*balanceRow
|
||||
if err := mod.Fields("DATE_FORMAT("+timeColumn+", '%Y-%m') as account_period, "+balanceColumn+" as balance_after").
|
||||
Where(deptIdColumn, deptId).
|
||||
WhereIn(timeColumn, maxTimeList).
|
||||
Order(timeColumn + " ASC").
|
||||
Scan(&allResults); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询"+errMsg+"失败,请稍后重试!")
|
||||
}
|
||||
|
||||
for _, item := range allResults {
|
||||
result[item.AccountPeriod] = item.BalanceAfter
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AccountTrend 获取收入金额、支出金额折线图。
|
||||
func (s *sCommonAgentBalance) AccountTrend(ctx context.Context, in *model.TAgentBalanceTrendParam) (res *model.TAgentBalanceChartData, err error) {
|
||||
months, startMonth, endMonth, err := agentBalanceMonthRange(in.StartMonth, in.EndMonth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
type billRow struct {
|
||||
AccountPeriod string `orm:"account_period"`
|
||||
TotalIncome float64 `orm:"total_income"`
|
||||
TotalExpense float64 `orm:"total_expense"`
|
||||
}
|
||||
var bills []*billRow
|
||||
|
||||
accountPeriodColumn := dao.TProfitAccountMonthBill.Columns().AccountPeriod
|
||||
deptIdColumn := dao.TProfitAccountMonthBill.Columns().DeptId
|
||||
totalIncomeColumn := dao.TProfitAccountMonthBill.Columns().TotalIncome
|
||||
totalExpenseColumn := dao.TProfitAccountMonthBill.Columns().TotalExpense
|
||||
mod := handler.Model(dao.TProfitAccountMonthBill.Ctx(ctx))
|
||||
|
||||
if in.AccountType == "2" {
|
||||
accountPeriodColumn = dao.TConsumeAccountMonthBill.Columns().AccountPeriod
|
||||
deptIdColumn = dao.TConsumeAccountMonthBill.Columns().DeptId
|
||||
totalIncomeColumn = dao.TConsumeAccountMonthBill.Columns().TotalIncome
|
||||
totalExpenseColumn = dao.TConsumeAccountMonthBill.Columns().TotalExpense
|
||||
mod = handler.Model(dao.TConsumeAccountMonthBill.Ctx(ctx))
|
||||
}
|
||||
|
||||
if err = mod.Fields(accountPeriodColumn, totalIncomeColumn, totalExpenseColumn).
|
||||
Where(deptIdColumn, deptId).
|
||||
WhereBetween(accountPeriodColumn, startMonth, endMonth).
|
||||
Scan(&bills); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询账户总览趋势失败,请稍后重试!")
|
||||
}
|
||||
|
||||
billMap := make(map[string]*billRow, len(bills))
|
||||
for _, item := range bills {
|
||||
billMap[item.AccountPeriod] = item
|
||||
}
|
||||
|
||||
incomeValues := make([]float64, 0, len(months))
|
||||
expenseValues := make([]float64, 0, len(months))
|
||||
for _, month := range months {
|
||||
if item, ok := billMap[month]; ok {
|
||||
incomeValues = append(incomeValues, item.TotalIncome)
|
||||
expenseValues = append(expenseValues, item.TotalExpense)
|
||||
continue
|
||||
}
|
||||
incomeValues = append(incomeValues, 0)
|
||||
expenseValues = append(expenseValues, 0)
|
||||
}
|
||||
|
||||
return &model.TAgentBalanceChartData{
|
||||
Labels: months,
|
||||
Series: []*model.TAgentBalanceChartSeries{
|
||||
{Name: "收入金额", Values: incomeValues},
|
||||
{Name: "支出金额", Values: expenseValues},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// agentBalanceMonthRange 返回月份范围。
|
||||
func agentBalanceMonthRange(startMonth, endMonth string) (months []string, startTime time.Time, endTime time.Time, err error) {
|
||||
now := time.Now()
|
||||
if endMonth == "" {
|
||||
endMonth = now.Format("2006-01")
|
||||
}
|
||||
if startMonth == "" {
|
||||
tempEndTime, parseErr := time.Parse("2006-01", endMonth)
|
||||
if parseErr != nil {
|
||||
err = gerror.New("结束月份格式不正确,应为YYYY-MM")
|
||||
return
|
||||
}
|
||||
startMonth = tempEndTime.AddDate(0, -11, 0).Format("2006-01")
|
||||
}
|
||||
|
||||
startTime, err = time.Parse("2006-01", startMonth)
|
||||
if err != nil {
|
||||
err = gerror.New("开始月份格式不正确,应为YYYY-MM")
|
||||
return
|
||||
}
|
||||
endTime, err = time.Parse("2006-01", endMonth)
|
||||
if err != nil {
|
||||
err = gerror.New("结束月份格式不正确,应为YYYY-MM")
|
||||
return
|
||||
}
|
||||
if startTime.After(endTime) {
|
||||
err = gerror.New("开始月份不能大于结束月份")
|
||||
return
|
||||
}
|
||||
|
||||
for t := startTime; !t.After(endTime); t = t.AddDate(0, 1, 0) {
|
||||
months = append(months, t.Format("2006-01"))
|
||||
}
|
||||
return months, startTime, endTime, nil
|
||||
}
|
||||
|
||||
const (
|
||||
financeIncomeExpenseIncome = 0
|
||||
financeIncomeExpenseExpense = 1
|
||||
|
||||
financeBalanceTransactionTypeTransferToWithdraw = 2
|
||||
financeBalanceTransactionTypeManualAdjust = 4
|
||||
financeBalanceTransactionChannelBalance = 3
|
||||
|
||||
financeWithdrawableChangeTypeCommission = 0
|
||||
financeWithdrawableChangeTypeBalanceTransferIn = 3
|
||||
|
||||
financeApplyTypeBalanceToWithdraw = 2
|
||||
financeAuditTypeFirst = 1
|
||||
financeApplyStatusPending = 0
|
||||
financeApplyStatusApproved = 1
|
||||
financeApplyStatusRejected = 2
|
||||
financeAuditResultPass = 0
|
||||
financeAuditResultReject = 1
|
||||
financeAccountStatusNormal = 0
|
||||
financeWithdrawAmountStatusNormal = 0
|
||||
financeCommissionStatusPending = 0
|
||||
financeCommissionStatusSettled = 1
|
||||
)
|
||||
|
||||
// BalanceTransferApply 发起余额转可提现申请。申请时先扣余额并写余额明细,申请单保持待审核。
|
||||
func (s *sCommonAgentBalance) BalanceTransferApply(ctx context.Context, in *model.TAgentBalanceTransferApplyParam) (*model.TAgentBalanceTransferApplyModel, error) {
|
||||
if in == nil {
|
||||
in = &model.TAgentBalanceTransferApplyParam{}
|
||||
}
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
applyNo := "BWT" + strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
userId := contexts.GetUserId(ctx)
|
||||
now := gtime.Now()
|
||||
|
||||
res := &model.TAgentBalanceTransferApplyModel{
|
||||
ApplyNo: applyNo,
|
||||
ApplyAmount: in.Amount,
|
||||
}
|
||||
|
||||
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
account := &entity.TAgentBalance{}
|
||||
columns := dao.TAgentBalance.Columns()
|
||||
if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
Where(columns.DeptId, deptId).
|
||||
Where(columns.TenantId, tenantId).
|
||||
Scan(account); err != nil {
|
||||
return gerror.Wrap(err, "查询企业客户财务账户失败,请稍后重试!")
|
||||
}
|
||||
if account.Id == 0 {
|
||||
return gerror.New("企业客户财务账户不存在")
|
||||
}
|
||||
if account.Status != financeAccountStatusNormal {
|
||||
return gerror.New("企业客户财务账户状态异常")
|
||||
}
|
||||
if account.WithdrawAmountStatus != financeWithdrawAmountStatusNormal {
|
||||
return gerror.New("可提现金额已冻结")
|
||||
}
|
||||
if account.Balance < in.Amount {
|
||||
return gerror.New("账户余额不足")
|
||||
}
|
||||
|
||||
balanceAfter := account.Balance - in.Amount
|
||||
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
WherePri(account.Id).
|
||||
Data(g.Map{
|
||||
columns.Balance: balanceAfter,
|
||||
columns.UpdatedBy: userId,
|
||||
columns.UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "扣减账户余额失败,请稍后重试!")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取账户余额更新结果失败")
|
||||
}
|
||||
if affected == 0 {
|
||||
return gerror.New("账户余额未更新,请稍后重试")
|
||||
}
|
||||
|
||||
if err = service.CommonTBalanceDetail().CreateBalanceTransferDetail(ctx, tx, &model.TBalanceTransferDetailCreateParam{
|
||||
DeptId: deptId,
|
||||
SerialNo: applyNo,
|
||||
TransactionTime: now,
|
||||
IncomeExpenseType: financeIncomeExpenseExpense,
|
||||
TransactionType: financeBalanceTransactionTypeTransferToWithdraw,
|
||||
TransactionChannel: financeBalanceTransactionChannelBalance,
|
||||
Amount: in.Amount,
|
||||
BeforeBalance: account.Balance,
|
||||
BalanceAfter: balanceAfter,
|
||||
OrderSn: applyNo,
|
||||
Remark: "余额转可提现申请扣减余额",
|
||||
TenantId: tenantId,
|
||||
CreatedDept: deptId,
|
||||
CreatedBy: userId,
|
||||
CreatedAt: now,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var topDept entity.SysDept
|
||||
if err := dao.SysDept.Ctx(ctx).
|
||||
Fields(dao.SysDept.Columns().DeptId, dao.SysDept.Columns().ParentId).
|
||||
Where(dao.SysDept.Columns().ParentId, 0).
|
||||
Scan(&topDept); err != nil {
|
||||
return gerror.Wrap(err, "查询审核机构失败,请稍后重试!")
|
||||
}
|
||||
if topDept.DeptId == 0 {
|
||||
return gerror.New("审核机构不存在")
|
||||
}
|
||||
|
||||
if err = service.CommonTFinanceApply().CreateBalanceTransferApply(ctx, tx, &model.TFinanceApplyCreateBalanceTransferParam{
|
||||
ApplyNo: applyNo,
|
||||
ApplyDeptId: deptId,
|
||||
AuditDeptId: topDept.DeptId,
|
||||
AvailableAmountSnapshot: account.WithdrawAmount,
|
||||
ApplyAmount: in.Amount,
|
||||
Remark: in.Remark,
|
||||
TenantId: tenantId,
|
||||
CreatedDept: deptId,
|
||||
CreatedBy: userId,
|
||||
CreatedAt: now,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res.BalanceAfter = balanceAfter
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "余额转可提现申请失败,请稍后重试!")
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// BalanceTransferAudit 审核余额转可提现申请。通过时加可提现金额,驳回时退回申请时扣掉的余额。
|
||||
func (s *sCommonAgentBalance) BalanceTransferAudit(ctx context.Context, in *model.TAgentBalanceTransferAuditParam) error {
|
||||
if in == nil {
|
||||
in = &model.TAgentBalanceTransferAuditParam{}
|
||||
}
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
auditNo, err := batchimport.NextRandomNo("BTA")
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成审核单号失败")
|
||||
}
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
currentDeptId := contexts.GetDeptId(ctx)
|
||||
currentUserId := contexts.GetUserId(ctx)
|
||||
if tenantId == "" {
|
||||
return gerror.New("当前租户不能为空")
|
||||
}
|
||||
if currentDeptId <= 0 {
|
||||
return gerror.New("当前登录机构不能为空")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
apply, err := service.CommonTFinanceApply().GetPendingByApplyNo(ctx, tx, in.ApplyNo, tenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = service.CommonTFinanceAuditRecord().CreateRecord(ctx, tx, &model.TFinanceAuditRecordCreateParam{
|
||||
AuditNo: auditNo,
|
||||
ApplyNo: apply.ApplyNo,
|
||||
AuditDeptId: currentDeptId,
|
||||
AuditorUserId: currentUserId,
|
||||
AuditResult: in.AuditResult,
|
||||
AuditOpinion: in.AuditOpinion,
|
||||
AuditTime: now,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
applyStatus := financeApplyStatusApproved
|
||||
if in.AuditResult == financeAuditResultReject {
|
||||
applyStatus = financeApplyStatusRejected
|
||||
}
|
||||
if err = service.CommonTFinanceApply().UpdateAuditStatus(ctx, tx, &model.TFinanceApplyUpdateAuditStatusParam{
|
||||
ApplyNo: apply.ApplyNo,
|
||||
ApplyStatus: applyStatus,
|
||||
AuditTime: now,
|
||||
UpdatedBy: currentUserId,
|
||||
UpdatedAt: now,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account := &entity.TAgentBalance{}
|
||||
columns := dao.TAgentBalance.Columns()
|
||||
if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
Where(columns.DeptId, apply.ApplyDeptId).
|
||||
Where(columns.TenantId, apply.TenantId).
|
||||
Scan(account); err != nil {
|
||||
return gerror.Wrap(err, "查询企业客户财务账户失败,请稍后重试!")
|
||||
}
|
||||
if account.Id == 0 {
|
||||
return gerror.New("企业客户财务账户不存在")
|
||||
}
|
||||
if account.Status != financeAccountStatusNormal {
|
||||
return gerror.New("企业客户财务账户状态异常")
|
||||
}
|
||||
|
||||
if in.AuditResult == financeAuditResultPass {
|
||||
if account.WithdrawAmountStatus != financeWithdrawAmountStatusNormal {
|
||||
return gerror.New("可提现金额已冻结")
|
||||
}
|
||||
before := account.WithdrawAmount
|
||||
after := before + apply.ApplyAmount
|
||||
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
WherePri(account.Id).
|
||||
Data(g.Map{
|
||||
columns.WithdrawAmount: after,
|
||||
columns.UpdatedBy: currentUserId,
|
||||
columns.UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "增加可提现金额失败,请稍后重试!")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取账户余额更新结果失败")
|
||||
}
|
||||
if affected == 0 {
|
||||
return gerror.New("账户余额未更新,请稍后重试")
|
||||
}
|
||||
|
||||
return service.CommonTFinWithdrawableChange().CreateBalanceTransferInChange(ctx, tx, &model.TFinWithdrawableBalanceTransferInParam{
|
||||
DeptId: apply.ApplyDeptId,
|
||||
ChangeAmount: apply.ApplyAmount,
|
||||
BeforeBalance: before,
|
||||
AfterBalance: after,
|
||||
AccountType: apply.AccountType,
|
||||
BankName: apply.BankName,
|
||||
BankAccount: apply.BankAccount,
|
||||
AccountName: apply.AccountName,
|
||||
IdentityCard: apply.IdentityCard,
|
||||
SourceNo: apply.ApplyNo,
|
||||
Remark: "余额转可提现审核通过",
|
||||
AccountPeriod: now.Format("Y-m"),
|
||||
TenantId: apply.TenantId,
|
||||
CreatedDept: apply.ApplyDeptId,
|
||||
CreatedBy: currentUserId,
|
||||
CreatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
before := account.Balance
|
||||
after := before + apply.ApplyAmount
|
||||
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
WherePri(account.Id).
|
||||
Data(g.Map{
|
||||
columns.Balance: after,
|
||||
columns.UpdatedBy: currentUserId,
|
||||
columns.UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "退回账户余额失败,请稍后重试!")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取账户余额更新结果失败")
|
||||
}
|
||||
if affected == 0 {
|
||||
return gerror.New("账户余额未更新,请稍后重试")
|
||||
}
|
||||
|
||||
remark := "余额转可提现申请驳回退款"
|
||||
if in.AuditOpinion != "" {
|
||||
remark += ":" + in.AuditOpinion
|
||||
}
|
||||
return service.CommonTBalanceDetail().CreateBalanceTransferDetail(ctx, tx, &model.TBalanceTransferDetailCreateParam{
|
||||
DeptId: apply.ApplyDeptId,
|
||||
SerialNo: apply.ApplyNo,
|
||||
TransactionTime: now,
|
||||
IncomeExpenseType: financeIncomeExpenseIncome,
|
||||
TransactionType: financeBalanceTransactionTypeManualAdjust,
|
||||
TransactionChannel: financeBalanceTransactionChannelBalance,
|
||||
Amount: apply.ApplyAmount,
|
||||
BeforeBalance: before,
|
||||
BalanceAfter: after,
|
||||
OrderSn: apply.ApplyNo,
|
||||
Remark: remark,
|
||||
TenantId: apply.TenantId,
|
||||
CreatedDept: apply.ApplyDeptId,
|
||||
CreatedBy: currentUserId,
|
||||
CreatedAt: now,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// SettleCommissionByPeriod 按账期区间结算待结算分润,同步到企业账户可提现金额并写入可提现明细。
|
||||
func (s *sCommonAgentBalance) SettleCommissionByPeriod(ctx context.Context, in *model.TAgentBalanceCommissionSettlementParam) (*model.TAgentBalanceCommissionSettlementResult, error) {
|
||||
if in == nil {
|
||||
in = &model.TAgentBalanceCommissionSettlementParam{}
|
||||
}
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &model.TAgentBalanceCommissionSettlementResult{
|
||||
StartPeriod: in.StartPeriod,
|
||||
EndPeriod: in.EndPeriod,
|
||||
}
|
||||
|
||||
endPeriodTime, _ := time.Parse("2006-01", in.EndPeriod)
|
||||
endNextPeriod := endPeriodTime.AddDate(0, 1, 0).Format("2006-01")
|
||||
var lastId int64
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
commissionColumns := dao.TFinCommissionDetail.Columns()
|
||||
for {
|
||||
var details []*entity.TFinCommissionDetail
|
||||
mod := handler.Model(dao.TFinCommissionDetail.Ctx(ctx), &handler.Option{
|
||||
FilterTenant: false,
|
||||
FilterAuth: false,
|
||||
}).
|
||||
Fields(
|
||||
commissionColumns.Id,
|
||||
commissionColumns.BenefitDeptId,
|
||||
commissionColumns.CommissionAmount,
|
||||
commissionColumns.AccountPeriod,
|
||||
commissionColumns.TenantId,
|
||||
commissionColumns.CreatedDept,
|
||||
).
|
||||
Where(commissionColumns.CommissionStatus, financeCommissionStatusPending).
|
||||
WhereGTE(commissionColumns.AccountPeriod, in.StartPeriod).
|
||||
WhereLT(commissionColumns.AccountPeriod, endNextPeriod).
|
||||
WhereGT(commissionColumns.Id, lastId).
|
||||
OrderAsc(commissionColumns.Id).
|
||||
Limit(1000)
|
||||
if tenantId != "" {
|
||||
mod = mod.Where(commissionColumns.TenantId, tenantId)
|
||||
}
|
||||
if err := mod.Scan(&details); err != nil {
|
||||
return res, gerror.Wrap(err, "扫描待结算分润明细失败,请稍后重试!")
|
||||
}
|
||||
if len(details) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
res.ScanCount += len(details)
|
||||
for _, item := range details {
|
||||
if item.Id > lastId {
|
||||
lastId = item.Id
|
||||
}
|
||||
}
|
||||
|
||||
groups := make(map[model.TAgentBalanceCommissionSettlementGroupKey]*model.TAgentBalanceCommissionSettlementGroup)
|
||||
for _, item := range details {
|
||||
if item.BenefitDeptId <= 0 || item.AccountPeriod == "" || item.CommissionAmount <= 0 {
|
||||
continue
|
||||
}
|
||||
key := model.TAgentBalanceCommissionSettlementGroupKey{
|
||||
DeptId: item.BenefitDeptId,
|
||||
AccountPeriod: item.AccountPeriod,
|
||||
TenantId: item.TenantId,
|
||||
}
|
||||
group := groups[key]
|
||||
if group == nil {
|
||||
group = &model.TAgentBalanceCommissionSettlementGroup{
|
||||
DeptId: item.BenefitDeptId,
|
||||
AccountPeriod: item.AccountPeriod,
|
||||
TenantId: item.TenantId,
|
||||
CreatedDept: item.CreatedDept,
|
||||
}
|
||||
groups[key] = group
|
||||
}
|
||||
group.Ids = append(group.Ids, item.Id)
|
||||
group.Amount += item.CommissionAmount
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
if err := s.settleCommissionGroup(ctx, group); err != nil {
|
||||
res.FailGroupCount++
|
||||
g.Log().Errorf(ctx, "分润结算失败 deptId:%d accountPeriod:%s amount:%v err:%+v", group.DeptId, group.AccountPeriod, group.Amount, err)
|
||||
continue
|
||||
}
|
||||
res.SuccessGroupCount++
|
||||
res.SettledCount += len(group.Ids)
|
||||
res.SettledAmount += group.Amount
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *sCommonAgentBalance) settleCommissionGroup(ctx context.Context, group *model.TAgentBalanceCommissionSettlementGroup) error {
|
||||
if group == nil || len(group.Ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := gtime.Now()
|
||||
userId := contexts.GetUserId(ctx)
|
||||
accountColumns := dao.TAgentBalance.Columns()
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
account := &entity.TAgentBalance{}
|
||||
if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
Where(accountColumns.DeptId, group.DeptId).
|
||||
Where(accountColumns.TenantId, group.TenantId).
|
||||
LockUpdate().
|
||||
Scan(account); err != nil {
|
||||
return gerror.Wrap(err, "锁定企业客户财务账户失败,请稍后重试!")
|
||||
}
|
||||
if account.Id == 0 {
|
||||
return gerror.New("企业客户财务账户不存在")
|
||||
}
|
||||
if account.Status != financeAccountStatusNormal {
|
||||
return gerror.New("企业客户财务账户状态异常")
|
||||
}
|
||||
if account.WithdrawAmountStatus != financeWithdrawAmountStatusNormal {
|
||||
return gerror.New("可提现金额已冻结")
|
||||
}
|
||||
|
||||
if err := service.CommonTFinCommissionDetail().MarkSettled(ctx, tx, &model.TFinCommissionSettleUpdateParam{
|
||||
Ids: group.Ids,
|
||||
SettlementTime: now,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
before := account.WithdrawAmount
|
||||
after := before + group.Amount
|
||||
sourceNo := fmt.Sprintf("CMS%sD%d%s", strings.ReplaceAll(group.AccountPeriod, "-", ""), group.DeptId, now.Format("150405"))
|
||||
if err := service.CommonTFinWithdrawableChange().CreateCommissionSettlementChange(ctx, tx, &model.TFinWithdrawableCommissionSettlementParam{
|
||||
DeptId: group.DeptId,
|
||||
ChangeAmount: group.Amount,
|
||||
BeforeBalance: before,
|
||||
AfterBalance: after,
|
||||
SourceNo: sourceNo,
|
||||
Remark: "分润结算转可提现",
|
||||
AccountPeriod: group.AccountPeriod,
|
||||
TenantId: group.TenantId,
|
||||
CreatedDept: group.CreatedDept,
|
||||
CreatedBy: userId,
|
||||
CreatedAt: now,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()).
|
||||
WherePri(account.Id).
|
||||
Data(g.Map{
|
||||
accountColumns.WithdrawAmount: after,
|
||||
accountColumns.UpdatedBy: userId,
|
||||
accountColumns.UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新企业客户可提现金额失败,请稍后重试!")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取企业客户可提现金额更新结果失败")
|
||||
}
|
||||
if affected == 0 {
|
||||
return gerror.New("企业客户可提现金额未更新,请稍后重试")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"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"
|
||||
)
|
||||
|
||||
type sCommonTBalanceDetail struct{}
|
||||
|
||||
func NewCommonTBalanceDetail() *sCommonTBalanceDetail {
|
||||
return &sCommonTBalanceDetail{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTBalanceDetail(NewCommonTBalanceDetail())
|
||||
}
|
||||
|
||||
// Model 余额明细表ORM模型
|
||||
func (s *sCommonTBalanceDetail) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TBalanceDetail.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取余额明细表列表
|
||||
func (s *sCommonTBalanceDetail) List(ctx context.Context, in *model.TBalanceDetailListParam) (list []*model.TBalanceDetailListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
sysDeptTable := dao.SysDept.Table()
|
||||
orderTable := dao.TOrder.Table()
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.FieldsPrefix(dao.TBalanceDetail.Table(), model.TBalanceDetailListModel{})
|
||||
mod = mod.Fields(sysDeptTable + ".dept_name as deptName")
|
||||
mod = mod.Fields(orderTable + ".order_type as orderType")
|
||||
mod = mod.Fields(orderTable + ".pay_type as payType")
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Where(dao.TBalanceDetail.Columns().DeptId, deptId)
|
||||
|
||||
// 查询明细ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TBalanceDetail.Columns().Id, in.Id)
|
||||
}
|
||||
g.Log().Info(ctx, "month==:", in.Month, " len:", len(in.Month))
|
||||
// 查询收支类型
|
||||
if in.IncomeExpenseType != nil && *in.IncomeExpenseType >= 0 {
|
||||
mod = mod.Where(dao.TBalanceDetail.Columns().IncomeExpenseType, *in.IncomeExpenseType)
|
||||
}
|
||||
// 查询交易类型
|
||||
if in.TransactionType != nil && *in.TransactionType >= 0 {
|
||||
mod = mod.Where(dao.TBalanceDetail.Columns().TransactionType, *in.TransactionType)
|
||||
}
|
||||
// 查询交易渠道
|
||||
if in.TransactionChannel != nil && *in.TransactionChannel >= 0 {
|
||||
mod = mod.Where(dao.TBalanceDetail.Columns().TransactionChannel, *in.TransactionChannel)
|
||||
}
|
||||
// // 查询支付类型
|
||||
// if in.PayType != nil && *in.PayType >= 0 {
|
||||
// mod = mod.Where(dao.TBalanceDetail.Columns().PayType, *in.PayType)
|
||||
// }
|
||||
|
||||
// 查询交易时间范围
|
||||
if len(in.Month) == 2 {
|
||||
t1, err := time.ParseInLocation("2006-01", in.Month[0], time.Local)
|
||||
t2, err2 := time.ParseInLocation("2006-01", in.Month[1], time.Local)
|
||||
if err == nil && err2 == nil {
|
||||
startOfMonth := gtime.NewFromTime(t1).StartOfMonth()
|
||||
endOfMonth := gtime.NewFromTime(t2).EndOfMonth()
|
||||
mod = mod.WhereGTE(dao.TBalanceDetail.Columns().TransactionTime, startOfMonth)
|
||||
mod = mod.WhereLTE(dao.TBalanceDetail.Columns().TransactionTime, endOfMonth)
|
||||
}
|
||||
}
|
||||
// 查询账期(根据 transaction_time 筛选月份)
|
||||
if in.AccountPeriod != "" {
|
||||
// 统一转换为 "2026-06" 格式后解析
|
||||
accountPeriodStr := in.AccountPeriod
|
||||
if len(accountPeriodStr) == 6 {
|
||||
accountPeriodStr = accountPeriodStr[:4] + "-" + accountPeriodStr[4:]
|
||||
}
|
||||
t := gtime.NewFromStr(accountPeriodStr + "-01")
|
||||
startOfMonth := t.StartOfDay()
|
||||
endOfMonth := startOfMonth.EndOfMonth()
|
||||
mod = mod.WhereBetween(dao.TBalanceDetail.Columns().TransactionTime, startOfMonth, endOfMonth)
|
||||
}
|
||||
|
||||
mod = mod.LeftJoin(sysDeptTable, dao.TBalanceDetail.Table()+"."+dao.TBalanceDetail.Columns().DeptId+" COLLATE utf8mb4_general_ci="+sysDeptTable+"."+dao.SysDept.Columns().DeptId)
|
||||
mod = mod.LeftJoin(orderTable, dao.TBalanceDetail.Table()+"."+dao.TBalanceDetail.Columns().OrderSn+" COLLATE utf8mb4_general_ci="+orderTable+"."+dao.TOrder.Columns().OrderSn)
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TBalanceDetail.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取余额明细表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
|
||||
// 根据 TransactionTime 赋值账期
|
||||
for i := range list {
|
||||
if list[i].TransactionTime != nil {
|
||||
list[i].TradingMonth = list[i].TransactionTime.Format("Y-m")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出余额明细(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTBalanceDetail) Export(ctx context.Context, in *model.TBalanceDetailListParam) (err error) {
|
||||
var req exportcenter.Request
|
||||
req = exportcenter.Request{
|
||||
BizKey: "tBalanceDetail",
|
||||
BizName: "余额明细",
|
||||
PermCode: "cpm:common:tBalanceDetail:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TBalanceDetailListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
// 与列表 getDisplayTransactionChannel 保持一致,先归一化历史渠道值。
|
||||
if len(req.Columns) > 0 && (item.TransactionChannel == 4 || (item.TransactionChannel == 0 && item.IncomeExpenseType == 1 && item.TransactionType == 1)) {
|
||||
item.TransactionChannel = 3
|
||||
}
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
}
|
||||
_, err = exportcenter.Submit(ctx, &req)
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增余额明细表
|
||||
func (s *sCommonTBalanceDetail) Edit(ctx context.Context, in *model.TBalanceDetailEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TBalanceDetailUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改余额明细表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TBalanceDetailInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增余额明细表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除余额明细表
|
||||
func (s *sCommonTBalanceDetail) Delete(ctx context.Context, in *model.TBalanceDetailDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除余额明细表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取余额明细表指定信息
|
||||
func (s *sCommonTBalanceDetail) View(ctx context.Context, in *model.TBalanceDetailViewParam) (res *model.TBalanceDetailViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取余额明细表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CreateBalanceTransferDetail 写入余额转可提现相关的余额明细。
|
||||
func (s *sCommonTBalanceDetail) CreateBalanceTransferDetail(ctx context.Context, tx gdb.TX, in *model.TBalanceTransferDetailCreateParam) error {
|
||||
if in == nil {
|
||||
return gerror.New("余额明细参数不能为空")
|
||||
}
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
userId := contexts.GetUserId(ctx)
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
createdAt := gtime.Now()
|
||||
columns := dao.TBalanceDetail.Columns()
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()).Data(g.Map{
|
||||
columns.DeptId: in.DeptId,
|
||||
columns.SerialNo: in.SerialNo,
|
||||
columns.TransactionTime: in.TransactionTime,
|
||||
columns.IncomeExpenseType: in.IncomeExpenseType,
|
||||
columns.TransactionType: in.TransactionType,
|
||||
columns.TransactionChannel: in.TransactionChannel,
|
||||
columns.Amount: in.Amount,
|
||||
columns.BeforeBalance: in.BeforeBalance,
|
||||
columns.BalanceAfter: in.BalanceAfter,
|
||||
columns.OrderSn: in.OrderSn,
|
||||
columns.Remark: in.Remark,
|
||||
columns.TenantId: tenantId,
|
||||
columns.CreatedDept: deptId,
|
||||
columns.CreatedBy: userId,
|
||||
columns.CreatedAt: createdAt,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增余额转可提现余额明细失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
"math"
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonConsumeAccountMonthBill struct{}
|
||||
|
||||
func NewCommonConsumeAccountMonthBill() *sCommonConsumeAccountMonthBill {
|
||||
return &sCommonConsumeAccountMonthBill{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonConsumeAccountMonthBill(NewCommonConsumeAccountMonthBill())
|
||||
}
|
||||
|
||||
// Model 消费账户月账单表ORM模型
|
||||
func (s *sCommonConsumeAccountMonthBill) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TConsumeAccountMonthBill.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取消费账户月账单表列表
|
||||
func (s *sCommonConsumeAccountMonthBill) List(ctx context.Context, in *model.TConsumeAccountMonthBillListParam) (list []*model.TConsumeAccountMonthBillListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.TConsumeAccountMonthBillListModel{})
|
||||
|
||||
// 查询账单ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TConsumeAccountMonthBill.Columns().Id, in.Id)
|
||||
}
|
||||
|
||||
// 查询账单状态:1正常,2已调整,3已作废
|
||||
if in.BillStatus > 0 {
|
||||
mod = mod.Where(dao.TConsumeAccountMonthBill.Columns().BillStatus, in.BillStatus)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TConsumeAccountMonthBill.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TConsumeAccountMonthBill.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取消费账户月账单表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出消费账户月账单表
|
||||
func (s *sCommonConsumeAccountMonthBill) Export(ctx context.Context, in *model.TConsumeAccountMonthBillListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TConsumeAccountMonthBillExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出消费账户月账单表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TConsumeAccountMonthBillExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增消费账户月账单表
|
||||
func (s *sCommonConsumeAccountMonthBill) Edit(ctx context.Context, in *model.TConsumeAccountMonthBillEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TConsumeAccountMonthBillUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改消费账户月账单表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TConsumeAccountMonthBillInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增消费账户月账单表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除消费账户月账单表
|
||||
func (s *sCommonConsumeAccountMonthBill) Delete(ctx context.Context, in *model.TConsumeAccountMonthBillDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除消费账户月账单表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取消费账户月账单表指定信息
|
||||
func (s *sCommonConsumeAccountMonthBill) View(ctx context.Context, in *model.TConsumeAccountMonthBillViewParam) (res *model.TConsumeAccountMonthBillViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取消费账户月账单表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetSummary 获取月账单消费账户汇总信息
|
||||
func (s *sCommonConsumeAccountMonthBill) GetConsumptionBillSummary(ctx context.Context, month string) (res *model.TConsumptionBillSummaryModel, err error) {
|
||||
// 计算上月账期(基于当前时间)
|
||||
// now := time.Now()
|
||||
// lastMonthTime := now.AddDate(0, -1, 0)
|
||||
// lastMonthPeriod := lastMonthTime.Format("2006-01")
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
var bill *model.TConsumeAccountMonthBillViewModel
|
||||
if err = dao.TConsumeAccountMonthBill.Ctx(ctx).
|
||||
Where(dao.TConsumeAccountMonthBill.Columns().AccountPeriod, month).
|
||||
Where(dao.TConsumeAccountMonthBill.Columns().DeptId, deptId).
|
||||
Scan(&bill); err != nil {
|
||||
err = gerror.Wrap(err, "查询上月账单失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if bill == nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &model.TConsumptionBillSummaryModel{}
|
||||
res.TotalIncome = bill.TotalIncome
|
||||
res.TotalExpense = bill.TotalExpense
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SettleConsumeAccountMonthBill 按账期统计消费账户月账单。
|
||||
func (s *sCommonConsumeAccountMonthBill) SettleConsumeAccountMonthBill(ctx context.Context, in *model.TConsumeAccountMonthBillSettleParam) (res *model.TConsumeAccountMonthBillSettleResult, err error) {
|
||||
if in == nil {
|
||||
in = &model.TConsumeAccountMonthBillSettleParam{}
|
||||
}
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
|
||||
}
|
||||
if _, err = time.Parse("2006-01", in.AccountPeriod); err != nil {
|
||||
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
|
||||
}
|
||||
|
||||
startTime, err := time.Parse("2006-01", in.AccountPeriod)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
|
||||
}
|
||||
endTime := startTime.AddDate(0, 1, 0)
|
||||
now := gtime.Now()
|
||||
userId := contexts.GetUserId(ctx)
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
billColumns := dao.TConsumeAccountMonthBill.Columns()
|
||||
detailColumns := dao.TBalanceDetail.Columns()
|
||||
|
||||
type detailRow struct {
|
||||
Id int64 `orm:"id"`
|
||||
DeptId int64 `orm:"dept_id"`
|
||||
TenantId string `orm:"tenant_id"`
|
||||
CreatedDept int64 `orm:"created_dept"`
|
||||
IncomeExpenseType int `orm:"income_expense_type"`
|
||||
TransactionType int `orm:"transaction_type"`
|
||||
Amount float64 `orm:"amount"`
|
||||
BeforeBalance float64 `orm:"before_balance"`
|
||||
BalanceAfter float64 `orm:"balance_after"`
|
||||
TransactionTime *gtime.Time `orm:"transaction_time"`
|
||||
}
|
||||
|
||||
var rows []*detailRow
|
||||
mod := handler.Model(dao.TBalanceDetail.Ctx(ctx), &handler.Option{
|
||||
FilterTenant: false,
|
||||
FilterAuth: false,
|
||||
}).
|
||||
Fields(
|
||||
detailColumns.Id,
|
||||
detailColumns.DeptId,
|
||||
detailColumns.TenantId,
|
||||
detailColumns.CreatedDept,
|
||||
detailColumns.IncomeExpenseType,
|
||||
detailColumns.TransactionType,
|
||||
detailColumns.TransactionStatus,
|
||||
detailColumns.Amount,
|
||||
detailColumns.BeforeBalance,
|
||||
detailColumns.BalanceAfter,
|
||||
detailColumns.TransactionTime,
|
||||
).
|
||||
WhereGTE(detailColumns.TransactionTime, startTime).
|
||||
WhereLT(detailColumns.TransactionTime, endTime).
|
||||
Where(detailColumns.TransactionStatus, 1)
|
||||
if tenantId != "" {
|
||||
mod = mod.Where(detailColumns.TenantId, tenantId)
|
||||
}
|
||||
if err = mod.OrderAsc(detailColumns.TenantId).OrderAsc(detailColumns.DeptId).OrderAsc(detailColumns.TransactionTime).OrderAsc(detailColumns.Id).Scan(&rows); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询消费流水失败,请稍后重试!")
|
||||
}
|
||||
|
||||
type billGroup struct {
|
||||
DeptId int64
|
||||
TenantId string
|
||||
CreatedDept int64
|
||||
AccountPeriod string
|
||||
OpeningBalance float64
|
||||
ClosingBalance float64
|
||||
RechargeIncome float64
|
||||
WithdrawableTransferInIncome float64
|
||||
ManualAdjustIncome float64
|
||||
RefundIncome float64
|
||||
ConsumeExpense float64
|
||||
BalanceTransferOutExpense float64
|
||||
ManualAdjustExpense float64
|
||||
ConsumeCreditRepayExpense float64
|
||||
TotalIncome float64
|
||||
TotalExpense float64
|
||||
IncomeCount int
|
||||
ExpenseCount int
|
||||
DetailCount int
|
||||
}
|
||||
|
||||
groups := make(map[string]*billGroup)
|
||||
keys := make([]string, 0, 16)
|
||||
for _, row := range rows {
|
||||
if row == nil || row.DeptId <= 0 || row.TransactionTime == nil {
|
||||
continue
|
||||
}
|
||||
switch row.TransactionType {
|
||||
case 0, 1, 2, 3, 4:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:%d", row.TenantId, row.DeptId)
|
||||
group := groups[key]
|
||||
if group == nil {
|
||||
group = &billGroup{
|
||||
DeptId: row.DeptId,
|
||||
TenantId: row.TenantId,
|
||||
CreatedDept: row.CreatedDept,
|
||||
AccountPeriod: in.AccountPeriod,
|
||||
OpeningBalance: row.BeforeBalance,
|
||||
}
|
||||
groups[key] = group
|
||||
keys = append(keys, key)
|
||||
}
|
||||
group.ClosingBalance = row.BalanceAfter
|
||||
group.DetailCount++
|
||||
|
||||
switch row.TransactionType {
|
||||
case 0:
|
||||
if row.IncomeExpenseType == 0 {
|
||||
group.RechargeIncome += row.Amount
|
||||
}
|
||||
case 1:
|
||||
if row.IncomeExpenseType == 1 {
|
||||
group.ConsumeExpense += row.Amount
|
||||
}
|
||||
case 2:
|
||||
if row.IncomeExpenseType == 1 {
|
||||
group.BalanceTransferOutExpense += row.Amount
|
||||
}
|
||||
case 3:
|
||||
if row.IncomeExpenseType == 0 {
|
||||
group.WithdrawableTransferInIncome += row.Amount
|
||||
}
|
||||
case 4:
|
||||
if row.IncomeExpenseType == 0 {
|
||||
group.ManualAdjustIncome += row.Amount
|
||||
} else if row.IncomeExpenseType == 1 {
|
||||
group.ManualAdjustExpense += row.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if row.IncomeExpenseType == 0 {
|
||||
group.TotalIncome += row.Amount
|
||||
group.IncomeCount++
|
||||
} else if row.IncomeExpenseType == 1 {
|
||||
group.TotalExpense += row.Amount
|
||||
group.ExpenseCount++
|
||||
}
|
||||
}
|
||||
|
||||
res = &model.TConsumeAccountMonthBillSettleResult{
|
||||
AccountPeriod: in.AccountPeriod,
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
for _, key := range keys {
|
||||
group := groups[key]
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
closingBalance := group.OpeningBalance + group.TotalIncome - group.TotalExpense
|
||||
if math.Abs(closingBalance-group.ClosingBalance) > 0.01 {
|
||||
return gerror.New(fmt.Sprintf("消费账单余额勾稽失败:部门 %d,账期 %s,计算余额 %.2f,流水余额 %.2f", group.DeptId, group.AccountPeriod, closingBalance, group.ClosingBalance))
|
||||
}
|
||||
|
||||
lookup := tx.Ctx(ctx).Model(dao.TConsumeAccountMonthBill.Table()).
|
||||
Fields(billColumns.Id).
|
||||
Where(billColumns.DeptId, group.DeptId).
|
||||
Where(billColumns.AccountPeriod, group.AccountPeriod)
|
||||
if group.TenantId != "" {
|
||||
lookup = lookup.Where(billColumns.TenantId, group.TenantId)
|
||||
}
|
||||
|
||||
var existing struct {
|
||||
Id int64 `orm:"id"`
|
||||
}
|
||||
if err := lookup.LockUpdate().Scan(&existing); err != nil && err != sql.ErrNoRows {
|
||||
return gerror.Wrap(err, "查询消费月账单失败,请稍后重试!")
|
||||
}
|
||||
|
||||
tenantValue := group.TenantId
|
||||
if tenantValue == "" {
|
||||
tenantValue = tenantId
|
||||
}
|
||||
if tenantValue == "" {
|
||||
return gerror.New("租户ID不能为空")
|
||||
}
|
||||
|
||||
createdDeptValue := group.CreatedDept
|
||||
if createdDeptValue == 0 {
|
||||
createdDeptValue = contexts.GetDeptId(ctx)
|
||||
}
|
||||
if createdDeptValue == 0 {
|
||||
return gerror.New("创建部门不能为空")
|
||||
}
|
||||
|
||||
data := g.Map{
|
||||
billColumns.DeptId: group.DeptId,
|
||||
billColumns.AccountPeriod: group.AccountPeriod,
|
||||
billColumns.OpeningBalance: group.OpeningBalance,
|
||||
billColumns.RechargeIncome: group.RechargeIncome,
|
||||
billColumns.WithdrawableTransferInIncome: group.WithdrawableTransferInIncome,
|
||||
billColumns.ManualAdjustIncome: group.ManualAdjustIncome,
|
||||
billColumns.RefundIncome: 0,
|
||||
billColumns.ConsumeExpense: group.ConsumeExpense,
|
||||
billColumns.BalanceTransferOutExpense: group.BalanceTransferOutExpense,
|
||||
billColumns.ManualAdjustExpense: group.ManualAdjustExpense,
|
||||
billColumns.ConsumeCreditRepayExpense: group.ConsumeCreditRepayExpense,
|
||||
billColumns.TotalIncome: group.TotalIncome,
|
||||
billColumns.TotalExpense: group.TotalExpense,
|
||||
billColumns.ClosingBalance: closingBalance,
|
||||
billColumns.IncomeCount: group.IncomeCount,
|
||||
billColumns.ExpenseCount: group.ExpenseCount,
|
||||
billColumns.CreditUsed: 0,
|
||||
billColumns.CreditRepaid: 0,
|
||||
billColumns.BillStatus: 1,
|
||||
billColumns.SettleTime: now,
|
||||
billColumns.Remark: "根据余额明细统计生成",
|
||||
billColumns.TenantId: tenantValue,
|
||||
billColumns.UpdatedBy: userId,
|
||||
billColumns.UpdatedAt: now,
|
||||
}
|
||||
|
||||
if existing.Id > 0 {
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TConsumeAccountMonthBill.Table()).
|
||||
WherePri(existing.Id).
|
||||
Data(data).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新消费月账单失败,请稍后重试!")
|
||||
}
|
||||
res.UpdateCount++
|
||||
} else {
|
||||
data[billColumns.CreatedDept] = createdDeptValue
|
||||
data[billColumns.CreatedBy] = userId
|
||||
data[billColumns.CreatedAt] = now
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TConsumeAccountMonthBill.Table()).
|
||||
Data(data).
|
||||
Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增消费月账单失败,请稍后重试!")
|
||||
}
|
||||
res.InsertCount++
|
||||
}
|
||||
|
||||
res.DeptCount++
|
||||
res.DetailCount += group.DetailCount
|
||||
res.TotalIncome += group.TotalIncome
|
||||
res.TotalExpense += group.TotalExpense
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTCreditDetail struct{}
|
||||
|
||||
func NewCommonTCreditDetail() *sCommonTCreditDetail {
|
||||
return &sCommonTCreditDetail{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTCreditDetail(NewCommonTCreditDetail())
|
||||
}
|
||||
|
||||
// Model 信用额度明细表ORM模型
|
||||
func (s *sCommonTCreditDetail) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TCreditDetail.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取信用额度明细表列表
|
||||
func (s *sCommonTCreditDetail) List(ctx context.Context, in *model.TCreditDetailListParam) (list []*model.TCreditDetailListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.TCreditDetailListModel{})
|
||||
|
||||
// 查询明细ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TCreditDetail.Columns().Id, in.Id)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TCreditDetail.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TCreditDetail.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取信用额度明细表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出信用额度明细表
|
||||
func (s *sCommonTCreditDetail) Export(ctx context.Context, in *model.TCreditDetailListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TCreditDetailExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出信用额度明细表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TCreditDetailExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增信用额度明细表
|
||||
func (s *sCommonTCreditDetail) Edit(ctx context.Context, in *model.TCreditDetailEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TCreditDetailUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改信用额度明细表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TCreditDetailInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增信用额度明细表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除信用额度明细表
|
||||
func (s *sCommonTCreditDetail) Delete(ctx context.Context, in *model.TCreditDetailDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除信用额度明细表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取信用额度明细表指定信息
|
||||
func (s *sCommonTCreditDetail) View(ctx context.Context, in *model.TCreditDetailViewParam) (res *model.TCreditDetailViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取信用额度明细表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Package iot
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sIotTDivisionCode struct{}
|
||||
|
||||
func NewIotTDivisionCode() *sIotTDivisionCode {
|
||||
return &sIotTDivisionCode{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterIotTDivisionCode(NewIotTDivisionCode())
|
||||
}
|
||||
|
||||
// Model 村级行政区划表ORM模型
|
||||
func (s *sIotTDivisionCode) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TDivisionCode.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取村级行政区划表列表
|
||||
func (s *sIotTDivisionCode) List(ctx context.Context, in *model.TDivisionCodeListParam) (list []*model.TDivisionCodeListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 根据父级代码查询子级列表
|
||||
if in.RegionParent != "" {
|
||||
mod = mod.Where(dao.TDivisionCode.Columns().RegionParent, in.RegionParent)
|
||||
}
|
||||
|
||||
// 根据名称查询
|
||||
if in.RegionName != "" {
|
||||
mod = mod.WhereLike(dao.TDivisionCode.Columns().RegionName, "%"+in.RegionName+"%")
|
||||
}
|
||||
|
||||
// 根据代码查询
|
||||
if in.RegionCode != "" {
|
||||
mod = mod.Where(dao.TDivisionCode.Columns().RegionCode, in.RegionCode)
|
||||
}
|
||||
|
||||
// 根据等级查询
|
||||
if in.RegionLevel > 0 {
|
||||
mod = mod.Where(dao.TDivisionCode.Columns().RegionLevel, in.RegionLevel)
|
||||
}
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.TDivisionCodeListModel{})
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TDivisionCode.Columns().RegionId)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取村级行政区划表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出村级行政区划表
|
||||
func (s *sIotTDivisionCode) Export(ctx context.Context, in *model.TDivisionCodeListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TDivisionCodeExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出村级行政区划表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TDivisionCodeExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增村级行政区划表
|
||||
func (s *sIotTDivisionCode) Edit(ctx context.Context, in *model.TDivisionCodeEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.RegionId > 0 {
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TDivisionCodeUpdateFields{}).
|
||||
WherePri(in.RegionId).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改村级行政区划表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TDivisionCodeInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增村级行政区划表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除村级行政区划表
|
||||
func (s *sIotTDivisionCode) Delete(ctx context.Context, in *model.TDivisionCodeDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.RegionId).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除村级行政区划表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取村级行政区划表指定信息
|
||||
func (s *sIotTDivisionCode) View(ctx context.Context, in *model.TDivisionCodeViewParam) (res *model.TDivisionCodeViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.RegionId).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取村级行政区划表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
"strconv"
|
||||
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTFinCommissionDetail struct{}
|
||||
|
||||
func NewCommonTFinCommissionDetail() *sCommonTFinCommissionDetail {
|
||||
return &sCommonTFinCommissionDetail{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTFinCommissionDetail(NewCommonTFinCommissionDetail())
|
||||
}
|
||||
|
||||
// Model 分润明细表ORM模型
|
||||
func (s *sCommonTFinCommissionDetail) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TFinCommissionDetail.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取分润明细表列表
|
||||
func (s *sCommonTFinCommissionDetail) List(ctx context.Context, in *model.TFinCommissionDetailListParam) (list []*model.TFinCommissionDetailListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.TFinCommissionDetailListModel{})
|
||||
|
||||
// 查询主键ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TFinCommissionDetail.Columns().Id, in.Id)
|
||||
}
|
||||
|
||||
// 查询分润状态:0待结算,1可提现
|
||||
if in.CommissionStatus > 0 {
|
||||
mod = mod.Where(dao.TFinCommissionDetail.Columns().CommissionStatus, in.CommissionStatus)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TFinCommissionDetail.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TFinCommissionDetail.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取分润明细表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出分润明细表
|
||||
func (s *sCommonTFinCommissionDetail) Export(ctx context.Context, in *model.TFinCommissionDetailListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TFinCommissionDetailExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出分润明细表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TFinCommissionDetailExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增分润明细表
|
||||
func (s *sCommonTFinCommissionDetail) Edit(ctx context.Context, in *model.TFinCommissionDetailEditParam) (err error) {
|
||||
// 验证'CommissionNo'唯一
|
||||
if err = xgorm.IsUnique(ctx, &dao.TFinCommissionDetail, g.Map{dao.TFinCommissionDetail.Columns().CommissionNo: in.CommissionNo}, "分润明细编号已存在", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TFinCommissionDetailUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改分润明细表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TFinCommissionDetailInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增分润明细表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除分润明细表
|
||||
func (s *sCommonTFinCommissionDetail) Delete(ctx context.Context, in *model.TFinCommissionDetailDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除分润明细表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取分润明细表指定信息
|
||||
func (s *sCommonTFinCommissionDetail) View(ctx context.Context, in *model.TFinCommissionDetailViewParam) (res *model.TFinCommissionDetailViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取分润明细表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarkSettled 批量标记分润明细为可提现状态。
|
||||
func (s *sCommonTFinCommissionDetail) MarkSettled(ctx context.Context, tx gdb.TX, in *model.TFinCommissionSettleUpdateParam) error {
|
||||
if in == nil {
|
||||
return gerror.New("分润结算更新参数不能为空")
|
||||
}
|
||||
if len(in.Ids) == 0 {
|
||||
return gerror.New("分润明细ID不能为空")
|
||||
}
|
||||
settlementTime := in.SettlementTime
|
||||
if settlementTime == nil {
|
||||
settlementTime = gtime.Now()
|
||||
}
|
||||
userId := contexts.GetUserId(ctx)
|
||||
columns := dao.TFinCommissionDetail.Columns()
|
||||
result, err := tx.Ctx(ctx).Model(dao.TFinCommissionDetail.Table()).
|
||||
WhereIn(columns.Id, in.Ids).
|
||||
Where(columns.CommissionStatus, financeCommissionStatusPending).
|
||||
Data(g.Map{
|
||||
columns.CommissionStatus: financeCommissionStatusSettled,
|
||||
columns.SettlementTime: settlementTime,
|
||||
columns.UpdatedBy: userId,
|
||||
columns.UpdatedAt: settlementTime,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新分润明细结算状态失败,请稍后重试!")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取分润明细结算状态更新结果失败")
|
||||
}
|
||||
if affected != int64(len(in.Ids)) {
|
||||
return gerror.New("部分分润明细状态已变化,请下次任务重试")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 分层分润概览 分润表
|
||||
// 一级客户收入 benefit_dept_level=1 commission_amount相加
|
||||
// 二级客户收入 benefit_dept_level=2 commission_amount相加
|
||||
// 三级客户收入 benefit_dept_level=3 commission_amount相加
|
||||
// 总收入 一级二级三级加起来总和
|
||||
func (s *sCommonTFinCommissionDetail) GetCommissionOverview(ctx context.Context, in *model.TCommissionOverviewParam) (res *model.TCommissionOverviewModel, err error) {
|
||||
res = &model.TCommissionOverviewModel{}
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
// 解析账期参数
|
||||
var startPeriod, endPeriod string
|
||||
if in.Type == 1 { // 年
|
||||
startPeriod = in.Year + "-01"
|
||||
endPeriod = in.Year + "-12"
|
||||
} else if in.Type == 2 { // 月
|
||||
startPeriod = in.Month
|
||||
endPeriod = in.Month
|
||||
}
|
||||
|
||||
// 查询一级客户收入
|
||||
type levelRow struct {
|
||||
CommissionAmount float64 `orm:"commission_amount"`
|
||||
}
|
||||
var level1Result *levelRow
|
||||
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptLevel, 1).
|
||||
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
|
||||
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
|
||||
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount").
|
||||
Scan(&level1Result); err != nil {
|
||||
err = gerror.Wrap(err, "查询一级客户收入失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if level1Result != nil {
|
||||
res.Level1Income = level1Result.CommissionAmount
|
||||
}
|
||||
|
||||
// 查询二级客户收入
|
||||
var level2Result *levelRow
|
||||
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptLevel, 2).
|
||||
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
|
||||
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
|
||||
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount").
|
||||
Scan(&level2Result); err != nil {
|
||||
err = gerror.Wrap(err, "查询二级客户收入失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if level2Result != nil {
|
||||
res.Level2Income = level2Result.CommissionAmount
|
||||
}
|
||||
|
||||
// 查询三级客户收入
|
||||
var level3Result *levelRow
|
||||
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptLevel, 3).
|
||||
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
|
||||
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
|
||||
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount").
|
||||
Scan(&level3Result); err != nil {
|
||||
err = gerror.Wrap(err, "查询三级客户收入失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if level3Result != nil {
|
||||
res.Level3Income = level3Result.CommissionAmount
|
||||
}
|
||||
|
||||
// 计算总收入
|
||||
res.TotalIncome = res.Level1Income + res.Level2Income + res.Level3Income
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetLevelSummary 获取层级汇总表
|
||||
// 客户数 根据 benefit_dept_level 收益机构层级分组
|
||||
// 收入金额 根据收益机构id去 t_profit_account_month_bill 查,dept_id=收益机构id,收入=分润结算+余额转提现
|
||||
// 分润金额 根据收益机构id去 t_profit_account_month_bill 查,commission_income
|
||||
// 顶级利润贡献 等于分润金额
|
||||
// 收入占比
|
||||
// 环比 选年与去年对比,选月与上个月对比
|
||||
func (s *sCommonTFinCommissionDetail) GetLevelSummary(ctx context.Context, in *model.TLevelSummaryParam) (res *model.TLevelSummaryModel, err error) {
|
||||
res = &model.TLevelSummaryModel{}
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
// 解析账期参数
|
||||
var startPeriod, endPeriod string
|
||||
if in.Type == 1 { // 年
|
||||
startPeriod = in.Year + "-01"
|
||||
endPeriod = in.Year + "-12"
|
||||
} else if in.Type == 2 { // 月
|
||||
startPeriod = in.Month
|
||||
endPeriod = in.Month
|
||||
}
|
||||
|
||||
// 查询各层级的客户数和分润金额(从分润表)
|
||||
type levelData struct {
|
||||
Level int `orm:"level"`
|
||||
CustomerCount int `orm:"customer_count"`
|
||||
CommissionAmount float64 `orm:"commission_amount"`
|
||||
}
|
||||
var levelResults []*levelData
|
||||
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
|
||||
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
|
||||
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
|
||||
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount, COUNT(DISTINCT " + dao.TFinCommissionDetail.Columns().PayerDeptId + ") as customer_count, " + dao.TFinCommissionDetail.Columns().BenefitDeptLevel + " as level").
|
||||
Group(dao.TFinCommissionDetail.Columns().BenefitDeptLevel).
|
||||
Scan(&levelResults); err != nil {
|
||||
err = gerror.Wrap(err, "查询层级数据失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建层级与分润金额的映射
|
||||
levelCommissionMap := make(map[int]float64)
|
||||
for _, lr := range levelResults {
|
||||
levelCommissionMap[lr.Level] = lr.CommissionAmount
|
||||
}
|
||||
|
||||
// 查询各层级的收入金额(从盈利月账单表:total_income)
|
||||
// 先从分润表获取各层级的机构ID列表
|
||||
type deptLevelRow struct {
|
||||
Level int `orm:"level"`
|
||||
DeptId int64 `orm:"dept_id"`
|
||||
}
|
||||
var deptLevelResults []*deptLevelRow
|
||||
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
|
||||
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
|
||||
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
|
||||
Fields("DISTINCT " + dao.TFinCommissionDetail.Columns().BenefitDeptLevel + " as level, " + dao.TFinCommissionDetail.Columns().BenefitDeptId + " as dept_id").
|
||||
Scan(&deptLevelResults); err != nil {
|
||||
err = gerror.Wrap(err, "查询层级机构失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
|
||||
// 根据层级汇总机构ID
|
||||
levelDeptMap := make(map[int][]int64)
|
||||
for _, dlr := range deptLevelResults {
|
||||
levelDeptMap[dlr.Level] = append(levelDeptMap[dlr.Level], dlr.DeptId)
|
||||
}
|
||||
|
||||
// 构建收入金额映射
|
||||
incomeMap := make(map[int]float64)
|
||||
for level, deptIds := range levelDeptMap {
|
||||
if len(deptIds) == 0 {
|
||||
continue
|
||||
}
|
||||
incomeResult, err := s.getProfitAccountIncome(ctx, &model.IncomeQueryParams{
|
||||
DeptIds: deptIds,
|
||||
StartPeriod: startPeriod,
|
||||
EndPeriod: endPeriod,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
incomeMap[level] = incomeResult
|
||||
}
|
||||
|
||||
// 计算总收入
|
||||
var totalIncome float64
|
||||
for _, item := range levelResults {
|
||||
totalIncome += item.CommissionAmount
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
for level := 1; level <= 3; level++ {
|
||||
item := model.TLevelSummaryItem{
|
||||
Level: level,
|
||||
}
|
||||
|
||||
// 设置客户数、分润金额
|
||||
for _, lr := range levelResults {
|
||||
if lr.Level == level {
|
||||
item.CustomerCount = lr.CustomerCount
|
||||
item.CommissionAmount = lr.CommissionAmount
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 设置收入金额
|
||||
item.IncomeAmount = incomeMap[level]
|
||||
|
||||
// 计算收入占比
|
||||
if totalIncome > 0 {
|
||||
item.IncomeRatio = item.CommissionAmount / totalIncome * 100
|
||||
}
|
||||
|
||||
res.Items = append(res.Items, item)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getProfitAccountIncome 查询盈利账户收入
|
||||
func (s *sCommonTFinCommissionDetail) getProfitAccountIncome(ctx context.Context, params *model.IncomeQueryParams) (float64, error) {
|
||||
type incomeRow struct {
|
||||
TotalIncome float64 `orm:"total_income"`
|
||||
}
|
||||
var row *incomeRow
|
||||
if err := handler.Model(dao.TProfitAccountMonthBill.Ctx(ctx)).
|
||||
WhereIn(dao.TProfitAccountMonthBill.Columns().DeptId, params.DeptIds).
|
||||
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, params.StartPeriod).
|
||||
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, params.EndPeriod).
|
||||
Fields("SUM(" + dao.TProfitAccountMonthBill.Columns().TotalIncome + ") as total_income").
|
||||
Scan(&row); err != nil {
|
||||
return 0, gerror.Wrap(err, "查询盈利账户收入失败")
|
||||
}
|
||||
if row == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return row.TotalIncome, nil
|
||||
}
|
||||
|
||||
// mustAtoi 安全转换字符串为int
|
||||
func mustAtoi(s string) int {
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type sCommonTFinWithdrawableChange struct{}
|
||||
|
||||
func NewCommonTFinWithdrawableChange() *sCommonTFinWithdrawableChange {
|
||||
return &sCommonTFinWithdrawableChange{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTFinWithdrawableChange(NewCommonTFinWithdrawableChange())
|
||||
}
|
||||
|
||||
// Model 可提现变动明细表ORM模型
|
||||
func (s *sCommonTFinWithdrawableChange) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TFinWithdrawableChange.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取可提现变动明细表列表
|
||||
func (s *sCommonTFinWithdrawableChange) List(ctx context.Context, in *model.TFinWithdrawableChangeListParam) (list []*model.TFinWithdrawableChangeListModel, totalCount int, err error) {
|
||||
|
||||
mod := dao.TFinWithdrawableChange.Ctx(ctx)
|
||||
sysDeptTable := dao.SysDept.Table()
|
||||
orderTable := dao.TOrder.Table()
|
||||
// mod = mod.FieldsPrefix(dao.TSimCardInfo.Table(), model.TSimCardInfoListModel{})
|
||||
// mod = mod.Fields(simChannels + ".chan_name as chanName, chan_alias as chanAlias")
|
||||
|
||||
mod = mod.FieldsPrefix(dao.TFinWithdrawableChange.Table(), model.TFinWithdrawableChangeListModel{})
|
||||
mod = mod.Fields(sysDeptTable + ".dept_name as deptName")
|
||||
mod = mod.Fields(orderTable + ".order_type as orderType")
|
||||
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Where(dao.TFinWithdrawableChange.Columns().DeptId, deptId)
|
||||
|
||||
// 查询主键ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TFinWithdrawableChange.Columns().Id, in.Id)
|
||||
}
|
||||
// 查询月份范围(根据 account_period 字段)
|
||||
if len(in.Month) == 2 {
|
||||
mod = mod.WhereGTE(dao.TFinWithdrawableChange.Columns().AccountPeriod, in.Month[0])
|
||||
mod = mod.WhereLTE(dao.TFinWithdrawableChange.Columns().AccountPeriod, in.Month[1])
|
||||
}
|
||||
// 查询收支类型
|
||||
if in.IncomeExpenseType != nil {
|
||||
mod = mod.Where(dao.TFinWithdrawableChange.Columns().IncomeExpenseType, *in.IncomeExpenseType)
|
||||
}
|
||||
// 查询变动类型
|
||||
if in.ChangeType != nil {
|
||||
mod = mod.Where(dao.TFinWithdrawableChange.Columns().ChangeType, *in.ChangeType)
|
||||
}
|
||||
// 查询账期
|
||||
if in.AccountPeriod != "" {
|
||||
mod = mod.Where(dao.TFinWithdrawableChange.Columns().AccountPeriod, in.AccountPeriod)
|
||||
}
|
||||
|
||||
mod = mod.LeftJoin(sysDeptTable, dao.TFinWithdrawableChange.Table()+"."+dao.TFinWithdrawableChange.Columns().DeptId+" COLLATE utf8mb4_general_ci="+sysDeptTable+"."+dao.SysDept.Columns().DeptId)
|
||||
mod = mod.LeftJoin(orderTable, dao.TFinWithdrawableChange.Table()+"."+dao.TFinWithdrawableChange.Columns().SourceNo+" COLLATE utf8mb4_general_ci="+orderTable+"."+dao.TOrder.Columns().OrderSn)
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TFinWithdrawableChange.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取可提现变动明细表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
// 根据 TransactionTime 赋值账期
|
||||
for i := range list {
|
||||
if list[i].CreatedAt != nil {
|
||||
list[i].TradingMonth = list[i].CreatedAt.Format("Y-m")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出可提现变动明细(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTFinWithdrawableChange) Export(ctx context.Context, in *model.TFinWithdrawableChangeListParam) (err error) {
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tFinWithdrawableChange",
|
||||
BizName: "可提现变动明细",
|
||||
PermCode: "cpm:common:tFinWithdrawableChange:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TFinWithdrawableChangeListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增可提现变动明细表
|
||||
func (s *sCommonTFinWithdrawableChange) Edit(ctx context.Context, in *model.TFinWithdrawableChangeEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TFinWithdrawableChangeUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改可提现变动明细表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TFinWithdrawableChangeInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增可提现变动明细表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除可提现变动明细表
|
||||
func (s *sCommonTFinWithdrawableChange) Delete(ctx context.Context, in *model.TFinWithdrawableChangeDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除可提现变动明细表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取可提现变动明细表指定信息
|
||||
func (s *sCommonTFinWithdrawableChange) View(ctx context.Context, in *model.TFinWithdrawableChangeViewParam) (res *model.TFinWithdrawableChangeViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取可提现变动明细表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CreateBalanceTransferInChange 写入余额转可提现审核通过后的可提现金额变动明细。
|
||||
func (s *sCommonTFinWithdrawableChange) CreateBalanceTransferInChange(ctx context.Context, tx gdb.TX, in *model.TFinWithdrawableBalanceTransferInParam) error {
|
||||
if in == nil {
|
||||
return gerror.New("可提现变动明细参数不能为空")
|
||||
}
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
userId := contexts.GetUserId(ctx)
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
createdAt := gtime.Now()
|
||||
columns := dao.TFinWithdrawableChange.Columns()
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TFinWithdrawableChange.Table()).Data(g.Map{
|
||||
columns.DeptId: in.DeptId,
|
||||
columns.ChangeType: financeWithdrawableChangeTypeBalanceTransferIn,
|
||||
columns.IncomeExpenseType: financeIncomeExpenseIncome,
|
||||
columns.ChangeAmount: in.ChangeAmount,
|
||||
columns.BeforeBalance: in.BeforeBalance,
|
||||
columns.AfterBalance: in.AfterBalance,
|
||||
columns.AccountType: in.AccountType,
|
||||
columns.BankName: in.BankName,
|
||||
columns.BankAccount: in.BankAccount,
|
||||
columns.AccountName: in.AccountName,
|
||||
columns.IdentityCard: in.IdentityCard,
|
||||
columns.SourceNo: in.SourceNo,
|
||||
columns.Remark: in.Remark,
|
||||
columns.AccountPeriod: in.AccountPeriod,
|
||||
columns.TenantId: tenantId,
|
||||
columns.CreatedDept: deptId,
|
||||
columns.CreatedBy: userId,
|
||||
columns.CreatedAt: createdAt,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增可提现变动明细失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateCommissionSettlementChange 写入分润结算产生的可提现明细。
|
||||
func (s *sCommonTFinWithdrawableChange) CreateCommissionSettlementChange(ctx context.Context, tx gdb.TX, in *model.TFinWithdrawableCommissionSettlementParam) error {
|
||||
if in == nil {
|
||||
return gerror.New("分润结算可提现明细参数不能为空")
|
||||
}
|
||||
if in.DeptId <= 0 {
|
||||
return gerror.New("机构ID不能为空")
|
||||
}
|
||||
if in.ChangeAmount <= 0 {
|
||||
return gerror.New("分润结算金额必须大于0")
|
||||
}
|
||||
if in.AccountPeriod == "" {
|
||||
return gerror.New("账期不能为空")
|
||||
}
|
||||
if in.SourceNo == "" {
|
||||
return gerror.New("结算批次号不能为空")
|
||||
}
|
||||
|
||||
tenantId := in.TenantId
|
||||
if tenantId == "" {
|
||||
tenantId = contexts.GetTenantId(ctx)
|
||||
}
|
||||
userId := in.CreatedBy
|
||||
if userId <= 0 {
|
||||
userId = contexts.GetUserId(ctx)
|
||||
}
|
||||
deptId := in.CreatedDept
|
||||
if deptId <= 0 {
|
||||
deptId = contexts.GetDeptId(ctx)
|
||||
}
|
||||
if deptId <= 0 {
|
||||
deptId = in.DeptId
|
||||
}
|
||||
createdAt := in.CreatedAt
|
||||
if createdAt == nil {
|
||||
createdAt = gtime.Now()
|
||||
}
|
||||
|
||||
columns := dao.TFinWithdrawableChange.Columns()
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TFinWithdrawableChange.Table()).Data(g.Map{
|
||||
columns.DeptId: in.DeptId,
|
||||
columns.ChangeType: financeWithdrawableChangeTypeCommission,
|
||||
columns.IncomeExpenseType: financeIncomeExpenseIncome,
|
||||
columns.ChangeAmount: in.ChangeAmount,
|
||||
columns.BeforeBalance: in.BeforeBalance,
|
||||
columns.AfterBalance: in.AfterBalance,
|
||||
columns.AccountType: 0,
|
||||
columns.SourceNo: in.SourceNo,
|
||||
columns.Remark: in.Remark,
|
||||
columns.AccountPeriod: in.AccountPeriod,
|
||||
columns.TenantId: tenantId,
|
||||
columns.CreatedDept: deptId,
|
||||
columns.CreatedBy: userId,
|
||||
columns.CreatedAt: createdAt,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增分润结算可提现明细失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"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/convert"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type sCommonTFinanceApply struct{}
|
||||
|
||||
func NewCommonTFinanceApply() *sCommonTFinanceApply {
|
||||
return &sCommonTFinanceApply{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTFinanceApply(NewCommonTFinanceApply())
|
||||
}
|
||||
|
||||
// Model 财务申请表ORM模型
|
||||
func (s *sCommonTFinanceApply) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TFinanceApply.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取财务申请表列表
|
||||
func (s *sCommonTFinanceApply) List(ctx context.Context, in *model.TFinanceApplyListParam) (list []*model.TFinanceApplyListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
applyTable := dao.TFinanceApply.Table()
|
||||
applyDeptAlias := "apply_dept"
|
||||
auditorDeptAlias := "auditor_dept"
|
||||
sysDeptTable := dao.SysDept.Table()
|
||||
mod = mod.LeftJoin(sysDeptTable+" as "+applyDeptAlias, applyDeptAlias+"."+dao.SysDept.Columns().DeptId+"="+applyTable+"."+dao.TFinanceApply.Columns().ApplyDeptId)
|
||||
mod = mod.LeftJoin(sysDeptTable+" as "+auditorDeptAlias, auditorDeptAlias+"."+dao.SysDept.Columns().DeptId+"="+applyTable+"."+dao.TFinanceApply.Columns().AuditDeptId)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.FieldsPrefix(applyTable, model.TFinanceApplyListModel{})
|
||||
mod = mod.Fields(applyDeptAlias + ".dept_name as applyDeptName")
|
||||
mod = mod.Fields(auditorDeptAlias + ".dept_name as auditDeptName")
|
||||
|
||||
// 查询主键ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TFinanceApply.Columns().Id, in.Id)
|
||||
}
|
||||
if in.ApplyNo != nil {
|
||||
mod = mod.Where(dao.TFinanceApply.Columns().ApplyNo, *in.ApplyNo)
|
||||
}
|
||||
|
||||
// 查询状态:0待审核,1已通过,2已驳回,3已打款,4已取消
|
||||
if in.ApplyStatus != nil {
|
||||
mod = mod.Where(dao.TFinanceApply.Columns().ApplyStatus, *in.ApplyStatus)
|
||||
}
|
||||
if in.ApplyType != nil {
|
||||
mod = mod.Where(dao.TFinanceApply.Columns().ApplyType, *in.ApplyType)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TFinanceApply.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TFinanceApply.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取财务申请表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出财务申请(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTFinanceApply) Export(ctx context.Context, in *model.TFinanceApplyListParam) (err error) {
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tFinanceApply",
|
||||
BizName: "财务申请",
|
||||
PermCode: "cpm:common:tFinanceApply:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TFinanceApplyListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增财务申请表
|
||||
func (s *sCommonTFinanceApply) Edit(ctx context.Context, in *model.TFinanceApplyEditParam) (err error) {
|
||||
// 验证'ApplyNo'唯一
|
||||
if err = xgorm.IsUnique(ctx, &dao.TFinanceApply, g.Map{dao.TFinanceApply.Columns().ApplyNo: in.ApplyNo}, "申请单号已存在", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TFinanceApplyUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改财务申请表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TFinanceApplyInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增财务申请表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除财务申请表
|
||||
func (s *sCommonTFinanceApply) Delete(ctx context.Context, in *model.TFinanceApplyDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除财务申请表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取财务申请表指定信息
|
||||
func (s *sCommonTFinanceApply) View(ctx context.Context, in *model.TFinanceApplyViewParam) (res *model.TFinanceApplyViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取财务申请表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CreateBalanceTransferApply 创建余额转可提现的待审核申请单。
|
||||
func (s *sCommonTFinanceApply) CreateBalanceTransferApply(ctx context.Context, tx gdb.TX, in *model.TFinanceApplyCreateBalanceTransferParam) error {
|
||||
if in == nil {
|
||||
return gerror.New("财务申请参数不能为空")
|
||||
}
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
userId := contexts.GetUserId(ctx)
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
createdAt := gtime.Now()
|
||||
columns := dao.TFinanceApply.Columns()
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TFinanceApply.Table()).Data(g.Map{
|
||||
columns.ApplyNo: in.ApplyNo,
|
||||
columns.ApplyType: financeApplyTypeBalanceToWithdraw,
|
||||
columns.AuditType: financeAuditTypeFirst,
|
||||
columns.ApplyDeptId: in.ApplyDeptId,
|
||||
columns.AuditDeptId: in.AuditDeptId,
|
||||
columns.AvailableAmountSnapshot: in.AvailableAmountSnapshot,
|
||||
columns.ApplyAmount: in.ApplyAmount,
|
||||
columns.AccountType: 0,
|
||||
columns.ApplyStatus: financeApplyStatusPending,
|
||||
columns.ApplyTime: createdAt,
|
||||
columns.Remark: in.Remark,
|
||||
columns.TenantId: tenantId,
|
||||
columns.CreatedDept: deptId,
|
||||
columns.CreatedBy: userId,
|
||||
columns.CreatedAt: createdAt,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增余额转可提现申请失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPendingByApplyNo 按申请单号查询待审核申请。
|
||||
func (s *sCommonTFinanceApply) GetPendingByApplyNo(ctx context.Context, tx gdb.TX, applyNo string, tenantId string) (*entity.TFinanceApply, error) {
|
||||
apply := &entity.TFinanceApply{}
|
||||
columns := dao.TFinanceApply.Columns()
|
||||
if err := tx.Ctx(ctx).Model(dao.TFinanceApply.Table()).
|
||||
Where(columns.ApplyNo, applyNo).
|
||||
Where(columns.ApplyStatus, financeApplyStatusPending).
|
||||
Where(columns.TenantId, tenantId).
|
||||
Scan(apply); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询财务申请失败,请稍后重试!")
|
||||
}
|
||||
if apply.Id == 0 {
|
||||
return nil, gerror.New("财务申请不存在或已审核")
|
||||
}
|
||||
return apply, nil
|
||||
}
|
||||
|
||||
// UpdateAuditStatus 更新财务申请的审核状态。
|
||||
func (s *sCommonTFinanceApply) UpdateAuditStatus(ctx context.Context, tx gdb.TX, in *model.TFinanceApplyUpdateAuditStatusParam) error {
|
||||
if in == nil {
|
||||
return gerror.New("财务申请审核状态参数不能为空")
|
||||
}
|
||||
userId := contexts.GetUserId(ctx)
|
||||
updatedAt := gtime.Now()
|
||||
columns := dao.TFinanceApply.Columns()
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TFinanceApply.Table()).
|
||||
Where(columns.ApplyNo, in.ApplyNo).
|
||||
Data(g.Map{
|
||||
columns.ApplyStatus: in.ApplyStatus,
|
||||
columns.AuditTime: in.AuditTime,
|
||||
columns.UpdatedBy: userId,
|
||||
columns.UpdatedAt: updatedAt,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新财务申请审核状态失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type sCommonTFinanceAuditRecord struct{}
|
||||
|
||||
func NewCommonTFinanceAuditRecord() *sCommonTFinanceAuditRecord {
|
||||
return &sCommonTFinanceAuditRecord{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTFinanceAuditRecord(NewCommonTFinanceAuditRecord())
|
||||
}
|
||||
|
||||
// Model 财务审核记录表ORM模型
|
||||
func (s *sCommonTFinanceAuditRecord) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TFinanceAuditRecord.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取财务审核记录表列表
|
||||
func (s *sCommonTFinanceAuditRecord) List(ctx context.Context, in *model.TFinanceAuditRecordListParam) (list []*model.TFinanceAuditRecordListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
auditRecordTable := dao.TFinanceAuditRecord.Table()
|
||||
sysDept := dao.SysDept.Table()
|
||||
sysUser := dao.SysUser.Table()
|
||||
mod = mod.LeftJoin(sysDept, sysDept+"."+dao.SysDept.Columns().DeptId+"="+auditRecordTable+"."+dao.TFinanceAuditRecord.Columns().AuditDeptId)
|
||||
mod = mod.LeftJoin(sysUser, sysUser+"."+dao.SysUser.Columns().UserId+"="+auditRecordTable+"."+dao.TFinanceAuditRecord.Columns().AuditorUserId)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.FieldsPrefix(auditRecordTable, model.TFinanceAuditRecordListModel{})
|
||||
mod = mod.Fields(sysDept + ".dept_name as auditorDeptName")
|
||||
mod = mod.Fields(sysUser + ".user_name as auditorUserName")
|
||||
|
||||
// 查询主键ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TFinanceAuditRecord.Columns().Id, in.Id)
|
||||
}
|
||||
if in.ApplyNo != nil {
|
||||
mod = mod.Where(dao.TFinanceAuditRecord.Columns().ApplyNo, *in.ApplyNo)
|
||||
}
|
||||
if in.AuditResult != nil {
|
||||
mod = mod.Where(dao.TFinanceAuditRecord.Columns().AuditResult, *in.AuditResult)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TFinanceAuditRecord.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TFinanceAuditRecord.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取财务审核记录表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出财务审核记录(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTFinanceAuditRecord) Export(ctx context.Context, in *model.TFinanceAuditRecordListParam) (err error) {
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tFinanceAuditRecord",
|
||||
BizName: "财务审核记录",
|
||||
PermCode: "cpm:common:tFinanceAuditRecord:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TFinanceAuditRecordListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增财务审核记录表
|
||||
func (s *sCommonTFinanceAuditRecord) Edit(ctx context.Context, in *model.TFinanceAuditRecordEditParam) (err error) {
|
||||
// 验证'AuditNo'唯一
|
||||
if err = xgorm.IsUnique(ctx, &dao.TFinanceAuditRecord, g.Map{dao.TFinanceAuditRecord.Columns().AuditNo: in.AuditNo}, "审核记录编号已存在", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TFinanceAuditRecordUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改财务审核记录表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TFinanceAuditRecordInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增财务审核记录表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除财务审核记录表
|
||||
func (s *sCommonTFinanceAuditRecord) Delete(ctx context.Context, in *model.TFinanceAuditRecordDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除财务审核记录表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取财务审核记录表指定信息
|
||||
func (s *sCommonTFinanceAuditRecord) View(ctx context.Context, in *model.TFinanceAuditRecordViewParam) (res *model.TFinanceAuditRecordViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取财务审核记录表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CreateRecord 写入财务审核记录。
|
||||
func (s *sCommonTFinanceAuditRecord) CreateRecord(ctx context.Context, tx gdb.TX, in *model.TFinanceAuditRecordCreateParam) error {
|
||||
if in == nil {
|
||||
return gerror.New("财务审核记录参数不能为空")
|
||||
}
|
||||
columns := dao.TFinanceAuditRecord.Columns()
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
userId := contexts.GetUserId(ctx)
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
createdAt := gtime.Now()
|
||||
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TFinanceAuditRecord.Table()).Data(g.Map{
|
||||
columns.AuditNo: in.AuditNo,
|
||||
columns.ApplyNo: in.ApplyNo,
|
||||
columns.AuditDeptId: in.AuditDeptId,
|
||||
columns.AuditorUserId: in.AuditorUserId,
|
||||
columns.AuditResult: in.AuditResult,
|
||||
columns.AuditOpinion: in.AuditOpinion,
|
||||
columns.AuditTime: in.AuditTime,
|
||||
columns.TenantId: tenantId,
|
||||
columns.CreatedDept: deptId,
|
||||
columns.CreatedBy: userId,
|
||||
columns.CreatedAt: createdAt,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增财务审核记录失败,请稍后重试!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/websocket"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"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/convert"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTMessageLog struct{}
|
||||
|
||||
func NewCommonTMessageLog() *sCommonTMessageLog {
|
||||
return &sCommonTMessageLog{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTMessageLog(NewCommonTMessageLog())
|
||||
}
|
||||
|
||||
// Model 消息日志ORM模型
|
||||
func (s *sCommonTMessageLog) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TMessageLog.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取消息日志列表
|
||||
func (s *sCommonTMessageLog) List(ctx context.Context, in *model.TMessageLogListParam) (list []*model.TMessageLogListModel, totalCount int, err error) {
|
||||
mod := dao.TMessageLog.Ctx(ctx)
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.TMessageLogListModel{})
|
||||
g.Log().Info(ctx, "查询用户ID:", contexts.GetUserId(ctx))
|
||||
|
||||
if in.MessageType != nil {
|
||||
mod = mod.Where(dao.TMessageLog.Columns().MessageType, in.MessageType)
|
||||
}
|
||||
|
||||
if title := strings.TrimSpace(in.MessageTitle); title != "" {
|
||||
mod = mod.WhereLike(dao.TMessageLog.Columns().MessageTitle, "%"+title+"%")
|
||||
}
|
||||
|
||||
// 查询发送时间范围
|
||||
if in.SendTimeStart != nil {
|
||||
mod = mod.WhereGTE(dao.TMessageLog.Columns().SendTime, in.SendTimeStart)
|
||||
}
|
||||
if in.SendTimeEnd != nil {
|
||||
mod = mod.WhereLTE(dao.TMessageLog.Columns().SendTime, in.SendTimeEnd.EndOfDay())
|
||||
}
|
||||
|
||||
// 兼容旧的单日发送时间查询
|
||||
if in.SendTime != nil {
|
||||
mod = mod.Where(fmt.Sprintf("DATE(%s) = ?", dao.TMessageLog.Columns().SendTime), in.SendTime.Format("Y-m-d"))
|
||||
}
|
||||
|
||||
// 查询用户ID(JSON字段,使用JSON_CONTAINS查询)
|
||||
userId := contexts.GetUserId(ctx)
|
||||
var sysUser *entity.SysUser
|
||||
dao.SysUser.Ctx(ctx).WherePri(userId).Scan(&sysUser)
|
||||
userType := sysUser.UserType
|
||||
|
||||
// 1. 查找顶级部门(ParentId = 0)
|
||||
var topDept entity.SysDept
|
||||
err = dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().ParentId, 0).
|
||||
Scan(&topDept)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查顶级部门是否存在
|
||||
if topDept.DeptId > 0 && deptId != topDept.DeptId {
|
||||
mod = mod.Where(dao.TMessageLog.Columns().IsRecall, 0)
|
||||
mod = mod.Where(fmt.Sprintf("JSON_CONTAINS(%s, CAST(? AS JSON))", dao.TMessageLog.Columns().UserIds), userId)
|
||||
if userType == "sys_user" {
|
||||
mod = mod.Where(dao.TMessageLog.Columns().SendUserType, 1)
|
||||
} else if userType == "app_user" {
|
||||
mod = mod.Where(dao.TMessageLog.Columns().SendUserType, 2)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TMessageLog.Columns().MessageLogId)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取消息日志列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出消息日志(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTMessageLog) Export(ctx context.Context, in *model.TMessageLogListParam) (err error) {
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tMessageLog",
|
||||
BizName: "消息日志",
|
||||
PermCode: "cpm:common:tMessageLog:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TMessageLogListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增消息日志
|
||||
func (s *sCommonTMessageLog) Edit(ctx context.Context, in *model.TMessageLogEditParam) (err error) {
|
||||
var (
|
||||
pushUserIds []uint64
|
||||
pushPayload g.Map
|
||||
)
|
||||
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.MessageLogId > 0 {
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TMessageLogUpdateFields{}).
|
||||
WherePri(in.MessageLogId).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改消息日志失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
in.SendTime = gtime.Now()
|
||||
|
||||
result, err := tx.Ctx(ctx).Model(dao.TMessageLog.Table()).
|
||||
Fields(model.TMessageLogInsertFields{}).
|
||||
Data(in).Insert()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, "新增消息日志失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取插入的消息日志ID
|
||||
messageLogId, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, "获取消息日志ID失败")
|
||||
return
|
||||
}
|
||||
pushPayload = g.Map{
|
||||
"messageLogId": messageLogId,
|
||||
"messageType": gconv.Uint(in.MessageType),
|
||||
"sendUserType": gconv.Uint(in.SendUserType),
|
||||
"messageTitle": gconv.String(in.MessageTitle),
|
||||
"messageContent": gconv.String(in.MessageContent),
|
||||
"messageLink": gconv.String(in.MessageLink),
|
||||
"sendTime": in.SendTime,
|
||||
"isRecall": gconv.Uint(in.IsRecall),
|
||||
"isRead": 0,
|
||||
}
|
||||
|
||||
optionalUserMessageColumns := s.userMessageOptionalColumns(ctx)
|
||||
// 根据 user_ids 往用户消息表插入记录
|
||||
if in.UserIds != nil && len(in.UserIds.Interfaces()) > 0 {
|
||||
userIds := in.UserIds.Interfaces()
|
||||
for _, userId := range userIds {
|
||||
userIdInt := gconv.Int64(userId)
|
||||
if userIdInt <= 0 {
|
||||
continue
|
||||
}
|
||||
userMessageData := g.Map{
|
||||
dao.TUserMessage.Columns().MessageLogId: messageLogId,
|
||||
dao.TUserMessage.Columns().UserId: userIdInt,
|
||||
dao.TUserMessage.Columns().TenantId: tenantId,
|
||||
dao.TUserMessage.Columns().CreatedDept: createdDept,
|
||||
dao.TUserMessage.Columns().CreatedBy: createdBy,
|
||||
dao.TUserMessage.Columns().CreatedAt: in.CreatedAt,
|
||||
}
|
||||
if _, ok := optionalUserMessageColumns["user_rank"]; ok {
|
||||
userMessageData["user_rank"] = 0
|
||||
}
|
||||
if _, ok := optionalUserMessageColumns["title"]; ok {
|
||||
userMessageData["title"] = messageLogStringValue(in.MessageTitle)
|
||||
}
|
||||
if _, ok := optionalUserMessageColumns["content"]; ok {
|
||||
userMessageData["content"] = messageLogStringValue(in.MessageContent)
|
||||
}
|
||||
if _, ok := optionalUserMessageColumns["link"]; ok {
|
||||
userMessageData["link"] = messageLogStringValue(in.MessageLink)
|
||||
}
|
||||
_, err = tx.Ctx(ctx).Model(dao.TUserMessage.Table()).Data(userMessageData).Insert()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, "新增用户消息失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
pushUserIds = append(pushUserIds, uint64(userIdInt))
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, userId := range pushUserIds {
|
||||
g.Log().Info(ctx, "推送消息到用户 %v", userId)
|
||||
websocket.SendToUser(userId, &websocket.WResponse{
|
||||
Event: "message:new",
|
||||
Data: pushPayload,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sCommonTMessageLog) userMessageOptionalColumns(ctx context.Context) map[string]struct{} {
|
||||
result, err := g.DB().GetAll(ctx, "SHOW COLUMNS FROM "+dao.TUserMessage.Table())
|
||||
if err != nil {
|
||||
return map[string]struct{}{}
|
||||
}
|
||||
columns := make(map[string]struct{}, len(result))
|
||||
for _, row := range result {
|
||||
field := row["Field"].String()
|
||||
if field != "" {
|
||||
columns[field] = struct{}{}
|
||||
}
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
func messageLogStringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
// Delete 删除消息日志
|
||||
func (s *sCommonTMessageLog) Delete(ctx context.Context, in *model.TMessageLogDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.MessageLogId).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除消息日志失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取消息日志指定信息,并标记已读
|
||||
func (s *sCommonTMessageLog) View(ctx context.Context, in *model.TMessageLogViewParam) (res *model.TMessageLogViewModel, err error) {
|
||||
if err = dao.TMessageLog.Ctx(ctx).WherePri(in.MessageLogId).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取消息日志信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
|
||||
// 标记用户消息为已读
|
||||
userId := contexts.GetUserId(ctx)
|
||||
now := gtime.Now()
|
||||
_, err = dao.TUserMessage.Ctx(ctx).Where(g.Map{
|
||||
dao.TUserMessage.Columns().MessageLogId: in.MessageLogId,
|
||||
dao.TUserMessage.Columns().UserId: userId,
|
||||
dao.TUserMessage.Columns().IsRead: 0,
|
||||
}).Data(g.Map{
|
||||
dao.TUserMessage.Columns().IsRead: 1,
|
||||
dao.TUserMessage.Columns().UpdatedBy: userId,
|
||||
dao.TUserMessage.Columns().UpdatedAt: now,
|
||||
}).Update()
|
||||
if err != nil {
|
||||
// 已读标记失败不影响主业务,仅记录日志
|
||||
g.Log().Warning(ctx, "标记消息已读失败: %v", err)
|
||||
err = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 未读通知的默认与最大返回条数,避免一次拉取过多消息。
|
||||
const (
|
||||
messageUnreadDefaultLimit = 50
|
||||
messageUnreadMaxLimit = 200
|
||||
)
|
||||
|
||||
// unreadQuery 构造当前用户未读消息查询。
|
||||
// 以用户消息表为准:只查本人的未读记录,关联消息日志排除已撤回消息,
|
||||
// 消息日志已删除(关联为空)时也不提醒。
|
||||
// 这里不按 send_user_type 过滤:收件人 user_id 全部来自 sys_user 单表,
|
||||
// 该字段只表示消息面向哪个控制台,按其过滤会漏掉真正发给本人的消息。
|
||||
func (s *sCommonTMessageLog) unreadQuery(ctx context.Context, userId int64) *gdb.Model {
|
||||
var (
|
||||
userTable = dao.TUserMessage.Table()
|
||||
logTable = dao.TMessageLog.Table()
|
||||
userColumns = dao.TUserMessage.Columns()
|
||||
logColumns = dao.TMessageLog.Columns()
|
||||
)
|
||||
return dao.TUserMessage.Ctx(ctx).
|
||||
LeftJoin(logTable+" ON "+logTable+"."+logColumns.MessageLogId+" = "+userTable+"."+userColumns.MessageLogId).
|
||||
Where(userTable+"."+userColumns.UserId, userId).
|
||||
Where(userTable+"."+userColumns.IsRead, 0).
|
||||
Where(logTable+"."+logColumns.IsRecall, 0)
|
||||
}
|
||||
|
||||
// UnreadList 获取当前登录用户的未读消息通知。
|
||||
// 这是右上角消息提醒的数据来源:登录、刷新、断线重连后都必须重新拉取,
|
||||
// 否则离线期间的消息会因错过 websocket 推送而不再出现提醒。
|
||||
func (s *sCommonTMessageLog) UnreadList(ctx context.Context, in *model.TMessageLogUnreadListParam) (list []*model.TMessageLogUnreadListModel, totalCount int, err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
err = gerror.New("未获取到当前登录用户信息")
|
||||
return
|
||||
}
|
||||
|
||||
limit := messageUnreadDefaultLimit
|
||||
if in != nil && in.Limit > 0 {
|
||||
limit = in.Limit
|
||||
}
|
||||
if limit > messageUnreadMaxLimit {
|
||||
limit = messageUnreadMaxLimit
|
||||
}
|
||||
|
||||
var (
|
||||
userTable = dao.TUserMessage.Table()
|
||||
logTable = dao.TMessageLog.Table()
|
||||
userColumn = dao.TUserMessage.Columns()
|
||||
logColumn = dao.TMessageLog.Columns()
|
||||
)
|
||||
if err = s.unreadQuery(ctx, userId).
|
||||
Fields(userTable + "." + userColumn.MessageLogId + "," +
|
||||
logTable + "." + logColumn.MessageType + "," +
|
||||
logTable + "." + logColumn.SendUserType + "," +
|
||||
logTable + "." + logColumn.MessageTitle + "," +
|
||||
logTable + "." + logColumn.MessageContent + "," +
|
||||
logTable + "." + logColumn.MessageLink + "," +
|
||||
logTable + "." + logColumn.SendTime + "," +
|
||||
userTable + "." + userColumn.IsRead).
|
||||
OrderDesc(userTable + "." + userColumn.MessageId).
|
||||
Limit(limit).
|
||||
Scan(&list); err != nil {
|
||||
err = gerror.Wrap(err, "获取未读消息失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*model.TMessageLogUnreadListModel{}
|
||||
}
|
||||
// 总数不受列表条数上限影响,用于判断是否还有更多未读。
|
||||
if totalCount, err = s.unreadQuery(ctx, userId).Count(); err != nil {
|
||||
err = gerror.Wrap(err, "获取未读消息数量失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Read 标记当前登录用户的指定消息为已读。
|
||||
// 只允许操作本人的用户消息记录,避免越权修改他人已读状态。
|
||||
func (s *sCommonTMessageLog) Read(ctx context.Context, in *model.TMessageLogReadParam) (err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
return gerror.New("未获取到当前登录用户信息")
|
||||
}
|
||||
if in == nil || in.MessageLogId <= 0 {
|
||||
return gerror.New("消息ID不能为空")
|
||||
}
|
||||
if _, err = dao.TUserMessage.Ctx(ctx).Where(g.Map{
|
||||
dao.TUserMessage.Columns().MessageLogId: in.MessageLogId,
|
||||
dao.TUserMessage.Columns().UserId: userId,
|
||||
}).Data(g.Map{
|
||||
dao.TUserMessage.Columns().IsRead: 1,
|
||||
dao.TUserMessage.Columns().UpdatedBy: userId,
|
||||
dao.TUserMessage.Columns().UpdatedAt: gtime.Now(),
|
||||
}).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "标记消息已读失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll 标记当前登录用户的全部未读消息为已读。
|
||||
func (s *sCommonTMessageLog) ReadAll(ctx context.Context, in *model.TMessageLogReadAllParam) (err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId <= 0 {
|
||||
return gerror.New("未获取到当前登录用户信息")
|
||||
}
|
||||
if _, err = dao.TUserMessage.Ctx(ctx).
|
||||
Where(dao.TUserMessage.Columns().UserId, userId).
|
||||
Where(dao.TUserMessage.Columns().IsRead, 0).
|
||||
Data(g.Map{
|
||||
dao.TUserMessage.Columns().IsRead: 1,
|
||||
dao.TUserMessage.Columns().UpdatedBy: userId,
|
||||
dao.TUserMessage.Columns().UpdatedAt: gtime.Now(),
|
||||
}).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "标记全部消息已读失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/websocket"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"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/convert"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTNotifyLog struct{}
|
||||
|
||||
func NewCommonTNotifyLog() *sCommonTNotifyLog {
|
||||
return &sCommonTNotifyLog{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTNotifyLog(NewCommonTNotifyLog())
|
||||
}
|
||||
|
||||
// Model 通知日志ORM模型
|
||||
func (s *sCommonTNotifyLog) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TNotifyLog.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取通知日志列表
|
||||
func (s *sCommonTNotifyLog) List(ctx context.Context, in *model.TNotifyLogListParam) (list []*model.TNotifyLogListModel, totalCount int, err error) {
|
||||
mod := dao.TNotifyLog.Ctx(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.TNotifyLogListModel{})
|
||||
|
||||
if title := strings.TrimSpace(in.NotifyTitle); title != "" {
|
||||
mod = mod.WhereLike(dao.TNotifyLog.Columns().NotifyTitle, "%"+title+"%")
|
||||
}
|
||||
|
||||
// 查询发送用户类型
|
||||
if in.SendUserType != nil && *in.SendUserType > 0 {
|
||||
mod = mod.Where(dao.TNotifyLog.Columns().SendUserType, *in.SendUserType)
|
||||
}
|
||||
|
||||
// 查询发送时间范围
|
||||
if in.SendTimeStart != nil {
|
||||
mod = mod.WhereGTE(dao.TNotifyLog.Columns().SendTime, in.SendTimeStart)
|
||||
}
|
||||
if in.SendTimeEnd != nil {
|
||||
mod = mod.WhereLTE(dao.TNotifyLog.Columns().SendTime, in.SendTimeEnd.EndOfDay())
|
||||
}
|
||||
|
||||
// 兼容旧的单日发送时间查询
|
||||
if in.SendTime != nil {
|
||||
mod = mod.Where(fmt.Sprintf("DATE(%s) = ?", dao.TNotifyLog.Columns().SendTime), in.SendTime.Format("Y-m-d"))
|
||||
}
|
||||
|
||||
userId := contexts.GetUserId(ctx)
|
||||
var sysUser *entity.SysUser
|
||||
dao.SysUser.Ctx(ctx).WherePri(userId).Scan(&sysUser)
|
||||
userType := sysUser.UserType
|
||||
// 1. 查找顶级部门(ParentId = 0)
|
||||
var topDept entity.SysDept
|
||||
err = dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().ParentId, 0).
|
||||
Scan(&topDept)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户部门ID
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
// 只有非顶级用户才根据用户类型过滤
|
||||
if deptId != topDept.DeptId {
|
||||
if userType == "sys_user" {
|
||||
mod = mod.Where(dao.TNotifyLog.Columns().SendUserType, 1)
|
||||
} else if userType == "app_user" {
|
||||
mod = mod.Where(dao.TNotifyLog.Columns().SendUserType, 2)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TNotifyLog.Columns().NotifyId)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取通知日志列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出通知日志(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTNotifyLog) Export(ctx context.Context, in *model.TNotifyLogListParam) (err error) {
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tNotifyLog",
|
||||
BizName: "通知日志",
|
||||
PermCode: "cpm:common:tNotifyLog:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TNotifyLogListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增通知日志
|
||||
func (s *sCommonTNotifyLog) Edit(ctx context.Context, in *model.TNotifyLogEditParam) (err error) {
|
||||
var (
|
||||
pushPayload g.Map
|
||||
pushUserIds []uint64
|
||||
)
|
||||
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.NotifyId > 0 {
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TNotifyLogUpdateFields{}).
|
||||
WherePri(in.NotifyId).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改通知日志失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
in.SendTime = gtime.Now()
|
||||
|
||||
result, err := s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TNotifyLogInsertFields{}).
|
||||
Data(in).Insert()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, "新增通知日志失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
|
||||
notifyId, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, "获取通知ID失败")
|
||||
return
|
||||
}
|
||||
|
||||
pushPayload = g.Map{
|
||||
"notifyId": notifyId,
|
||||
"notifyTitle": gconv.String(in.NotifyTitle),
|
||||
"notifyContent": gconv.String(in.NotifyContent),
|
||||
"sendUserType": gconv.Int(in.SendUserType),
|
||||
"sendTime": in.SendTime,
|
||||
}
|
||||
|
||||
var userType string
|
||||
if in.SendUserType != nil {
|
||||
switch *in.SendUserType {
|
||||
case 1:
|
||||
userType = "sys_user"
|
||||
case 2:
|
||||
userType = "app_user"
|
||||
}
|
||||
}
|
||||
if userType != "" {
|
||||
var users []*entity.SysUser
|
||||
if err = tx.Ctx(ctx).Model(dao.SysUser.Table()).
|
||||
Fields(dao.SysUser.Columns().UserId).
|
||||
Where(dao.SysUser.Columns().TenantId, tenantId).
|
||||
Where(dao.SysUser.Columns().UserType, userType).
|
||||
Where(dao.SysUser.Columns().Status, "0").
|
||||
Scan(&users); err != nil {
|
||||
err = gerror.Wrap(err, "获取通知推送用户失败")
|
||||
return
|
||||
}
|
||||
for _, user := range users {
|
||||
if user.UserId > 0 {
|
||||
pushUserIds = append(pushUserIds, uint64(user.UserId))
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, userId := range pushUserIds {
|
||||
g.Log().Info(ctx, "推送通知到用户 %v", userId)
|
||||
websocket.SendToUser(userId, &websocket.WResponse{
|
||||
Event: "notify:new",
|
||||
Data: pushPayload,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除通知日志
|
||||
func (s *sCommonTNotifyLog) Delete(ctx context.Context, in *model.TNotifyLogDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.NotifyId).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除通知日志失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取通知日志指定信息
|
||||
func (s *sCommonTNotifyLog) View(ctx context.Context, in *model.TNotifyLogViewParam) (res *model.TNotifyLogViewModel, err error) {
|
||||
if err = dao.TNotifyLog.Ctx(ctx).WherePri(in.NotifyId).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取通知日志信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
"strconv"
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonProfitAccountMonthBill struct{}
|
||||
|
||||
func NewCommonProfitAccountMonthBill() *sCommonProfitAccountMonthBill {
|
||||
return &sCommonProfitAccountMonthBill{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonProfitAccountMonthBill(NewCommonProfitAccountMonthBill())
|
||||
}
|
||||
|
||||
// Model 盈利账户月账单表ORM模型
|
||||
func (s *sCommonProfitAccountMonthBill) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TProfitAccountMonthBill.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取盈利账户月账单表列表
|
||||
func (s *sCommonProfitAccountMonthBill) List(ctx context.Context, in *model.TProfitAccountMonthBillListParam) (list []*model.TProfitAccountMonthBillListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.TProfitAccountMonthBillListModel{})
|
||||
|
||||
// 查询账单ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TProfitAccountMonthBill.Columns().Id, in.Id)
|
||||
}
|
||||
|
||||
// 查询账单状态:1正常,2已调整,3已作废
|
||||
if in.BillStatus > 0 {
|
||||
mod = mod.Where(dao.TProfitAccountMonthBill.Columns().BillStatus, in.BillStatus)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TProfitAccountMonthBill.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TProfitAccountMonthBill.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取盈利账户月账单表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出盈利账户月账单表
|
||||
func (s *sCommonProfitAccountMonthBill) Export(ctx context.Context, in *model.TProfitAccountMonthBillListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.TProfitAccountMonthBillExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出盈利账户月账单表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TProfitAccountMonthBillExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增盈利账户月账单表
|
||||
func (s *sCommonProfitAccountMonthBill) Edit(ctx context.Context, in *model.TProfitAccountMonthBillEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TProfitAccountMonthBillUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改盈利账户月账单表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TProfitAccountMonthBillInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增盈利账户月账单表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除盈利账户月账单表
|
||||
func (s *sCommonProfitAccountMonthBill) Delete(ctx context.Context, in *model.TProfitAccountMonthBillDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除盈利账户月账单表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取盈利账户月账单表指定信息
|
||||
func (s *sCommonProfitAccountMonthBill) View(ctx context.Context, in *model.TProfitAccountMonthBillViewParam) (res *model.TProfitAccountMonthBillViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取盈利账户月账单表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SettleProfitAccountMonthBill 按可提现变动明细统计并写入盈利账户月账单。
|
||||
func (s *sCommonProfitAccountMonthBill) SettleProfitAccountMonthBill(ctx context.Context, in *model.TProfitAccountMonthBillSettleParam) (res *model.TProfitAccountMonthBillSettleResult, err error) {
|
||||
if in == nil {
|
||||
in = &model.TProfitAccountMonthBillSettleParam{}
|
||||
}
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
|
||||
}
|
||||
|
||||
_, err = time.Parse("2006-01", in.AccountPeriod)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "账期格式错误,请使用YYYY-MM")
|
||||
}
|
||||
now := gtime.Now()
|
||||
userId := contexts.GetUserId(ctx)
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
changeColumns := dao.TFinWithdrawableChange.Columns()
|
||||
billColumns := dao.TProfitAccountMonthBill.Columns()
|
||||
|
||||
type changeRow struct {
|
||||
Id int64 `orm:"id"`
|
||||
DeptId int64 `orm:"dept_id"`
|
||||
TenantId string `orm:"tenant_id"`
|
||||
CreatedDept int64 `orm:"created_dept"`
|
||||
ChangeType int `orm:"change_type"`
|
||||
IncomeExpenseType int `orm:"income_expense_type"`
|
||||
ChangeAmount float64 `orm:"change_amount"`
|
||||
BeforeBalance float64 `orm:"before_balance"`
|
||||
CreatedAt *gtime.Time `orm:"created_at"`
|
||||
}
|
||||
|
||||
var rows []*changeRow
|
||||
mod := handler.Model(dao.TFinWithdrawableChange.Ctx(ctx), &handler.Option{
|
||||
FilterTenant: false,
|
||||
FilterAuth: false,
|
||||
}).
|
||||
Fields(
|
||||
changeColumns.Id,
|
||||
changeColumns.DeptId,
|
||||
changeColumns.TenantId,
|
||||
changeColumns.CreatedDept,
|
||||
changeColumns.ChangeType,
|
||||
changeColumns.IncomeExpenseType,
|
||||
changeColumns.ChangeAmount,
|
||||
changeColumns.BeforeBalance,
|
||||
changeColumns.CreatedAt,
|
||||
).
|
||||
Where(changeColumns.AccountPeriod, in.AccountPeriod)
|
||||
if tenantId != "" {
|
||||
mod = mod.Where(changeColumns.TenantId, tenantId)
|
||||
}
|
||||
if err = mod.OrderAsc(changeColumns.TenantId).OrderAsc(changeColumns.DeptId).OrderAsc(changeColumns.CreatedAt).OrderAsc(changeColumns.Id).Scan(&rows); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询可提现变动明细失败,请稍后重试!")
|
||||
}
|
||||
|
||||
type billGroup struct {
|
||||
DeptId int64
|
||||
TenantId string
|
||||
CreatedDept int64
|
||||
AccountPeriod string
|
||||
OpeningBalance float64
|
||||
CommissionIncome float64
|
||||
BalanceTransferInIncome float64
|
||||
WithdrawalExpense float64
|
||||
WithdrawableTransferOutExpense float64
|
||||
ProfitCreditRepayExpense float64
|
||||
TotalIncome float64
|
||||
TotalExpense float64
|
||||
IncomeCount int
|
||||
ExpenseCount int
|
||||
DetailCount int
|
||||
}
|
||||
|
||||
groups := make(map[string]*billGroup)
|
||||
keys := make([]string, 0, 16)
|
||||
for _, row := range rows {
|
||||
if row == nil || row.DeptId <= 0 {
|
||||
continue
|
||||
}
|
||||
switch row.ChangeType {
|
||||
case 0, 1, 2, 3, 4:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
key := fmt.Sprintf("%s:%d", row.TenantId, row.DeptId)
|
||||
group := groups[key]
|
||||
if group == nil {
|
||||
group = &billGroup{
|
||||
DeptId: row.DeptId,
|
||||
TenantId: row.TenantId,
|
||||
CreatedDept: row.CreatedDept,
|
||||
AccountPeriod: in.AccountPeriod,
|
||||
OpeningBalance: row.BeforeBalance,
|
||||
}
|
||||
groups[key] = group
|
||||
keys = append(keys, key)
|
||||
}
|
||||
group.DetailCount++
|
||||
switch row.ChangeType {
|
||||
case 0:
|
||||
group.CommissionIncome += row.ChangeAmount
|
||||
case 1:
|
||||
group.WithdrawalExpense += row.ChangeAmount
|
||||
case 2:
|
||||
group.WithdrawableTransferOutExpense += row.ChangeAmount
|
||||
case 3:
|
||||
group.BalanceTransferInIncome += row.ChangeAmount
|
||||
case 4:
|
||||
group.ProfitCreditRepayExpense += row.ChangeAmount
|
||||
}
|
||||
if row.IncomeExpenseType == 0 {
|
||||
group.TotalIncome += row.ChangeAmount
|
||||
group.IncomeCount++
|
||||
} else if row.IncomeExpenseType == 1 {
|
||||
group.TotalExpense += row.ChangeAmount
|
||||
group.ExpenseCount++
|
||||
}
|
||||
}
|
||||
|
||||
res = &model.TProfitAccountMonthBillSettleResult{
|
||||
AccountPeriod: in.AccountPeriod,
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
for _, key := range keys {
|
||||
group := groups[key]
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
totalIncome := group.TotalIncome
|
||||
totalExpense := group.TotalExpense
|
||||
closingBalance := group.OpeningBalance + totalIncome - totalExpense
|
||||
group.TotalIncome = totalIncome
|
||||
group.TotalExpense = totalExpense
|
||||
|
||||
lookup := tx.Ctx(ctx).Model(dao.TProfitAccountMonthBill.Table()).
|
||||
Fields(billColumns.Id).
|
||||
Where(billColumns.DeptId, group.DeptId).
|
||||
Where(billColumns.AccountPeriod, group.AccountPeriod)
|
||||
if group.TenantId != "" {
|
||||
lookup = lookup.Where(billColumns.TenantId, group.TenantId)
|
||||
}
|
||||
var existing struct {
|
||||
Id int64 `orm:"id"`
|
||||
}
|
||||
if err := lookup.LockUpdate().Scan(&existing); err != nil && err != sql.ErrNoRows {
|
||||
return gerror.Wrap(err, "查询盈利账户月账单失败,请稍后重试!")
|
||||
}
|
||||
|
||||
tenantValue := group.TenantId
|
||||
if tenantValue == "" {
|
||||
tenantValue = tenantId
|
||||
}
|
||||
if tenantValue == "" {
|
||||
return gerror.New("租户ID不能为空")
|
||||
}
|
||||
createdDeptValue := group.CreatedDept
|
||||
if createdDeptValue == 0 {
|
||||
createdDeptValue = contexts.GetDeptId(ctx)
|
||||
}
|
||||
if createdDeptValue == 0 {
|
||||
return gerror.New("创建部门不能为空")
|
||||
}
|
||||
|
||||
data := g.Map{
|
||||
billColumns.DeptId: group.DeptId,
|
||||
billColumns.AccountPeriod: group.AccountPeriod,
|
||||
billColumns.OpeningBalance: group.OpeningBalance,
|
||||
billColumns.CommissionIncome: group.CommissionIncome,
|
||||
billColumns.BalanceTransferInIncome: group.BalanceTransferInIncome,
|
||||
billColumns.WithdrawalExpense: group.WithdrawalExpense,
|
||||
billColumns.WithdrawableTransferOutExpense: group.WithdrawableTransferOutExpense,
|
||||
billColumns.ProfitCreditRepayExpense: group.ProfitCreditRepayExpense,
|
||||
billColumns.TotalIncome: totalIncome,
|
||||
billColumns.TotalExpense: totalExpense,
|
||||
billColumns.ClosingBalance: closingBalance,
|
||||
billColumns.IncomeCount: group.IncomeCount,
|
||||
billColumns.ExpenseCount: group.ExpenseCount,
|
||||
billColumns.BillStatus: 1,
|
||||
billColumns.SettleTime: now,
|
||||
billColumns.SettleBy: userId,
|
||||
billColumns.Remark: "根据可提现变动明细统计生成",
|
||||
billColumns.TenantId: tenantValue,
|
||||
billColumns.UpdatedBy: userId,
|
||||
billColumns.UpdatedAt: now,
|
||||
}
|
||||
if existing.Id > 0 {
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TProfitAccountMonthBill.Table()).
|
||||
WherePri(existing.Id).
|
||||
Data(data).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新盈利账户月账单失败,请稍后重试!")
|
||||
}
|
||||
res.UpdateCount++
|
||||
} else {
|
||||
data[billColumns.CreatedDept] = createdDeptValue
|
||||
data[billColumns.CreatedBy] = userId
|
||||
data[billColumns.CreatedAt] = now
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TProfitAccountMonthBill.Table()).
|
||||
Data(data).
|
||||
Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增盈利账户月账单失败,请稍后重试!")
|
||||
}
|
||||
res.InsertCount++
|
||||
}
|
||||
res.DeptCount++
|
||||
res.DetailCount += group.DetailCount
|
||||
res.TotalIncome += totalIncome
|
||||
res.TotalExpense += totalExpense
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 获取月账单盈利账户汇总信息
|
||||
func (s *sCommonProfitAccountMonthBill) GetProfitBillSummary(ctx context.Context, month string) (res *model.TProfitBillModel, err error) {
|
||||
// 计算上月账期(基于当前时间)
|
||||
// now := time.Now()
|
||||
// lastMonthTime := now.AddDate(0, -1, 0)
|
||||
// lastMonthPeriod := lastMonthTime.Format("2006-01")
|
||||
|
||||
var bill *model.TProfitAccountMonthBillViewModel
|
||||
if err = dao.TProfitAccountMonthBill.Ctx(ctx).
|
||||
Where(dao.TProfitAccountMonthBill.Columns().AccountPeriod, month).
|
||||
Where(dao.TProfitAccountMonthBill.Columns().DeptId, contexts.GetDeptId(ctx)).
|
||||
Scan(&bill); err != nil {
|
||||
err = gerror.Wrap(err, "查询上月账单失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if bill == nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &model.TProfitBillModel{}
|
||||
res.TotalIncome = bill.TotalIncome
|
||||
res.TotalExpense = bill.TotalExpense
|
||||
return
|
||||
}
|
||||
|
||||
// GetCoreFinancialData 获取核心财务指标
|
||||
func (s *sCommonProfitAccountMonthBill) GetCoreFinancialData(ctx context.Context, in *model.TCommissionOverviewParam) (res *model.TCoreFinancialDataModel, err error) {
|
||||
res = &model.TCoreFinancialDataModel{}
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
// 解析账期参数
|
||||
var startPeriod, endPeriod string
|
||||
if in.Type == 1 { // 年
|
||||
startPeriod = in.Year + "-01"
|
||||
endPeriod = in.Year + "-12"
|
||||
} else if in.Type == 2 { // 月
|
||||
startPeriod = in.Month
|
||||
endPeriod = in.Month
|
||||
}
|
||||
|
||||
// 1. 获取总收入:统计 t_profit_account_month_bill 的 total_income
|
||||
type totalIncomeRow struct {
|
||||
TotalIncome float64 `orm:"total_income"`
|
||||
}
|
||||
var totalIncomeResult *totalIncomeRow
|
||||
if err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Where(dao.TProfitAccountMonthBill.Columns().DeptId, deptId).
|
||||
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, startPeriod).
|
||||
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, endPeriod).
|
||||
Fields("SUM(" + dao.TProfitAccountMonthBill.Columns().TotalIncome + ") as total_income").
|
||||
Scan(&totalIncomeResult); err != nil {
|
||||
err = gerror.Wrap(err, "查询总收入失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if totalIncomeResult != nil {
|
||||
res.TotalIncome = totalIncomeResult.TotalIncome
|
||||
}
|
||||
|
||||
// 2. 获取顶级利润:查询 t_fin_commission_detail 中顶级部门的 commission_income
|
||||
// 顶级部门ID通常为1或当前部门的顶级父部门
|
||||
|
||||
type topProfitRow struct {
|
||||
CommissionIncome float64 `orm:"commission_income"`
|
||||
}
|
||||
var topProfitResult *topProfitRow
|
||||
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, deptId).
|
||||
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, startPeriod).
|
||||
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, endPeriod).
|
||||
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_income").
|
||||
Scan(&topProfitResult); err != nil {
|
||||
err = gerror.Wrap(err, "查询顶级利润失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if topProfitResult != nil {
|
||||
res.TopLevelProfit = topProfitResult.CommissionIncome
|
||||
}
|
||||
|
||||
// 3. 获取待审核提现:从 t_profit_account_month_bill 查询 withdrawable_transfer_out_expense
|
||||
type pendingWithdrawRow struct {
|
||||
WithdrawableTransferOutExpense float64 `orm:"withdrawable_transfer_out_expense"`
|
||||
}
|
||||
var pendingWithdrawResult *pendingWithdrawRow
|
||||
if err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Where(dao.TProfitAccountMonthBill.Columns().DeptId, deptId).
|
||||
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, startPeriod).
|
||||
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, endPeriod).
|
||||
Fields("SUM(" + dao.TProfitAccountMonthBill.Columns().WithdrawableTransferOutExpense + ") as withdrawable_transfer_out_expense").
|
||||
Scan(&pendingWithdrawResult); err != nil {
|
||||
err = gerror.Wrap(err, "查询待审核提现失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if pendingWithdrawResult != nil {
|
||||
res.PendingWithdrawAmount = pendingWithdrawResult.WithdrawableTransferOutExpense
|
||||
}
|
||||
|
||||
// 4. 获取已提现金额:统计 t_profit_account_month_bill 的 balance_transfer_in_income
|
||||
type withdrawedRow struct {
|
||||
WithdrawalExpense float64 `orm:"withdrawal_expense"`
|
||||
}
|
||||
var withdrawedResult *withdrawedRow
|
||||
if err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Where(dao.TProfitAccountMonthBill.Columns().DeptId, deptId).
|
||||
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, startPeriod).
|
||||
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, endPeriod).
|
||||
Fields("SUM(" + dao.TProfitAccountMonthBill.Columns().WithdrawalExpense + ") as withdrawal_expense").
|
||||
Scan(&withdrawedResult); err != nil {
|
||||
err = gerror.Wrap(err, "查询已提现金额失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if withdrawedResult != nil {
|
||||
res.WithdrawedAmount = withdrawedResult.WithdrawalExpense
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetFinancialTrend 获取财务趋势折线图
|
||||
// 指标类型:1总收入,2顶级利润,3待审核提现,4已提现金额
|
||||
// 月份视图:按天查询 t_fin_withdrawable_change
|
||||
// 年份视图:按月查询 t_profit_account_month_bill
|
||||
func (s *sCommonProfitAccountMonthBill) GetFinancialTrend(ctx context.Context, in *model.TFinancialTrendParam) (res *model.TFinancialTrendModel, err error) {
|
||||
res = &model.TFinancialTrendModel{}
|
||||
|
||||
// 默认查询所有指标
|
||||
indicators := in.Indicators
|
||||
if len(indicators) == 0 {
|
||||
indicators = []int{1, 2, 3, 4}
|
||||
}
|
||||
|
||||
// 指标名称映射
|
||||
indicatorNames := map[int]string{
|
||||
1: "总收入",
|
||||
2: "顶级利润",
|
||||
3: "待审核提现",
|
||||
4: "已提现金额",
|
||||
}
|
||||
|
||||
if in.Type == 1 {
|
||||
// 年视图:按月查询 t_profit_account_month_bill
|
||||
year := in.Year
|
||||
months := []string{"01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"}
|
||||
res.Labels = make([]string, 0, 12)
|
||||
for _, m := range months {
|
||||
res.Labels = append(res.Labels, year+"-"+m)
|
||||
}
|
||||
|
||||
type monthBillRow struct {
|
||||
AccountPeriod string `orm:"account_period"`
|
||||
TotalIncome float64 `orm:"total_income"`
|
||||
CommissionIncome float64 `orm:"commission_income"`
|
||||
BalanceTransferInIncome float64 `orm:"balance_transfer_in_income"`
|
||||
WithdrawalExpense float64 `orm:"withdrawal_expense"`
|
||||
}
|
||||
var bills []*monthBillRow
|
||||
if err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Where(dao.TProfitAccountMonthBill.Columns().DeptId, contexts.GetDeptId(ctx)).
|
||||
WhereGTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, year+"-01").
|
||||
WhereLTE(dao.TProfitAccountMonthBill.Columns().AccountPeriod, year+"-12").
|
||||
Fields(
|
||||
dao.TProfitAccountMonthBill.Columns().AccountPeriod,
|
||||
dao.TProfitAccountMonthBill.Columns().TotalIncome,
|
||||
dao.TProfitAccountMonthBill.Columns().CommissionIncome,
|
||||
dao.TProfitAccountMonthBill.Columns().BalanceTransferInIncome,
|
||||
dao.TProfitAccountMonthBill.Columns().WithdrawalExpense,
|
||||
).
|
||||
Scan(&bills); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询财务趋势数据失败,请稍后重试!")
|
||||
}
|
||||
|
||||
// 构建月份映射
|
||||
billMap := make(map[string]*monthBillRow)
|
||||
for _, bill := range bills {
|
||||
billMap[bill.AccountPeriod] = bill
|
||||
}
|
||||
|
||||
// 构建每个指标的数据数组
|
||||
res.Series = make([]model.TFinancialTrendSeries, 0, len(indicators))
|
||||
for _, ind := range indicators {
|
||||
series := model.TFinancialTrendSeries{
|
||||
Name: indicatorNames[ind],
|
||||
Type: ind,
|
||||
Values: make([]float64, 0, 12),
|
||||
}
|
||||
for _, period := range res.Labels {
|
||||
var val float64
|
||||
if bill, ok := billMap[period]; ok {
|
||||
switch ind {
|
||||
case 1:
|
||||
val = bill.TotalIncome
|
||||
case 2:
|
||||
val = bill.CommissionIncome
|
||||
case 3:
|
||||
val = bill.BalanceTransferInIncome
|
||||
case 4:
|
||||
val = bill.WithdrawalExpense
|
||||
}
|
||||
}
|
||||
series.Values = append(series.Values, val)
|
||||
}
|
||||
res.Series = append(res.Series, series)
|
||||
}
|
||||
|
||||
} else if in.Type == 2 {
|
||||
// 月视图:按天查询 t_fin_withdrawable_change
|
||||
month := in.Month
|
||||
t := gtime.NewFromStr(month + "-01")
|
||||
daysInMonth := t.EndOfMonth().Day()
|
||||
|
||||
res.Labels = make([]string, 0, daysInMonth)
|
||||
for day := 1; day <= daysInMonth; day++ {
|
||||
res.Labels = append(res.Labels, month+"-"+strconv.Itoa(day))
|
||||
}
|
||||
|
||||
// 解析月份获取开始和结束日期
|
||||
startDate := month + "-01"
|
||||
endDate := month + "-" + strconv.Itoa(daysInMonth)
|
||||
|
||||
type dailyRow struct {
|
||||
Date string `orm:"date"`
|
||||
IncomeAmt float64 `orm:"income_amt"`
|
||||
Expense1Amt float64 `orm:"expense1_amt"`
|
||||
Expense3Amt float64 `orm:"expense3_amt"`
|
||||
}
|
||||
var dailyResults []*dailyRow
|
||||
if err = handler.Model(dao.TFinWithdrawableChange.Ctx(ctx)).
|
||||
Where(dao.TFinWithdrawableChange.Columns().DeptId, contexts.GetDeptId(ctx)).
|
||||
WhereGTE(dao.TFinWithdrawableChange.Columns().CreatedAt, startDate).
|
||||
WhereLTE(dao.TFinWithdrawableChange.Columns().CreatedAt, endDate).
|
||||
Fields(`
|
||||
DATE_FORMAT(` + dao.TFinWithdrawableChange.Columns().CreatedAt + `, '%Y-%m-%d') as date,
|
||||
SUM(CASE WHEN ` + dao.TFinWithdrawableChange.Columns().IncomeExpenseType + `=0 THEN ` + dao.TFinWithdrawableChange.Columns().ChangeAmount + ` ELSE 0 END) as income_amt,
|
||||
SUM(CASE WHEN ` + dao.TFinWithdrawableChange.Columns().IncomeExpenseType + `=1 THEN ` + dao.TFinWithdrawableChange.Columns().ChangeAmount + ` ELSE 0 END) as expense1_amt,
|
||||
SUM(CASE WHEN ` + dao.TFinWithdrawableChange.Columns().IncomeExpenseType + `=3 THEN ` + dao.TFinWithdrawableChange.Columns().ChangeAmount + ` ELSE 0 END) as expense3_amt
|
||||
`).
|
||||
Group("DATE_FORMAT(" + dao.TFinWithdrawableChange.Columns().CreatedAt + ", '%Y-%m-%d')").
|
||||
Order("date ASC").
|
||||
Scan(&dailyResults); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询日趋势数据失败,请稍后重试!")
|
||||
}
|
||||
|
||||
// 构建日期映射
|
||||
dailyMap := make(map[string]*dailyRow)
|
||||
for _, row := range dailyResults {
|
||||
dailyMap[row.Date] = row
|
||||
}
|
||||
|
||||
// 查询顶级利润(从分润表)
|
||||
type commissionRow struct {
|
||||
CommissionAmount float64 `orm:"commission_amount"`
|
||||
}
|
||||
var commissionResult *commissionRow
|
||||
if err = handler.Model(dao.TFinCommissionDetail.Ctx(ctx)).
|
||||
Where(dao.TFinCommissionDetail.Columns().BenefitDeptId, contexts.GetDeptId(ctx)).
|
||||
WhereGTE(dao.TFinCommissionDetail.Columns().AccountPeriod, month).
|
||||
WhereLTE(dao.TFinCommissionDetail.Columns().AccountPeriod, month).
|
||||
Fields("SUM(" + dao.TFinCommissionDetail.Columns().CommissionAmount + ") as commission_amount").
|
||||
Scan(&commissionResult); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询顶级利润失败,请稍后重试!")
|
||||
}
|
||||
|
||||
topProfit := 0.0
|
||||
if commissionResult != nil {
|
||||
topProfit = commissionResult.CommissionAmount
|
||||
}
|
||||
|
||||
// 构建每个指标的数据数组
|
||||
res.Series = make([]model.TFinancialTrendSeries, 0, len(indicators))
|
||||
for _, ind := range indicators {
|
||||
series := model.TFinancialTrendSeries{
|
||||
Name: indicatorNames[ind],
|
||||
Type: ind,
|
||||
Values: make([]float64, 0, daysInMonth),
|
||||
}
|
||||
for _, date := range res.Labels {
|
||||
var val float64
|
||||
switch ind {
|
||||
case 1: // 总收入
|
||||
if row, ok := dailyMap[date]; ok {
|
||||
val = row.IncomeAmt
|
||||
}
|
||||
case 2: // 顶级利润
|
||||
val = topProfit
|
||||
case 3: // 待审核提现
|
||||
if row, ok := dailyMap[date]; ok {
|
||||
val = row.Expense3Amt
|
||||
}
|
||||
case 4: // 已提现金额
|
||||
if row, ok := dailyMap[date]; ok {
|
||||
val = row.Expense1Amt
|
||||
}
|
||||
}
|
||||
series.Values = append(series.Values, val)
|
||||
}
|
||||
res.Series = append(res.Series, series)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/websocket"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"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/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"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/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sCommonTWoCommunication struct{}
|
||||
|
||||
func NewCommonTWoCommunication() *sCommonTWoCommunication {
|
||||
return &sCommonTWoCommunication{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTWoCommunication(NewCommonTWoCommunication())
|
||||
}
|
||||
|
||||
func (s *sCommonTWoCommunication) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TWoCommunication.Ctx(ctx), option...)
|
||||
}
|
||||
func (s *sCommonTWoCommunication) List(ctx context.Context, in *model.TWoCommunicationListParam) (list []*model.TWoCommunicationListModel, totalCount int, err error) {
|
||||
if in == nil || strings.TrimSpace(in.WoNo) == "" {
|
||||
return nil, 0, gerror.New("请指定工单编号")
|
||||
}
|
||||
if _, err = getWorkOrder(ctx, in.WoNo); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
mod := s.Model(ctx, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: false,
|
||||
})
|
||||
mod = mod.Fields(model.TWoCommunicationListModel{})
|
||||
|
||||
if in.WoNo != "" {
|
||||
mod = mod.Where(dao.TWoCommunication.Columns().WoNo, in.WoNo)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
mod = mod.WhereBetween(dao.TWoCommunication.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
mod = mod.OrderDesc(dao.TWoCommunication.Columns().Id)
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "get work order communication list failed")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sCommonTWoCommunication) Export(ctx context.Context, in *model.TWoCommunicationListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tags, err := convert.GetEntityDescTags(model.TWoCommunicationExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "export-work-order-communication-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("total %v rows, %v pages, page %v, current %v rows", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TWoCommunicationExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sCommonTWoCommunication) Edit(ctx context.Context, in *model.TWoCommunicationEditParam) (err error) {
|
||||
if err = xgorm.IsUnique(ctx, &dao.TWoCommunication, g.Map{dao.TWoCommunication.Columns().CommNo: in.CommNo}, "communication number exists", in.Id); err != nil {
|
||||
return
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TWoCommunicationUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "update work order communication failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TWoCommunicationInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "create work order communication failed")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// CreateInitialMessage 创建工单时同步生成首条沟通消息。
|
||||
func (s *sCommonTWoCommunication) CreateInitialMessage(ctx context.Context, tx gdb.TX, in *model.TWoCommunicationInitialMessageParam) error {
|
||||
if in == nil {
|
||||
return gerror.New("invalid request")
|
||||
}
|
||||
if in.WoNo == "" {
|
||||
return gerror.New("work order number cannot be empty")
|
||||
}
|
||||
if in.Content == "" {
|
||||
return nil
|
||||
}
|
||||
now := in.CreatedAt
|
||||
if now == nil {
|
||||
now = gtime.Now()
|
||||
}
|
||||
columns := dao.TWoCommunication.Columns()
|
||||
data := g.Map{
|
||||
columns.WoNo: in.WoNo,
|
||||
columns.CommType: in.CommType,
|
||||
columns.SenderType: in.SenderType,
|
||||
columns.SenderDeptId: in.SenderDeptId,
|
||||
columns.SenderUserId: in.SenderUserId,
|
||||
columns.SenderName: in.SenderName,
|
||||
columns.Content: in.Content,
|
||||
columns.CommTime: now,
|
||||
columns.TenantId: in.TenantId,
|
||||
columns.CreatedDept: in.CreatedDept,
|
||||
columns.CreatedBy: in.CreatedBy,
|
||||
columns.CreatedAt: now,
|
||||
columns.UpdatedBy: in.CreatedBy,
|
||||
columns.UpdatedAt: now,
|
||||
}
|
||||
if len(in.AttachmentUrls) > 0 {
|
||||
data[columns.AttachmentUrls] = gjson.New(in.AttachmentUrls)
|
||||
}
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TWoCommunication.Table()).Data(data).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "create initial work order communication failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sCommonTWoCommunication) Message(ctx context.Context, in *model.TWoMainMessageParam) (err error) {
|
||||
if in == nil {
|
||||
return gerror.New("invalid request")
|
||||
}
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
current, err := getWorkOrder(ctx, in.WoNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
senderName := contexts.GetUserName(ctx)
|
||||
|
||||
// 处理文件上传
|
||||
if in.File != nil {
|
||||
ossResult, err := service.SysOss().Upload(ctx, &model.SysOssUploadParam{
|
||||
File: in.File,
|
||||
FileType: "file",
|
||||
NewFileType: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "file upload failed")
|
||||
}
|
||||
if ossResult != nil && ossResult.Url != "" {
|
||||
in.AttachmentUrls = append(in.AttachmentUrls, ossResult.Url)
|
||||
}
|
||||
}
|
||||
|
||||
// 写入前核对终态;发送人由当前登录用户确定。
|
||||
if current.CurrentStatus == 4 || current.CurrentStatus == 5 {
|
||||
return gerror.New("已完结或已取消的工单不能回复")
|
||||
}
|
||||
content := strings.TrimSpace(in.Content)
|
||||
for _, attachment := range in.AttachmentUrls {
|
||||
var file *entity.SysOss
|
||||
columns := dao.SysOss.Columns()
|
||||
if err = dao.SysOss.Ctx(ctx).Where(columns.TenantId, tenantId).Where(columns.CreatedBy, createdBy).Where(columns.Url, attachment).Scan(&file); err != nil {
|
||||
return gerror.Wrap(err, "校验工单附件失败")
|
||||
}
|
||||
if file == nil || file.OssId == 0 || file.FileSize > 10*1024*1024 {
|
||||
return gerror.New("附件不存在、不属于当前用户或超过10MB")
|
||||
}
|
||||
}
|
||||
if content == "" && len(in.AttachmentUrls) == 0 {
|
||||
return gerror.New("请输入内容或上传附件")
|
||||
}
|
||||
if utf8.RuneCountInString(content) > 2000 || len(in.AttachmentUrls) > 6 {
|
||||
return gerror.New("内容最多2000字,附件最多6个")
|
||||
}
|
||||
isCreator := current.CreatorUserId == createdBy
|
||||
commType := 0
|
||||
if isCreator {
|
||||
commType = 1
|
||||
}
|
||||
now := gtime.Now()
|
||||
communication := do.TWoCommunication{WoNo: current.WoNo, CommType: commType, SenderDeptId: createdDept, SenderUserId: createdBy, SenderName: senderName, Content: content, TenantId: tenantId, CreatedDept: createdDept, CreatedBy: createdBy, UpdatedBy: createdBy}
|
||||
communication.CommNo = fmt.Sprintf("comm%d", gtime.TimestampNano())
|
||||
communication.WoId = current.Id
|
||||
communication.CommTime = now
|
||||
if isCreator {
|
||||
communication.SenderType = current.CreatorType
|
||||
} else {
|
||||
communication.SenderType = 2
|
||||
}
|
||||
if len(in.AttachmentUrls) > 0 {
|
||||
communication.AttachmentUrls = gjson.New(in.AttachmentUrls)
|
||||
}
|
||||
if _, err = dao.TWoCommunication.Ctx(ctx).TX(tx).Data(communication).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "保存工单回复失败")
|
||||
}
|
||||
update := do.TWoMain{UpdatedBy: createdBy}
|
||||
// 创建人在待确认阶段继续反馈视为未解决;客服补充回复不重新打开工单。
|
||||
if isCreator && current.CurrentStatus == workOrderStatusResolved {
|
||||
update.CurrentStatus = workOrderStatusProcessing
|
||||
if err = service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{WoNo: current.WoNo, OperationType: workOrderOperationProcess, BeforeStatus: workOrderStatusResolved, AfterStatus: workOrderStatusProcessing}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !isCreator && current.FirstResponseTime == nil {
|
||||
update.FirstResponseTime = now
|
||||
}
|
||||
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().Id, current.Id).Where(dao.TWoMain.Columns().TenantId, tenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).Data(update).Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新工单失败")
|
||||
}
|
||||
if !isCreator {
|
||||
if err = saveWorkOrderNotice(ctx, tx, ¤t, createdBy, "工单收到回复", content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wsTag := "ws:tag:wo:communication:" + in.WoNo
|
||||
g.Log().Infof(ctx, "work order communication websocket broadcast tag:%s count:%d", wsTag, websocket.GetTagCount(wsTag))
|
||||
websocket.SendToTag(wsTag, &websocket.WResponse{
|
||||
Event: "ws:event:wo:communication:message",
|
||||
Data: g.Map{
|
||||
"woNo": in.WoNo,
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
func (s *sCommonTWoCommunication) Delete(ctx context.Context, in *model.TWoCommunicationDeleteParam) (err error) {
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "delete work order communication failed")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sCommonTWoCommunication) View(ctx context.Context, in *model.TWoCommunicationViewParam) (res *model.TWoCommunicationViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "get work order communication failed")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/websocket"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"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/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
"strings"
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
workOrderAutoCloseDelay = 72 * time.Hour
|
||||
|
||||
workOrderStatusAssigned = 1 //已分派
|
||||
workOrderStatusProcessing = 2 //处理中
|
||||
workOrderStatusResolved = 3 //已解决
|
||||
workOrderStatusClosed = 4 //已关单
|
||||
workOrderStatusCanceled = 5 //已取消
|
||||
|
||||
workOrderOperationCreate = 0 //创建
|
||||
workOrderOperationAssign = 1 //分派
|
||||
workOrderOperationProcess = 2 //处理
|
||||
workOrderOperationFinish = 3 //完成
|
||||
workOrderOperationClose = 4 //关单
|
||||
workOrderOperationCancel = 5 //取消
|
||||
)
|
||||
|
||||
type sCommonTWoMain struct{}
|
||||
|
||||
func NewCommonTWoMain() *sCommonTWoMain {
|
||||
return &sCommonTWoMain{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTWoMain(NewCommonTWoMain())
|
||||
}
|
||||
|
||||
func getWorkOrder(ctx context.Context, woNo string) (current entity.TWoMain, err error) {
|
||||
if contexts.GetUserId(ctx) <= 0 || strings.TrimSpace(woNo) == "" {
|
||||
return current, gerror.New("请登录并指定工单编号")
|
||||
}
|
||||
query := dao.TWoMain.Ctx(ctx).Where(dao.TWoMain.Columns().TenantId, contexts.GetTenantId(ctx)).Where(dao.TWoMain.Columns().WoNo, strings.TrimSpace(woNo))
|
||||
if g.DB().GetConfig().Type != "sqlite" {
|
||||
query = query.LockUpdate()
|
||||
}
|
||||
if err = query.Scan(¤t); err != nil {
|
||||
return current, gerror.Wrap(err, "query work order failed")
|
||||
}
|
||||
if current.WoNo == "" {
|
||||
return current, gerror.New("work order not found")
|
||||
}
|
||||
if !contexts.IsSuperAdmin(ctx) && current.CreatorUserId != contexts.GetUserId(ctx) && current.HandlerDeptId != contexts.GetDeptId(ctx) && current.HandlerUserId != contexts.GetUserId(ctx) {
|
||||
return current, gerror.New("无权访问该工单")
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func (s *sCommonTWoMain) notifyWorkOrderChanged(ctx context.Context, woNo string) {
|
||||
if woNo == "" {
|
||||
return
|
||||
}
|
||||
websocket.SendToTag("ws:tag:wo:communication:"+woNo, &websocket.WResponse{
|
||||
Event: "ws:event:wo:communication:transfer",
|
||||
Data: g.Map{
|
||||
"woNo": woNo,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Model returns the work order main ORM model
|
||||
func (s *sCommonTWoMain) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TWoMain.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List returns work order main records.
|
||||
func (s *sCommonTWoMain) List(ctx context.Context, in *model.TWoMainListParam) (list []*model.TWoMainListModel, totalCount int, err error) {
|
||||
if contexts.GetUserId(ctx) <= 0 {
|
||||
return nil, 0, gerror.New("请先登录")
|
||||
}
|
||||
mod := s.Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false})
|
||||
mod = mod.Fields(model.TWoMainListModel{})
|
||||
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TWoMain.Columns().Id, in.Id)
|
||||
}
|
||||
if in.CurrentStatus > 0 {
|
||||
mod = mod.Where(dao.TWoMain.Columns().CurrentStatus, in.CurrentStatus)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
mod = mod.WhereBetween(dao.TWoMain.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
if in.WoNo != "" {
|
||||
mod = mod.Where(dao.TWoMain.Columns().WoNo, in.WoNo)
|
||||
}
|
||||
if in.WoType > 0 {
|
||||
mod = mod.Where(dao.TWoMain.Columns().WoType, in.WoType)
|
||||
}
|
||||
if !contexts.IsSuperAdmin(ctx) {
|
||||
columns := dao.TWoMain.Columns()
|
||||
mod = mod.Where("("+columns.CreatorUserId+" = ? OR "+columns.HandlerUserId+" = ? OR "+columns.HandlerDeptId+" = ?)", contexts.GetUserId(ctx), contexts.GetUserId(ctx), contexts.GetDeptId(ctx))
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
mod = mod.OrderDesc(dao.TWoMain.Columns().Id)
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "get work order list failed")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sCommonTWoMain) Export(ctx context.Context, in *model.TWoMainListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tags, err := convert.GetEntityDescTags(model.TWoMainExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "export-work-order-main-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("total %v rows, %v pages, page %v, current %v rows", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []model.TWoMainExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// 添加工单
|
||||
func (s *sCommonTWoMain) Edit(ctx context.Context, in *model.TWoMainEditParam) (err error) {
|
||||
|
||||
if in.Id > 0 && in.WoNo != nil {
|
||||
err = xgorm.IsUnique(ctx, &dao.TWoMain, g.Map{dao.TWoMain.Columns().WoNo: in.WoNo}, "work order number exists", in.Id)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
WherePri(in.Id).
|
||||
Fields(model.TWoMainUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "update work order failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
woNo := fmt.Sprintf("gd%d", gtime.TimestampMilli())
|
||||
in.WoNo = &woNo
|
||||
in.CreatorDeptId = &createdDept
|
||||
in.CreatorUserId = &createdBy
|
||||
creatorName := contexts.GetUserName(ctx)
|
||||
in.CreatorName = &creatorName
|
||||
currentStatus := workOrderStatusProcessing
|
||||
in.CurrentStatus = ¤tStatus
|
||||
|
||||
// 查询当前部门信息
|
||||
var currentDept entity.SysDept
|
||||
err = dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().DeptId, createdDept).
|
||||
Scan(¤tDept)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 分派到上级部门
|
||||
var handlerDeptId int64
|
||||
var handlerUserId int64
|
||||
if currentDept.ParentId != 0 && currentDept.ParentId != 100 {
|
||||
// 如果有上级部门,分派到上级部门
|
||||
handlerDeptId = currentDept.ParentId
|
||||
var parentDept entity.SysDept
|
||||
err = dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().DeptId, handlerDeptId).
|
||||
Scan(&parentDept)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handlerUserId = gconv.Int64(parentDept.Leader)
|
||||
} else {
|
||||
// 如果没有上级部门,分派到当前部门
|
||||
handlerDeptId = createdDept
|
||||
handlerUserId = gconv.Int64(currentDept.Leader)
|
||||
}
|
||||
|
||||
in.HandlerDeptId = &handlerDeptId
|
||||
in.HandlerUserId = &handlerUserId
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TWoMain.Table()).
|
||||
Fields(model.TWoMainInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "create work order failed")
|
||||
return err
|
||||
}
|
||||
content := ""
|
||||
if in.Content != nil {
|
||||
content = *in.Content
|
||||
}
|
||||
attachmentUrls := append([]string{}, in.AttachmentUrls...)
|
||||
if len(attachmentUrls) == 0 && in.Attachment != nil {
|
||||
attachment := strings.TrimSpace(*in.Attachment)
|
||||
if attachment != "" {
|
||||
attachmentUrls = append(attachmentUrls, attachment)
|
||||
}
|
||||
}
|
||||
creatorType := 0
|
||||
if in.CreatorType != nil {
|
||||
creatorType = *in.CreatorType
|
||||
}
|
||||
if err = service.CommonTWoCommunication().CreateInitialMessage(ctx, tx, &model.TWoCommunicationInitialMessageParam{
|
||||
WoNo: *in.WoNo,
|
||||
CommType: 1,
|
||||
SenderType: creatorType,
|
||||
SenderDeptId: createdDept,
|
||||
SenderUserId: createdBy,
|
||||
SenderName: creatorName,
|
||||
Content: content,
|
||||
AttachmentUrls: attachmentUrls,
|
||||
TenantId: tenantId,
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: in.CreatedAt,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
|
||||
WoNo: *in.WoNo,
|
||||
OperationType: workOrderOperationCreate,
|
||||
AfterStatus: currentStatus,
|
||||
})
|
||||
})
|
||||
}
|
||||
func (s *sCommonTWoMain) Message(ctx context.Context, in *model.TWoMainMessageParam) (err error) {
|
||||
return service.CommonTWoCommunication().Message(ctx, in)
|
||||
}
|
||||
|
||||
// 转单:转给顶级部门(ParentId=0)的负责人
|
||||
func (s *sCommonTWoMain) Transfer(ctx context.Context, in *model.TWoMainTransferParam) (err error) {
|
||||
if in == nil {
|
||||
return gerror.New("invalid request")
|
||||
}
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
current, err := getWorkOrder(ctx, in.WoNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if current.CurrentStatus != workOrderStatusAssigned && current.CurrentStatus != workOrderStatusProcessing {
|
||||
return gerror.New("工单状态不允许此操作")
|
||||
}
|
||||
if current.CreatorUserId == contexts.GetUserId(ctx) && !contexts.IsSuperAdmin(ctx) {
|
||||
return gerror.New("请由处理人员执行此操作")
|
||||
}
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
|
||||
// 1. 查找顶级部门(ParentId = 0)
|
||||
var topDept entity.SysDept
|
||||
err = dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().ParentId, 0).
|
||||
Where(dao.SysDept.Columns().TenantId, current.TenantId).
|
||||
OrderAsc(dao.SysDept.Columns().OrderNum).OrderAsc(dao.SysDept.Columns().DeptId).
|
||||
Scan(&topDept)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "查询顶级部门失败")
|
||||
}
|
||||
if topDept.DeptId == 0 {
|
||||
return gerror.New("未找到顶级部门")
|
||||
}
|
||||
|
||||
// 3. 更新工单,转给顶级部门及其负责人
|
||||
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().TenantId, current.TenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).
|
||||
Where(dao.TWoMain.Columns().WoNo, in.WoNo).
|
||||
Data(do.TWoMain{
|
||||
AssignDeptId: topDept.DeptId,
|
||||
AssignUserId: topDept.Leader,
|
||||
HandlerDeptId: topDept.DeptId,
|
||||
HandlerUserId: topDept.Leader,
|
||||
CurrentStatus: workOrderStatusProcessing,
|
||||
UpdatedBy: updatedBy,
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "转单失败")
|
||||
}
|
||||
|
||||
// 4. 记录操作日志
|
||||
operationType := workOrderOperationAssign
|
||||
beforeStatus := current.CurrentStatus
|
||||
afterStatus := workOrderStatusProcessing
|
||||
return service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
|
||||
WoNo: in.WoNo,
|
||||
OperationType: operationType,
|
||||
OperationDeptId: topDept.DeptId,
|
||||
OperationUserId: topDept.Leader,
|
||||
TargetDeptId: topDept.DeptId,
|
||||
BeforeStatus: beforeStatus,
|
||||
AfterStatus: afterStatus,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.notifyWorkOrderChanged(ctx, in.WoNo)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解决
|
||||
func (s *sCommonTWoMain) Resolve(ctx context.Context, in *model.TWoMainResolveParam) (err error) {
|
||||
if in == nil {
|
||||
return gerror.New("invalid request")
|
||||
}
|
||||
var resolvedAt *gtime.Time
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
current, err := getWorkOrder(ctx, in.WoNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current.CurrentStatus != workOrderStatusAssigned && current.CurrentStatus != workOrderStatusProcessing {
|
||||
return gerror.New("工单状态不允许此操作")
|
||||
}
|
||||
if current.CreatorUserId == contexts.GetUserId(ctx) && !contexts.IsSuperAdmin(ctx) {
|
||||
return gerror.New("请由处理人员执行此操作")
|
||||
}
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
creatorDept := contexts.GetDeptId(ctx)
|
||||
now := gtime.Now()
|
||||
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().TenantId, current.TenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).
|
||||
Where(dao.TWoMain.Columns().WoNo, in.WoNo).
|
||||
Data(do.TWoMain{
|
||||
CurrentStatus: workOrderStatusResolved,
|
||||
ResolveTime: now,
|
||||
HandlerDeptId: creatorDept,
|
||||
HandlerUserId: updatedBy,
|
||||
UpdatedBy: updatedBy,
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "operation failed")
|
||||
}
|
||||
if err = service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
|
||||
WoNo: in.WoNo,
|
||||
OperationType: workOrderOperationFinish,
|
||||
BeforeStatus: current.CurrentStatus,
|
||||
AfterStatus: workOrderStatusResolved,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = saveWorkOrderNotice(ctx, tx, ¤t, updatedBy, "工单待确认解决", "问题已标记解决,请在72小时内确认或反馈未解决。"); err != nil {
|
||||
return err
|
||||
}
|
||||
resolvedAt = now
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resolvedAt != nil {
|
||||
s.publishAutoClose(ctx, in.WoNo, resolvedAt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sCommonTWoMain) publishAutoClose(ctx context.Context, woNo string, resolvedAt *gtime.Time) {
|
||||
if !rabbitmq.Enabled(ctx) {
|
||||
g.Log().Errorf(ctx, "work order auto close skipped, rabbitmq disabled, woNo:%s", woNo)
|
||||
return
|
||||
}
|
||||
message := model.WorkOrderAutoCloseMessage{
|
||||
WoNo: woNo,
|
||||
ResolveTimeTs: resolvedAt.Timestamp(),
|
||||
}
|
||||
if err := rabbitmq.PublishDelayed(ctx, consts.QueueWorkOrderAutoCloseDelay, consts.QueueWorkOrderAutoClose, workOrderAutoCloseDelay, message); err != nil {
|
||||
g.Log().Errorf(ctx, "publish work order auto close failed, woNo:%s, err:%+v", woNo, err)
|
||||
return
|
||||
}
|
||||
g.Log().Infof(ctx, "work order auto close published, woNo:%s, delay:%s", woNo, workOrderAutoCloseDelay)
|
||||
}
|
||||
|
||||
func (s *sCommonTWoMain) 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 != workOrderStatusResolved {
|
||||
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, workOrderStatusResolved).
|
||||
Data(do.TWoMain{
|
||||
CurrentStatus: workOrderStatusClosed,
|
||||
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: workOrderOperationClose,
|
||||
OperationDeptId: current.HandlerDeptId,
|
||||
OperationUserId: operatorID,
|
||||
BeforeStatus: workOrderStatusResolved,
|
||||
AfterStatus: workOrderStatusClosed,
|
||||
TenantId: current.TenantId,
|
||||
CreatedDept: current.CreatedDept,
|
||||
CreatedBy: operatorID,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "保存自动完结记录失败")
|
||||
}
|
||||
return saveWorkOrderNotice(ctx, tx, ¤t, 0, "工单已自动完结", "问题标记解决超过72小时,工单已自动完结。")
|
||||
})
|
||||
}
|
||||
|
||||
// 关单
|
||||
func (s *sCommonTWoMain) Close(ctx context.Context, in *model.TWoMainCloseParam) (err error) {
|
||||
if in == nil {
|
||||
return gerror.New("invalid request")
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
current, err := getWorkOrder(ctx, in.WoNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current.CurrentStatus != workOrderStatusResolved {
|
||||
return gerror.New("工单状态不允许此操作")
|
||||
}
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
now := gtime.Now()
|
||||
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().TenantId, current.TenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).
|
||||
Where(dao.TWoMain.Columns().WoNo, in.WoNo).
|
||||
Data(do.TWoMain{
|
||||
CurrentStatus: workOrderStatusClosed,
|
||||
CloseTime: now,
|
||||
UpdatedBy: updatedBy,
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "operation failed")
|
||||
}
|
||||
return service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
|
||||
WoNo: in.WoNo,
|
||||
OperationType: workOrderOperationClose,
|
||||
BeforeStatus: current.CurrentStatus,
|
||||
AfterStatus: workOrderStatusClosed,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 取消
|
||||
func (s *sCommonTWoMain) Cancel(ctx context.Context, in *model.TWoMainCancelParam) (err error) {
|
||||
if in == nil {
|
||||
return gerror.New("invalid request")
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
current, err := getWorkOrder(ctx, in.WoNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current.CurrentStatus != workOrderStatusAssigned && current.CurrentStatus != workOrderStatusProcessing {
|
||||
return gerror.New("工单状态不允许此操作")
|
||||
}
|
||||
if current.CreatorUserId != contexts.GetUserId(ctx) && !contexts.IsSuperAdmin(ctx) {
|
||||
return gerror.New("只有创建人可以取消工单")
|
||||
}
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
now := gtime.Now()
|
||||
if _, err = dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().TenantId, current.TenantId).Where(dao.TWoMain.Columns().CurrentStatus, current.CurrentStatus).
|
||||
Where(dao.TWoMain.Columns().WoNo, in.WoNo).
|
||||
Data(do.TWoMain{
|
||||
CurrentStatus: workOrderStatusCanceled,
|
||||
CloseTime: now,
|
||||
UpdatedBy: updatedBy,
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "operation failed")
|
||||
}
|
||||
return service.CommonTWoRecord().AddOperation(ctx, &model.TWoRecordAddOperationParam{
|
||||
WoNo: in.WoNo,
|
||||
OperationType: workOrderOperationCancel,
|
||||
BeforeStatus: current.CurrentStatus,
|
||||
AfterStatus: workOrderStatusCanceled,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 评价
|
||||
func (s *sCommonTWoMain) Evaluate(ctx context.Context, in *model.TWoMainEvaluateParam) error {
|
||||
if in == nil || in.SatisfactionLevel < 1 || in.SatisfactionLevel > 5 {
|
||||
return gerror.New("请选择1至5星评价")
|
||||
}
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
current, err := getWorkOrder(ctx, in.WoNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userID := contexts.GetUserId(ctx)
|
||||
if current.CreatorUserId != userID || current.CurrentStatus != workOrderStatusClosed || current.EvalTime != nil || current.SatisfactionLevel != 0 {
|
||||
return gerror.New("只有创建人可以对已完结工单评价一次")
|
||||
}
|
||||
result, err := dao.TWoMain.Ctx(ctx).TX(tx).Where(dao.TWoMain.Columns().Id, current.Id).Where(dao.TWoMain.Columns().TenantId, current.TenantId).WhereNull(dao.TWoMain.Columns().EvalTime).Data(do.TWoMain{SatisfactionLevel: in.SatisfactionLevel, EvalContent: in.EvalContent, EvalTime: gtime.Now(), UpdatedBy: userID}).Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "保存评价失败")
|
||||
}
|
||||
count, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count != 1 {
|
||||
return gerror.New("工单已评价,请刷新")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sCommonTWoMain) Delete(ctx context.Context, in *model.TWoMainDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "get work order list failed")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View returns a work order main record.
|
||||
func (s *sCommonTWoMain) View(ctx context.Context, in *model.TWoMainViewParam) (res *model.TWoMainViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "get work order list failed")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type sCommonTWoRecord struct{}
|
||||
|
||||
func NewCommonTWoRecord() *sCommonTWoRecord {
|
||||
return &sCommonTWoRecord{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterCommonTWoRecord(NewCommonTWoRecord())
|
||||
}
|
||||
|
||||
// Model 工单记录表ORM模型
|
||||
func (s *sCommonTWoRecord) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TWoRecord.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取工单记录表列表
|
||||
func (s *sCommonTWoRecord) List(ctx context.Context, in *model.TWoRecordListParam) (list []*model.TWoRecordListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
mod = mod.Fields(model.TWoRecordListModel{})
|
||||
|
||||
// 直接使用 dao 创建 model,避免自动 handler 对关联表添加软删除条件
|
||||
mod = s.Model(ctx)
|
||||
|
||||
// 手动添加租户过滤
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
if tenantId != "" {
|
||||
mod = mod.Where(dao.TWoRecord.Columns().TenantId, tenantId)
|
||||
}
|
||||
|
||||
cardTable := dao.TWoRecord.Table()
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.FieldsPrefix(cardTable, model.TWoRecordListModel{})
|
||||
mod = mod.Fields("op_dept.dept_name AS operation_dept_name")
|
||||
mod = mod.Fields("op_user.user_name AS operation_user_name")
|
||||
mod = mod.Fields("t_dept.dept_name AS target_dept_name")
|
||||
mod = mod.Fields("t_user.user_name AS target_user_name")
|
||||
|
||||
// 左关联部门表获取操作机构名称
|
||||
mod = mod.LeftJoin("sys_dept AS op_dept ON op_dept.dept_id = " + cardTable + ".operation_dept_id")
|
||||
|
||||
// 左关联用户表获取操作用户名称
|
||||
mod = mod.LeftJoin("sys_user AS op_user ON op_user.user_id = " + cardTable + ".operation_user_id")
|
||||
|
||||
// 左关联部门表获取目标部门名称
|
||||
mod = mod.LeftJoin("sys_dept AS t_dept ON t_dept.dept_id = " + cardTable + ".target_dept_id")
|
||||
|
||||
// 左关联用户表获取目标用户名称
|
||||
mod = mod.LeftJoin("sys_user AS t_user ON t_user.user_id = " + cardTable + ".target_user_id")
|
||||
|
||||
// 查询主键ID
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TWoRecord.Columns().Id, in.Id)
|
||||
}
|
||||
|
||||
// 查询变更前状态
|
||||
if in.BeforeStatus > 0 {
|
||||
mod = mod.Where(dao.TWoRecord.Columns().BeforeStatus, in.BeforeStatus)
|
||||
}
|
||||
|
||||
// 查询变更后状态
|
||||
if in.AfterStatus > 0 {
|
||||
mod = mod.Where(dao.TWoRecord.Columns().AfterStatus, in.AfterStatus)
|
||||
}
|
||||
|
||||
// 查询工单编号
|
||||
if in.WoNo != "" {
|
||||
mod = mod.Where(dao.TWoRecord.Columns().WoNo, in.WoNo)
|
||||
}
|
||||
|
||||
// 查询操作类型
|
||||
if in.OperationType > 0 {
|
||||
mod = mod.Where(dao.TWoRecord.Columns().OperationType, in.OperationType)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TWoRecord.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TWoRecord.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取工单记录表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出工单记录(异步:提交任务后到下载中心取文件)
|
||||
func (s *sCommonTWoRecord) Export(ctx context.Context, in *model.TWoRecordListParam) (err error) {
|
||||
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
|
||||
BizKey: "tWoRecord",
|
||||
BizName: "工单记录",
|
||||
PermCode: "cpm:gen:tWoRecord:export",
|
||||
QueryParam: in,
|
||||
Tags: func() ([]string, error) {
|
||||
return convert.GetEntityDescTags(model.TWoRecordListModel{})
|
||||
},
|
||||
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
|
||||
query := *in
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
list, count, err := s.List(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows = make([]interface{}, 0, len(list))
|
||||
for _, item := range list {
|
||||
rows = append(rows, item)
|
||||
}
|
||||
return rows, count, nil
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Edit 修改/新增工单记录表
|
||||
func (s *sCommonTWoRecord) Edit(ctx context.Context, in *model.TWoRecordEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(model.TWoRecordUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改工单记录表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增 创建工单后 新增工单记录
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
in.CreatedAt = gtime.Now()
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(model.TWoRecordInsertFields{}).
|
||||
Data(in).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增工单记录表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// 添加工单记录表
|
||||
func (s *sCommonTWoRecord) AddOperation(ctx context.Context, in *model.TWoRecordAddOperationParam) (err error) {
|
||||
operationDeptId := contexts.GetDeptId(ctx)
|
||||
operationUserId := contexts.GetUserId(ctx)
|
||||
param := &model.TWoRecordEditParam{
|
||||
WoNo: &in.WoNo,
|
||||
OperationType: &in.OperationType,
|
||||
OperationDeptId: &operationDeptId,
|
||||
OperationUserId: &operationUserId,
|
||||
BeforeStatus: &in.BeforeStatus,
|
||||
AfterStatus: &in.AfterStatus,
|
||||
}
|
||||
if in.TargetDeptId > 0 {
|
||||
param.TargetDeptId = &in.TargetDeptId
|
||||
param.TargetUserId = &in.TargetUserId
|
||||
}
|
||||
return s.Edit(ctx, param)
|
||||
}
|
||||
|
||||
func (s *sCommonTWoRecord) Delete(ctx context.Context, in *model.TWoRecordDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除工单记录表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取工单记录表指定信息
|
||||
func (s *sCommonTWoRecord) View(ctx context.Context, in *model.TWoRecordViewParam) (res *model.TWoRecordViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取工单记录表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWorkOrderAccess(t *testing.T) {
|
||||
ctx := setupBatchExecutionLogListDB(t)
|
||||
if _, err := g.DB().Exec(ctx, "CREATE TABLE t_wo_main (id INTEGER, wo_no TEXT, wo_type TEXT, related_id TEXT, title TEXT, content TEXT, attachment TEXT, creator_type TEXT, creator_dept_id TEXT, creator_user_id INTEGER, creator_name TEXT, creator_phone TEXT, assign_dept_id TEXT, assign_user_id TEXT, current_status TEXT, handler_dept_id INTEGER, handler_user_id INTEGER, first_response_time TEXT, resolve_time TEXT, close_time TEXT, satisfaction_level TEXT, eval_content TEXT, eval_time TEXT, remark TEXT, tenant_id TEXT, created_dept TEXT, created_by TEXT, created_at TEXT, updated_by TEXT, updated_at TEXT)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "INSERT INTO t_wo_main (id,wo_no,tenant_id,creator_user_id,handler_user_id,handler_dept_id) VALUES (1, 'WO', 'A', 10, 20, 30)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, item := range []struct {
|
||||
name, tenant string
|
||||
user, dept int64
|
||||
allowed bool
|
||||
}{
|
||||
{"creator", "A", 10, 40, true}, {"handler", "A", 20, 40, true}, {"handler department", "A", 21, 30, true}, {"unrelated", "A", 11, 40, false}, {"other tenant", "B", 10, 30, false}, {"anonymous", "A", 0, 30, false},
|
||||
} {
|
||||
t.Run(item.name, func(t *testing.T) {
|
||||
scoped := contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: item.user, DeptId: item.dept, TenantId: item.tenant}}})
|
||||
_, err := getWorkOrder(scoped, "WO")
|
||||
if (err == nil) != item.allowed {
|
||||
t.Fatalf("allowed=%v error=%v", item.allowed, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("状态记录", func(t *testing.T) {
|
||||
testWorkOrderRecordStatuses(t, ctx)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package common
|
||||
|
||||
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-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// saveWorkOrderNotice 与工单操作使用同一事务,通知失败时回滚,避免遗漏。
|
||||
func saveWorkOrderNotice(ctx context.Context, tx gdb.TX, current *entity.TWoMain, operator int64, title, content string) error {
|
||||
if current.CreatorUserId <= 0 || current.CreatorUserId == operator {
|
||||
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: title, MessageContent: content, MessageLink: "/pages/business/views/workOrderDetail/index?woNo=" + url.QueryEscape(current.WoNo), SendTime: gtime.Now(), IsRecall: 0, TenantId: current.TenantId, CreatedDept: current.CreatorDeptId, CreatedBy: operator}).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: operator}).Insert()
|
||||
return gerror.Wrap(err, "保存工单用户通知失败")
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func testWorkOrderRecordStatuses(t *testing.T, ctx context.Context) {
|
||||
t.Helper()
|
||||
for _, statement := range []string{
|
||||
"CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, parent_id INTEGER, leader TEXT, tenant_id TEXT)",
|
||||
"CREATE TABLE t_wo_record (id INTEGER PRIMARY KEY, wo_no TEXT, operation_type INTEGER, operation_dept_id INTEGER, operation_user_id INTEGER, target_dept_id INTEGER, target_user_id INTEGER, before_status INTEGER, after_status INTEGER, tenant_id TEXT, created_dept INTEGER, created_by INTEGER, created_at TEXT, updated_by INTEGER, updated_at TEXT)",
|
||||
} {
|
||||
if _, err := g.DB().Exec(ctx, statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx = contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 7, DeptId: 10, TenantId: "A"}}})
|
||||
if _, err := dao.SysDept.Ctx(ctx).Data(do.SysDept{DeptId: 10, ParentId: 0, Leader: "7", TenantId: "A"}).Insert(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("创建记录与主表状态一致", func(t *testing.T) {
|
||||
title := "工单状态回归"
|
||||
input := &model.TWoMainEditParam{Title: &title}
|
||||
if err := NewCommonTWoMain().Edit(ctx, input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var main entity.TWoMain
|
||||
if err := dao.TWoMain.Ctx(ctx).Where(dao.TWoMain.Columns().WoNo, *input.WoNo).Scan(&main); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var record entity.TWoRecord
|
||||
if err := dao.TWoRecord.Ctx(ctx).Where(dao.TWoRecord.Columns().WoNo, *input.WoNo).Scan(&record); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if main.CurrentStatus != workOrderStatusProcessing || record.AfterStatus != main.CurrentStatus || record.BeforeStatus != 0 || record.OperationType != workOrderOperationCreate {
|
||||
t.Fatalf("主表与创建记录状态不一致: main=%+v record=%+v", main, record)
|
||||
}
|
||||
if record.OperationUserId != 7 || record.OperationDeptId != 10 || record.TenantId != "A" {
|
||||
t.Fatalf("操作人或租户信息错误: %+v", record)
|
||||
}
|
||||
missing, err := dao.TWoRecord.Ctx(ctx).Where(dao.TWoRecord.Columns().Id, record.Id).WhereNull(dao.TWoRecord.Columns().BeforeStatus).Count()
|
||||
if err != nil || missing != 0 {
|
||||
t.Fatalf("创建前状态应显式写入0: missing=%d err=%v", missing, err)
|
||||
}
|
||||
})
|
||||
|
||||
for _, item := range []struct {
|
||||
name string
|
||||
operation, before, after int
|
||||
}{
|
||||
{"分派", workOrderOperationAssign, workOrderStatusAssigned, workOrderStatusProcessing},
|
||||
{"处理", workOrderOperationProcess, workOrderStatusResolved, workOrderStatusProcessing},
|
||||
{"完成", workOrderOperationFinish, workOrderStatusProcessing, workOrderStatusResolved},
|
||||
{"关单", workOrderOperationClose, workOrderStatusResolved, workOrderStatusClosed},
|
||||
{"取消", workOrderOperationCancel, workOrderStatusProcessing, workOrderStatusCanceled},
|
||||
} {
|
||||
t.Run(item.name, func(t *testing.T) {
|
||||
input := &model.TWoRecordAddOperationParam{WoNo: item.name, OperationType: item.operation, BeforeStatus: item.before, AfterStatus: item.after}
|
||||
if err := NewCommonTWoRecord().AddOperation(ctx, input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var record entity.TWoRecord
|
||||
if err := dao.TWoRecord.Ctx(ctx).Where(dao.TWoRecord.Columns().WoNo, item.name).Scan(&record); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if record.OperationType != item.operation || record.BeforeStatus != item.before || record.AfterStatus != item.after {
|
||||
t.Fatalf("操作记录状态错误: %+v", record)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/genin"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
|
||||
|
||||
"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/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sGenTestDemo struct{}
|
||||
|
||||
func NewGenTestDemo() *sGenTestDemo {
|
||||
return &sGenTestDemo{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterGenTestDemo(NewGenTestDemo())
|
||||
}
|
||||
|
||||
// Model 测试单表ORM模型
|
||||
func (s *sGenTestDemo) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.TestDemo.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取测试单表列表
|
||||
func (s *sGenTestDemo) List(ctx context.Context, in *genin.TestDemoListParam) (list []*genin.TestDemoListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.FieldsPrefix(dao.TestDemo.Table(), genin.TestDemoListModel{})
|
||||
mod = mod.Fields(xgorm.JoinFields(ctx, genin.TestDemoListModel{}, &dao.SysDept, "dept"))
|
||||
|
||||
// 关联表字段
|
||||
mod = mod.LeftJoinOnFields(dao.SysDept.Table(), dao.TestDemo.Columns().DeptId, "=", dao.SysDept.Columns().DeptId)
|
||||
|
||||
// 查询主键
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TestDemo.Columns().Id, in.Id)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TestDemo.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 查询部门名称
|
||||
if in.DeptDeptName != "" {
|
||||
mod = mod.WherePrefixLike(dao.SysDept.Table(), dao.SysDept.Columns().DeptName, in.DeptDeptName)
|
||||
}
|
||||
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TestDemo.Table() + "." + dao.TestDemo.Columns().Id)
|
||||
|
||||
// 操作人摘要信息
|
||||
mod = mod.Hook(hook.MemberSummary)
|
||||
|
||||
// 查询数据
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取测试单表列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出测试单表
|
||||
func (s *sGenTestDemo) Export(ctx context.Context, in *genin.TestDemoListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(genin.TestDemoExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出测试单表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
|
||||
exports []genin.TestDemoExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增测试单表
|
||||
func (s *sGenTestDemo) Edit(ctx context.Context, in *genin.TestDemoEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
in.UpdatedAt = gtime.Now()
|
||||
in.UpdatedBy = contexts.GetUserId(ctx)
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(genin.TestDemoUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改测试单表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
in.TenantId = contexts.GetTenantId(ctx)
|
||||
in.CreatedDept = contexts.GetDeptId(ctx)
|
||||
in.CreatedAt = gtime.Now()
|
||||
in.CreatedBy = contexts.GetUserId(ctx)
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(genin.TestDemoInsertFields{}).
|
||||
Data(in).OmitEmptyData().Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增测试单表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除测试单表
|
||||
func (s *sGenTestDemo) Delete(ctx context.Context, in *genin.TestDemoDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
|
||||
dao.TestDemo.Columns().DeletedBy: contexts.GetUserId(ctx),
|
||||
dao.TestDemo.Columns().DeletedAt: gtime.Now(),
|
||||
}).Unscoped().Update(); err != nil {
|
||||
err = gerror.Wrap(err, "删除测试单表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取测试单表指定信息
|
||||
func (s *sGenTestDemo) View(ctx context.Context, in *genin.TestDemoViewParam) (res *genin.TestDemoViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取测试单表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// package gen_codes
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package gen_codes
|
||||
@@ -0,0 +1,218 @@
|
||||
// package gen_codes
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package gen_codes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgen"
|
||||
genconsts "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgen/gen_consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
"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/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysGenTable struct {
|
||||
}
|
||||
|
||||
func SysGenTableNew() *sSysGenTable {
|
||||
return &sSysGenTable{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysGenTable(SysGenTableNew())
|
||||
}
|
||||
func (l *sSysGenTable) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: false,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.SysGenTable.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// 列表
|
||||
func (s *sSysGenTable) List(ctx context.Context, param *model.SysGenTableListParam) (output []*model.SysGenTableListModel, total int, err error) {
|
||||
m := s.Model(ctx)
|
||||
if param.GenType != 0 {
|
||||
m = m.Where(dao.SysGenTable.Columns().GenType, param.GenType)
|
||||
}
|
||||
if param.VarName != "" {
|
||||
m = m.Where(dao.SysGenTable.Columns().VarName, param.VarName)
|
||||
}
|
||||
if param.Status != "" {
|
||||
m = m.Where(dao.SysGenTable.Columns().Status, param.Status)
|
||||
}
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output = make([]*model.SysGenTableListModel, 0)
|
||||
err = m.Page(param.Page, param.PageSize).OrderDesc(dao.SysGenTable.Columns().TableId).Scan(&output)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 详情
|
||||
func (s *sSysGenTable) View(ctx context.Context, param *model.SysGenTableViewParam) (output *model.SysGenTableViewModel, err error) {
|
||||
if param.TableId == 0 {
|
||||
err = gerror.New("参数错误")
|
||||
return
|
||||
}
|
||||
m := s.Model(ctx)
|
||||
err = m.Where(dao.SysGenTable.Columns().TableId, param.TableId).Scan(&output)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
func (s *sSysGenTable) Add(ctx context.Context, param *model.SysGenTableAddParam) (output *model.SysGenTableAddModel, err error) {
|
||||
data := do.SysGenTable{}
|
||||
gconv.Struct(param, &data)
|
||||
data.Options = "{}"
|
||||
data.MasterColumns = "[]"
|
||||
data.Status = "1"
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.CreatedAt = gtime.Now()
|
||||
m := s.Model(ctx)
|
||||
id, err := m.Data(data).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output = &model.SysGenTableAddModel{
|
||||
TableId: id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 修改
|
||||
func (s *sSysGenTable) Edit(ctx context.Context, param *model.SysGenTableEditParam) (output *model.SysGenTableEditModel, err error) {
|
||||
if param == nil || param.TableId == 0 {
|
||||
err = gerror.New("参数错误")
|
||||
return
|
||||
}
|
||||
data := do.SysGenTable{}
|
||||
gconv.Struct(param, &data)
|
||||
if param.Options != nil {
|
||||
data.Options = gjson.New(*param.Options).MustToJsonString()
|
||||
}
|
||||
if param.MasterColumns != nil {
|
||||
data.MasterColumns = gjson.New(*param.MasterColumns).MustToJsonString()
|
||||
}
|
||||
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
m := s.Model(ctx)
|
||||
_, err = m.Data(data).OmitNil().Where(dao.SysGenTable.Columns().TableId, param.TableId).Update()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output = &model.SysGenTableEditModel{
|
||||
TableId: param.TableId,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysGenTable) Delete(ctx context.Context, param *model.SysGenTableDeleteParam) (output *model.SysGenTableDeleteModel, err error) {
|
||||
if len(param.TableIds) == 0 {
|
||||
err = gerror.New("参数错误")
|
||||
return
|
||||
}
|
||||
m := s.Model(ctx)
|
||||
_, err = m.WhereIn(dao.SysGenTable.Columns().TableId, param.TableIds).Delete()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output = &model.SysGenTableDeleteModel{
|
||||
TableIds: param.TableIds,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取选择项
|
||||
func (s *sSysGenTable) Selects(ctx context.Context) (output *model.SelectsModel, err error) {
|
||||
output, err = xgen.GenCodesSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取表选择项
|
||||
func (s *sSysGenTable) TableSelect(ctx context.Context, param *model.GenCodesTableSelectParam) (output []*model.GenCodesTableSelectModel, err error) {
|
||||
output, err = xgen.GenCodesTableSelect(ctx, param)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取字段选择项
|
||||
func (s *sSysGenTable) ColumnSelect(ctx context.Context, param *model.GenCodesColumnSelectParam) (output []*model.GenCodesColumnSelectModel, err error) {
|
||||
output, err = xgen.GenCodesColumnSelect(ctx, param)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取字段列表
|
||||
func (s *sSysGenTable) ColumnList(ctx context.Context, param *model.GenCodesColumnListParam) (output []*model.GenCodesColumnListModel, err error) {
|
||||
output, err = xgen.GenCodesColumnList(ctx, param)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 预览
|
||||
func (s *sSysGenTable) Preview(ctx context.Context, param *model.GenCodesPreviewParam) (output *model.GenCodesPreviewModel, err error) {
|
||||
output, err = xgen.GenCodesPreview(ctx, param)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 构建
|
||||
func (s *sSysGenTable) Build(ctx context.Context, param *model.GenCodesBuildParam) (output *model.GenCodesBuildModel, err error) {
|
||||
editParam := &model.SysGenTableEditParam{}
|
||||
gconv.Struct(param, editParam)
|
||||
status := genconsts.GenCodesStatusOk
|
||||
editParam.Status = &status
|
||||
_, err = s.Edit(ctx, editParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output, err = xgen.GenCodesBuild(ctx, param)
|
||||
if err != nil {
|
||||
status = genconsts.GenCodesStatusFail
|
||||
editParam.Status = &status
|
||||
_, err = s.Edit(ctx, &model.SysGenTableEditParam{
|
||||
TableId: editParam.TableId,
|
||||
Status: &status,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sHome struct{}
|
||||
|
||||
func NewHome() *sHome {
|
||||
return &sHome{}
|
||||
}
|
||||
func init() {
|
||||
service.RegisterHome(NewHome())
|
||||
}
|
||||
|
||||
func (s *sHome) GetCardStatistics(ctx context.Context) (stats *model.CardStatistics, err error) {
|
||||
stats = new(model.CardStatistics)
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
table := dao.TSimCardInfo.Table()
|
||||
|
||||
// 1. TotalCount - 卡总数
|
||||
totalCount, err := g.DB().Model(table).Ctx(ctx).Where("dept_id", deptId).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TotalCount = int64(totalCount)
|
||||
|
||||
// 正确的日期处理:先转为本月1号,再加1个月,彻底规避月底溢出 Bug
|
||||
now := gtime.Now()
|
||||
monthStartStr := now.Format("Y-m-01 00:00:00")
|
||||
monthEndStr := now.AddDate(0, 1, 0).Format("Y-m-01 00:00:00")
|
||||
|
||||
// 2. MonthlyNewCount - 本月新增
|
||||
monthlyNewCount, err := g.DB().Model(table).Ctx(ctx).
|
||||
Where("dept_id", deptId).
|
||||
Where("boss_activate_at >= ?", monthStartStr).
|
||||
Where("boss_activate_at < ?", monthEndStr).
|
||||
Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MonthlyNewCount = int64(monthlyNewCount)
|
||||
|
||||
// 3. MonthlyFlowUsage - 本月流量使用
|
||||
var flowResult struct {
|
||||
Total float64
|
||||
}
|
||||
err = g.DB().Model(table).Ctx(ctx).
|
||||
Fields("IFNULL(SUM(used_flow_mb), 0) as total").
|
||||
Where("dept_id", deptId).
|
||||
Scan(&flowResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MonthlyFlowUsage = int64(flowResult.Total)
|
||||
|
||||
// 4. MonthlyActiveCount - 本月活跃卡数
|
||||
monthlyActiveCount, err := g.DB().Model(table).Ctx(ctx).
|
||||
Where("dept_id", deptId).
|
||||
Where("used_flow_mb > 0").
|
||||
Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MonthlyActiveCount = int64(monthlyActiveCount)
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetCardStatusStatistics 获取卡状态统计
|
||||
func (s *sHome) GetCardStatusStatistics(ctx context.Context) (stats *model.CardStatusStatistics, err error) {
|
||||
stats = new(model.CardStatusStatistics)
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
table := dao.TSimCardInfo.Table()
|
||||
|
||||
// 卡状态统计
|
||||
// 待激活(1)
|
||||
pendingActivationCount, _ := g.DB().Model(table).Ctx(ctx).Where("dept_id", deptId).Where("boss_card_status", 1).Count()
|
||||
stats.PendingActivationCount = int64(pendingActivationCount)
|
||||
|
||||
// 已激活(2)
|
||||
activatedCount, _ := g.DB().Model(table).Ctx(ctx).Where("dept_id", deptId).Where("boss_card_status", 2).Count()
|
||||
stats.ActivatedCount = int64(activatedCount)
|
||||
|
||||
// 停机(4)
|
||||
stoppedCount, _ := g.DB().Model(table).Ctx(ctx).Where("dept_id", deptId).Where("boss_card_status", 4).Count()
|
||||
stats.StoppedCount = int64(stoppedCount)
|
||||
|
||||
// 可测试(6)
|
||||
testableCount, _ := g.DB().Model(table).Ctx(ctx).Where("dept_id", deptId).Where("boss_card_status", 6).Count()
|
||||
stats.TestableCount = int64(testableCount)
|
||||
|
||||
// 库存(7)
|
||||
inventoryCount, _ := g.DB().Model(table).Ctx(ctx).Where("dept_id", deptId).Where("boss_card_status", 7).Count()
|
||||
stats.InventoryCount = int64(inventoryCount)
|
||||
|
||||
// 预销户(8)
|
||||
preCancelCount, _ := g.DB().Model(table).Ctx(ctx).Where("dept_id", deptId).Where("boss_card_status", 8).Count()
|
||||
stats.PreCancelCount = int64(preCancelCount)
|
||||
|
||||
// 已销户(9)
|
||||
canceledCount, _ := g.DB().Model(table).Ctx(ctx).Where("dept_id", deptId).Where("boss_card_status", 9).Count()
|
||||
stats.CanceledCount = int64(canceledCount)
|
||||
|
||||
// 未知(0)
|
||||
unknownCount, _ := g.DB().Model(table).Ctx(ctx).Where("dept_id", deptId).Where("boss_card_status", 0).Count()
|
||||
stats.UnknownCount = int64(unknownCount)
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetDeptFlowUsageChart 获取部门流量使用图表数据,用于折线图展示
|
||||
// period: 7d 前7天, 1m 前6个月
|
||||
func (s *sHome) GetDeptFlowUsageChart(ctx context.Context, period string) (*model.FlowUsageChartData, error) {
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
table := dao.TDeptFlowDaily.Table()
|
||||
|
||||
// 使用标准库 time 获取当前时间
|
||||
now := time.Now()
|
||||
|
||||
var (
|
||||
startDateStr string
|
||||
endDateStr string
|
||||
groupBy string // SQL 分组字段
|
||||
labels = make([]string, 0)
|
||||
values = make([]int64, 0)
|
||||
)
|
||||
|
||||
switch period {
|
||||
case "7d":
|
||||
// 前7天:包含今天在内的过去7天
|
||||
startDateStr = now.AddDate(0, 0, -6).Format("2006-01-02")
|
||||
endDateStr = now.Format("2006-01-02")
|
||||
groupBy = "stat_date" // 按天分组
|
||||
|
||||
case "1m":
|
||||
// 前6个月:从6个月前的1号开始,方便按月对齐
|
||||
startDateStr = now.AddDate(0, -5, 0).Format("2006-01-01")
|
||||
endDateStr = now.Format("2006-01-02")
|
||||
// 使用 MySQL 的 DATE_FORMAT 函数按月聚合
|
||||
groupBy = "DATE_FORMAT(stat_date, '%Y-%m')"
|
||||
|
||||
default:
|
||||
// 默认当月(按天)
|
||||
startDateStr = now.Format("2006-01-01")
|
||||
endDateStr = now.Format("2006-01-02")
|
||||
groupBy = "stat_date"
|
||||
}
|
||||
|
||||
// 1. 从数据库查询聚合数据
|
||||
var list []struct {
|
||||
TimeKey string `json:"time_key"`
|
||||
UsedFlowMb int64 `json:"used_flow_mb"`
|
||||
}
|
||||
|
||||
fields := fmt.Sprintf("%s as time_key, IFNULL(SUM(total_used_flow_mb), 0) as used_flow_mb", groupBy)
|
||||
|
||||
err := g.DB().Model(table).Ctx(ctx).
|
||||
Fields(fields).
|
||||
Where("dept_id", deptId).
|
||||
Where("stat_date >= ? AND stat_date <= ?", startDateStr, endDateStr).
|
||||
Group(groupBy).
|
||||
Order("time_key ASC").
|
||||
Scan(&list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 将查询结果转换为 Map,方便 O(1) 复杂度查找
|
||||
dataMap := make(map[string]int64)
|
||||
for _, item := range list {
|
||||
dataMap[item.TimeKey] = item.UsedFlowMb
|
||||
}
|
||||
|
||||
// 3. 核心:将字符串转回标准 time.Time 对象,用来做连续日期循环
|
||||
startTime, _ := time.Parse("2006-01-02", startDateStr)
|
||||
endTime, _ := time.Parse("2006-01-02", endDateStr)
|
||||
|
||||
if period == "1m" {
|
||||
// 【6个月模式】:按月循环
|
||||
for t := startTime; !t.After(endTime); t = t.AddDate(0, 1, 0) {
|
||||
// 匹配数据库返回的 key 格式 "2026-06"
|
||||
mapKey := t.Format("2006-01")
|
||||
|
||||
// 放入返回的 Labels 数组 (格式要求:2026-06)
|
||||
labels = append(labels, mapKey)
|
||||
|
||||
// 提取数据或补 0
|
||||
if val, exists := dataMap[mapKey]; exists {
|
||||
values = append(values, val)
|
||||
} else {
|
||||
values = append(values, 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 【7天/默认模式】:按天循环
|
||||
for t := startTime; !t.After(endTime); t = t.AddDate(0, 0, 1) {
|
||||
// 匹配数据库返回的 key 格式 "2026-06-25"
|
||||
mapKey := t.Format("2006-01-02")
|
||||
|
||||
// 放入返回的 Labels 数组 (格式要求:06-25)
|
||||
labels = append(labels, t.Format("01-02"))
|
||||
|
||||
// 提取数据或补 0
|
||||
if val, exists := dataMap[mapKey]; exists {
|
||||
values = append(values, val)
|
||||
} else {
|
||||
values = append(values, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &model.FlowUsageChartData{
|
||||
Labels: labels,
|
||||
Values: values,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTopUsageCardsYesterday 获取前一天流量使用前五名的卡片
|
||||
func (s *sHome) GetTopUsageCardsYesterday(ctx context.Context) ([]model.TopUsageCard, error) {
|
||||
return service.TrafficUsage().GetTopUsageCardsYesterday(ctx)
|
||||
}
|
||||
|
||||
// GetFlowPoolUsageList 获取该用户下所有流量池的流量使用情况
|
||||
func (s *sHome) GetFlowPoolUsageList(ctx context.Context) ([]model.FlowPoolUsageItem, error) {
|
||||
deptId := contexts.GetDeptId(ctx)
|
||||
|
||||
var pools []model.FlowPoolUsageItem
|
||||
err := g.DB().Model("t_sim_flow_pool").
|
||||
Fields("flow_pool_code, total_flow_mb, used_flow_mb").
|
||||
Where("dept_id", deptId).
|
||||
Where("pool_status", 0).
|
||||
Scan(&pools)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pools, nil
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package logic
|
||||
|
||||
import _ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/imeiqueue"
|
||||
@@ -0,0 +1,13 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
func TestImeiQueueRegisteredAtApplicationStartup(t *testing.T) {
|
||||
if service.ImeiQueue() == nil {
|
||||
t.Fatal("IMEI queue implementation is not registered")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package imeiqueue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const fullSyncOperation = "imei_full_sync"
|
||||
|
||||
type sImeiQueue struct{}
|
||||
|
||||
var (
|
||||
imeiNow = time.Now
|
||||
imeiRunID = uuid32.S
|
||||
imeiRabbitEnabled = rabbitmq.Enabled
|
||||
imeiPublish = rabbitmq.Publish
|
||||
)
|
||||
|
||||
func init() { service.RegisterImeiQueue(New()) }
|
||||
|
||||
func New() *sImeiQueue { return &sImeiQueue{} }
|
||||
|
||||
func (s *sImeiQueue) StartImeiRun(ctx context.Context) (string, error) {
|
||||
return s.StartImeiRunScoped(ctx, nil)
|
||||
}
|
||||
|
||||
// StartImeiRunScoped starts an IMEI run restricted to scope.Channels. A nil
|
||||
// scope, or one whose channels normalize to empty, starts a full sync.
|
||||
//
|
||||
// The runId prefix keeps IMEI runs distinguishable from status runs in logs and
|
||||
// in the Redis key space (each chain has its own prefix anyway).
|
||||
func (s *sImeiQueue) StartImeiRunScoped(ctx context.Context, scope *model.SyncScope) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
normalized, err := model.NormalizeScope(scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !imeiRabbitEnabled(ctx) {
|
||||
return "", gerror.New("imei queue start skipped: rabbitmq is disabled")
|
||||
}
|
||||
now := imeiNow()
|
||||
message := model.ImeiQueueStartMessage{
|
||||
RunID: fmt.Sprintf("imei-%s-%s", now.Format("20060102150405"), imeiRunID()),
|
||||
Operation: fullSyncOperation,
|
||||
TriggeredAt: now.Format(time.RFC3339),
|
||||
Scope: normalized,
|
||||
}
|
||||
if err := imeiPublish(ctx, consts.QueueImeiQueueStart, message); err != nil {
|
||||
return "", gerror.Wrap(err, "publish imei queue start")
|
||||
}
|
||||
g.Log().Infof(ctx, "event=imei_queue_trigger status=published runId=%s scopeChannels=%d", message.RunID, normalized.ChannelCount())
|
||||
return message.RunID, nil
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package imeiqueue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
)
|
||||
|
||||
func stubImeiTrigger(t *testing.T) {
|
||||
t.Helper()
|
||||
var (
|
||||
now = imeiNow
|
||||
runID = imeiRunID
|
||||
enabled = imeiRabbitEnabled
|
||||
publish = imeiPublish
|
||||
)
|
||||
t.Cleanup(func() {
|
||||
imeiNow, imeiRunID, imeiRabbitEnabled, imeiPublish = now, runID, enabled, publish
|
||||
})
|
||||
imeiNow = func() time.Time { return time.Date(2026, 10, 13, 4, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60)) }
|
||||
imeiRunID = func() string { return "test-run" }
|
||||
imeiRabbitEnabled = func(context.Context) bool { return true }
|
||||
}
|
||||
|
||||
func TestImeiTriggerPublishesOneRabbitMQStartWithScope(t *testing.T) {
|
||||
stubImeiTrigger(t)
|
||||
if consts.QueueImeiQueueStart != "iotlcm.imei.queue.start" {
|
||||
t.Fatal("IMEI start queue does not match Worker")
|
||||
}
|
||||
calls := 0
|
||||
var messages []model.ImeiQueueStartMessage
|
||||
imeiPublish = func(ctx context.Context, queue string, data any) error {
|
||||
calls++
|
||||
if ctx == nil || queue != consts.QueueImeiQueueStart {
|
||||
t.Errorf("queue=%q, context=%v", queue, ctx)
|
||||
}
|
||||
messages = append(messages, data.(model.ImeiQueueStartMessage))
|
||||
return nil
|
||||
}
|
||||
for _, scope := range []*model.SyncScope{nil, {Channels: []string{}}, {Channels: []string{"b", "a", "b"}, TriggerSource: model.TriggerSourceManual}} {
|
||||
runID, err := New().StartImeiRunScoped(nil, scope)
|
||||
if err != nil || runID != "imei-20261013040000-test-run" {
|
||||
t.Fatalf("run=%q, error=%v", runID, err)
|
||||
}
|
||||
}
|
||||
if calls != 3 || messages[0].Scope != nil || messages[1].Scope != nil {
|
||||
t.Fatalf("calls=%d, messages=%+v", calls, messages)
|
||||
}
|
||||
message := messages[2]
|
||||
if message.Operation != "imei_full_sync" || message.TriggeredAt != "2026-10-13T04:00:00+08:00" || !reflect.DeepEqual(message.Scope.Channels, []string{"a", "b"}) || message.Scope.TriggerSource != model.TriggerSourceManual {
|
||||
t.Fatalf("message=%+v", message)
|
||||
}
|
||||
for i, message := range messages {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.Unmarshal(data, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["runId"] != message.RunID || body["operation"] != "imei_full_sync" || body["triggeredAt"] != message.TriggeredAt {
|
||||
t.Fatalf("Worker JSON contract: %s", data)
|
||||
}
|
||||
if _, present := body["scope"]; present != (i == 2) {
|
||||
t.Fatalf("full/scoped message compatibility: %s", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImeiTriggerRejectsInvalidScopeDisabledMQAndPublishFailure(t *testing.T) {
|
||||
stubImeiTrigger(t)
|
||||
calls := 0
|
||||
publishErr := errors.New("broker unavailable")
|
||||
imeiPublish = func(context.Context, string, any) error { calls++; return publishErr }
|
||||
if _, err := New().StartImeiRunScoped(context.Background(), &model.SyncScope{Channels: []string{"bad/channel"}}); err == nil {
|
||||
t.Fatal("invalid scope accepted")
|
||||
}
|
||||
imeiRabbitEnabled = func(context.Context) bool { return false }
|
||||
if _, err := New().StartImeiRun(context.Background()); err == nil {
|
||||
t.Fatal("disabled RabbitMQ accepted")
|
||||
}
|
||||
if calls != 0 {
|
||||
t.Fatal("invalid scope or disabled RabbitMQ invoked publisher")
|
||||
}
|
||||
imeiRabbitEnabled = func(context.Context) bool { return true }
|
||||
if runID, err := New().StartImeiRun(context.Background()); runID != "" || !errors.Is(err, publishErr) {
|
||||
t.Fatalf("run=%q, error=%v", runID, err)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("failed publish retried unexpectedly: %d", calls)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const cardChannelAssocTable = "card_channel_assoc"
|
||||
|
||||
type cardChannelAssocRecord struct {
|
||||
Id int64 `orm:"id"`
|
||||
ChannelKey string `orm:"channel_id"`
|
||||
MatchType int `orm:"match_type"`
|
||||
Iccid string `orm:"iccid"`
|
||||
IccidStart string `orm:"iccid_start"`
|
||||
IccidEnd string `orm:"iccid_end"`
|
||||
Status int `orm:"status"`
|
||||
}
|
||||
|
||||
type cardChannelDisplayRecord struct {
|
||||
ChanKey string
|
||||
ChanName string
|
||||
ChanAlias string
|
||||
TempKey string
|
||||
}
|
||||
|
||||
type cardChannelLookupRecord struct {
|
||||
Id int64 `orm:"id"`
|
||||
ChanKey string `orm:"chan_key"`
|
||||
ChanName string `orm:"chan_name"`
|
||||
ChanAlias string `orm:"chan_alias"`
|
||||
TempKey string `orm:"temp_key"`
|
||||
}
|
||||
|
||||
func applyCardChannelAssocFilter(mod *gdb.Model, cardInfoTable string, chanKey string) *gdb.Model {
|
||||
if mod == nil {
|
||||
return mod
|
||||
}
|
||||
chanKey = strings.TrimSpace(chanKey)
|
||||
if chanKey == "" {
|
||||
return mod
|
||||
}
|
||||
return mod.Where(cardChannelAssocExistsSQL(cardInfoTable), chanKey)
|
||||
}
|
||||
|
||||
func cardChannelAssocExistsSQL(cardInfoTable string) string {
|
||||
cardInfoTable = strings.TrimSpace(cardInfoTable)
|
||||
if cardInfoTable == "" {
|
||||
cardInfoTable = dao.TSimCardInfo.Table()
|
||||
}
|
||||
channelTable := dao.TSimChannels.Table()
|
||||
cardColumns := dao.TSimCardInfo.Columns()
|
||||
channelColumns := dao.TSimChannels.Columns()
|
||||
return fmt.Sprintf(
|
||||
"EXISTS (SELECT 1 FROM `%s` a LEFT JOIN `%s` c ON c.`%s` = a.`channel_id` WHERE a.`status` = 1 AND c.`%s` = `%s`.`%s` AND c.`%s` = ? AND ((a.`match_type` = 1 AND a.`iccid` COLLATE utf8mb4_general_ci = `%s`.`%s`) OR (a.`match_type` = 2 AND a.`iccid_start` COLLATE utf8mb4_general_ci <= `%s`.`%s` AND a.`iccid_end` COLLATE utf8mb4_general_ci >= `%s`.`%s`)))",
|
||||
cardChannelAssocTable,
|
||||
channelTable,
|
||||
channelColumns.ChanKey,
|
||||
channelColumns.TenantId,
|
||||
cardInfoTable,
|
||||
cardColumns.TenantId,
|
||||
channelColumns.ChanKey,
|
||||
cardInfoTable,
|
||||
cardColumns.Iccid,
|
||||
cardInfoTable,
|
||||
cardColumns.Iccid,
|
||||
cardInfoTable,
|
||||
cardColumns.Iccid,
|
||||
)
|
||||
}
|
||||
|
||||
func enrichCardChannelList(ctx context.Context, list []*model.TSimCardInfoListModel) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
channelCache := make(map[string]cardChannelDisplayRecord)
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
display, ok, err := resolveCardChannelDisplayByIccid(ctx, item.Iccid, channelCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
item.ChanKey = display.ChanKey
|
||||
item.ChanName = display.ChanName
|
||||
item.ChanAlias = display.ChanAlias
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func enrichCardChannelView(ctx context.Context, item *model.TSimCardInfoViewModel) error {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
display, ok, err := resolveCardChannelDisplayByIccid(ctx, item.Iccid, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item.ChanKey = display.ChanKey
|
||||
item.ChanName = display.ChanName
|
||||
item.ChanAlias = display.ChanAlias
|
||||
return nil
|
||||
}
|
||||
|
||||
func enrichSimKeyChannelFromAssoc(ctx context.Context, simKey *model.SimKey) error {
|
||||
if simKey == nil {
|
||||
return nil
|
||||
}
|
||||
display, ok, err := resolveCardChannelDisplayByIccid(ctx, simKey.ICCID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok && strings.TrimSpace(display.ChanKey) != "" {
|
||||
simKey.ChanKey = display.ChanKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveCardChannelDisplayByIccid(ctx context.Context, iccid string, channelCache map[string]cardChannelDisplayRecord) (cardChannelDisplayRecord, bool, error) {
|
||||
assoc, err := resolveCardChannelAssocByIccid(ctx, iccid)
|
||||
if err != nil || assoc == nil {
|
||||
return cardChannelDisplayRecord{}, false, err
|
||||
}
|
||||
return resolveCardChannelDisplayByAssoc(ctx, assoc, channelCache)
|
||||
}
|
||||
|
||||
func resolveCardChannelDisplayByAssoc(ctx context.Context, assoc *cardChannelAssocRecord, channelCache map[string]cardChannelDisplayRecord) (cardChannelDisplayRecord, bool, error) {
|
||||
if assoc == nil {
|
||||
return cardChannelDisplayRecord{}, false, nil
|
||||
}
|
||||
return resolveCardChannelDisplayByChannelKey(ctx, assoc.ChannelKey, channelCache)
|
||||
}
|
||||
|
||||
func resolveCardChannelAssocByIccid(ctx context.Context, iccid string) (*cardChannelAssocRecord, error) {
|
||||
iccid = strings.TrimSpace(iccid)
|
||||
if iccid == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var exact *cardChannelAssocRecord
|
||||
if err := g.DB().Ctx(ctx).Model(cardChannelAssocTable).
|
||||
Fields("id", "channel_id", "match_type", "iccid", "iccid_start", "iccid_end", "status").
|
||||
Where("status", 1).
|
||||
Where("match_type", 1).
|
||||
Where("iccid COLLATE utf8mb4_general_ci = ?", iccid).
|
||||
OrderDesc("id").
|
||||
Limit(1).
|
||||
Scan(&exact); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询卡通道单卡关联失败")
|
||||
}
|
||||
if exact != nil && strings.TrimSpace(exact.ChannelKey) != "" {
|
||||
return exact, nil
|
||||
}
|
||||
|
||||
var ranged *cardChannelAssocRecord
|
||||
if err := g.DB().Ctx(ctx).Model(cardChannelAssocTable).
|
||||
Fields("id", "channel_id", "match_type", "iccid", "iccid_start", "iccid_end", "status").
|
||||
Where("status", 1).
|
||||
Where("match_type", 2).
|
||||
Where("iccid_start COLLATE utf8mb4_general_ci <= ? AND iccid_end COLLATE utf8mb4_general_ci >= ?", iccid, iccid).
|
||||
OrderDesc("id").
|
||||
Limit(1).
|
||||
Scan(&ranged); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询卡通道号段关联失败")
|
||||
}
|
||||
if ranged != nil && strings.TrimSpace(ranged.ChannelKey) != "" {
|
||||
return ranged, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func resolveCardChannelDisplayByChannelKey(ctx context.Context, chanKey string, channelCache map[string]cardChannelDisplayRecord) (cardChannelDisplayRecord, bool, error) {
|
||||
chanKey = strings.TrimSpace(chanKey)
|
||||
if chanKey == "" {
|
||||
return cardChannelDisplayRecord{}, false, nil
|
||||
}
|
||||
if channelCache != nil {
|
||||
if cached, ok := channelCache[chanKey]; ok {
|
||||
return cached, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
channel, err := loadCardChannelByKey(ctx, chanKey)
|
||||
if err != nil {
|
||||
return cardChannelDisplayRecord{}, false, err
|
||||
}
|
||||
if channel == nil {
|
||||
return cardChannelDisplayRecord{}, false, nil
|
||||
}
|
||||
|
||||
display := cardChannelDisplayRecord{
|
||||
ChanKey: strings.TrimSpace(channel.ChanKey),
|
||||
ChanName: strings.TrimSpace(channel.ChanName),
|
||||
ChanAlias: strings.TrimSpace(channel.ChanAlias),
|
||||
TempKey: strings.TrimSpace(channel.TempKey),
|
||||
}
|
||||
if channelCache != nil {
|
||||
channelCache[chanKey] = display
|
||||
}
|
||||
return display, true, nil
|
||||
}
|
||||
|
||||
func loadCardChannelByKey(ctx context.Context, chanKey string) (*cardChannelLookupRecord, error) {
|
||||
chanKey = strings.TrimSpace(chanKey)
|
||||
if chanKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var channel *cardChannelLookupRecord
|
||||
columns := dao.TSimChannels.Columns()
|
||||
if err := cardChannelLookupModel(ctx).
|
||||
Where(columns.ChanKey, chanKey).
|
||||
Limit(1).
|
||||
Scan(&channel); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询通道信息失败")
|
||||
}
|
||||
if channel != nil && strings.TrimSpace(channel.ChanKey) != "" {
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
channel = nil
|
||||
if err := dao.TSimChannels.Ctx(ctx).
|
||||
Fields(columns.Id, columns.ChanKey, columns.ChanName, columns.ChanAlias, columns.TempKey).
|
||||
Where(columns.ChanKey, chanKey).
|
||||
Limit(1).
|
||||
Scan(&channel); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询通道信息失败")
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func cardChannelLookupModel(ctx context.Context) *gdb.Model {
|
||||
columns := dao.TSimChannels.Columns()
|
||||
return dao.TSimChannels.Ctx(ctx).
|
||||
Fields(columns.Id, columns.ChanKey, columns.ChanName, columns.ChanAlias, columns.TempKey).
|
||||
Where(columns.TenantId, contexts.GetTenantId(ctx))
|
||||
}
|
||||
|
||||
func logCardChannelDisplayResolverError(ctx context.Context, err error) {
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "load card channel display failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type cardDeptDisplayNode struct {
|
||||
DeptID int64 `orm:"dept_id"`
|
||||
ParentID int64 `orm:"parent_id"`
|
||||
DeptName string `orm:"dept_name"`
|
||||
}
|
||||
|
||||
func newCardDeptDisplayResolver(ctx context.Context) (func(int64, string) string, error) {
|
||||
nodes, err := loadCardDeptDisplayNodes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
viewerDeptID := contexts.GetDeptId(ctx)
|
||||
return func(cardDeptID int64, fallback string) string {
|
||||
return resolveCardDisplayDeptName(nodes, viewerDeptID, cardDeptID, fallback)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadCardDeptDisplayNodes(ctx context.Context) (map[int64]cardDeptDisplayNode, error) {
|
||||
nodes := make(map[int64]cardDeptDisplayNode)
|
||||
tenantID := contexts.GetTenantId(ctx)
|
||||
if tenantID == "" {
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
var departments []*cardDeptDisplayNode
|
||||
mod := service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysDept.Table()).
|
||||
Fields(
|
||||
dao.SysDept.Columns().DeptId,
|
||||
dao.SysDept.Columns().ParentId,
|
||||
dao.SysDept.Columns().DeptName,
|
||||
).
|
||||
Where(dao.SysDept.Columns().TenantId, tenantID).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt)
|
||||
if err := mod.Scan(&departments); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, department := range departments {
|
||||
if department == nil || department.DeptID <= 0 {
|
||||
continue
|
||||
}
|
||||
nodes[department.DeptID] = *department
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func resolveCardDisplayDeptName(
|
||||
nodes map[int64]cardDeptDisplayNode,
|
||||
viewerDeptID int64,
|
||||
cardDeptID int64,
|
||||
fallback string,
|
||||
) string {
|
||||
cardDept, ok := nodes[cardDeptID]
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
if viewerDeptID <= 0 || viewerDeptID == cardDeptID {
|
||||
return cardDept.DeptName
|
||||
}
|
||||
|
||||
path := make([]int64, 0, 4)
|
||||
visited := make(map[int64]struct{})
|
||||
currentID := cardDeptID
|
||||
for currentID > 0 {
|
||||
if _, exists := visited[currentID]; exists {
|
||||
return fallback
|
||||
}
|
||||
visited[currentID] = struct{}{}
|
||||
path = append(path, currentID)
|
||||
|
||||
current, exists := nodes[currentID]
|
||||
if !exists {
|
||||
return fallback
|
||||
}
|
||||
if currentID == viewerDeptID {
|
||||
if len(path) == 1 {
|
||||
return current.DeptName
|
||||
}
|
||||
return nodes[path[len(path)-2]].DeptName
|
||||
}
|
||||
currentID = current.ParentID
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func logCardDeptDisplayResolverError(ctx context.Context, err error) {
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "load card department display nodes failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cardpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func (s *Service) UpdateCardsAfterRenew(ctx context.Context, tx gdb.TX, renew *RenewCurrentPackageOutput, userId int64) error {
|
||||
if renew == nil || len(renew.NewBindsByIccid) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := gtime.Now()
|
||||
for _, iccid := range renew.Iccids {
|
||||
card := renew.CardsByIccid[iccid]
|
||||
if card == nil {
|
||||
return gerror.Newf("ICCID卡信息不存在:%s", iccid)
|
||||
}
|
||||
bind := renew.NewBindsByIccid[iccid]
|
||||
if bind == nil {
|
||||
return gerror.Newf("ICCID续费订购记录不存在:%s", iccid)
|
||||
}
|
||||
packageStatus, _ := calcPackageStatuses(now, bind.EffectiveTime, bind.ExpectedExpireTime)
|
||||
result, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
|
||||
Where(dao.TSimCardInfo.Columns().Iccid, iccid).
|
||||
Data(do.TSimCardInfo{
|
||||
FlowPkgEndAt: bind.ExpectedExpireTime,
|
||||
PackageStatus: packageStatus,
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新续费后卡套餐到期时间失败")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取续费后卡信息更新数量失败")
|
||||
}
|
||||
if affected != 1 {
|
||||
return gerror.Newf("ICCID卡信息已变化,请刷新后重试:%s", iccid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
package cardpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/packageduration"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func (s *Service) RenewCurrentPackage(ctx context.Context, tx gdb.TX, in *RenewCurrentPackageInput) (*RenewCurrentPackageOutput, error) {
|
||||
if err := validateRenewInput(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iccids := uniqueStrings(in.Iccids)
|
||||
cardsByIccid, err := s.repo.LockCardsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定卡信息失败")
|
||||
}
|
||||
if len(cardsByIccid) != len(iccids) {
|
||||
return nil, gerror.New("部分卡信息已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
latestBindsByIccid, err := s.repo.LockLatestBindsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定最新订购失败")
|
||||
}
|
||||
if len(latestBindsByIccid) != len(iccids) {
|
||||
return nil, gerror.New("部分卡没有订购记录,不能续费")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
|
||||
currentPackageCode := ""
|
||||
customerAgentId := int64(0)
|
||||
var renewBeginAt *gtime.Time
|
||||
for _, iccid := range iccids {
|
||||
card := cardsByIccid[iccid]
|
||||
bind := latestBindsByIccid[iccid]
|
||||
if err := validateRenewCardStatus(iccid, card); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bind == nil {
|
||||
return nil, gerror.Newf("ICCID没有订购记录:%s", iccid)
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone {
|
||||
return nil, gerror.Newf("ICCID没有可续费套餐:%s", iccid)
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) {
|
||||
return nil, gerror.Newf("ICCID当前套餐与最新订购记录不一致:%s", iccid)
|
||||
}
|
||||
baseExpireAt := renewBaseExpireTime(card, bind)
|
||||
if baseExpireAt == nil {
|
||||
return nil, gerror.Newf("ICCID最新订购没有预计到期时间:%s", iccid)
|
||||
}
|
||||
periodBegin := renewPeriodBeginAt(baseExpireAt, in.PackageBeginTime)
|
||||
if renewExpireAfterAllowedWindow(baseExpireAt, now) {
|
||||
return nil, gerror.Newf("距离到期超过6个月,不能续费:%s", iccid)
|
||||
}
|
||||
if renewBeginAt == nil {
|
||||
renewBeginAt = periodBegin
|
||||
} else if !sameRenewTime(renewBeginAt, periodBegin) {
|
||||
return nil, gerror.New("当前批次下卡不是统一续费批次")
|
||||
}
|
||||
if sourceBatchNo != "" && strings.TrimSpace(card.OutboundBatchNo) != sourceBatchNo {
|
||||
return nil, gerror.Newf("ICCID不属于来源出库批次:%s", iccid)
|
||||
}
|
||||
if currentPackageCode == "" {
|
||||
currentPackageCode = strings.TrimSpace(card.CurrentPackageCode)
|
||||
}
|
||||
if currentPackageCode != strings.TrimSpace(card.CurrentPackageCode) {
|
||||
return nil, gerror.New("当前批次下存在多个套餐,不能统一续费")
|
||||
}
|
||||
if customerAgentId == 0 {
|
||||
customerAgentId = card.DeptId
|
||||
}
|
||||
if customerAgentId != card.DeptId {
|
||||
return nil, gerror.New("当前批次下存在多个客户,不能统一续费")
|
||||
}
|
||||
}
|
||||
|
||||
customer, err := s.loadTargetDept(ctx, tx, customerAgentId, in.Operator, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetPackageCode := strings.TrimSpace(in.PackageCode)
|
||||
if targetPackageCode == "" {
|
||||
targetPackageCode = currentPackageCode
|
||||
}
|
||||
currentPkg, err := s.loadPackage(ctx, tx, currentPackageCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !renewPackageAllowed(currentPkg, targetPackageCode) {
|
||||
return nil, gerror.New("续费套餐不在当前套餐可续费范围内")
|
||||
}
|
||||
pkg := currentPkg
|
||||
if targetPackageCode != currentPackageCode {
|
||||
pkg, err = s.loadPackage(ctx, tx, targetPackageCode)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
durationUnit, durationValue, durationMonths, giftDurationValue, giftDurationMonths := normalizeRenewPackageDuration(in)
|
||||
if durationMonths <= 0 {
|
||||
return nil, gerror.New("续费时长必须大于0")
|
||||
}
|
||||
amount, err := parseAmount(in.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quantity := len(iccids)
|
||||
unitPrice := 0.0
|
||||
if quantity > 0 {
|
||||
unitPrice = roundAmount(amount / float64(quantity))
|
||||
}
|
||||
|
||||
firstBaseExpireAt := renewBaseExpireTime(cardsByIccid[iccids[0]], latestBindsByIccid[iccids[0]])
|
||||
minBeginAt := renewPeriodBeginAt(firstBaseExpireAt, in.PackageBeginTime)
|
||||
maxEndAt := firstBaseExpireAt
|
||||
for _, iccid := range iccids {
|
||||
baseExpireAt := renewBaseExpireTime(cardsByIccid[iccid], latestBindsByIccid[iccid])
|
||||
periodBegin := renewPeriodBeginAt(baseExpireAt, in.PackageBeginTime)
|
||||
if minBeginAt == nil || (periodBegin != nil && periodBegin.Before(minBeginAt)) {
|
||||
minBeginAt = periodBegin
|
||||
}
|
||||
endAt := addMonths(periodBegin, durationMonths+giftDurationMonths)
|
||||
if maxEndAt == nil || (endAt != nil && endAt.After(maxEndAt)) {
|
||||
maxEndAt = endAt
|
||||
}
|
||||
}
|
||||
if in.PackageEndTime != nil && !sameRenewTime(maxEndAt, in.PackageEndTime) {
|
||||
return nil, gerror.New("续费结束日期与续费时长不一致")
|
||||
}
|
||||
|
||||
orderSn, err := batchimport.NextRandomNo(orderNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orderInput := &CreatePackageSubscriptionInput{
|
||||
BatchNo: in.BatchNo,
|
||||
CustomerAgentId: customerAgentId,
|
||||
PackageCode: targetPackageCode,
|
||||
PackageBeginTime: minBeginAt,
|
||||
DurationMonths: durationMonths,
|
||||
GiftDurationMonths: giftDurationMonths,
|
||||
DurationDisplayValue: durationValue,
|
||||
GiftDurationDisplayValue: giftDurationValue,
|
||||
DurationUnit: durationUnit,
|
||||
Amount: in.Amount,
|
||||
PayType: in.PayType,
|
||||
PayStatus: in.PayStatus,
|
||||
BalanceAmount: in.BalanceAmount,
|
||||
OfflinePaidAmount: in.OfflinePaidAmount,
|
||||
PayTime: in.PayTime,
|
||||
OfflineContractCompleted: in.OfflineContractCompleted,
|
||||
BuyerNote: in.BuyerNote,
|
||||
AdminNote: in.AdminNote,
|
||||
BusinessType: consts.PackageBusinessTypeRenew,
|
||||
OrderRemark: chooseString(in.OrderRemark, "卡续费业务订单载体"),
|
||||
BindRemark: in.BindRemark,
|
||||
Operator: in.Operator,
|
||||
}
|
||||
orderId, err := s.insertOrder(ctx, tx, orderInput, orderSn, amount, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = s.insertOrderItem(ctx, tx, orderInput, orderId, orderSn, pkg, amount, unitPrice, quantity, minBeginAt, maxEndAt, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bindRows := make([]do.TSimCardBindPkg, 0, quantity)
|
||||
newBindsByIccid := make(map[string]*entity.TSimCardBindPkg, quantity)
|
||||
for _, iccid := range iccids {
|
||||
card := cardsByIccid[iccid]
|
||||
latestBind := latestBindsByIccid[iccid]
|
||||
periodBegin := renewPeriodBeginAt(renewBaseExpireTime(card, latestBind), in.PackageBeginTime)
|
||||
periodEnd := addMonths(periodBegin, durationMonths+giftDurationMonths)
|
||||
_, effectiveStatus := calcPackageStatuses(now, periodBegin, periodEnd)
|
||||
subscriptionNo, err := batchimport.NextRandomNo(subscriptionNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row := do.TSimCardBindPkg{
|
||||
SubscriptionNo: subscriptionNo,
|
||||
BatchNo: in.BatchNo,
|
||||
Iccid: iccid,
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
DeptId: fmt.Sprintf("%d", customerAgentId),
|
||||
PackageType: pkg.PackageType,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PreviousPackageCode: currentPackageCode,
|
||||
BusinessType: consts.PackageBusinessTypeRenew,
|
||||
EffectiveStatus: effectiveStatus,
|
||||
HandleTime: now,
|
||||
EffectiveTime: periodBegin,
|
||||
ExpectedExpireTime: periodEnd,
|
||||
DurationMonths: durationMonths,
|
||||
GiftDurationMonths: giftDurationMonths,
|
||||
OrderCode: orderSn,
|
||||
Remark: strings.TrimSpace(in.BindRemark),
|
||||
TenantId: in.Operator.TenantId,
|
||||
CreatedDept: in.Operator.DeptId,
|
||||
CreatedBy: in.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
bindRows = append(bindRows, row)
|
||||
newBindsByIccid[iccid] = &entity.TSimCardBindPkg{
|
||||
SubscriptionNo: subscriptionNo,
|
||||
BatchNo: in.BatchNo,
|
||||
Iccid: iccid,
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
DeptId: fmt.Sprintf("%d", customerAgentId),
|
||||
PackageType: pkg.PackageType,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PreviousPackageCode: currentPackageCode,
|
||||
BusinessType: consts.PackageBusinessTypeRenew,
|
||||
EffectiveStatus: effectiveStatus,
|
||||
HandleTime: now,
|
||||
EffectiveTime: periodBegin,
|
||||
ExpectedExpireTime: periodEnd,
|
||||
DurationMonths: durationMonths,
|
||||
GiftDurationMonths: giftDurationMonths,
|
||||
OrderCode: orderSn,
|
||||
Remark: strings.TrimSpace(in.BindRemark),
|
||||
TenantId: in.Operator.TenantId,
|
||||
CreatedDept: in.Operator.DeptId,
|
||||
CreatedBy: in.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
if err = s.InsertCardPackageBinds(ctx, tx, bindRows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &RenewCurrentPackageOutput{
|
||||
Iccids: iccids,
|
||||
OrderId: orderId,
|
||||
OrderSn: orderSn,
|
||||
Customer: customer,
|
||||
Package: pkg,
|
||||
CardsByIccid: cardsByIccid,
|
||||
LatestBindsByIccid: latestBindsByIccid,
|
||||
NewBindsByIccid: newBindsByIccid,
|
||||
BindRows: bindRows,
|
||||
RenewTime: now,
|
||||
PackageCode: targetPackageCode,
|
||||
Amount: amount,
|
||||
UnitPrice: unitPrice,
|
||||
Quantity: quantity,
|
||||
}
|
||||
if err = s.UpdateCardsAfterRenew(ctx, tx, out, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func renewExpireAfterAllowedWindow(expireAt, now *gtime.Time) bool {
|
||||
if expireAt == nil {
|
||||
return false
|
||||
}
|
||||
if now == nil {
|
||||
now = gtime.Now()
|
||||
}
|
||||
return expireAt.After(now.AddDate(0, 6, 0))
|
||||
}
|
||||
|
||||
func sameRenewTime(left, right *gtime.Time) bool {
|
||||
switch {
|
||||
case left == nil && right == nil:
|
||||
return true
|
||||
case left == nil || right == nil:
|
||||
return false
|
||||
default:
|
||||
return !left.Before(right) && !left.After(right)
|
||||
}
|
||||
}
|
||||
|
||||
func renewPeriodBeginAt(latest, requested *gtime.Time) *gtime.Time {
|
||||
if requested != nil {
|
||||
return renewDayEndAt(requested)
|
||||
}
|
||||
return renewBeginDateFromExpire(latest)
|
||||
}
|
||||
|
||||
func renewBeginDateFromExpire(expireAt *gtime.Time) *gtime.Time {
|
||||
if expireAt == nil {
|
||||
return nil
|
||||
}
|
||||
return renewDayEndAt(expireAt)
|
||||
}
|
||||
|
||||
func renewBaseExpireTime(card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) *gtime.Time {
|
||||
var expireAt *gtime.Time
|
||||
if bind != nil {
|
||||
expireAt = laterRenewExpireTime(expireAt, bind.ExpectedExpireTime)
|
||||
}
|
||||
if card != nil {
|
||||
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgEndAt)
|
||||
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgPresentAt)
|
||||
expireAt = laterRenewExpireTime(expireAt, card.FlowPkgRealPresentAt)
|
||||
}
|
||||
return renewDayEndAt(expireAt)
|
||||
}
|
||||
|
||||
func laterRenewExpireTime(current, candidate *gtime.Time) *gtime.Time {
|
||||
if candidate == nil || candidate.IsZero() {
|
||||
return current
|
||||
}
|
||||
if current == nil || current.IsZero() || candidate.After(current) {
|
||||
return candidate
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func renewDayEndAt(t *gtime.Time) *gtime.Time {
|
||||
if t == nil || t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
tt := t.Time
|
||||
return gtime.NewFromTime(time.Date(tt.Year(), tt.Month(), tt.Day(), 23, 59, 59, 0, tt.Location()))
|
||||
}
|
||||
|
||||
func renewPackageAllowed(currentPkg *entity.TSimFlowPackageInfo, targetPackageCode string) bool {
|
||||
targetPackageCode = strings.TrimSpace(targetPackageCode)
|
||||
if currentPkg == nil || targetPackageCode == "" {
|
||||
return false
|
||||
}
|
||||
var codes []string
|
||||
if currentPkg.RenewablePackageCodes != nil {
|
||||
_ = currentPkg.RenewablePackageCodes.Scan(&codes)
|
||||
}
|
||||
for _, code := range uniqueStrings(codes) {
|
||||
if strings.TrimSpace(code) == targetPackageCode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeRenewPackageDuration(in *RenewCurrentPackageInput) (durationUnit string, durationValue int, durationMonths int, giftDurationValue int, giftDurationMonths int) {
|
||||
durationUnit = consts.RenewDurationUnitYear
|
||||
if in == nil {
|
||||
return
|
||||
}
|
||||
durationValue = in.DurationDisplayValue
|
||||
durationMonths = in.DurationMonths
|
||||
if durationValue > 0 {
|
||||
// Renewal duration is always expressed in years. The database keeps
|
||||
// the resulting subscription duration in months for lifecycle logic.
|
||||
durationMonths = packageduration.RenewMonths(durationValue)
|
||||
}
|
||||
|
||||
giftDurationMonths = in.GiftDurationMonths
|
||||
giftDurationValue = in.GiftDurationDisplayValue
|
||||
if giftDurationValue <= 0 {
|
||||
giftDurationValue = giftDurationMonths
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func validateRenewCardStatus(iccid string, card *entity.TSimCardInfo) error {
|
||||
if card == nil {
|
||||
return gerror.Newf("ICCID不存在正式卡记录:%s", iccid)
|
||||
}
|
||||
if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped {
|
||||
return gerror.Newf("ICCID当前为停机状态,不能续费:%s", iccid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRenewInput(in *RenewCurrentPackageInput) error {
|
||||
if in == nil {
|
||||
return gerror.New("续费参数不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.BatchNo) == "" {
|
||||
return gerror.New("续费批次号不能为空")
|
||||
}
|
||||
if len(in.Iccids) == 0 {
|
||||
return gerror.New("续费卡不能为空")
|
||||
}
|
||||
if in.DurationMonths <= 0 && in.DurationDisplayValue <= 0 {
|
||||
return gerror.New("续费时长必须大于0")
|
||||
}
|
||||
if in.GiftDurationMonths < 0 {
|
||||
return gerror.New("赠送时长不能小于0")
|
||||
}
|
||||
payType := in.PayType
|
||||
if payType == 0 {
|
||||
payType = consts.PayTypeOffline
|
||||
}
|
||||
if payType == consts.PayTypeOffline && !in.OfflineContractCompleted {
|
||||
return gerror.New("请确认线下合同、销售和走账已完成")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func chooseString(primary, fallback string) string {
|
||||
primary = strings.TrimSpace(primary)
|
||||
if primary != "" {
|
||||
return primary
|
||||
}
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cardpackage
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
)
|
||||
|
||||
func TestValidateRenewCardStatusRejectsStoppedCard(t *testing.T) {
|
||||
err := validateRenewCardStatus("89860401788404184188", &entity.TSimCardInfo{
|
||||
BossCardStatus: consts.V3ApiSimBossCardStatusStopped,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("validateRenewCardStatus() error = nil, want stopped card error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "当前为停机状态") {
|
||||
t.Fatalf("validateRenewCardStatus() error = %q, want stopped card error", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package cardpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"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/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
const cardPackageBindInsertBatchSize = 500
|
||||
|
||||
type Repository struct{}
|
||||
|
||||
func NewRepository() *Repository {
|
||||
return &Repository{}
|
||||
}
|
||||
|
||||
func (r *Repository) LockCardsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardInfo, error) {
|
||||
result := make(map[string]*entity.TSimCardInfo)
|
||||
uniqueIccids := uniqueStrings(iccids)
|
||||
if len(uniqueIccids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var list []*entity.TSimCardInfo
|
||||
if err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
|
||||
WhereIn(dao.TSimCardInfo.Columns().Iccid, uniqueIccids).
|
||||
LockUpdate().
|
||||
Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item != nil && item.Iccid != "" {
|
||||
result[item.Iccid] = item
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) LockCurrentActiveBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardBindPkg, error) {
|
||||
result := make(map[string]*entity.TSimCardBindPkg)
|
||||
priorities := make(map[string]int)
|
||||
uniqueIccids := uniqueStrings(iccids)
|
||||
if len(uniqueIccids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var list []*entity.TSimCardBindPkg
|
||||
if err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().Iccid, uniqueIccids).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().EffectiveStatus, []int{consts.EffectiveStatusPending, consts.EffectiveStatusEffective}).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().EffectiveStatus).
|
||||
OrderAsc(dao.TSimCardBindPkg.Columns().EffectiveTime).
|
||||
OrderAsc(dao.TSimCardBindPkg.Columns().Id).
|
||||
LockUpdate().
|
||||
Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
if item == nil || item.Iccid == "" {
|
||||
continue
|
||||
}
|
||||
priority := bindPriority(item.EffectiveStatus)
|
||||
currentPriority, exists := priorities[item.Iccid]
|
||||
if exists && priority < currentPriority {
|
||||
continue
|
||||
}
|
||||
if exists && priority == currentPriority {
|
||||
result[item.Iccid] = nil
|
||||
continue
|
||||
}
|
||||
priorities[item.Iccid] = priority
|
||||
result[item.Iccid] = item
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) LockLatestBindsByIccids(ctx context.Context, tx gdb.TX, iccids []string) (map[string]*entity.TSimCardBindPkg, error) {
|
||||
result := make(map[string]*entity.TSimCardBindPkg)
|
||||
uniqueIccids := uniqueStrings(iccids)
|
||||
if len(uniqueIccids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var list []*entity.TSimCardBindPkg
|
||||
if err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().Iccid, uniqueIccids).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().PackageType, []int{101, 102}).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().EffectiveStatus, []int{
|
||||
consts.EffectiveStatusPending,
|
||||
consts.EffectiveStatusEffective,
|
||||
consts.EffectiveStatusExpired,
|
||||
}).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().ExpectedExpireTime).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().Id).
|
||||
LockUpdate().
|
||||
Scan(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
effectiveCount := make(map[string]int)
|
||||
for _, item := range list {
|
||||
if item == nil || item.Iccid == "" {
|
||||
continue
|
||||
}
|
||||
iccid := strings.TrimSpace(item.Iccid)
|
||||
if item.EffectiveStatus == consts.EffectiveStatusEffective {
|
||||
effectiveCount[iccid]++
|
||||
if effectiveCount[iccid] > 1 {
|
||||
return nil, gerror.Newf("同一张卡存在多个生效中的套餐订购记录:%s", iccid)
|
||||
}
|
||||
}
|
||||
if _, exists := result[iccid]; exists {
|
||||
continue
|
||||
}
|
||||
result[iccid] = item
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ActiveBindCountsByOrderCodes(ctx context.Context, tx gdb.TX, orderCodes []string) (map[string]int, error) {
|
||||
result := make(map[string]int)
|
||||
uniqueOrderCodes := uniqueStrings(orderCodes)
|
||||
if len(uniqueOrderCodes) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var list []struct {
|
||||
OrderCode string `orm:"order_code"`
|
||||
Count int `orm:"count"`
|
||||
}
|
||||
err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
|
||||
Fields(dao.TSimCardBindPkg.Columns().OrderCode, "COUNT(1) AS count").
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().OrderCode, uniqueOrderCodes).
|
||||
Group(dao.TSimCardBindPkg.Columns().OrderCode).
|
||||
Scan(&list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
result[item.OrderCode] = item.Count
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) LoadTargetDept(ctx context.Context, tx gdb.TX, deptId int64, tenantId string) (*entity.SysDept, error) {
|
||||
var customer *entity.SysDept
|
||||
mod := tx.Ctx(ctx).Model(dao.SysDept.Table()).
|
||||
Where(dao.SysDept.Columns().DeptId, deptId)
|
||||
if tenantId != "" {
|
||||
mod = mod.Where(dao.SysDept.Columns().TenantId, tenantId)
|
||||
}
|
||||
if err := mod.Scan(&customer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
func (r *Repository) LoadPackage(ctx context.Context, tx gdb.TX, packageCode string) (*entity.TSimFlowPackageInfo, error) {
|
||||
var pkg *entity.TSimFlowPackageInfo
|
||||
err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()).
|
||||
Where(dao.TSimFlowPackageInfo.Columns().PackageCode, strings.TrimSpace(packageCode)).
|
||||
Scan(&pkg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (r *Repository) InsertOrder(ctx context.Context, tx gdb.TX, data do.TOrder) (int64, error) {
|
||||
return tx.Ctx(ctx).Model(dao.TOrder.Table()).Data(data).InsertAndGetId()
|
||||
}
|
||||
|
||||
func (r *Repository) InsertOrderItem(ctx context.Context, tx gdb.TX, data do.TOrderItem) error {
|
||||
_, err := tx.Ctx(ctx).Model(dao.TOrderItem.Table()).Data(data).Insert()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) InsertCardPackageBinds(ctx context.Context, tx gdb.TX, rows []do.TSimCardBindPkg) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
for start := 0; start < len(rows); start += cardPackageBindInsertBatchSize {
|
||||
end := start + cardPackageBindInsertBatchSize
|
||||
if end > len(rows) {
|
||||
end = len(rows)
|
||||
}
|
||||
if _, err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).Data(rows[start:end]).Insert(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteBindsBySubscriptionNos(ctx context.Context, tx gdb.TX, subscriptionNos []string) (int64, error) {
|
||||
uniqueNos := uniqueStrings(subscriptionNos)
|
||||
if len(uniqueNos) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
result, err := tx.Ctx(ctx).Model(dao.TSimCardBindPkg.Table()).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().SubscriptionNo, uniqueNos).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
func uniqueStrings(list []string) []string {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(list))
|
||||
result := make([]string, 0, len(list))
|
||||
for _, item := range list {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func bindPriority(status int) int {
|
||||
switch status {
|
||||
case consts.EffectiveStatusEffective:
|
||||
return 2
|
||||
case consts.EffectiveStatusPending:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cardpackage
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCardPackageBindChunkSizeSupportsFiftyThousandCards(t *testing.T) {
|
||||
const totalCards = 50000
|
||||
if cardPackageBindInsertBatchSize <= 0 || cardPackageBindInsertBatchSize > 500 {
|
||||
t.Fatalf("cardPackageBindInsertBatchSize = %d, want 1..500", cardPackageBindInsertBatchSize)
|
||||
}
|
||||
if chunks := (totalCards + cardPackageBindInsertBatchSize - 1) / cardPackageBindInsertBatchSize; chunks != 100 {
|
||||
t.Fatalf("bind insert chunks = %d, want 100", chunks)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
package cardpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
orderNoPrefix = "SO"
|
||||
subscriptionNoPrefix = "SUB"
|
||||
)
|
||||
|
||||
func (s *Service) CreatePackageSubscription(ctx context.Context, tx gdb.TX, in *CreatePackageSubscriptionInput) (*CreatePackageSubscriptionOutput, error) {
|
||||
if err := validateSubscribeInput(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
customer, err := s.loadTargetDept(ctx, tx, in.CustomerAgentId, in.Operator, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg, err := s.loadPackage(ctx, tx, strings.TrimSpace(in.PackageCode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
periodBegin := in.PackageBeginTime
|
||||
periodEnd := packageEndAt(periodBegin, in.DurationMonths+in.GiftDurationMonths, in.EndAtPreviousMonthEnd)
|
||||
packageStatus := CalcPackageStatus(now, periodBegin, periodEnd)
|
||||
effectiveStatus := CalcEffectiveStatus(now, periodBegin, periodEnd)
|
||||
amount, err := parseAmount(in.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quantity := len(in.Cards)
|
||||
unitPrice := 0.0
|
||||
if quantity > 0 {
|
||||
unitPrice = roundAmount(amount / float64(quantity))
|
||||
}
|
||||
businessType := in.BusinessType
|
||||
if businessType == 0 {
|
||||
businessType = consts.PackageBusinessTypeSubscribe
|
||||
}
|
||||
|
||||
orderSn, err := batchimport.NextRandomNo(orderNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orderId, err := s.insertOrder(ctx, tx, in, orderSn, amount, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.insertOrderItem(ctx, tx, in, orderId, orderSn, pkg, amount, unitPrice, quantity, periodBegin, periodEnd, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bindRows := make([]do.TSimCardBindPkg, 0, quantity)
|
||||
bindsByIccid := make(map[string]*entity.TSimCardBindPkg, quantity)
|
||||
for _, card := range in.Cards {
|
||||
iccid := strings.TrimSpace(card.Iccid)
|
||||
if iccid == "" {
|
||||
return nil, gerror.New("订购卡ICCID不能为空")
|
||||
}
|
||||
subscriptionNo, err := batchimport.NextRandomNo(subscriptionNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindRow := do.TSimCardBindPkg{
|
||||
SubscriptionNo: subscriptionNo,
|
||||
BatchNo: in.BatchNo,
|
||||
Iccid: iccid,
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
DeptId: fmt.Sprintf("%d", in.CustomerAgentId),
|
||||
PackageType: pkg.PackageType,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PreviousPackageCode: strings.TrimSpace(in.PreviousPackageCode),
|
||||
BusinessType: businessType,
|
||||
EffectiveStatus: effectiveStatus,
|
||||
HandleTime: now,
|
||||
EffectiveTime: periodBegin,
|
||||
ExpectedExpireTime: periodEnd,
|
||||
DurationMonths: in.DurationMonths,
|
||||
GiftDurationMonths: in.GiftDurationMonths,
|
||||
OrderCode: orderSn,
|
||||
Remark: strings.TrimSpace(in.BindRemark),
|
||||
TenantId: in.Operator.TenantId,
|
||||
CreatedDept: in.Operator.DeptId,
|
||||
CreatedBy: in.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
bindRows = append(bindRows, bindRow)
|
||||
bindsByIccid[iccid] = &entity.TSimCardBindPkg{
|
||||
SubscriptionNo: subscriptionNo,
|
||||
BatchNo: in.BatchNo,
|
||||
Iccid: iccid,
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
DeptId: fmt.Sprintf("%d", in.CustomerAgentId),
|
||||
PackageType: pkg.PackageType,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PreviousPackageCode: strings.TrimSpace(in.PreviousPackageCode),
|
||||
BusinessType: businessType,
|
||||
EffectiveStatus: effectiveStatus,
|
||||
HandleTime: now,
|
||||
EffectiveTime: periodBegin,
|
||||
ExpectedExpireTime: periodEnd,
|
||||
DurationMonths: in.DurationMonths,
|
||||
GiftDurationMonths: in.GiftDurationMonths,
|
||||
OrderCode: orderSn,
|
||||
Remark: strings.TrimSpace(in.BindRemark),
|
||||
TenantId: in.Operator.TenantId,
|
||||
CreatedDept: in.Operator.DeptId,
|
||||
CreatedBy: in.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
if !in.SkipBindInsert {
|
||||
if err = s.InsertCardPackageBinds(ctx, tx, bindRows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &CreatePackageSubscriptionOutput{
|
||||
OrderId: orderId,
|
||||
OrderSn: orderSn,
|
||||
Customer: customer,
|
||||
Package: pkg,
|
||||
PackageBeginAt: periodBegin,
|
||||
PackageEndAt: periodEnd,
|
||||
SubscribeTime: now,
|
||||
PackageStatus: packageStatus,
|
||||
EffectiveStatus: effectiveStatus,
|
||||
Amount: amount,
|
||||
UnitPrice: unitPrice,
|
||||
Quantity: quantity,
|
||||
BindsByIccid: bindsByIccid,
|
||||
BindRows: bindRows,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) InsertCardPackageBinds(ctx context.Context, tx gdb.TX, rows []do.TSimCardBindPkg) error {
|
||||
if err := s.repo.InsertCardPackageBinds(ctx, tx, rows); err != nil {
|
||||
return gerror.Wrap(err, "写入卡套餐绑定失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSubscribeInput(in *CreatePackageSubscriptionInput) error {
|
||||
if in == nil {
|
||||
return gerror.New("订购参数不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.BatchNo) == "" {
|
||||
return gerror.New("订购批次号不能为空")
|
||||
}
|
||||
if len(in.Cards) == 0 {
|
||||
return gerror.New("订购卡不能为空")
|
||||
}
|
||||
if in.CustomerAgentId <= 0 {
|
||||
return gerror.New("客户不能为空")
|
||||
}
|
||||
if strings.TrimSpace(in.PackageCode) == "" {
|
||||
return gerror.New("套餐不能为空")
|
||||
}
|
||||
if in.PackageBeginTime == nil {
|
||||
return gerror.New("套餐开始时间不能为空")
|
||||
}
|
||||
if in.DurationMonths <= 0 {
|
||||
return gerror.New("套餐时长必须大于0")
|
||||
}
|
||||
if in.GiftDurationMonths < 0 {
|
||||
return gerror.New("赠送时长不能小于0")
|
||||
}
|
||||
payType := in.PayType
|
||||
if payType == 0 {
|
||||
payType = consts.PayTypeOffline
|
||||
}
|
||||
if payType == consts.PayTypeOffline && !in.OfflineContractCompleted {
|
||||
return gerror.New("请确认线下合同、销售和走账已完成")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) loadTargetDept(ctx context.Context, tx gdb.TX, deptId int64, operator batchimport.OperatorContext, allowDescendant bool) (*entity.SysDept, error) {
|
||||
if operator.DeptId == 0 {
|
||||
return nil, gerror.New("当前用户未绑定机构")
|
||||
}
|
||||
|
||||
customer, err := s.repo.LoadTargetDept(ctx, tx, deptId, operator.TenantId)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询客户/机构失败")
|
||||
}
|
||||
if customer == nil || customer.DeptId == 0 {
|
||||
return nil, gerror.New("客户/机构不存在或不在可操作范围内")
|
||||
}
|
||||
if customer.DeptId == operator.DeptId || customer.ParentId == operator.DeptId {
|
||||
return customer, nil
|
||||
}
|
||||
if allowDescendant && deptAncestorsContain(customer.Ancestors, operator.DeptId) {
|
||||
return customer, nil
|
||||
}
|
||||
if customer.DeptId != operator.DeptId && customer.ParentId != operator.DeptId {
|
||||
return nil, gerror.New("客户/机构不存在或不在可操作范围内")
|
||||
}
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
func deptAncestorsContain(ancestors string, deptId int64) bool {
|
||||
if deptId <= 0 {
|
||||
return false
|
||||
}
|
||||
target := strconv.FormatInt(deptId, 10)
|
||||
for _, item := range strings.Split(strings.Trim(ancestors, ","), ",") {
|
||||
if strings.TrimSpace(item) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) loadPackage(ctx context.Context, tx gdb.TX, packageCode string) (*entity.TSimFlowPackageInfo, error) {
|
||||
pkg, err := s.repo.LoadPackage(ctx, tx, packageCode)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询套餐失败")
|
||||
}
|
||||
if pkg == nil {
|
||||
return nil, gerror.New("套餐不存在")
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (s *Service) insertOrder(ctx context.Context, tx gdb.TX, in *CreatePackageSubscriptionInput, orderSn string, amount float64, now *gtime.Time) (int64, error) {
|
||||
remark := strings.TrimSpace(in.OrderRemark)
|
||||
if remark == "" {
|
||||
remark = "顶级出库划拨业务订单载体"
|
||||
}
|
||||
payment := normalizeSubscriptionPayment(in, amount, now)
|
||||
orderId, err := s.repo.InsertOrder(ctx, tx, do.TOrder{
|
||||
OrderSn: orderSn,
|
||||
ParentOrderId: 0,
|
||||
ParentOrderSn: "",
|
||||
CustomerType: consts.CustomerTypeAgent,
|
||||
CustomerAgentId: in.CustomerAgentId,
|
||||
EndUserId: 0,
|
||||
AgentId: in.Operator.DeptId,
|
||||
OrderType: orderTypeForBusinessType(in.BusinessType),
|
||||
OrderStatus: consts.OrderStatusCompleted,
|
||||
ShippingStatus: 0,
|
||||
ReturnStatus: 0,
|
||||
Consignee: "",
|
||||
Mobile: "",
|
||||
Email: "",
|
||||
Address: "",
|
||||
BuyerNote: strings.TrimSpace(in.BuyerNote),
|
||||
AdminNote: strings.TrimSpace(in.AdminNote),
|
||||
Remark: remark,
|
||||
ShippingMethod: 3,
|
||||
LogisticsId: 0,
|
||||
LogisticsName: "",
|
||||
ShippingTypeId: 0,
|
||||
ShippingTypeName: "",
|
||||
TrackingNo: "",
|
||||
PayType: payment.PayType,
|
||||
PayStatus: payment.PayStatus,
|
||||
PayOrderSn: "",
|
||||
PayTime: payment.PayTime,
|
||||
TotalAmount: amount,
|
||||
ProductAmount: amount,
|
||||
Balance: payment.BalanceAmount,
|
||||
PaidAmount: amount,
|
||||
OfflinePaidAmount: payment.OfflinePaidAmount,
|
||||
IsNeedCommission: 0,
|
||||
DistributionStatus: 0,
|
||||
TenantId: in.Operator.TenantId,
|
||||
CreatedDept: in.Operator.DeptId,
|
||||
CreatedBy: in.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "创建订单失败")
|
||||
}
|
||||
return orderId, nil
|
||||
}
|
||||
|
||||
func orderTypeForBusinessType(businessType int) int {
|
||||
switch businessType {
|
||||
case consts.PackageBusinessTypeRenew:
|
||||
return consts.OrderTypeCardRenew
|
||||
case consts.PackageBusinessTypeChange:
|
||||
return consts.OrderTypeCardChange
|
||||
default:
|
||||
return consts.OrderTypeCardSubscribe
|
||||
}
|
||||
}
|
||||
|
||||
type subscriptionPayment struct {
|
||||
PayType int
|
||||
PayStatus int
|
||||
BalanceAmount float64
|
||||
OfflinePaidAmount float64
|
||||
PayTime *gtime.Time
|
||||
}
|
||||
|
||||
func normalizeSubscriptionPayment(in *CreatePackageSubscriptionInput, amount float64, now *gtime.Time) subscriptionPayment {
|
||||
payment := subscriptionPayment{
|
||||
PayType: consts.PayTypeOffline,
|
||||
PayStatus: consts.PayStatusPaid,
|
||||
OfflinePaidAmount: amount,
|
||||
PayTime: now,
|
||||
}
|
||||
if in == nil {
|
||||
return payment
|
||||
}
|
||||
if in.PayType > 0 {
|
||||
payment.PayType = in.PayType
|
||||
}
|
||||
if in.PayStatus > 0 {
|
||||
payment.PayStatus = in.PayStatus
|
||||
}
|
||||
if in.PayTime != nil {
|
||||
payment.PayTime = in.PayTime
|
||||
}
|
||||
switch payment.PayType {
|
||||
case consts.PayTypeBalance:
|
||||
payment.BalanceAmount = roundAmount(in.BalanceAmount)
|
||||
if payment.BalanceAmount == 0 && amount > 0 {
|
||||
payment.BalanceAmount = amount
|
||||
}
|
||||
payment.OfflinePaidAmount = 0
|
||||
default:
|
||||
payment.OfflinePaidAmount = roundAmount(in.OfflinePaidAmount)
|
||||
if payment.OfflinePaidAmount == 0 && amount > 0 {
|
||||
payment.OfflinePaidAmount = amount
|
||||
}
|
||||
payment.BalanceAmount = roundAmount(in.BalanceAmount)
|
||||
}
|
||||
return payment
|
||||
}
|
||||
|
||||
func (s *Service) insertOrderItem(ctx context.Context, tx gdb.TX, in *CreatePackageSubscriptionInput, orderId int64, orderSn string, pkg *entity.TSimFlowPackageInfo, amount, unitPrice float64, quantity int, beginAt, endAt *gtime.Time, now *gtime.Time) error {
|
||||
durationLabel := strings.TrimSpace(in.DurationDisplayLabel)
|
||||
if durationLabel == "" {
|
||||
durationLabel = fmt.Sprintf("%d个月", in.DurationMonths)
|
||||
}
|
||||
if strings.TrimSpace(in.DurationDisplayLabel) == "" && in.DurationDisplayValue > 0 && strings.TrimSpace(in.DurationUnit) != "" {
|
||||
durationLabel = fmt.Sprintf("%d%s", in.DurationDisplayValue, consts.RenewDurationUnitText(strings.TrimSpace(in.DurationUnit)))
|
||||
}
|
||||
skuData := marshalJSON([]g.Map{
|
||||
{"skuName": "套餐", "skuValue": pkg.PackageName, "packageCode": pkg.PackageCode, "packageType": pkg.PackageType},
|
||||
{"skuName": "数量", "skuValue": fmt.Sprintf("%d", quantity)},
|
||||
{"skuName": "生效时间", "skuValue": formatTime(beginAt)},
|
||||
{"skuName": "时长", "skuValue": durationLabel},
|
||||
})
|
||||
extraSkuData := marshalJSON(g.Map{
|
||||
"outboundBatchNo": in.BatchNo,
|
||||
"businessBatchNo": in.BatchNo,
|
||||
"offlineContractCompleted": in.OfflineContractCompleted,
|
||||
"packageBeginTime": formatTime(beginAt),
|
||||
"packageEndTime": formatTime(endAt),
|
||||
"durationMonths": in.DurationMonths,
|
||||
"giftDurationMonths": in.GiftDurationMonths,
|
||||
"durationValue": in.DurationDisplayValue,
|
||||
"giftDurationValue": in.GiftDurationDisplayValue,
|
||||
"durationLabel": durationLabel,
|
||||
"giftDurationLabel": strings.TrimSpace(in.GiftDurationDisplayLabel),
|
||||
"durationUnit": strings.TrimSpace(in.DurationUnit),
|
||||
"amount": amount,
|
||||
})
|
||||
if err := s.repo.InsertOrderItem(ctx, tx, do.TOrderItem{
|
||||
OrderId: orderId,
|
||||
OrderSn: orderSn,
|
||||
CustomerType: consts.CustomerTypeAgent,
|
||||
CustomerAgentId: in.CustomerAgentId,
|
||||
EndUserId: 0,
|
||||
Price: unitPrice,
|
||||
Quantity: quantity,
|
||||
ProductId: pkg.Id,
|
||||
ProductName: pkg.PackageName,
|
||||
ProductSn: pkg.PackageCode,
|
||||
PicThumb: "",
|
||||
SkuId: 0,
|
||||
SkuData: skuData,
|
||||
DeliveryQuantity: 0,
|
||||
ProductType: consts.ProductTypeCard,
|
||||
IsGift: 0,
|
||||
AgentId: in.Operator.DeptId,
|
||||
PrepayPrice: 0,
|
||||
Commission: "",
|
||||
OriginPrice: unitPrice,
|
||||
ExtraSkuData: extraSkuData,
|
||||
PayMode: consts.PayModeYearMonth,
|
||||
ServiceBeginTime: beginAt,
|
||||
ServiceEndTime: endAt,
|
||||
TenantId: in.Operator.TenantId,
|
||||
CreatedDept: in.Operator.DeptId,
|
||||
CreatedBy: in.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}); err != nil {
|
||||
return gerror.Wrap(err, "创建订单项失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addMonths(t *gtime.Time, months int) *gtime.Time {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
if months <= 0 {
|
||||
return endOfDayAt(t.Time)
|
||||
}
|
||||
beginAt := t.Time
|
||||
targetMonth := time.Date(
|
||||
beginAt.Year(),
|
||||
beginAt.Month()+time.Month(months),
|
||||
1,
|
||||
beginAt.Hour(),
|
||||
beginAt.Minute(),
|
||||
beginAt.Second(),
|
||||
beginAt.Nanosecond(),
|
||||
beginAt.Location(),
|
||||
)
|
||||
day := beginAt.Day()
|
||||
lastDay := time.Date(
|
||||
targetMonth.Year(),
|
||||
targetMonth.Month()+1,
|
||||
0,
|
||||
beginAt.Hour(),
|
||||
beginAt.Minute(),
|
||||
beginAt.Second(),
|
||||
beginAt.Nanosecond(),
|
||||
beginAt.Location(),
|
||||
).Day()
|
||||
if day > lastDay {
|
||||
day = lastDay
|
||||
}
|
||||
return endOfDayAt(time.Date(
|
||||
targetMonth.Year(),
|
||||
targetMonth.Month(),
|
||||
day,
|
||||
beginAt.Hour(),
|
||||
beginAt.Minute(),
|
||||
beginAt.Second(),
|
||||
beginAt.Nanosecond(),
|
||||
beginAt.Location(),
|
||||
))
|
||||
}
|
||||
|
||||
func endOfDayAt(t time.Time) *gtime.Time {
|
||||
return gtime.NewFromTime(time.Date(
|
||||
t.Year(),
|
||||
t.Month(),
|
||||
t.Day(),
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
0,
|
||||
t.Location(),
|
||||
))
|
||||
}
|
||||
|
||||
func packageEndAt(beginAt *gtime.Time, months int, previousMonthEnd bool) *gtime.Time {
|
||||
endAt := addMonths(beginAt, months)
|
||||
if !previousMonthEnd || endAt == nil {
|
||||
return endAt
|
||||
}
|
||||
return previousMonthEndAt(endAt)
|
||||
}
|
||||
|
||||
func previousMonthEndAt(t *gtime.Time) *gtime.Time {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
tt := t.Time
|
||||
return gtime.NewFromTime(time.Date(tt.Year(), tt.Month(), 0, 23, 59, 59, 0, tt.Location()))
|
||||
}
|
||||
|
||||
func CalcPackageStatus(now, beginAt, endAt *gtime.Time) int {
|
||||
if beginAt != nil && beginAt.After(now) {
|
||||
return consts.PackageStatusPending
|
||||
}
|
||||
if endAt != nil && !endAt.After(now) {
|
||||
return consts.PackageStatusExpired
|
||||
}
|
||||
return consts.PackageStatusEffective
|
||||
}
|
||||
|
||||
func calcPackageStatuses(now, beginAt, endAt *gtime.Time) (packageStatus, effectiveStatus int) {
|
||||
return CalcPackageStatus(now, beginAt, endAt), CalcEffectiveStatus(now, beginAt, endAt)
|
||||
}
|
||||
|
||||
func CalcEffectiveStatus(now, beginAt, endAt *gtime.Time) int {
|
||||
if beginAt != nil && beginAt.After(now) {
|
||||
return consts.EffectiveStatusPending
|
||||
}
|
||||
if endAt != nil && !endAt.After(now) {
|
||||
return consts.EffectiveStatusExpired
|
||||
}
|
||||
return consts.EffectiveStatusEffective
|
||||
}
|
||||
|
||||
func parseAmount(value string) (float64, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, nil
|
||||
}
|
||||
amount, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return 0, gerror.New("订单金额格式不正确")
|
||||
}
|
||||
if amount < 0 {
|
||||
return 0, gerror.New("订单金额不能小于0")
|
||||
}
|
||||
return roundAmount(amount), nil
|
||||
}
|
||||
|
||||
func roundAmount(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
func marshalJSON(value any) string {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package cardpackage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func TestOrderTypeForBusinessType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
businessType int
|
||||
want int
|
||||
}{
|
||||
{name: "subscribe", businessType: consts.PackageBusinessTypeSubscribe, want: consts.OrderTypeCardSubscribe},
|
||||
{name: "renew", businessType: consts.PackageBusinessTypeRenew, want: consts.OrderTypeCardRenew},
|
||||
{name: "change", businessType: consts.PackageBusinessTypeChange, want: consts.OrderTypeCardChange},
|
||||
{name: "unknown defaults to subscribe", businessType: 99, want: consts.OrderTypeCardSubscribe},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := orderTypeForBusinessType(test.businessType); got != test.want {
|
||||
t.Fatalf("orderTypeForBusinessType(%d) = %d, want %d", test.businessType, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPackageStatuses(t *testing.T) {
|
||||
now := gtime.NewFromStr("2026-10-27 12:00:00")
|
||||
tests := []struct {
|
||||
name string
|
||||
beginAt, endAt *gtime.Time
|
||||
wantPackage, wantBind int
|
||||
}{
|
||||
{
|
||||
name: "renew period already effective",
|
||||
beginAt: gtime.NewFromStr("2026-09-30 23:59:59"),
|
||||
endAt: gtime.NewFromStr("2027-09-30 23:59:59"),
|
||||
wantPackage: consts.PackageStatusEffective,
|
||||
wantBind: consts.EffectiveStatusEffective,
|
||||
},
|
||||
{
|
||||
name: "renew period starts in future",
|
||||
beginAt: gtime.NewFromStr("2026-11-01 23:59:59"),
|
||||
endAt: gtime.NewFromStr("2027-11-01 23:59:59"),
|
||||
wantPackage: consts.PackageStatusPending,
|
||||
wantBind: consts.EffectiveStatusPending,
|
||||
},
|
||||
{
|
||||
name: "renew period already expired",
|
||||
beginAt: gtime.NewFromStr("2025-09-30 23:59:59"),
|
||||
endAt: gtime.NewFromStr("2026-09-30 23:59:59"),
|
||||
wantPackage: consts.PackageStatusExpired,
|
||||
wantBind: consts.EffectiveStatusExpired,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
packageStatus, effectiveStatus := calcPackageStatuses(now, test.beginAt, test.endAt)
|
||||
if packageStatus != test.wantPackage || effectiveStatus != test.wantBind {
|
||||
t.Fatalf("calcPackageStatuses() = (%d, %d), want (%d, %d)", packageStatus, effectiveStatus, test.wantPackage, test.wantBind)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package cardpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"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/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func (s *Service) TerminateCurrentPackage(ctx context.Context, tx gdb.TX, in *TerminateCurrentPackageInput) (*TerminateCurrentPackageOutput, error) {
|
||||
if in == nil || len(in.Iccids) == 0 {
|
||||
return nil, gerror.New("退订卡不能为空")
|
||||
}
|
||||
|
||||
iccids := uniqueStrings(in.Iccids)
|
||||
cardsByIccid, err := s.repo.LockCardsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定卡信息失败")
|
||||
}
|
||||
if len(cardsByIccid) != len(iccids) {
|
||||
return nil, gerror.New("部分卡信息已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
bindsByIccid, err := s.repo.LockCurrentActiveBindsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定当前订购失败")
|
||||
}
|
||||
if len(bindsByIccid) != len(iccids) {
|
||||
return nil, gerror.New("部分卡没有当前订购,不能退订")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
|
||||
packageCode := ""
|
||||
orderCodeSet := make(map[string]struct{}, len(iccids))
|
||||
orderUnsubCounts := make(map[string]int, len(iccids))
|
||||
logRows := make([]do.TSimBindPackageLogs, 0, len(iccids))
|
||||
subscriptionNos := make([]string, 0, len(iccids))
|
||||
|
||||
for _, iccid := range iccids {
|
||||
card := cardsByIccid[iccid]
|
||||
bind := bindsByIccid[iccid]
|
||||
if card == nil {
|
||||
return nil, gerror.Newf("ICCID不存在正式卡记录:%s", iccid)
|
||||
}
|
||||
if bind == nil {
|
||||
return nil, gerror.Newf("ICCID当前订购数据异常或不存在:%s", iccid)
|
||||
}
|
||||
if card.DeptId != in.Operator.DeptId && card.CreatedDept != in.Operator.DeptId {
|
||||
return nil, gerror.Newf("ICCID不属于当前机构:%s", iccid)
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone {
|
||||
return nil, gerror.Newf("ICCID没有可退订套餐:%s", iccid)
|
||||
}
|
||||
if packageCode == "" {
|
||||
packageCode = strings.TrimSpace(bind.PackageCode)
|
||||
}
|
||||
if packageCode != strings.TrimSpace(bind.PackageCode) {
|
||||
return nil, gerror.New("当前批次下存在多个套餐,不能统一退订")
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) {
|
||||
return nil, gerror.Newf("ICCID当前套餐与订购记录不一致:%s", iccid)
|
||||
}
|
||||
if sourceBatchNo != "" && strings.TrimSpace(bind.BatchNo) != sourceBatchNo {
|
||||
return nil, gerror.Newf("ICCID当前订购不属于来源出库批次:%s", iccid)
|
||||
}
|
||||
|
||||
orderCode := strings.TrimSpace(bind.OrderCode)
|
||||
if orderCode != "" {
|
||||
orderCodeSet[orderCode] = struct{}{}
|
||||
orderUnsubCounts[orderCode]++
|
||||
}
|
||||
subscriptionNos = append(subscriptionNos, bind.SubscriptionNo)
|
||||
logRows = append(logRows, bindToHistory(bind, now, in.BatchNo, in.Operator.UserId))
|
||||
}
|
||||
|
||||
if len(logRows) > 0 {
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TSimBindPackageLogs.Table()).
|
||||
Data(logRows).
|
||||
OnConflict(dao.TSimBindPackageLogs.Columns().SubscriptionNo).
|
||||
Save(); err != nil {
|
||||
return nil, gerror.Wrap(err, "写入订购历史失败")
|
||||
}
|
||||
}
|
||||
|
||||
affected, err := s.repo.DeleteBindsBySubscriptionNos(ctx, tx, subscriptionNos)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "删除当前订购失败")
|
||||
}
|
||||
if int(affected) != len(iccids) {
|
||||
return nil, gerror.New("部分当前订购已变化,请刷新后重试")
|
||||
}
|
||||
|
||||
orderCodes := mapKeys(orderCodeSet)
|
||||
if err = s.updateOrdersAfterUnsub(ctx, tx, orderCodes, orderUnsubCounts, in.BatchNo, now, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TerminateCurrentPackageOutput{
|
||||
Iccids: iccids,
|
||||
CardsByIccid: cardsByIccid,
|
||||
BindsByIccid: bindsByIccid,
|
||||
PackageCode: packageCode,
|
||||
OrderCodes: orderCodes,
|
||||
UnsubTime: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func bindToHistory(bind *entity.TSimCardBindPkg, actualExpireAt *gtime.Time, unsubBatchNo string, userId int64) do.TSimBindPackageLogs {
|
||||
return do.TSimBindPackageLogs{
|
||||
SubscriptionNo: bind.SubscriptionNo,
|
||||
BatchNo: bind.BatchNo,
|
||||
Iccid: bind.Iccid,
|
||||
Msisdn: bind.Msisdn,
|
||||
DeptId: bind.DeptId,
|
||||
PackageType: bind.PackageType,
|
||||
PackageCode: bind.PackageCode,
|
||||
PreviousPackageCode: bind.PreviousPackageCode,
|
||||
BusinessType: bind.BusinessType,
|
||||
EffectiveStatus: consts.EffectiveStatusTerminated,
|
||||
HandleTime: bind.HandleTime,
|
||||
EffectiveTime: bind.EffectiveTime,
|
||||
ExpectedExpireTime: bind.ExpectedExpireTime,
|
||||
ActualExpireTime: actualExpireAt,
|
||||
DurationMonths: bind.DurationMonths,
|
||||
GiftDurationMonths: bind.GiftDurationMonths,
|
||||
OrderCode: bind.OrderCode,
|
||||
Remark: appendRemark(bind.Remark, fmt.Sprintf("unsubscribeBatchNo=%s", unsubBatchNo)),
|
||||
TenantId: bind.TenantId,
|
||||
CreatedDept: bind.CreatedDept,
|
||||
CreatedBy: bind.CreatedBy,
|
||||
CreatedAt: bind.CreatedAt,
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: actualExpireAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) updateOrdersAfterUnsub(ctx context.Context, tx gdb.TX, orderCodes []string, unsubCounts map[string]int, batchNo string, unsubTime *gtime.Time, userId int64) error {
|
||||
if len(orderCodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
activeCounts, err := s.repo.ActiveBindCountsByOrderCodes(ctx, tx, orderCodes)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "统计订单剩余订购失败")
|
||||
}
|
||||
for _, orderCode := range orderCodes {
|
||||
returnStatus := consts.OrderReturnStatusSuccess
|
||||
if activeCounts[orderCode] > 0 {
|
||||
returnStatus = consts.OrderReturnStatusPartial
|
||||
}
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TOrder.Table()).
|
||||
Where(dao.TOrder.Columns().OrderSn, orderCode).
|
||||
Data(do.TOrder{
|
||||
ReturnStatus: returnStatus,
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: unsubTime,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新订单退订状态失败")
|
||||
}
|
||||
if err = updateOrderItemExtra(ctx, tx, orderCode, batchNo, unsubCounts[orderCode], unsubTime, userId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateOrderItemExtra(ctx context.Context, tx gdb.TX, orderCode, batchNo string, unsubQuantity int, unsubTime *gtime.Time, userId int64) error {
|
||||
var item *entity.TOrderItem
|
||||
if err := tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
|
||||
Where(dao.TOrderItem.Columns().OrderSn, orderCode).
|
||||
Limit(1).
|
||||
Scan(&item); err != nil {
|
||||
return gerror.Wrap(err, "查询订单项失败")
|
||||
}
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
extra := g.Map{}
|
||||
if strings.TrimSpace(item.ExtraSkuData) != "" {
|
||||
_ = json.Unmarshal([]byte(item.ExtraSkuData), &extra)
|
||||
}
|
||||
extra["lastUnsubscribeBatchNo"] = batchNo
|
||||
extra["lastUnsubscribeTime"] = formatTime(unsubTime)
|
||||
extra["unsubscribedQuantity"] = unsubQuantity
|
||||
|
||||
data, err := json.Marshal(extra)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成订单项退订扩展失败")
|
||||
}
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
|
||||
Where(dao.TOrderItem.Columns().ItemId, item.ItemId).
|
||||
Data(do.TOrderItem{
|
||||
ExtraSkuData: string(data),
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: unsubTime,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新订单项退订扩展失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendRemark(old, addition string) string {
|
||||
old = strings.TrimSpace(old)
|
||||
addition = strings.TrimSpace(addition)
|
||||
if old == "" {
|
||||
return addition
|
||||
}
|
||||
if addition == "" {
|
||||
return old
|
||||
}
|
||||
return old + "; " + addition
|
||||
}
|
||||
|
||||
func mapKeys(set map[string]struct{}) []string {
|
||||
result := make([]string, 0, len(set))
|
||||
for key := range set {
|
||||
if strings.TrimSpace(key) != "" {
|
||||
result = append(result, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func formatTime(t *gtime.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("Y-m-d H:i:s")
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package cardpackage
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{repo: NewRepository()}
|
||||
}
|
||||
|
||||
type TerminateCurrentPackageInput struct {
|
||||
BatchNo string
|
||||
SourceBatchNo string
|
||||
Iccids []string
|
||||
Operator batchimport.OperatorContext
|
||||
}
|
||||
|
||||
type TerminateCurrentPackageOutput struct {
|
||||
Iccids []string
|
||||
CardsByIccid map[string]*entity.TSimCardInfo
|
||||
BindsByIccid map[string]*entity.TSimCardBindPkg
|
||||
PackageCode string
|
||||
OrderCodes []string
|
||||
UnsubTime *gtime.Time
|
||||
}
|
||||
|
||||
type SubscribeCard struct {
|
||||
Iccid string
|
||||
Msisdn string
|
||||
Imsi string
|
||||
}
|
||||
|
||||
type CreatePackageSubscriptionInput struct {
|
||||
BatchNo string
|
||||
Cards []SubscribeCard
|
||||
CustomerAgentId int64
|
||||
PackageCode string
|
||||
PackageBeginTime *gtime.Time
|
||||
DurationMonths int
|
||||
GiftDurationMonths int
|
||||
DurationDisplayValue int
|
||||
GiftDurationDisplayValue int
|
||||
DurationDisplayLabel string
|
||||
GiftDurationDisplayLabel string
|
||||
DurationUnit string
|
||||
EndAtPreviousMonthEnd bool
|
||||
Amount string
|
||||
PayType int
|
||||
PayStatus int
|
||||
BalanceAmount float64
|
||||
OfflinePaidAmount float64
|
||||
PayTime *gtime.Time
|
||||
OfflineContractCompleted bool
|
||||
BuyerNote string
|
||||
AdminNote string
|
||||
PreviousPackageCode string
|
||||
BusinessType int
|
||||
OrderRemark string
|
||||
BindRemark string
|
||||
SkipBindInsert bool
|
||||
Operator batchimport.OperatorContext
|
||||
}
|
||||
|
||||
type CreatePackageSubscriptionOutput struct {
|
||||
OrderId int64
|
||||
OrderSn string
|
||||
Customer *entity.SysDept
|
||||
Package *entity.TSimFlowPackageInfo
|
||||
PackageBeginAt *gtime.Time
|
||||
PackageEndAt *gtime.Time
|
||||
SubscribeTime *gtime.Time
|
||||
PackageStatus int
|
||||
EffectiveStatus int
|
||||
Amount float64
|
||||
UnitPrice float64
|
||||
Quantity int
|
||||
BindsByIccid map[string]*entity.TSimCardBindPkg
|
||||
BindRows []do.TSimCardBindPkg
|
||||
}
|
||||
|
||||
type RenewCurrentPackageInput struct {
|
||||
BatchNo string
|
||||
SourceBatchNo string
|
||||
PackageCode string
|
||||
Iccids []string
|
||||
DurationMonths int
|
||||
GiftDurationMonths int
|
||||
DurationDisplayValue int
|
||||
GiftDurationDisplayValue int
|
||||
DurationUnit string
|
||||
PackageBeginTime *gtime.Time
|
||||
PackageEndTime *gtime.Time
|
||||
Amount string
|
||||
PayType int
|
||||
PayStatus int
|
||||
BalanceAmount float64
|
||||
OfflinePaidAmount float64
|
||||
PayTime *gtime.Time
|
||||
OfflineContractCompleted bool
|
||||
BuyerNote string
|
||||
AdminNote string
|
||||
OrderRemark string
|
||||
BindRemark string
|
||||
Operator batchimport.OperatorContext
|
||||
}
|
||||
|
||||
type RenewCurrentPackageOutput struct {
|
||||
Iccids []string
|
||||
OrderId int64
|
||||
OrderSn string
|
||||
Customer *entity.SysDept
|
||||
Package *entity.TSimFlowPackageInfo
|
||||
CardsByIccid map[string]*entity.TSimCardInfo
|
||||
LatestBindsByIccid map[string]*entity.TSimCardBindPkg
|
||||
NewBindsByIccid map[string]*entity.TSimCardBindPkg
|
||||
BindRows []do.TSimCardBindPkg
|
||||
RenewTime *gtime.Time
|
||||
PackageCode string
|
||||
Amount float64
|
||||
UnitPrice float64
|
||||
Quantity int
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type channelDisplayScope struct {
|
||||
TenantID string
|
||||
DeptID int64
|
||||
FullName bool
|
||||
}
|
||||
|
||||
// Channel names are private to the tenant's top organization. A broad data
|
||||
// scope or an administrator role does not grant downstream organizations names.
|
||||
func canViewChannelFullName(ctx context.Context) bool {
|
||||
return resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) {
|
||||
var dept *entity.SysDept
|
||||
cols := dao.SysDept.Columns()
|
||||
err := dao.SysDept.Ctx(ctx).Fields(cols.DeptId, cols.ParentId, cols.Ancestors).
|
||||
Where(cols.TenantId, contexts.GetTenantId(ctx)).
|
||||
Where(cols.DeptId, contexts.GetDeptId(ctx)).Scan(&dept)
|
||||
return dept, err
|
||||
})
|
||||
}
|
||||
|
||||
func resolveChannelDisplayScope(ctx context.Context, load func() (*entity.SysDept, error)) bool {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil || user.DeptId <= 0 || strings.TrimSpace(user.TenantId) == "" {
|
||||
return false
|
||||
}
|
||||
const key = "iot.channelDisplayScope"
|
||||
if cached, ok := contexts.GetDataValue(ctx, key).(channelDisplayScope); ok && cached.TenantID == user.TenantId && cached.DeptID == user.DeptId {
|
||||
return cached.FullName
|
||||
}
|
||||
dept, err := load()
|
||||
fullName := err == nil && dept != nil && dept.DeptId == user.DeptId &&
|
||||
(dept.ParentId == 0 || strings.Trim(dept.Ancestors, ",") == "0")
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "resolve channel display organization failed: %v", err)
|
||||
}
|
||||
contexts.SetDataValue(ctx, key, channelDisplayScope{TenantID: user.TenantId, DeptID: user.DeptId, FullName: fullName})
|
||||
return fullName
|
||||
}
|
||||
|
||||
func channelDisplayNameColumn(ctx context.Context) string {
|
||||
if canViewChannelFullName(ctx) {
|
||||
return dao.TSimChannels.Columns().ChanName
|
||||
}
|
||||
return dao.TSimChannels.Columns().ChanAlias
|
||||
}
|
||||
|
||||
func channelDisplayNameSQL(ctx context.Context, table string) string {
|
||||
return fmt.Sprintf("COALESCE(NULLIF(TRIM(%s.%s), ''), '--')", table, channelDisplayNameColumn(ctx))
|
||||
}
|
||||
|
||||
func channelDisplayName(fullName bool, name, alias string) string {
|
||||
if fullName {
|
||||
return name
|
||||
}
|
||||
if alias = strings.TrimSpace(alias); alias != "" {
|
||||
return alias
|
||||
}
|
||||
return "--"
|
||||
}
|
||||
|
||||
func applyCardChannelDisplayNames(ctx context.Context, list []*model.TSimCardInfoListModel) {
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
fullName := canViewChannelFullName(ctx)
|
||||
for _, item := range list {
|
||||
if item != nil {
|
||||
item.ChanName = channelDisplayName(fullName, item.ChanName, item.ChanAlias)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
)
|
||||
|
||||
func channelDisplayTestContext(tenant string, dept int64) context.Context {
|
||||
return contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
|
||||
ID: 1, TenantId: tenant, DeptId: dept,
|
||||
Roles: []model.Role{{RoleId: consts.SuperAdminRoleId, DataScope: consts.SysRoleDataScopeAll}},
|
||||
}}})
|
||||
}
|
||||
|
||||
func TestChannelDisplayScope(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
dept *entity.SysDept
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{name: "top organization", dept: &entity.SysDept{DeptId: 100, ParentId: 0}, want: true},
|
||||
{name: "top organization legacy ancestors", dept: &entity.SysDept{DeptId: 100, ParentId: 1, Ancestors: "0"}, want: true},
|
||||
{name: "downstream super admin still uses alias", dept: &entity.SysDept{DeptId: 100, ParentId: 50, Ancestors: "0,50"}},
|
||||
{name: "missing organization"},
|
||||
{name: "different organization", dept: &entity.SysDept{DeptId: 999, ParentId: 0}},
|
||||
{name: "lookup failure", err: errors.New("lookup failed")},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := channelDisplayTestContext("tenant-A", 100)
|
||||
got := resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) { return tc.dept, tc.err })
|
||||
if got != tc.want {
|
||||
t.Fatalf("full name = %v, want %v", got, tc.want)
|
||||
}
|
||||
if got := canViewChannelFullName(ctx); got != tc.want {
|
||||
t.Fatalf("cached scope = %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, ctx := range []context.Context{context.Background(), channelDisplayTestContext("", 100), channelDisplayTestContext("tenant-A", 0)} {
|
||||
if resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) {
|
||||
t.Fatal("invalid identity should not query organizations")
|
||||
return nil, nil
|
||||
}) {
|
||||
t.Fatal("invalid identity got full names")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDisplayScopeCacheIdentity(t *testing.T) {
|
||||
ctx := channelDisplayTestContext("tenant-A", 100)
|
||||
calls := 0
|
||||
load := func() (*entity.SysDept, error) {
|
||||
calls++
|
||||
return &entity.SysDept{DeptId: contexts.GetDeptId(ctx), ParentId: 0}, nil
|
||||
}
|
||||
if !resolveChannelDisplayScope(ctx, load) || !resolveChannelDisplayScope(ctx, load) || calls != 1 {
|
||||
t.Fatal("scope not cached for same identity")
|
||||
}
|
||||
contexts.GetUser(ctx).DeptId = 200
|
||||
resolveChannelDisplayScope(ctx, load)
|
||||
contexts.GetUser(ctx).TenantId = "tenant-B"
|
||||
resolveChannelDisplayScope(ctx, load)
|
||||
if calls != 3 {
|
||||
t.Fatalf("scope reused across organization or tenant: %d calls", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDisplayAliasAndExport(t *testing.T) {
|
||||
ctx := channelDisplayTestContext("tenant-A", 200)
|
||||
resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) {
|
||||
return &entity.SysDept{DeptId: 200, ParentId: 100, Ancestors: "0,100"}, nil
|
||||
})
|
||||
list := []*model.TSimCardInfoListModel{
|
||||
{ChanName: "PRIVATE UPSTREAM NAME", ChanAlias: " 中国移动223 "},
|
||||
{ChanName: "PRIVATE UPSTREAM NAME", ChanAlias: " "},
|
||||
nil,
|
||||
}
|
||||
applyCardChannelDisplayNames(ctx, list)
|
||||
if list[0].ChanName != "中国移动223" || list[1].ChanName != "--" {
|
||||
t.Fatalf("name leak: %+v %+v", list[0], list[1])
|
||||
}
|
||||
for _, card := range list[:2] {
|
||||
row := cancellationExportRow(&model.TSimCardCancellationListModel{ChanName: card.ChanName})
|
||||
if row[7] != card.ChanName {
|
||||
t.Fatalf("export name differs: %v", row[7])
|
||||
}
|
||||
}
|
||||
if channelDisplayName(true, "full name", "alias") != "full name" {
|
||||
t.Fatal("top organization name changed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDisplaySQL(t *testing.T) {
|
||||
db, err := gdb.New(gdb.ConfigNode{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close(context.Background()) })
|
||||
if _, err = db.Exec(context.Background(), "CREATE TABLE channels(chan_name TEXT,chan_alias TEXT)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, alias := range []interface{}{"中国移动223", "", " ", nil} {
|
||||
if _, err = db.Exec(context.Background(), "INSERT INTO channels VALUES(?,?)", "PRIVATE UPSTREAM NAME", alias); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, top := range []bool{false, true} {
|
||||
ctx := channelDisplayTestContext("tenant-A", 100)
|
||||
parent := int64(50)
|
||||
if top {
|
||||
parent = 0
|
||||
}
|
||||
resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) {
|
||||
return &entity.SysDept{DeptId: 100, ParentId: parent, Ancestors: "0,50"}, nil
|
||||
})
|
||||
values, err := db.GetArray(ctx, "SELECT "+channelDisplayNameSQL(ctx, "channels")+" FROM channels ORDER BY rowid")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, v := range values {
|
||||
want := "--"
|
||||
if i == 0 {
|
||||
want = "中国移动223"
|
||||
}
|
||||
if top {
|
||||
want = "PRIVATE UPSTREAM NAME"
|
||||
}
|
||||
if v.String() != want {
|
||||
t.Fatalf("top=%v row=%d got=%q want=%q", top, i, v.String(), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise the real list query and export formatter against an isolated database.
|
||||
func TestChannelDisplayCancellationList(t *testing.T) {
|
||||
previous := gdb.GetConfig("default")
|
||||
if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := g.DB()
|
||||
t.Cleanup(func() { _ = db.Close(context.Background()); _ = gdb.SetConfig(gdb.Config{"default": previous}) })
|
||||
statements := []string{
|
||||
"CREATE TABLE sys_dept(dept_id INTEGER,tenant_id TEXT,parent_id INTEGER,ancestors TEXT,dept_name TEXT,deleted_at TEXT)",
|
||||
"INSERT INTO sys_dept VALUES(100,'tenant-A',0,'0','Top',NULL),(200,'tenant-A',100,'0,100','Customer',NULL)",
|
||||
"CREATE TABLE t_sim_channels(id INTEGER,temp_key TEXT,chan_key TEXT,tenant_id TEXT,chan_name TEXT,chan_alias TEXT)",
|
||||
"CREATE TABLE t_sim_channel_template(temp_key TEXT,temp_name TEXT,temp_model TEXT)",
|
||||
"INSERT INTO t_sim_channels(id,chan_key,tenant_id,chan_name,chan_alias) VALUES(1,'SHARED','tenant-A','PRIVATE NAME A','中国移动223'),(2,'SHARED','tenant-B','PRIVATE NAME B','Other tenant alias')",
|
||||
"CREATE TABLE t_sim_flow_package_info(package_code TEXT,package_name TEXT,tenant_id TEXT)",
|
||||
"INSERT INTO t_sim_flow_package_info VALUES('PKG','套餐A','tenant-A'),('PKG','套餐B','tenant-B')",
|
||||
"CREATE TABLE t_sim_card_cancellation(id INTEGER,iccid TEXT,msisdn TEXT,imsi TEXT,imei TEXT,dept_id INTEGER,chan_key TEXT,cancel_batch_no TEXT,cancelled_at TEXT,boss_card_status INTEGER,current_package_code TEXT,inbound_batch_no TEXT,outbound_batch_no TEXT,allocation_batch_no TEXT,card_type INTEGER,net_model INTEGER,remark TEXT,created_by INTEGER,tenant_id TEXT)",
|
||||
"INSERT INTO t_sim_card_cancellation(id,iccid,dept_id,chan_key,created_by,tenant_id,current_package_code) VALUES(1,'CARD-A',200,'SHARED',0,'tenant-A','PKG'),(2,'CARD-B',200,'SHARED',0,'tenant-B','PKG')",
|
||||
}
|
||||
for _, sql := range statements {
|
||||
if _, err := db.Exec(context.Background(), sql); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, dept := range []int64{200, 100} {
|
||||
ctx := channelDisplayTestContext("tenant-A", dept)
|
||||
contexts.GetUser(ctx).Roles = []model.Role{{RoleId: 99, DataScope: consts.SysRoleDataScopeAll}}
|
||||
in := &model.TSimCardCancellationListParam{}
|
||||
in.Page = 1
|
||||
in.PageSize = 20
|
||||
list, total, err := NewIotTSimCardCancellation().List(ctx, in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if total != 1 || len(list) != 1 || list[0].Iccid != "CARD-A" {
|
||||
t.Fatalf("cross tenant rows: total=%d list=%+v", total, list)
|
||||
}
|
||||
want := "中国移动223"
|
||||
if dept == 100 {
|
||||
want = "PRIVATE NAME A"
|
||||
}
|
||||
if list[0].PackageName != "套餐A" {
|
||||
t.Fatalf("cross tenant package: %q", list[0].PackageName)
|
||||
}
|
||||
if list[0].ChanName != want {
|
||||
t.Fatalf("dept=%d name=%q want=%q", dept, list[0].ChanName, want)
|
||||
}
|
||||
if row := cancellationExportRow(list[0]); row[7] != want {
|
||||
t.Fatalf("export name=%v", row[7])
|
||||
}
|
||||
channelQuery := &model.TSimChannelsListParam{}
|
||||
channelQuery.Page = 1
|
||||
channelQuery.PageSize = 20
|
||||
channelQuery.ChanName = want
|
||||
channels, count, err := NewIotTSimChannels().List(ctx, channelQuery)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 1 || len(channels) != 1 || channels[0].ChanName != want {
|
||||
t.Fatalf("channel options: count=%d rows=%+v", count, channels)
|
||||
}
|
||||
stockNames, err := loadStockChannelNames(ctx, []string{"SHARED"})
|
||||
if err != nil || stockNames["SHARED"] != want {
|
||||
t.Fatalf("stock channel name=%v err=%v", stockNames, err)
|
||||
}
|
||||
channelView, err := NewIotTSimChannels().View(ctx, &model.TSimChannelsViewParam{Id: 1})
|
||||
if err != nil || channelView == nil || channelView.ChanName != want {
|
||||
t.Fatalf("channel view=%+v err=%v", channelView, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDisplayCancellationMissingAlias(t *testing.T) {
|
||||
ctx := channelDisplayTestContext("tenant-A", 200)
|
||||
resolveChannelDisplayScope(ctx, func() (*entity.SysDept, error) { return &entity.SysDept{DeptId: 200, ParentId: 100}, nil })
|
||||
for _, alias := range []string{"", " "} {
|
||||
name := channelDisplayName(canViewChannelFullName(ctx), "PRIVATE NAME", alias)
|
||||
encoded, err := json.Marshal(&model.TSimCardCancellationListModel{ChanName: name})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(encoded), "PRIVATE NAME") || name != "--" {
|
||||
t.Fatalf("response exposes full name: %s", encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/packageprice"
|
||||
"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"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type packagePriceSource = packageprice.Source
|
||||
type packageEffectiveBaseCosts = packageprice.BaseCosts
|
||||
type packageEffectivePriceRule = packageprice.Rule
|
||||
type packageEffectiveSalePrice = packageprice.SalePrice
|
||||
|
||||
func packagePriceSourceFromEntity(pkg *entity.TSimFlowPackageInfo) packagePriceSource {
|
||||
return packageprice.SourceFromPackageInfo(pkg)
|
||||
}
|
||||
|
||||
func packagePriceSourceFromPriceListItem(pkg *model.TSimFlowPackagePriceAgentDefaultPackagePricePackageItem) packagePriceSource {
|
||||
return packageprice.SourceFromPriceListItem(pkg)
|
||||
}
|
||||
|
||||
func resolvePackageEffectiveBaseCosts(ctx context.Context, tx gdb.TX, tenantID string, deptID int64, pkg packagePriceSource) (packageEffectiveBaseCosts, error) {
|
||||
return packageprice.ResolveEffectiveBaseCosts(ctx, tx, tenantID, deptID, pkg)
|
||||
}
|
||||
|
||||
func resolvePackageEffectiveSalePrice(ctx context.Context, tx gdb.TX, tenantID string, ownerDeptID int64, targetDeptID int64, pkg packagePriceSource) (packageEffectiveSalePrice, error) {
|
||||
return packageprice.ResolveEffectiveSalePrice(ctx, tx, tenantID, ownerDeptID, targetDeptID, pkg)
|
||||
}
|
||||
|
||||
func resolvePackageEffectiveBaseCostsDepth(ctx context.Context, tx gdb.TX, tenantID string, deptID int64, pkg packagePriceSource, visited map[int64]struct{}) (packageEffectiveBaseCosts, error) {
|
||||
if strings.TrimSpace(pkg.PackageCode) == "" {
|
||||
return packageEffectiveBaseCosts{}, gerror.New("套餐编码不能为空")
|
||||
}
|
||||
if deptID <= 0 {
|
||||
return packageSystemBaseCosts(pkg), nil
|
||||
}
|
||||
if _, ok := visited[deptID]; ok {
|
||||
return packageEffectiveBaseCosts{}, gerror.New("组织定价层级存在循环,无法计算生效成本")
|
||||
}
|
||||
visited[deptID] = struct{}{}
|
||||
|
||||
parentID, err := loadPackagePriceParentDeptID(ctx, tx, tenantID, deptID)
|
||||
if err != nil {
|
||||
return packageEffectiveBaseCosts{}, err
|
||||
}
|
||||
if parentID <= 0 {
|
||||
return packageSystemBaseCosts(pkg), nil
|
||||
}
|
||||
|
||||
parentBase, err := resolvePackageEffectiveBaseCostsDepth(ctx, tx, tenantID, parentID, pkg, visited)
|
||||
if err != nil {
|
||||
return packageEffectiveBaseCosts{}, err
|
||||
}
|
||||
parentRule, err := resolvePackageEffectivePriceRule(ctx, tx, tenantID, parentID, deptID, pkg)
|
||||
if err != nil {
|
||||
return packageEffectiveBaseCosts{}, err
|
||||
}
|
||||
return packageEffectiveBaseCosts{
|
||||
AgentCost: roundAmount(parentBase.AgentCost*parentRule.AgentCostRatio + parentRule.AgentCostProfit),
|
||||
TerminalCost: roundAmount(parentBase.TerminalCost*parentRule.TerminalCostRatio + parentRule.TerminalCostProfit),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func packageSystemBaseCosts(pkg packagePriceSource) packageEffectiveBaseCosts {
|
||||
return packageEffectiveBaseCosts{
|
||||
AgentCost: roundAmount(pkg.CostPrice),
|
||||
TerminalCost: roundAmount(pkg.TerminalCost),
|
||||
}
|
||||
}
|
||||
|
||||
func resolvePackageEffectivePriceRule(ctx context.Context, tx gdb.TX, tenantID string, ownerDeptID int64, targetDeptID int64, pkg packagePriceSource) (packageEffectivePriceRule, error) {
|
||||
rule := packageSystemPriceRule(pkg)
|
||||
if ownerDeptID <= 0 {
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
if targetDeptID > 0 {
|
||||
targetPrice, err := loadPackagePriceOverrideForCalc(ctx, tx, tenantID, ownerDeptID, 1, targetDeptID, pkg.PackageCode)
|
||||
if err != nil {
|
||||
return rule, err
|
||||
}
|
||||
if targetPrice != nil {
|
||||
return packagePriceRuleFromOverride("target", targetPrice), nil
|
||||
}
|
||||
}
|
||||
|
||||
defaultPrice, err := loadPackagePriceOverrideForCalc(ctx, tx, tenantID, ownerDeptID, 0, 0, pkg.PackageCode)
|
||||
if err != nil {
|
||||
return rule, err
|
||||
}
|
||||
if defaultPrice != nil {
|
||||
return packagePriceRuleFromOverride("agentDefault", defaultPrice), nil
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func packageSystemPriceRule(pkg packagePriceSource) packageEffectivePriceRule {
|
||||
return packageEffectivePriceRule{
|
||||
PriceSource: "system",
|
||||
AgentCostRatio: normalizePriceRatio(pkg.SysDefaultRatio),
|
||||
AgentCostProfit: pkg.SysDefaultProfit,
|
||||
TerminalCostRatio: normalizePriceRatio(pkg.SysDefaultTerminalRatio),
|
||||
TerminalCostProfit: pkg.SysDefaultTerminalProfit,
|
||||
}
|
||||
}
|
||||
|
||||
func packagePriceRuleFromOverride(priceSource string, price *entity.TSimFlowPackagePrice) packageEffectivePriceRule {
|
||||
if price == nil {
|
||||
return packageEffectivePriceRule{PriceSource: priceSource}
|
||||
}
|
||||
rule := packageEffectivePriceRule{
|
||||
PriceSource: priceSource,
|
||||
AgentCostRatio: normalizePriceRatio(price.AgentCostRatio),
|
||||
AgentCostProfit: price.AgentCostProfit,
|
||||
TerminalCostRatio: normalizePriceRatio(price.UserCostRatio),
|
||||
TerminalCostProfit: price.UserCostProfit,
|
||||
Remark: price.Remark,
|
||||
UpdatedAt: price.UpdatedAt,
|
||||
}
|
||||
if priceSource == "target" {
|
||||
rule.TargetPriceId = price.Id
|
||||
} else {
|
||||
rule.AgentDefaultPriceId = price.Id
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func loadPackagePriceParentDeptID(ctx context.Context, tx gdb.TX, tenantID string, deptID int64) (int64, error) {
|
||||
if deptID <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
dept := &entity.SysDept{}
|
||||
columns := dao.SysDept.Columns()
|
||||
err := packagePriceCalcModel(ctx, tx, dao.SysDept.Table()).
|
||||
Fields(columns.DeptId, columns.ParentId).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.DeptId, deptID).
|
||||
WhereNull(columns.DeletedAt).
|
||||
Scan(dept)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, gerror.New("组织不存在")
|
||||
}
|
||||
return 0, gerror.Wrap(err, "查询组织上级失败")
|
||||
}
|
||||
if dept.DeptId == 0 {
|
||||
return 0, gerror.New("组织不存在")
|
||||
}
|
||||
return dept.ParentId, nil
|
||||
}
|
||||
|
||||
func loadPackagePriceOverrideForCalc(ctx context.Context, tx gdb.TX, tenantID string, deptID int64, scopeType int, targetAgentID int64, packageCode string) (*entity.TSimFlowPackagePrice, error) {
|
||||
packageCode = strings.TrimSpace(packageCode)
|
||||
if tenantID == "" || deptID <= 0 || packageCode == "" {
|
||||
return nil, nil
|
||||
}
|
||||
price := &entity.TSimFlowPackagePrice{}
|
||||
columns := dao.TSimFlowPackagePrice.Columns()
|
||||
err := packagePriceCalcModel(ctx, tx, dao.TSimFlowPackagePrice.Table()).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.DeptId, deptID).
|
||||
Where(columns.AgentScopeType, scopeType).
|
||||
Where(columns.TargetAgentId, targetAgentID).
|
||||
Where(columns.PackageCode, packageCode).
|
||||
Where(columns.Enabled, 1).
|
||||
Scan(price)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, gerror.Wrap(err, "查询套餐定价失败")
|
||||
}
|
||||
if price.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return price, nil
|
||||
}
|
||||
|
||||
func packagePriceCalcModel(ctx context.Context, tx gdb.TX, table string) *gdb.Model {
|
||||
if tx != nil {
|
||||
return tx.Ctx(ctx).Model(table)
|
||||
}
|
||||
return g.DB().Ctx(ctx).Model(table)
|
||||
}
|
||||
|
||||
func addPackageDefaultPaymentMode(isTopDept bool) string {
|
||||
return "balance"
|
||||
}
|
||||
|
||||
func normalizeAddPackagePaymentIntent(paymentMode string, offlineContractCompleted bool, isTopDept bool) (string, bool, error) {
|
||||
paymentMode = strings.TrimSpace(paymentMode)
|
||||
if paymentMode == "" {
|
||||
paymentMode = addPackageDefaultPaymentMode(isTopDept)
|
||||
}
|
||||
switch paymentMode {
|
||||
case "balance":
|
||||
return "balance", false, nil
|
||||
case "offline":
|
||||
if !isTopDept {
|
||||
return "", false, gerror.New("当前组织加油包必须使用余额支付")
|
||||
}
|
||||
if !offlineContractCompleted {
|
||||
return "", false, gerror.New("请确认线下合同、销售和走账已完成")
|
||||
}
|
||||
return "offline", true, nil
|
||||
default:
|
||||
return "", false, gerror.New("加油包付款方式不正确")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAddPackagePaymentMode(ctx context.Context, paymentMode string, offlineContractCompleted bool) (string, bool, error) {
|
||||
isTopDept, _, err := service.AgentDept().IsTopDept(ctx)
|
||||
if err != nil {
|
||||
return "", false, gerror.Wrap(err, "判断当前组织类型失败")
|
||||
}
|
||||
return normalizeAddPackagePaymentIntent(paymentMode, offlineContractCompleted, isTopDept)
|
||||
}
|
||||
|
||||
func queryAddPackagePaymentContext(ctx context.Context) (*model.TSimAddPackageOptionsRes, error) {
|
||||
isTopDept, _, err := service.AgentDept().IsTopDept(ctx)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "判断当前组织类型失败")
|
||||
}
|
||||
availableBalance, err := queryAddPackageAvailableBalance(ctx, contexts.GetDeptId(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.TSimAddPackageOptionsRes{
|
||||
Items: []*model.TSimAddPackageOptionModel{},
|
||||
IsTopDept: isTopDept,
|
||||
DefaultPaymentMode: addPackageDefaultPaymentMode(isTopDept),
|
||||
AvailableBalance: availableBalance,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func queryAddPackageAvailableBalance(ctx context.Context, deptID int64) (float64, error) {
|
||||
if deptID <= 0 || strings.TrimSpace(contexts.GetTenantId(ctx)) == "" {
|
||||
return 0, gerror.New("当前用户未绑定组织或租户")
|
||||
}
|
||||
var account struct {
|
||||
ID int64 `orm:"id"`
|
||||
Balance float64 `orm:"balance"`
|
||||
}
|
||||
columns := dao.TAgentBalance.Columns()
|
||||
if err := dao.TAgentBalance.Ctx(ctx).
|
||||
Fields(columns.Id, columns.Balance).
|
||||
Where(columns.DeptId, deptID).
|
||||
Where(columns.TenantId, contexts.GetTenantId(ctx)).
|
||||
Scan(&account); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, gerror.Wrap(err, "查询账户余额失败")
|
||||
}
|
||||
if account.ID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return roundAmount(account.Balance), nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolvePackageEffectiveSalePriceUsesSystemDefaults(t *testing.T) {
|
||||
got, err := resolvePackageEffectiveSalePrice(
|
||||
context.Background(),
|
||||
nil,
|
||||
"tenant-1",
|
||||
0,
|
||||
0,
|
||||
packagePriceSource{
|
||||
PackageCode: "PKG-1",
|
||||
CostPrice: 10,
|
||||
TerminalCost: 20,
|
||||
SysDefaultRatio: 80,
|
||||
SysDefaultProfit: 1,
|
||||
SysDefaultTerminalRatio: 50,
|
||||
SysDefaultTerminalProfit: 2,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("resolvePackageEffectiveSalePrice() error = %v", err)
|
||||
}
|
||||
if got.AgentShipmentPrice != 9 {
|
||||
t.Fatalf("agent shipment price = %v, want 9", got.AgentShipmentPrice)
|
||||
}
|
||||
if got.TerminalSalePrice != 12 {
|
||||
t.Fatalf("terminal sale price = %v, want 12", got.TerminalSalePrice)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package packageduration
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
FlowCycleUnitMonth = "month"
|
||||
RenewUnitYear = "year"
|
||||
)
|
||||
|
||||
// Policy is the normalized duration contract used by package-related business.
|
||||
// The database keeps the existing fields; this type separates their meanings.
|
||||
type Policy struct {
|
||||
FlowCycleUnit string
|
||||
RenewUnit string
|
||||
RenewableYears []int
|
||||
ChangeMonths []int
|
||||
}
|
||||
|
||||
func FromPackage(pkg *entity.TSimFlowPackageInfo) (*Policy, error) {
|
||||
if pkg == nil {
|
||||
return nil, gerror.New("package information is empty")
|
||||
}
|
||||
years, err := RenewableYears(pkg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
months, err := ChangeMonths(pkg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Policy{
|
||||
FlowCycleUnit: FlowCycleUnitMonth,
|
||||
RenewUnit: RenewUnitYear,
|
||||
RenewableYears: years,
|
||||
ChangeMonths: months,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func MonthBillingMethodValue() string {
|
||||
return strconv.Itoa(consts.BillingMethodMonth)
|
||||
}
|
||||
|
||||
func RenewableYears(pkg *entity.TSimFlowPackageInfo) ([]int, error) {
|
||||
if pkg == nil {
|
||||
return nil, gerror.New("package information is empty")
|
||||
}
|
||||
return renewableYearsFromConfig(pkg.RenewableDurationConfig)
|
||||
}
|
||||
|
||||
func ChangeMonths(pkg *entity.TSimFlowPackageInfo) ([]int, error) {
|
||||
if pkg == nil {
|
||||
return nil, gerror.New("package information is empty")
|
||||
}
|
||||
values, legacyMonthConfig, err := rawDurationValues(pkg.RenewableDurationConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if legacyMonthConfig {
|
||||
return uniqueSorted(values), nil
|
||||
}
|
||||
|
||||
months := make([]int, 0, len(values))
|
||||
for _, year := range values {
|
||||
if year <= 0 {
|
||||
continue
|
||||
}
|
||||
months = append(months, year*12)
|
||||
}
|
||||
months = uniqueSorted(months)
|
||||
if len(months) == 0 {
|
||||
return nil, gerror.New("package has no valid change duration options")
|
||||
}
|
||||
return months, nil
|
||||
}
|
||||
|
||||
// NormalizeRenewableDurationConfig converts legacy month values such as
|
||||
// [12,24,36] into the new year values [1,2,3] before saving a package.
|
||||
func NormalizeRenewableDurationConfig(config *gjson.Json) (*gjson.Json, error) {
|
||||
values, legacyMonthConfig, err := rawDurationValues(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if legacyMonthConfig {
|
||||
years := make([]int, 0, len(values))
|
||||
for _, month := range values {
|
||||
if month > 0 && month%12 == 0 {
|
||||
years = append(years, month/12)
|
||||
}
|
||||
}
|
||||
years = uniqueSorted(years)
|
||||
if len(years) == 0 {
|
||||
return nil, gerror.New("package has no valid renewable duration options")
|
||||
}
|
||||
return gjson.New(years), nil
|
||||
}
|
||||
return gjson.New(uniqueSorted(values)), nil
|
||||
}
|
||||
|
||||
func NormalizeRenewableYears(values []int) []int {
|
||||
values = uniqueSorted(values)
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
legacyMonthConfig := true
|
||||
for _, value := range values {
|
||||
if value < 12 || value%12 != 0 {
|
||||
legacyMonthConfig = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !legacyMonthConfig {
|
||||
return values
|
||||
}
|
||||
years := make([]int, 0, len(values))
|
||||
for _, month := range values {
|
||||
if month > 0 && month%12 == 0 {
|
||||
years = append(years, month/12)
|
||||
}
|
||||
}
|
||||
return uniqueSorted(years)
|
||||
}
|
||||
|
||||
func RenewMonths(years int) int {
|
||||
if years <= 0 {
|
||||
return 0
|
||||
}
|
||||
return years * 12
|
||||
}
|
||||
|
||||
func renewableYearsFromConfig(config *gjson.Json) ([]int, error) {
|
||||
values, legacyMonthConfig, err := rawDurationValues(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
years := make([]int, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value <= 0 {
|
||||
continue
|
||||
}
|
||||
if legacyMonthConfig {
|
||||
if value%12 != 0 {
|
||||
continue
|
||||
}
|
||||
value /= 12
|
||||
}
|
||||
years = append(years, value)
|
||||
}
|
||||
years = uniqueSorted(years)
|
||||
if len(years) == 0 {
|
||||
return nil, gerror.New("package has no valid renewable duration options")
|
||||
}
|
||||
return years, nil
|
||||
}
|
||||
|
||||
func rawDurationValues(config *gjson.Json) ([]int, bool, error) {
|
||||
if config == nil || config.IsNil() {
|
||||
return nil, false, gerror.New("package has no renewable duration config")
|
||||
}
|
||||
values := config.Interfaces()
|
||||
if len(values) == 0 {
|
||||
return nil, false, gerror.New("package has no renewable duration config")
|
||||
}
|
||||
|
||||
result := make([]int, 0, len(values))
|
||||
for _, value := range values {
|
||||
parsed := gconv.Int(value)
|
||||
if parsed > 0 {
|
||||
result = append(result, parsed)
|
||||
}
|
||||
}
|
||||
result = uniqueSorted(result)
|
||||
if len(result) == 0 {
|
||||
return nil, false, gerror.New("package has no valid renewable duration options")
|
||||
}
|
||||
|
||||
legacyMonthConfig := true
|
||||
for _, value := range result {
|
||||
if value < 12 || value%12 != 0 {
|
||||
legacyMonthConfig = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return result, legacyMonthConfig, nil
|
||||
}
|
||||
|
||||
func uniqueSorted(values []int) []int {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[int]struct{}, len(values))
|
||||
result := make([]int, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
sort.Ints(result)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package packageprice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type Source struct {
|
||||
PackageCode string
|
||||
CostPrice float64
|
||||
TerminalCost float64
|
||||
SysDefaultRatio float64
|
||||
SysDefaultProfit float64
|
||||
SysDefaultTerminalRatio float64
|
||||
SysDefaultTerminalProfit float64
|
||||
}
|
||||
|
||||
type BaseCosts struct {
|
||||
AgentCost float64
|
||||
TerminalCost float64
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
PriceSource string
|
||||
AgentDefaultPriceId int64
|
||||
TargetPriceId int64
|
||||
AgentCostRatio float64
|
||||
AgentCostProfit float64
|
||||
TerminalCostRatio float64
|
||||
TerminalCostProfit float64
|
||||
Remark string
|
||||
UpdatedAt *gtime.Time
|
||||
}
|
||||
|
||||
type SalePrice struct {
|
||||
Base BaseCosts
|
||||
Rule Rule
|
||||
AgentShipmentPrice float64
|
||||
TerminalSalePrice float64
|
||||
}
|
||||
|
||||
type priceStore interface {
|
||||
ParentDeptID(ctx context.Context, tenantID string, deptID int64) (int64, error)
|
||||
PriceOverride(ctx context.Context, tenantID string, deptID int64, scopeType int, targetAgentID int64, packageCode string) (*entity.TSimFlowPackagePrice, error)
|
||||
}
|
||||
|
||||
type dbStore struct {
|
||||
tx gdb.TX
|
||||
}
|
||||
|
||||
func SourceFromPackageInfo(pkg *entity.TSimFlowPackageInfo) Source {
|
||||
if pkg == nil {
|
||||
return Source{}
|
||||
}
|
||||
return Source{
|
||||
PackageCode: strings.TrimSpace(pkg.PackageCode),
|
||||
CostPrice: pkg.CostPrice,
|
||||
TerminalCost: pkg.TerminalCost,
|
||||
SysDefaultRatio: pkg.SysDefaultRatio,
|
||||
SysDefaultProfit: pkg.SysDefaultProfit,
|
||||
SysDefaultTerminalRatio: pkg.SysDefaultTerminalRatio,
|
||||
SysDefaultTerminalProfit: pkg.SysDefaultTerminalProfit,
|
||||
}
|
||||
}
|
||||
|
||||
func SourceFromPriceListItem(pkg *model.TSimFlowPackagePriceAgentDefaultPackagePricePackageItem) Source {
|
||||
if pkg == nil {
|
||||
return Source{}
|
||||
}
|
||||
return Source{
|
||||
PackageCode: strings.TrimSpace(pkg.PackageCode),
|
||||
CostPrice: pkg.CostPrice,
|
||||
TerminalCost: pkg.TerminalCost,
|
||||
SysDefaultRatio: pkg.SysDefaultRatio,
|
||||
SysDefaultProfit: pkg.SysDefaultProfit,
|
||||
SysDefaultTerminalRatio: pkg.SysDefaultTerminalRatio,
|
||||
SysDefaultTerminalProfit: pkg.SysDefaultTerminalProfit,
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveEffectiveBaseCosts(ctx context.Context, tx gdb.TX, tenantID string, deptID int64, pkg Source) (BaseCosts, error) {
|
||||
return resolveEffectiveBaseCosts(ctx, dbStore{tx: tx}, tenantID, deptID, pkg)
|
||||
}
|
||||
|
||||
func ResolveEffectiveSalePrice(ctx context.Context, tx gdb.TX, tenantID string, ownerDeptID int64, targetDeptID int64, pkg Source) (SalePrice, error) {
|
||||
return resolveEffectiveSalePrice(ctx, dbStore{tx: tx}, tenantID, ownerDeptID, targetDeptID, pkg)
|
||||
}
|
||||
|
||||
func NormalizeRatio(ratio float64) float64 {
|
||||
if ratio > 10 {
|
||||
return ratio / 100
|
||||
}
|
||||
return ratio
|
||||
}
|
||||
|
||||
func RoundAmount(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
func resolveEffectiveBaseCosts(ctx context.Context, store priceStore, tenantID string, deptID int64, pkg Source) (BaseCosts, error) {
|
||||
visited := make(map[int64]struct{})
|
||||
return resolveEffectiveBaseCostsDepth(ctx, store, tenantID, deptID, pkg, visited)
|
||||
}
|
||||
|
||||
func resolveEffectiveSalePrice(ctx context.Context, store priceStore, tenantID string, ownerDeptID int64, targetDeptID int64, pkg Source) (SalePrice, error) {
|
||||
base, err := resolveEffectiveBaseCosts(ctx, store, tenantID, ownerDeptID, pkg)
|
||||
if err != nil {
|
||||
return SalePrice{}, err
|
||||
}
|
||||
rule, err := resolveEffectivePriceRule(ctx, store, tenantID, ownerDeptID, targetDeptID, pkg)
|
||||
if err != nil {
|
||||
return SalePrice{}, err
|
||||
}
|
||||
return SalePrice{
|
||||
Base: base,
|
||||
Rule: rule,
|
||||
AgentShipmentPrice: RoundAmount(base.AgentCost*rule.AgentCostRatio + rule.AgentCostProfit),
|
||||
TerminalSalePrice: RoundAmount(base.TerminalCost*rule.TerminalCostRatio + rule.TerminalCostProfit),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveEffectiveBaseCostsDepth(ctx context.Context, store priceStore, tenantID string, deptID int64, pkg Source, visited map[int64]struct{}) (BaseCosts, error) {
|
||||
if strings.TrimSpace(pkg.PackageCode) == "" {
|
||||
return BaseCosts{}, gerror.New("套餐编码不能为空")
|
||||
}
|
||||
if deptID <= 0 {
|
||||
return systemBaseCosts(pkg), nil
|
||||
}
|
||||
if _, ok := visited[deptID]; ok {
|
||||
return BaseCosts{}, gerror.New("组织定价层级存在循环,无法计算生效成本")
|
||||
}
|
||||
visited[deptID] = struct{}{}
|
||||
|
||||
parentID, err := store.ParentDeptID(ctx, tenantID, deptID)
|
||||
if err != nil {
|
||||
return BaseCosts{}, err
|
||||
}
|
||||
if parentID <= 0 {
|
||||
return systemBaseCosts(pkg), nil
|
||||
}
|
||||
|
||||
parentBase, err := resolveEffectiveBaseCostsDepth(ctx, store, tenantID, parentID, pkg, visited)
|
||||
if err != nil {
|
||||
return BaseCosts{}, err
|
||||
}
|
||||
parentRule, err := resolveEffectivePriceRule(ctx, store, tenantID, parentID, deptID, pkg)
|
||||
if err != nil {
|
||||
return BaseCosts{}, err
|
||||
}
|
||||
return BaseCosts{
|
||||
AgentCost: RoundAmount(parentBase.AgentCost*parentRule.AgentCostRatio + parentRule.AgentCostProfit),
|
||||
TerminalCost: RoundAmount(parentBase.TerminalCost*parentRule.TerminalCostRatio + parentRule.TerminalCostProfit),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func systemBaseCosts(pkg Source) BaseCosts {
|
||||
return BaseCosts{
|
||||
AgentCost: RoundAmount(pkg.CostPrice),
|
||||
TerminalCost: RoundAmount(pkg.TerminalCost),
|
||||
}
|
||||
}
|
||||
|
||||
func resolveEffectivePriceRule(ctx context.Context, store priceStore, tenantID string, ownerDeptID int64, targetDeptID int64, pkg Source) (Rule, error) {
|
||||
rule := systemPriceRule(pkg)
|
||||
if ownerDeptID <= 0 {
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
if targetDeptID > 0 {
|
||||
targetPrice, err := store.PriceOverride(ctx, tenantID, ownerDeptID, 1, targetDeptID, pkg.PackageCode)
|
||||
if err != nil {
|
||||
return rule, err
|
||||
}
|
||||
if targetPrice != nil {
|
||||
return ruleFromOverride("target", targetPrice), nil
|
||||
}
|
||||
}
|
||||
|
||||
defaultPrice, err := store.PriceOverride(ctx, tenantID, ownerDeptID, 0, 0, pkg.PackageCode)
|
||||
if err != nil {
|
||||
return rule, err
|
||||
}
|
||||
if defaultPrice != nil {
|
||||
return ruleFromOverride("agentDefault", defaultPrice), nil
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func systemPriceRule(pkg Source) Rule {
|
||||
return Rule{
|
||||
PriceSource: "system",
|
||||
AgentCostRatio: NormalizeRatio(pkg.SysDefaultRatio),
|
||||
AgentCostProfit: pkg.SysDefaultProfit,
|
||||
TerminalCostRatio: NormalizeRatio(pkg.SysDefaultTerminalRatio),
|
||||
TerminalCostProfit: pkg.SysDefaultTerminalProfit,
|
||||
}
|
||||
}
|
||||
|
||||
func ruleFromOverride(priceSource string, price *entity.TSimFlowPackagePrice) Rule {
|
||||
if price == nil {
|
||||
return Rule{PriceSource: priceSource}
|
||||
}
|
||||
rule := Rule{
|
||||
PriceSource: priceSource,
|
||||
AgentCostRatio: NormalizeRatio(price.AgentCostRatio),
|
||||
AgentCostProfit: price.AgentCostProfit,
|
||||
TerminalCostRatio: NormalizeRatio(price.UserCostRatio),
|
||||
TerminalCostProfit: price.UserCostProfit,
|
||||
Remark: price.Remark,
|
||||
UpdatedAt: price.UpdatedAt,
|
||||
}
|
||||
if priceSource == "target" {
|
||||
rule.TargetPriceId = price.Id
|
||||
} else {
|
||||
rule.AgentDefaultPriceId = price.Id
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func (s dbStore) ParentDeptID(ctx context.Context, tenantID string, deptID int64) (int64, error) {
|
||||
if deptID <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
dept := &entity.SysDept{}
|
||||
columns := dao.SysDept.Columns()
|
||||
err := s.model(ctx, dao.SysDept.Table()).
|
||||
Fields(columns.DeptId, columns.ParentId).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.DeptId, deptID).
|
||||
WhereNull(columns.DeletedAt).
|
||||
Scan(dept)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, gerror.New("组织不存在")
|
||||
}
|
||||
return 0, gerror.Wrap(err, "查询组织上级失败")
|
||||
}
|
||||
if dept.DeptId == 0 {
|
||||
return 0, gerror.New("组织不存在")
|
||||
}
|
||||
return dept.ParentId, nil
|
||||
}
|
||||
|
||||
func (s dbStore) PriceOverride(ctx context.Context, tenantID string, deptID int64, scopeType int, targetAgentID int64, packageCode string) (*entity.TSimFlowPackagePrice, error) {
|
||||
packageCode = strings.TrimSpace(packageCode)
|
||||
if tenantID == "" || deptID <= 0 || packageCode == "" {
|
||||
return nil, nil
|
||||
}
|
||||
price := &entity.TSimFlowPackagePrice{}
|
||||
columns := dao.TSimFlowPackagePrice.Columns()
|
||||
err := s.model(ctx, dao.TSimFlowPackagePrice.Table()).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.DeptId, deptID).
|
||||
Where(columns.AgentScopeType, scopeType).
|
||||
Where(columns.TargetAgentId, targetAgentID).
|
||||
Where(columns.PackageCode, packageCode).
|
||||
Where(columns.Enabled, 1).
|
||||
Scan(price)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, gerror.Wrap(err, "查询套餐定价失败")
|
||||
}
|
||||
if price.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return price, nil
|
||||
}
|
||||
|
||||
func (s dbStore) model(ctx context.Context, table string) *gdb.Model {
|
||||
if s.tx != nil {
|
||||
return s.tx.Ctx(ctx).Model(table)
|
||||
}
|
||||
return g.DB().Ctx(ctx).Model(table)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package packageprice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
)
|
||||
|
||||
type fakePriceStore struct {
|
||||
parents map[int64]int64
|
||||
prices map[priceKey]*entity.TSimFlowPackagePrice
|
||||
}
|
||||
|
||||
type priceKey struct {
|
||||
deptID int64
|
||||
scopeType int
|
||||
targetAgentID int64
|
||||
packageCode string
|
||||
}
|
||||
|
||||
func (s fakePriceStore) ParentDeptID(ctx context.Context, tenantID string, deptID int64) (int64, error) {
|
||||
return s.parents[deptID], nil
|
||||
}
|
||||
|
||||
func (s fakePriceStore) PriceOverride(ctx context.Context, tenantID string, deptID int64, scopeType int, targetAgentID int64, packageCode string) (*entity.TSimFlowPackagePrice, error) {
|
||||
return s.prices[priceKey{
|
||||
deptID: deptID,
|
||||
scopeType: scopeType,
|
||||
targetAgentID: targetAgentID,
|
||||
packageCode: packageCode,
|
||||
}], nil
|
||||
}
|
||||
|
||||
func TestResolveEffectiveSalePriceUsesRecursiveParentCost(t *testing.T) {
|
||||
store := fakePriceStore{
|
||||
parents: map[int64]int64{
|
||||
117: 100,
|
||||
131: 117,
|
||||
},
|
||||
prices: map[priceKey]*entity.TSimFlowPackagePrice{
|
||||
{deptID: 100, scopeType: 1, targetAgentID: 117, packageCode: "yd-100m-3y"}: {
|
||||
Id: 86,
|
||||
AgentCostRatio: 1,
|
||||
AgentCostProfit: 4,
|
||||
UserCostRatio: 1,
|
||||
UserCostProfit: 4,
|
||||
},
|
||||
{deptID: 117, scopeType: 0, targetAgentID: 0, packageCode: "yd-100m-3y"}: {
|
||||
Id: 91,
|
||||
AgentCostRatio: 1,
|
||||
AgentCostProfit: 1,
|
||||
UserCostRatio: 1,
|
||||
UserCostProfit: 1,
|
||||
},
|
||||
{deptID: 117, scopeType: 1, targetAgentID: 131, packageCode: "yd-100m-3y"}: {
|
||||
Id: 92,
|
||||
AgentCostRatio: 1,
|
||||
AgentCostProfit: 5,
|
||||
UserCostRatio: 1,
|
||||
UserCostProfit: 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := resolveEffectiveSalePrice(
|
||||
context.Background(),
|
||||
store,
|
||||
"000000",
|
||||
117,
|
||||
131,
|
||||
Source{
|
||||
PackageCode: "yd-100m-3y",
|
||||
CostPrice: 10,
|
||||
TerminalCost: 10,
|
||||
SysDefaultRatio: 1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveEffectiveSalePrice() error = %v", err)
|
||||
}
|
||||
if got.Base.AgentCost != 14 {
|
||||
t.Fatalf("base agent cost = %.2f, want 14.00", got.Base.AgentCost)
|
||||
}
|
||||
if got.AgentShipmentPrice != 19 {
|
||||
t.Fatalf("agent shipment price = %.2f, want 19.00", got.AgentShipmentPrice)
|
||||
}
|
||||
if got.Rule.PriceSource != "target" || got.Rule.TargetPriceId != 92 {
|
||||
t.Fatalf("rule = %+v, want target price 92", got.Rule)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package iot
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
"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"
|
||||
)
|
||||
|
||||
// EnqueueSimCardStatus is intentionally separate from SyncSimCardStatus. It
|
||||
// keeps the tenant/permission-filtered card lookup in Admin, then publishes a
|
||||
// complete routing message so Worker can process it asynchronously.
|
||||
func (s *sIotTSimCardInfo) EnqueueSimCardStatus(ctx context.Context, input *model.SyncSimCardStatusInput) error {
|
||||
if input == nil || input.Iccid == "" {
|
||||
return gerror.New("iccid is required")
|
||||
}
|
||||
|
||||
var card entity.TSimCardInfo
|
||||
if err := s.Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: true}).
|
||||
Where(dao.TSimCardInfo.Columns().Iccid, input.Iccid).
|
||||
Limit(1).
|
||||
Scan(&card); err != nil {
|
||||
return gerror.Wrap(err, "load card for queued status sync")
|
||||
}
|
||||
if card.Id <= 0 || card.ChanKey == "" {
|
||||
return gerror.New("card not found or channel is empty")
|
||||
}
|
||||
|
||||
var channel entity.TSimChannels
|
||||
channelQuery := dao.TSimChannels.Ctx(ctx).
|
||||
Where(dao.TSimChannels.Columns().ChanKey, card.ChanKey)
|
||||
if card.TenantId != "" {
|
||||
channelQuery = channelQuery.Where(dao.TSimChannels.Columns().TenantId, card.TenantId)
|
||||
}
|
||||
if err := channelQuery.Limit(1).Scan(&channel); err != nil {
|
||||
return gerror.Wrap(err, "load channel for queued status sync")
|
||||
}
|
||||
if channel.ChanKey == "" || channel.TempKey == "" {
|
||||
return gerror.New("channel template is empty")
|
||||
}
|
||||
|
||||
return service.SimStatusQueue().EnqueueCard(ctx, model.SimStatusQueueTaskMessage{
|
||||
CardID: card.Id,
|
||||
ICCID: card.Iccid,
|
||||
MSISDN: card.Msisdn,
|
||||
IMSI: card.Imsi,
|
||||
ChanKey: card.ChanKey,
|
||||
TempKey: channel.TempKey,
|
||||
Operator: channel.Operator,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
package addpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"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/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type AsyncConfirmInput struct {
|
||||
BatchNo string
|
||||
TargetBatchType int
|
||||
PackageCode string
|
||||
Remark string
|
||||
PaymentMode string
|
||||
OfflineContractCompleted bool
|
||||
EffectiveMode string
|
||||
Operator batchimport.OperatorContext
|
||||
}
|
||||
|
||||
type AsyncConfirmResult struct {
|
||||
BatchNo string
|
||||
BatchType int
|
||||
BatchAsyncStatus int
|
||||
}
|
||||
|
||||
func ConvertPrepareToAsync(ctx context.Context, tx gdb.TX, in *AsyncConfirmInput) (*AsyncConfirmResult, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("加油包确认参数不能为空")
|
||||
}
|
||||
batchNo := strings.TrimSpace(in.BatchNo)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("加油包批次号不能为空")
|
||||
}
|
||||
targetBatchType := in.TargetBatchType
|
||||
if targetBatchType != consts.BatchTypeCardAddPackage && targetBatchType != consts.BatchTypePoolAddPackage {
|
||||
return nil, gerror.New("加油包目标批次类型不正确")
|
||||
}
|
||||
|
||||
columns := dao.BatchMain.Columns()
|
||||
main := &entity.BatchMain{}
|
||||
err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Where(columns.TenantId, contexts.GetTenantId(ctx)).
|
||||
Where(columns.CreatedDept, contexts.GetDeptId(ctx)).
|
||||
LockUpdate().
|
||||
Scan(main)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询加油包预处理批次失败")
|
||||
}
|
||||
if main.Id == 0 {
|
||||
return nil, gerror.New("加油包预处理批次不存在")
|
||||
}
|
||||
if main.BatchType != consts.BatchTypePrepare {
|
||||
return nil, gerror.New("加油包批次不是预处理类型")
|
||||
}
|
||||
if main.BatchAsyncStatus != consts.BatchAsyncStatusSuccess {
|
||||
return nil, gerror.New("加油包预处理批次尚未完成")
|
||||
}
|
||||
if main.RunResult != consts.BatchRunResultSuccess {
|
||||
return nil, gerror.New("加油包预处理批次未成功")
|
||||
}
|
||||
if main.BatchInfo == nil {
|
||||
return nil, gerror.New("加油包预处理批次缺少快照")
|
||||
}
|
||||
if main.BatchInfo.Get("targetBatchType").Int() != targetBatchType {
|
||||
return nil, gerror.New("加油包预处理批次类型不匹配")
|
||||
}
|
||||
|
||||
opts, err := normalizeConfirmOptions(main.BatchInfo, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := gtime.Now()
|
||||
info := mergeConfirmBatchInfo(main.BatchInfo, opts)
|
||||
data := do.BatchMain{
|
||||
BatchType: targetBatchType,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
RunResult: consts.BatchRunResultPending,
|
||||
StartTime: nil,
|
||||
EndTime: nil,
|
||||
FailReason: "",
|
||||
ValidCount: 0,
|
||||
SuccessCount: 0,
|
||||
FailCount: 0,
|
||||
BatchInfo: gjson.New(info),
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
res, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(columns.Id, main.Id).
|
||||
Where(columns.BatchType, consts.BatchTypePrepare).
|
||||
Where(columns.BatchAsyncStatus, consts.BatchAsyncStatusSuccess).
|
||||
Where(columns.RunResult, consts.BatchRunResultSuccess).
|
||||
Data(data).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "更新加油包批次状态失败")
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取加油包批次更新结果失败")
|
||||
}
|
||||
if affected != 1 {
|
||||
return nil, gerror.New("加油包批次状态已变化,请重试")
|
||||
}
|
||||
|
||||
detailColumns := dao.BatchDetail.Columns()
|
||||
var details []*entity.BatchDetail
|
||||
if err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Fields(detailColumns.Id, detailColumns.RowNo, detailColumns.Snapshot).
|
||||
Where(detailColumns.BatchNo, batchNo).
|
||||
Where(detailColumns.BatchType, consts.BatchTypePrepare).
|
||||
Scan(&details); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询加油包预处理明细失败")
|
||||
}
|
||||
if len(details) == 0 {
|
||||
return nil, gerror.New("加油包预处理批次没有可确认明细")
|
||||
}
|
||||
for _, detail := range details {
|
||||
if detail == nil || detail.Id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(detailColumns.Id, detail.Id).
|
||||
Where(detailColumns.BatchNo, batchNo).
|
||||
Where(detailColumns.BatchType, consts.BatchTypePrepare).
|
||||
Data(do.BatchDetail{
|
||||
BatchType: targetBatchType,
|
||||
DetailStatus: consts.BatchDetailStatusPending,
|
||||
ErrorCode: "",
|
||||
ErrorReason: "",
|
||||
Snapshot: mergeConfirmDetailSnapshot(detail.Snapshot, opts),
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return nil, gerror.Wrap(err, "更新加油包批次明细类型失败")
|
||||
}
|
||||
}
|
||||
|
||||
return &AsyncConfirmResult{
|
||||
BatchNo: batchNo,
|
||||
BatchType: targetBatchType,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeConfirmOptions(info *gjson.Json, in *AsyncConfirmInput) (SubmitOptions, error) {
|
||||
opts, err := normalizeOptions(SubmitOptions{
|
||||
PackageCode: in.PackageCode,
|
||||
Remark: in.Remark,
|
||||
PaymentMode: in.PaymentMode,
|
||||
OfflineContractCompleted: in.OfflineContractCompleted,
|
||||
EffectiveMode: in.EffectiveMode,
|
||||
})
|
||||
if err != nil {
|
||||
return SubmitOptions{}, err
|
||||
}
|
||||
preparedPackageCode := strings.TrimSpace(info.Get("packageCode").String())
|
||||
if opts.PackageCode == "" {
|
||||
opts.PackageCode = preparedPackageCode
|
||||
}
|
||||
if opts.PackageCode == "" {
|
||||
return SubmitOptions{}, gerror.New("请选择加油包套餐")
|
||||
}
|
||||
if preparedPackageCode != "" && opts.PackageCode != preparedPackageCode {
|
||||
return SubmitOptions{}, gerror.New("加油包套餐与预处理结果不一致,请重新预处理")
|
||||
}
|
||||
if preparedPackageCode == "" {
|
||||
if err := validatePreparedPackageOption(info, opts.PackageCode); err != nil {
|
||||
return SubmitOptions{}, err
|
||||
}
|
||||
}
|
||||
if err := validatePreparedPaymentEnough(info, opts); err != nil {
|
||||
return SubmitOptions{}, err
|
||||
}
|
||||
preparedEffectiveMode := strings.TrimSpace(info.Get("effectiveMode").String())
|
||||
if preparedEffectiveMode != "" {
|
||||
normalized, err := NormalizeEffectiveMode(preparedEffectiveMode)
|
||||
if err != nil {
|
||||
return SubmitOptions{}, err
|
||||
}
|
||||
if opts.EffectiveMode != normalized {
|
||||
return SubmitOptions{}, gerror.New("加油包生效方式与预处理结果不一致,请重新预处理")
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func validatePreparedPackageOption(info *gjson.Json, packageCode string) error {
|
||||
if info == nil || info.IsNil() {
|
||||
return nil
|
||||
}
|
||||
packageCode = strings.TrimSpace(packageCode)
|
||||
hasOptions := false
|
||||
for _, key := range []string{"availablePackages", "packageOptions"} {
|
||||
value := info.Get(key)
|
||||
if value == nil || value.IsNil() {
|
||||
continue
|
||||
}
|
||||
hasOptions = true
|
||||
for _, raw := range value.Interfaces() {
|
||||
item := gjson.New(raw)
|
||||
code := strings.TrimSpace(item.Get("packageCode").String())
|
||||
if code == "" {
|
||||
code = strings.TrimSpace(item.Get("value").String())
|
||||
}
|
||||
if code != packageCode {
|
||||
continue
|
||||
}
|
||||
if priceAvailable := item.Get("priceAvailable"); priceAvailable != nil && !priceAvailable.IsNil() && !priceAvailable.Bool() {
|
||||
reason := strings.TrimSpace(item.Get("priceReason").String())
|
||||
if reason == "" {
|
||||
reason = "当前套餐暂时无法计算价格,请联系管理员维护定价"
|
||||
}
|
||||
return gerror.New(reason)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if hasOptions {
|
||||
return gerror.New("请选择预处理返回的可购加油包套餐")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePreparedPaymentEnough(info *gjson.Json, opts SubmitOptions) error {
|
||||
if strings.TrimSpace(opts.PaymentMode) != "balance" || info == nil || info.IsNil() {
|
||||
return nil
|
||||
}
|
||||
if option := findPreparedPackageOption(info, opts.PackageCode); option != nil {
|
||||
if err := validatePaymentAccountsEnough(option.Get("paymentAccounts")); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasPaymentAccounts(option.Get("paymentAccounts")) {
|
||||
return nil
|
||||
}
|
||||
totalAmount := option.Get("totalAmount").Float64()
|
||||
availableBalance := option.Get("availableBalance").Float64()
|
||||
if availableBalance <= 0 {
|
||||
availableBalance = info.Get("availableBalance").Float64()
|
||||
}
|
||||
if totalAmount > 0 && availableBalance+0.0001 < totalAmount {
|
||||
return gerror.Newf("账户余额不足,需支付%.2f元,当前余额%.2f元", totalAmount, availableBalance)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := validatePaymentAccountsEnough(info.Get("paymentAccounts")); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasPaymentAccounts(info.Get("paymentAccounts")) {
|
||||
return nil
|
||||
}
|
||||
totalAmount := info.Get("totalAmount").Float64()
|
||||
availableBalance := info.Get("availableBalance").Float64()
|
||||
if totalAmount > 0 && availableBalance+0.0001 < totalAmount {
|
||||
return gerror.Newf("账户余额不足,需支付%.2f元,当前余额%.2f元", totalAmount, availableBalance)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findPreparedPackageOption(info *gjson.Json, packageCode string) *gjson.Json {
|
||||
if info == nil || info.IsNil() {
|
||||
return nil
|
||||
}
|
||||
packageCode = strings.TrimSpace(packageCode)
|
||||
for _, key := range []string{"availablePackages", "packageOptions"} {
|
||||
value := info.Get(key)
|
||||
if value == nil || value.IsNil() {
|
||||
continue
|
||||
}
|
||||
for _, raw := range value.Interfaces() {
|
||||
item := gjson.New(raw)
|
||||
code := strings.TrimSpace(item.Get("packageCode").String())
|
||||
if code == "" {
|
||||
code = strings.TrimSpace(item.Get("value").String())
|
||||
}
|
||||
if code == packageCode {
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePaymentAccountsEnough(value *gvar.Var) error {
|
||||
if !hasPaymentAccounts(value) {
|
||||
return nil
|
||||
}
|
||||
for _, raw := range value.Interfaces() {
|
||||
account := gjson.New(raw)
|
||||
requiredAmount := account.Get("requiredAmount").Float64()
|
||||
availableBalance := account.Get("availableBalance").Float64()
|
||||
enoughValue := account.Get("balanceEnough")
|
||||
if enoughValue != nil && !enoughValue.IsNil() {
|
||||
if enoughValue.Bool() {
|
||||
continue
|
||||
}
|
||||
} else if availableBalance+0.0001 >= requiredAmount {
|
||||
continue
|
||||
}
|
||||
return gerror.Newf(
|
||||
"账户余额不足,付款部门%v需支付%.2f元,当前余额%.2f元",
|
||||
account.Get("payerDeptId").Val(),
|
||||
requiredAmount,
|
||||
availableBalance,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasPaymentAccounts(value *gvar.Var) bool {
|
||||
return value != nil && !value.IsNil() && len(value.Interfaces()) > 0
|
||||
}
|
||||
|
||||
func mergeConfirmDetailSnapshot(base *gjson.Json, opts SubmitOptions) *gjson.Json {
|
||||
data := g.Map{}
|
||||
if base != nil {
|
||||
for key, value := range base.Map() {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
data["packageCode"] = strings.TrimSpace(opts.PackageCode)
|
||||
if remark := strings.TrimSpace(opts.Remark); remark != "" {
|
||||
data["remark"] = remark
|
||||
}
|
||||
data["effectiveMode"] = strings.TrimSpace(opts.EffectiveMode)
|
||||
data["effectiveModeText"] = EffectiveModeText(opts.EffectiveMode)
|
||||
return gjson.New(data)
|
||||
}
|
||||
|
||||
func mergeConfirmBatchInfo(base *gjson.Json, opts SubmitOptions) g.Map {
|
||||
data := g.Map{}
|
||||
if base != nil {
|
||||
for key, value := range base.Map() {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
data["packageCode"] = strings.TrimSpace(opts.PackageCode)
|
||||
data["remark"] = strings.TrimSpace(opts.Remark)
|
||||
data["paymentMode"] = strings.TrimSpace(opts.PaymentMode)
|
||||
data["offlineContractCompleted"] = opts.OfflineContractCompleted
|
||||
data["effectiveMode"] = strings.TrimSpace(opts.EffectiveMode)
|
||||
data["effectiveModeText"] = EffectiveModeText(opts.EffectiveMode)
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package addpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/text"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
const (
|
||||
headerPackageCode = "加油包套餐编码"
|
||||
headerEffectiveMode = "生效方式"
|
||||
headerRemark = "备注"
|
||||
)
|
||||
|
||||
func parseExcelRows(ctx context.Context, filePath string, cfg Config, options SubmitOptions) ([]batchimport.Row, error) {
|
||||
parsedRows, err := excel.ParseFile(ctx, filePath, excel.ParseOptions{
|
||||
RequiredHeaders: []string{cfg.TargetHeader, headerPackageCode},
|
||||
SingleSheet: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(parsedRows))
|
||||
for _, parsed := range parsedRows {
|
||||
row := &Row{
|
||||
RowNo: parsed.RowNo,
|
||||
TargetCode: fieldValue(parsed, targetAliases(cfg)...),
|
||||
PackageCode: fieldValue(parsed, headerPackageCode, "加油包编码", "packageCode", "套餐编码"),
|
||||
EffectiveMode: fieldValue(parsed, headerEffectiveMode, "effectiveMode", "生效模式"),
|
||||
Remark: fieldValue(parsed, headerRemark, "remark"),
|
||||
}
|
||||
applyDefaultOptions(row, options)
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func parseInputRows(ctx context.Context, input string, options SubmitOptions) ([]batchimport.Row, error) {
|
||||
lines, err := text.ParseNonEmptyLines(ctx, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, text.ErrEmptyInput) {
|
||||
return nil, gerror.New("批量输入内容不能为空")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
row := &Row{RowNo: line.RowNo}
|
||||
fields := parseInputFields(line.Text)
|
||||
if len(fields) != 1 {
|
||||
row.AddErrorReason("格式错误,应为目标编码")
|
||||
} else {
|
||||
row.TargetCode = fields[0]
|
||||
}
|
||||
applyDefaultOptions(row, options)
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func fieldValue(row excel.ParsedRow, names ...string) string {
|
||||
for _, name := range names {
|
||||
if value := strings.TrimSpace(excel.Field(row, name)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func targetAliases(cfg Config) []string {
|
||||
if cfg.Operation == OperationPool {
|
||||
return []string{cfg.TargetHeader, "流量池编码", "流量池编号", "poolNo", "flowPoolCode", "targetCode"}
|
||||
}
|
||||
return []string{cfg.TargetHeader, "卡号", "iccid", "targetCode"}
|
||||
}
|
||||
|
||||
func parseInputFields(value string) []string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(value, "|") {
|
||||
return splitAndTrim(value, "|")
|
||||
}
|
||||
if strings.Contains(value, "\t") {
|
||||
return splitAndTrim(value, "\t")
|
||||
}
|
||||
return strings.Fields(value)
|
||||
}
|
||||
|
||||
func splitAndTrim(value, sep string) []string {
|
||||
parts := strings.Split(value, sep)
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
out = append(out, strings.TrimSpace(part))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func applyDefaultOptions(row *Row, options SubmitOptions) {
|
||||
if row == nil {
|
||||
return
|
||||
}
|
||||
row.TargetCode = strings.TrimSpace(row.TargetCode)
|
||||
row.PackageCode = strings.TrimSpace(row.PackageCode)
|
||||
row.Remark = strings.TrimSpace(row.Remark)
|
||||
row.EffectiveMode = strings.TrimSpace(row.EffectiveMode)
|
||||
|
||||
if options.PackageCode != "" {
|
||||
if row.PackageCode != "" && row.PackageCode != options.PackageCode {
|
||||
row.AddErrorReason("行内加油包编码与页面选择不一致")
|
||||
}
|
||||
row.PackageCode = options.PackageCode
|
||||
}
|
||||
if row.EffectiveMode == "" && options.EffectiveMode != "" {
|
||||
row.EffectiveMode = options.EffectiveMode
|
||||
}
|
||||
if row.Remark == "" {
|
||||
row.Remark = strings.TrimSpace(options.Remark)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeOptions(options SubmitOptions) (SubmitOptions, error) {
|
||||
options.PackageCode = strings.TrimSpace(options.PackageCode)
|
||||
options.Remark = strings.TrimSpace(options.Remark)
|
||||
mode, err := NormalizeEffectiveMode(options.EffectiveMode)
|
||||
if err != nil {
|
||||
return SubmitOptions{}, err
|
||||
}
|
||||
options.EffectiveMode = mode
|
||||
options.PaymentMode = strings.TrimSpace(options.PaymentMode)
|
||||
if options.PaymentMode == "" {
|
||||
options.PaymentMode = "balance"
|
||||
}
|
||||
switch options.PaymentMode {
|
||||
case "balance":
|
||||
options.OfflineContractCompleted = false
|
||||
case "offline":
|
||||
if !options.OfflineContractCompleted {
|
||||
return SubmitOptions{}, gerror.New("请确认线下合同、销售和走账已完成")
|
||||
}
|
||||
default:
|
||||
return SubmitOptions{}, gerror.New("加油包付款方式不正确")
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
package addpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type cardTarget struct {
|
||||
Iccid string `orm:"iccid"`
|
||||
CurrentPackageCode string `orm:"current_package_code"`
|
||||
PackageStatus int `orm:"package_status"`
|
||||
}
|
||||
|
||||
type cardLatestBindTarget struct {
|
||||
Iccid string `orm:"iccid"`
|
||||
PackageCode string `orm:"package_code"`
|
||||
EffectiveStatus int `orm:"effective_status"`
|
||||
ExpectedExpireTime *gtime.Time `orm:"expected_expire_time"`
|
||||
}
|
||||
|
||||
type poolTarget struct {
|
||||
FlowPoolCode string `orm:"flow_pool_code"`
|
||||
PoolPackageCode int64 `orm:"pool_package_code"`
|
||||
PoolStatus int `orm:"pool_status"`
|
||||
}
|
||||
|
||||
func (p *Processor) PrepareRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
if p.config.Operation == OperationPool {
|
||||
return p.preparePoolRows(ctx, rows)
|
||||
}
|
||||
return p.prepareCardRows(ctx, rows)
|
||||
}
|
||||
|
||||
func (p *Processor) prepareCardRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
typedRows, targetCodes, err := collectTypedRows(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(targetCodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var cards []*cardTarget
|
||||
cardColumns := dao.TSimCardInfo.Columns()
|
||||
if err = handler.Model(dao.TSimCardInfo.Ctx(ctx), &handler.Option{FilterTenant: true, FilterAuth: true}).
|
||||
Fields(cardColumns.Iccid, cardColumns.CurrentPackageCode, cardColumns.PackageStatus, cardColumns.TenantId, cardColumns.DeptId, cardColumns.CreatedDept, cardColumns.CreatedBy).
|
||||
WhereIn(cardColumns.Iccid, targetCodes).
|
||||
Scan(&cards); err != nil {
|
||||
return gerror.Wrap(err, "查询加油包卡信息失败")
|
||||
}
|
||||
|
||||
cardMap := make(map[string]*cardTarget, len(cards))
|
||||
for _, card := range cards {
|
||||
if card == nil {
|
||||
continue
|
||||
}
|
||||
iccid := strings.TrimSpace(card.Iccid)
|
||||
if iccid == "" {
|
||||
continue
|
||||
}
|
||||
cardMap[iccid] = card
|
||||
}
|
||||
|
||||
latestBinds, err := p.loadLatestCardBaseBinds(ctx, targetCodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
latestBindMap := make(map[string]*cardLatestBindTarget, len(latestBinds))
|
||||
for _, bind := range latestBinds {
|
||||
if bind == nil {
|
||||
continue
|
||||
}
|
||||
iccid := strings.TrimSpace(bind.Iccid)
|
||||
if iccid == "" || latestBindMap[iccid] != nil {
|
||||
continue
|
||||
}
|
||||
latestBindMap[iccid] = bind
|
||||
}
|
||||
|
||||
basePackageCodes := make([]string, 0, len(latestBindMap))
|
||||
for _, bind := range latestBindMap {
|
||||
if bind == nil {
|
||||
continue
|
||||
}
|
||||
basePackageCodes = append(basePackageCodes, bind.PackageCode)
|
||||
}
|
||||
|
||||
packages, err := p.loadPackageMap(ctx, append(basePackageCodes, rowPackageCodes(typedRows)...))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
for _, row := range typedRows {
|
||||
card := cardMap[row.TargetCode]
|
||||
if card == nil {
|
||||
row.AddErrorReason("ICCID不存在或无权限")
|
||||
continue
|
||||
}
|
||||
latestBind := latestBindMap[row.TargetCode]
|
||||
if latestBind == nil || strings.TrimSpace(latestBind.PackageCode) == "" {
|
||||
row.AddErrorReason("当前卡订购记录不存在")
|
||||
continue
|
||||
}
|
||||
row.Authorized = true
|
||||
if card.PackageStatus != consts.PackageStatusEffective {
|
||||
row.AddErrorReason("当前卡套餐未生效,不能订购加油包")
|
||||
continue
|
||||
}
|
||||
if bindExpired(latestBind, now) {
|
||||
row.AddErrorReason("当前卡基础套餐已到期,不能订购加油包")
|
||||
continue
|
||||
}
|
||||
if err := p.validateTargetPackage(row, packages, 103); err != nil {
|
||||
row.AddErrorReason(err.Error())
|
||||
continue
|
||||
}
|
||||
basePackage := packages[strings.TrimSpace(latestBind.PackageCode)]
|
||||
if basePackage == nil {
|
||||
row.AddErrorReason("当前卡基础套餐不存在")
|
||||
continue
|
||||
}
|
||||
if !containsString(packageCodesFromJSON(basePackage.TopupPackageCodes), row.PackageCode) {
|
||||
row.AddErrorReason("当前卡基础套餐不允许订购该加油包")
|
||||
continue
|
||||
}
|
||||
}
|
||||
return rowsValidationError(rows, "加油包卡信息校验失败")
|
||||
}
|
||||
|
||||
func (p *Processor) preparePoolRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
typedRows, targetCodes, err := collectTypedRows(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(targetCodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pools []*poolTarget
|
||||
poolColumns := dao.TSimFlowPool.Columns()
|
||||
if err = handler.Model(dao.TSimFlowPool.Ctx(ctx), &handler.Option{FilterTenant: true, FilterAuth: true}).
|
||||
Fields(poolColumns.FlowPoolCode, poolColumns.PoolPackageCode, poolColumns.PoolStatus, poolColumns.TenantId, poolColumns.DeptId, poolColumns.CreatedDept, poolColumns.CreatedBy).
|
||||
WhereIn(poolColumns.FlowPoolCode, targetCodes).
|
||||
Scan(&pools); err != nil {
|
||||
return gerror.Wrap(err, "查询流量池信息失败")
|
||||
}
|
||||
poolMap := make(map[string]*poolTarget, len(pools))
|
||||
for _, pool := range pools {
|
||||
if pool == nil {
|
||||
continue
|
||||
}
|
||||
code := strings.TrimSpace(pool.FlowPoolCode)
|
||||
poolMap[code] = pool
|
||||
}
|
||||
|
||||
packages, err := p.loadPackageMap(ctx, rowPackageCodes(typedRows))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range typedRows {
|
||||
pool := poolMap[row.TargetCode]
|
||||
if pool == nil {
|
||||
row.AddErrorReason("流量池编号不存在或无权限")
|
||||
continue
|
||||
}
|
||||
row.Authorized = true
|
||||
if pool.PoolStatus != 0 {
|
||||
row.AddErrorReason("流量池状态异常,不能订购加油包")
|
||||
continue
|
||||
}
|
||||
if err := p.validateTargetPackage(row, packages, 104); err != nil {
|
||||
row.AddErrorReason(err.Error())
|
||||
continue
|
||||
}
|
||||
targetPackage := packages[row.PackageCode]
|
||||
if targetPackage == nil {
|
||||
row.AddErrorReason("加油包不存在")
|
||||
continue
|
||||
}
|
||||
if targetPackage.PoolPackageCode != pool.PoolPackageCode {
|
||||
row.AddErrorReason("加油包不属于当前流量池套餐")
|
||||
continue
|
||||
}
|
||||
}
|
||||
return rowsValidationError(rows, "流量池加油包校验失败")
|
||||
}
|
||||
|
||||
func (p *Processor) loadLatestCardBaseBinds(ctx context.Context, iccids []string) ([]*cardLatestBindTarget, error) {
|
||||
iccids = uniqueStrings(iccids)
|
||||
if len(iccids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var binds []*cardLatestBindTarget
|
||||
if err := handler.Model(dao.TSimCardBindPkg.Ctx(ctx), &handler.Option{FilterTenant: true, FilterAuth: true}).
|
||||
Fields(
|
||||
dao.TSimCardBindPkg.Columns().Iccid,
|
||||
dao.TSimCardBindPkg.Columns().PackageCode,
|
||||
dao.TSimCardBindPkg.Columns().EffectiveStatus,
|
||||
dao.TSimCardBindPkg.Columns().ExpectedExpireTime,
|
||||
dao.TSimCardBindPkg.Columns().TenantId,
|
||||
dao.TSimCardBindPkg.Columns().DeptId,
|
||||
dao.TSimCardBindPkg.Columns().CreatedDept,
|
||||
dao.TSimCardBindPkg.Columns().CreatedBy,
|
||||
).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().Iccid, iccids).
|
||||
WhereIn(dao.TSimCardBindPkg.Columns().PackageType, []int{101, 102}).
|
||||
Where(dao.TSimCardBindPkg.Columns().EffectiveStatus, consts.EffectiveStatusEffective).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().ExpectedExpireTime).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().Id).
|
||||
Scan(&binds); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询卡最新订购记录失败")
|
||||
}
|
||||
return binds, nil
|
||||
}
|
||||
|
||||
func bindExpired(bind *cardLatestBindTarget, now *gtime.Time) bool {
|
||||
if bind == nil {
|
||||
return true
|
||||
}
|
||||
if bind.EffectiveStatus == consts.EffectiveStatusExpired {
|
||||
return true
|
||||
}
|
||||
if bind.ExpectedExpireTime == nil || now == nil {
|
||||
return false
|
||||
}
|
||||
return !bind.ExpectedExpireTime.After(now)
|
||||
}
|
||||
|
||||
func (p *Processor) validateTargetPackage(row *Row, packages map[string]*entity.TSimFlowPackageInfo, packageType int) error {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
pkg := packages[row.PackageCode]
|
||||
if pkg == nil {
|
||||
return gerror.New("加油包不存在")
|
||||
}
|
||||
if pkg.Enabled != 1 {
|
||||
return gerror.New("加油包未启用")
|
||||
}
|
||||
if pkg.PackageType != packageType {
|
||||
return gerror.Newf("加油包类型错误,应为%d", packageType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) loadPackageMap(ctx context.Context, codes []string) (map[string]*entity.TSimFlowPackageInfo, error) {
|
||||
out := make(map[string]*entity.TSimFlowPackageInfo)
|
||||
codes = uniqueStrings(codes)
|
||||
if len(codes) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
var packages []*entity.TSimFlowPackageInfo
|
||||
columns := dao.TSimFlowPackageInfo.Columns()
|
||||
if err := handler.Model(dao.TSimFlowPackageInfo.Ctx(ctx), &handler.Option{FilterTenant: true, FilterAuth: false}).
|
||||
WhereIn(columns.PackageCode, codes).
|
||||
Scan(&packages); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询加油包套餐信息失败")
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
if pkg == nil {
|
||||
continue
|
||||
}
|
||||
out[strings.TrimSpace(pkg.PackageCode)] = pkg
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func collectTypedRows(rows []batchimport.Row) ([]*Row, []string, error) {
|
||||
typedRows := make([]*Row, 0, len(rows))
|
||||
targetCodes := make([]string, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
return nil, nil, gerror.New("加油包批次行类型错误")
|
||||
}
|
||||
typedRows = append(typedRows, row)
|
||||
if row.TargetCode != "" {
|
||||
targetCodes = append(targetCodes, row.TargetCode)
|
||||
}
|
||||
}
|
||||
return typedRows, uniqueStrings(targetCodes), nil
|
||||
}
|
||||
|
||||
func rowPackageCodes(rows []*Row) []string {
|
||||
codes := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row != nil {
|
||||
codes = append(codes, row.PackageCode)
|
||||
}
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
func packageCodesFromJSON(jsonValue *gjson.Json) []string {
|
||||
if jsonValue == nil || jsonValue.IsNil() {
|
||||
return nil
|
||||
}
|
||||
values := jsonValue.Interfaces()
|
||||
out := make([]string, 0, len(values))
|
||||
for _, raw := range values {
|
||||
code := strings.TrimSpace(fmt.Sprint(raw))
|
||||
if code != "" {
|
||||
out = append(out, code)
|
||||
}
|
||||
}
|
||||
return uniqueStrings(out)
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
return false
|
||||
}
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func uniqueStrings(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(in))
|
||||
out := make([]string, 0, len(in))
|
||||
for _, value := range in {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
out = append(out, value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package addpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type PrepareProcessor struct {
|
||||
inner *Processor
|
||||
config batchimport.ProcessorConfig
|
||||
targetBatchType int
|
||||
}
|
||||
|
||||
func NewPrepareProcessor(operation Operation, options SubmitOptions) (*PrepareProcessor, error) {
|
||||
inner, err := NewProcessor(operation, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := inner.Config()
|
||||
cfg.Code = cfg.Code + "_prepare"
|
||||
cfg.BatchType = consts.BatchTypePrepare
|
||||
return &PrepareProcessor{
|
||||
inner: inner,
|
||||
config: cfg,
|
||||
targetBatchType: inner.config.BatchType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) Config() batchimport.ProcessorConfig {
|
||||
return p.config
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return p.inner.ParseRows(ctx, filePath)
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
// Keep submit non-blocking; the worker owns add package prepare validation.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
info := g.Map{}
|
||||
base := p.inner.BuildBatchInfo(ctx, req, result, importOss, resultOss, downloadId)
|
||||
if base != nil {
|
||||
for key, value := range base.Map() {
|
||||
info[key] = value
|
||||
}
|
||||
}
|
||||
info["operation"] = p.config.Code
|
||||
info["batchType"] = consts.BatchTypePrepare
|
||||
info["targetBatchType"] = p.targetBatchType
|
||||
info["targetBatchName"] = addPackageTargetBatchName(p.targetBatchType)
|
||||
info["targetLabel"] = p.inner.config.TargetLabel
|
||||
return gjson.New(info)
|
||||
}
|
||||
|
||||
func addPackageTargetBatchName(batchType int) string {
|
||||
switch batchType {
|
||||
case consts.BatchTypePoolAddPackage:
|
||||
return "流量池加油包"
|
||||
default:
|
||||
return "单卡加油包"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package addpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
config Config
|
||||
options SubmitOptions
|
||||
}
|
||||
|
||||
func NewProcessor(operation Operation, options SubmitOptions) (*Processor, error) {
|
||||
cfg, err := operation.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options, err = normalizeOptions(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Processor{config: cfg, options: options}, nil
|
||||
}
|
||||
|
||||
func (p *Processor) Config() batchimport.ProcessorConfig {
|
||||
maxFileSize := consts.SimCardAddPackageMaxFileSize
|
||||
if p.config.Operation == OperationPool {
|
||||
maxFileSize = consts.SimPoolAddPackageMaxFileSize
|
||||
}
|
||||
return batchimport.ProcessorConfig{
|
||||
Code: p.config.Code,
|
||||
BatchType: p.config.BatchType,
|
||||
BatchNoPrefix: p.config.BatchPrefix,
|
||||
TaskName: p.config.TaskName,
|
||||
ExportScene: p.config.ExportScene,
|
||||
ImportDir: p.config.ImportDir,
|
||||
ResultDir: p.config.ResultDir,
|
||||
MaxFileSize: int64(maxFileSize),
|
||||
TemplateHeaders: p.TemplateHeaders(),
|
||||
RequiredHeaders: []string{p.config.TargetHeader, headerPackageCode},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) TemplateHeaders() []string {
|
||||
return []string{p.config.TargetHeader, headerPackageCode, headerRemark}
|
||||
}
|
||||
|
||||
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return parseExcelRows(ctx, filePath, p.config, p.options)
|
||||
}
|
||||
|
||||
func (p *Processor) ValidateRows(_ context.Context, rows []batchimport.Row) error {
|
||||
firstByTarget := make(map[string]*Row, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
return gerror.New("加油包批次行类型错误")
|
||||
}
|
||||
row.TargetCode = strings.TrimSpace(row.TargetCode)
|
||||
row.PackageCode = strings.TrimSpace(row.PackageCode)
|
||||
row.Remark = strings.TrimSpace(row.Remark)
|
||||
row.EffectiveMode = strings.TrimSpace(row.EffectiveMode)
|
||||
if row.TargetCode == "" {
|
||||
row.AddErrorReason(p.config.TargetLabel + "不能为空")
|
||||
continue
|
||||
}
|
||||
if first, exists := firstByTarget[row.TargetCode]; exists {
|
||||
row.AddErrorReason(fmt.Sprintf("%s与第%d行重复", p.config.TargetLabel, first.RowNo))
|
||||
first.AddErrorReason(p.config.TargetLabel + "重复")
|
||||
continue
|
||||
}
|
||||
firstByTarget[row.TargetCode] = row
|
||||
if row.PackageCode == "" {
|
||||
row.AddErrorReason("加油包编码不能为空")
|
||||
continue
|
||||
}
|
||||
if _, err := NormalizeEffectiveMode(row.EffectiveMode); err != nil {
|
||||
row.AddErrorReason(err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
return rowsValidationError(rows, "加油包批次基础校验失败")
|
||||
}
|
||||
|
||||
func (p *Processor) BuildBatchInfo(_ context.Context, _ batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
info := g.Map{
|
||||
"operation": p.config.Code,
|
||||
"batchMode": string(p.config.Operation),
|
||||
"batchType": p.config.BatchType,
|
||||
"targetLabel": p.config.TargetLabel,
|
||||
"packageCode": p.options.PackageCode,
|
||||
"remark": p.options.Remark,
|
||||
"paymentMode": p.options.PaymentMode,
|
||||
"offlineContractCompleted": p.options.OfflineContractCompleted,
|
||||
"effectiveMode": p.options.EffectiveMode,
|
||||
"effectiveModeText": EffectiveModeText(p.options.EffectiveMode),
|
||||
"autoConfirm": p.options.AutoConfirm,
|
||||
"resultFile": g.Map{"status": "pending"},
|
||||
}
|
||||
if result != nil && result.ImportFileOssId > 0 {
|
||||
info["importFileOssId"] = result.ImportFileOssId
|
||||
} else if importOss != nil && importOss.OssId > 0 {
|
||||
info["importFileOssId"] = importOss.OssId
|
||||
}
|
||||
if resultOss != nil && resultOss.OssId > 0 {
|
||||
info["resultFileOssId"] = resultOss.OssId
|
||||
}
|
||||
if downloadId > 0 {
|
||||
info["resultDownloadId"] = downloadId
|
||||
}
|
||||
return gjson.New(info)
|
||||
}
|
||||
|
||||
func rowsValidationError(rows []batchimport.Row, prefix string) error {
|
||||
reasons := make([]string, 0)
|
||||
for _, item := range rows {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
rowReasons := item.GetErrorReasons()
|
||||
if len(rowReasons) == 0 {
|
||||
continue
|
||||
}
|
||||
reasons = append(reasons, fmt.Sprintf("第%d行:%s", item.GetRowNo(), strings.Join(rowReasons, ";")))
|
||||
if len(reasons) >= 5 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(reasons) == 0 {
|
||||
return nil
|
||||
}
|
||||
return gerror.New(prefix + ":" + strings.Join(reasons, ";"))
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package addpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/iot_excelize"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type Submitter struct {
|
||||
operation Operation
|
||||
}
|
||||
|
||||
func NewCardSubmitter() *Submitter {
|
||||
return &Submitter{operation: OperationCard}
|
||||
}
|
||||
|
||||
func NewPoolSubmitter() *Submitter {
|
||||
return &Submitter{operation: OperationPool}
|
||||
}
|
||||
|
||||
func (s *Submitter) DownloadTemplate(ctx context.Context) error {
|
||||
processor, err := NewProcessor(s.operation, SubmitOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f := excelize.NewFile()
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
headers := processor.TemplateHeaders()
|
||||
if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := iot_excelize.ApplyHeaderColumnsTextStyle(f); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.SetColWidth("Sheet1", "A", "B", 24); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.SetColWidth("Sheet1", "C", "C", 32); err != nil {
|
||||
return err
|
||||
}
|
||||
fileName := "sim_card_add_package_template"
|
||||
if s.operation == OperationPool {
|
||||
fileName = "sim_pool_add_package_template"
|
||||
}
|
||||
return iot_excelize.WriteHTTP(ctx, f, fileName)
|
||||
}
|
||||
|
||||
func (s *Submitter) SubmitExcel(ctx context.Context, file *ghttp.UploadFile, options SubmitOptions) (*SubmitResult, error) {
|
||||
options.AutoConfirm = !options.ManualConfirm
|
||||
processor, err := NewPrepareProcessor(s.operation, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := batchimport.SubmitRequest{
|
||||
SourceChannel: batchmetadata.Console,
|
||||
ExecutionMode: batchmetadata.Bulk,
|
||||
File: file,
|
||||
BizParams: &processor.inner.options,
|
||||
SourceType: consts.BatchSourceTypeFile,
|
||||
Operator: common.CurrentOperator(ctx),
|
||||
}
|
||||
result, err := batchimport.CreatePendingUpload(ctx, req, processor, batchimport.PendingOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
common.PublishBatchMessage(ctx, result, consts.QueueBatchExecute, string(s.operation)+"_add_package_prepare")
|
||||
res := pendingResultFromBatch(result, consts.BatchTypePrepare)
|
||||
res.TargetBatchType = processor.targetBatchType
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Submitter) SubmitInput(ctx context.Context, input string, options SubmitOptions) (*SubmitResult, error) {
|
||||
// Keep the legacy input/submit endpoint from bypassing async validation.
|
||||
return s.PrepareInput(ctx, input, options)
|
||||
}
|
||||
|
||||
func (s *Submitter) PrepareInput(ctx context.Context, input string, options SubmitOptions) (*SubmitResult, error) {
|
||||
if strings.TrimSpace(options.PackageCode) != "" || strings.TrimSpace(options.EffectiveMode) != "" {
|
||||
options.DefaultEffectiveMode = true
|
||||
}
|
||||
processor, err := NewPrepareProcessor(s.operation, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := parseInputRows(ctx, input, processor.inner.options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := batchimport.SubmitRequest{
|
||||
SourceChannel: batchmetadata.Console,
|
||||
ExecutionMode: batchmetadata.Bulk,
|
||||
BizParams: &processor.inner.options,
|
||||
SourceType: consts.BatchSourceTypeBatchInput,
|
||||
Operator: common.CurrentOperator(ctx),
|
||||
}
|
||||
result, err := batchimport.CreatePendingRows(ctx, req, processor, rows, batchimport.PendingOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
common.PublishBatchMessage(ctx, result, consts.QueueBatchExecute, string(s.operation)+"_add_package_prepare")
|
||||
res := pendingResultFromBatch(result, consts.BatchTypePrepare)
|
||||
res.TargetBatchType = processor.targetBatchType
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func pendingResultFromBatch(result *batchimport.Result, batchType int) *SubmitResult {
|
||||
res := &SubmitResult{
|
||||
BatchType: batchType,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
}
|
||||
if result == nil {
|
||||
return res
|
||||
}
|
||||
valid := result.ValidRows
|
||||
if result.TotalRows > 0 && valid == 0 {
|
||||
valid = result.TotalRows - result.ErrorRows
|
||||
if valid < 0 {
|
||||
valid = 0
|
||||
}
|
||||
}
|
||||
res.BatchNo = result.BatchNo
|
||||
res.BatchType = result.BatchType
|
||||
if batchType != result.BatchType {
|
||||
res.TargetBatchType = batchType
|
||||
}
|
||||
res.Total = result.TotalRows
|
||||
res.ValidCount = valid
|
||||
res.FailCount = result.ErrorRows
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package addpackage
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const (
|
||||
OperationCard Operation = "card"
|
||||
OperationPool Operation = "pool"
|
||||
)
|
||||
|
||||
type Operation string
|
||||
|
||||
type Config struct {
|
||||
Operation Operation
|
||||
Code string
|
||||
BatchType int
|
||||
BatchPrefix string
|
||||
TaskName string
|
||||
ExportScene int
|
||||
ImportDir string
|
||||
ResultDir string
|
||||
TargetHeader string
|
||||
TargetLabel string
|
||||
}
|
||||
|
||||
type SubmitOptions struct {
|
||||
PackageCode string
|
||||
Remark string
|
||||
PaymentMode string
|
||||
OfflineContractCompleted bool
|
||||
EffectiveMode string
|
||||
DefaultEffectiveMode bool
|
||||
ManualConfirm bool
|
||||
AutoConfirm bool
|
||||
}
|
||||
|
||||
type SubmitResult struct {
|
||||
BatchNo string
|
||||
BatchType int
|
||||
TargetBatchType int
|
||||
BatchAsyncStatus int
|
||||
Total int
|
||||
ValidCount int
|
||||
FailCount int
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
RowNo int
|
||||
TargetCode string
|
||||
PackageCode string
|
||||
Remark string
|
||||
EffectiveMode string
|
||||
Authorized bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (op Operation) Config() (Config, error) {
|
||||
switch op {
|
||||
case OperationCard:
|
||||
return Config{
|
||||
Operation: op,
|
||||
Code: "sim_card_add_package",
|
||||
BatchType: consts.BatchTypeCardAddPackage,
|
||||
BatchPrefix: consts.BatchNoPrefixCardAddPackage,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimCardAddPackage,
|
||||
ExportScene: consts.ResourceExportSceneCardAddPackage,
|
||||
ImportDir: consts.SimCardAddPackageSubDirImportExcel,
|
||||
ResultDir: consts.SimCardAddPackageSubDirResultExcel,
|
||||
TargetHeader: "ICCID",
|
||||
TargetLabel: "ICCID",
|
||||
}, nil
|
||||
case OperationPool:
|
||||
return Config{
|
||||
Operation: op,
|
||||
Code: "sim_pool_add_package",
|
||||
BatchType: consts.BatchTypePoolAddPackage,
|
||||
BatchPrefix: consts.BatchNoPrefixPoolAddPackage,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimPoolAddPackage,
|
||||
ExportScene: consts.ResourceExportScenePoolAddPackage,
|
||||
ImportDir: consts.SimPoolAddPackageSubDirImportExcel,
|
||||
ResultDir: consts.SimPoolAddPackageSubDirResultExcel,
|
||||
TargetHeader: "流量池",
|
||||
TargetLabel: "流量池",
|
||||
}, nil
|
||||
default:
|
||||
return Config{}, gerror.New("加油包操作类型不支持")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Row) GetRowNo() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
return r.RowNo
|
||||
}
|
||||
|
||||
func (r *Row) GetItemID() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(r.TargetCode)
|
||||
}
|
||||
|
||||
func (r *Row) GetErrorReasons() []string {
|
||||
if r == nil || strings.TrimSpace(r.Reason) == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{strings.TrimSpace(r.Reason)}
|
||||
}
|
||||
|
||||
func (r *Row) AddErrorReason(reason string) {
|
||||
if r == nil || strings.TrimSpace(r.Reason) != "" || strings.TrimSpace(reason) == "" {
|
||||
return
|
||||
}
|
||||
r.Reason = strings.TrimSpace(reason)
|
||||
}
|
||||
|
||||
func (r *Row) Snapshot() *gjson.Json {
|
||||
if r == nil {
|
||||
return gjson.New(g.Map{})
|
||||
}
|
||||
data := g.Map{
|
||||
"targetCode": strings.TrimSpace(r.TargetCode),
|
||||
"packageCode": strings.TrimSpace(r.PackageCode),
|
||||
"remark": strings.TrimSpace(r.Remark),
|
||||
"authorized": r.Authorized,
|
||||
}
|
||||
if mode := strings.TrimSpace(r.EffectiveMode); mode != "" {
|
||||
data["effectiveMode"] = mode
|
||||
data["effectiveModeText"] = EffectiveModeText(mode)
|
||||
}
|
||||
return gjson.New(data)
|
||||
}
|
||||
|
||||
const (
|
||||
EffectiveModeCurrentMonth = "current_month"
|
||||
)
|
||||
|
||||
func NormalizeEffectiveMode(mode string) (string, error) {
|
||||
switch strings.TrimSpace(mode) {
|
||||
case "", "immediate", "current_month", "this_month", "本月", "本月生效", "当月", "当月生效":
|
||||
return EffectiveModeCurrentMonth, nil
|
||||
case "next_month", "nextMonth", "下月", "次月", "次月生效":
|
||||
return "", gerror.New("加油包只支持本月生效")
|
||||
default:
|
||||
return "", gerror.New("加油包生效方式不正确")
|
||||
}
|
||||
}
|
||||
|
||||
func EffectiveModeText(mode string) string {
|
||||
_, _ = NormalizeEffectiveMode(mode)
|
||||
return "本月生效"
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package allocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/text"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
func ParseExcelFileRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
parsedRows, err := excel.ParseFile(ctx, filePath, excel.ParseOptions{
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
SingleSheet: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(parsedRows))
|
||||
for _, parsed := range parsedRows {
|
||||
row := &Row{
|
||||
RowNo: parsed.RowNo,
|
||||
Iccid: excel.Field(parsed, "ICCID"),
|
||||
Msisdn: excel.Field(parsed, "MSISDN"),
|
||||
Imsi: excel.Field(parsed, "IMSI"),
|
||||
TargetDeptKey: firstExcelField(parsed, "目标客户编码", "客户编码", "TARGETDEPTKEY", "TARGET_CUSTOMER_CODE"),
|
||||
TargetDeptName: firstExcelField(parsed, "目标客户名称", "客户名称", "TARGETDEPTNAME", "TARGET_CUSTOMER_NAME"),
|
||||
Remark: firstExcelField(parsed, "划拨备注", "备注", "REMARK"),
|
||||
}
|
||||
if value := firstExcelField(parsed, "目标客户ID", "目标部门ID", "TARGETDEPTID", "TARGET_CUSTOMER_ID"); value != "" {
|
||||
targetDeptID, parseErr := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
||||
if parseErr != nil || targetDeptID <= 0 {
|
||||
row.AddErrorReason("目标客户ID格式错误")
|
||||
} else {
|
||||
row.TargetDeptId = targetDeptID
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func ParseInputRows(ctx context.Context, input string, targetDeptID int64) ([]batchimport.Row, error) {
|
||||
lines, err := text.ParseWhitespaceFields(ctx, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, text.ErrEmptyInput) {
|
||||
return nil, gerror.New("批量输入内容不能为空")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
row := &Row{RowNo: line.RowNo, TargetDeptId: targetDeptID}
|
||||
switch len(line.Fields) {
|
||||
case 1:
|
||||
row.Iccid = line.Fields[0]
|
||||
case 3:
|
||||
row.Iccid = line.Fields[0]
|
||||
row.Msisdn = line.Fields[1]
|
||||
row.Imsi = line.Fields[2]
|
||||
default:
|
||||
row.AddErrorReason("格式错误,应为 ICCID 或 ICCID MSISDN IMSI")
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func firstExcelField(row excel.ParsedRow, names ...string) string {
|
||||
for _, name := range names {
|
||||
if value := excel.Field(row, name); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package allocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/allocationunits"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const (
|
||||
importDir = "sim-card-allocate/import"
|
||||
resultDir = "sim-card-allocate/result"
|
||||
maxFileSize = 50 << 20
|
||||
maxAllocateRows = 10000
|
||||
)
|
||||
|
||||
type Processor struct{}
|
||||
|
||||
func NewProcessor() *Processor {
|
||||
return &Processor{}
|
||||
}
|
||||
|
||||
func (p *Processor) Config() batchimport.ProcessorConfig {
|
||||
return batchimport.ProcessorConfig{
|
||||
Code: "sim_card_allocate",
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
BatchNoPrefix: consts.BatchNoPrefixAllocate,
|
||||
TaskName: "卡划拨结果",
|
||||
ExportScene: consts.ResourceExportSceneAllocate,
|
||||
ImportDir: importDir,
|
||||
ResultDir: resultDir,
|
||||
MaxFileSize: maxFileSize,
|
||||
MaxRows: maxAllocateRows,
|
||||
TemplateHeaders: []string{"ICCID", "MSISDN", "IMSI", "目标客户名称"},
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return ParseExcelFileRows(ctx, filePath)
|
||||
}
|
||||
|
||||
func (p *Processor) ValidateRows(_ context.Context, rows []batchimport.Row) error {
|
||||
firstByIccid := make(map[string]*Row, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
continue
|
||||
}
|
||||
row.Iccid = strings.TrimSpace(row.Iccid)
|
||||
row.Msisdn = strings.TrimSpace(row.Msisdn)
|
||||
row.Imsi = strings.TrimSpace(row.Imsi)
|
||||
row.TargetDeptKey = strings.TrimSpace(row.TargetDeptKey)
|
||||
row.TargetDeptName = strings.TrimSpace(row.TargetDeptName)
|
||||
row.Remark = strings.TrimSpace(row.Remark)
|
||||
if row.Iccid == "" {
|
||||
row.AddErrorReason("ICCID不能为空")
|
||||
continue
|
||||
}
|
||||
if first, exists := firstByIccid[row.Iccid]; exists {
|
||||
row.AddErrorReason(fmt.Sprintf("ICCID与第%d行重复", first.RowNo))
|
||||
first.AddErrorReason("ICCID重复")
|
||||
continue
|
||||
}
|
||||
firstByIccid[row.Iccid] = row
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) BuildBatchInfo(_ context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
options := optionsFromParams(req.BizParams)
|
||||
info := g.Map{
|
||||
"operation": "sim_card_allocate",
|
||||
"configName": "卡划拨",
|
||||
"submitScene": submitScene(req.SourceType),
|
||||
"targetDeptId": options.TargetDeptId,
|
||||
"remark": strings.TrimSpace(options.Remark),
|
||||
"resultFile": g.Map{"status": "pending"},
|
||||
}
|
||||
if result != nil && result.ImportFileOssId > 0 {
|
||||
info["importFileOssId"] = result.ImportFileOssId
|
||||
} else if importOss != nil && importOss.OssId > 0 {
|
||||
info["importFileOssId"] = importOss.OssId
|
||||
}
|
||||
if resultOss != nil && resultOss.OssId > 0 {
|
||||
info["resultFileOssId"] = resultOss.OssId
|
||||
}
|
||||
if downloadId > 0 {
|
||||
info["resultDownloadId"] = downloadId
|
||||
}
|
||||
return allocationunits.Merge(gjson.New(info), options.Units)
|
||||
}
|
||||
|
||||
func optionsFromParams(params any) SubmitOptions {
|
||||
if options, ok := params.(*SubmitOptions); ok && options != nil {
|
||||
return *options
|
||||
}
|
||||
return SubmitOptions{}
|
||||
}
|
||||
|
||||
func submitScene(sourceType int) string {
|
||||
switch sourceType {
|
||||
case consts.BatchSourceTypeFile:
|
||||
return "excel"
|
||||
case consts.BatchSourceTypeBatchInput:
|
||||
return "input"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package allocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func TestProcessorTemplateHeaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expected := []string{"ICCID", "MSISDN", "IMSI", "目标客户名称"}
|
||||
if !reflect.DeepEqual(NewProcessor().Config().TemplateHeaders, expected) {
|
||||
t.Fatalf("unexpected headers: %#v", NewProcessor().Config().TemplateHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExcelFileRowsCustomerFacingColumns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filePath := filepath.Join(t.TempDir(), "allocate.xlsx")
|
||||
f := excelize.NewFile()
|
||||
headers := []any{"ICCID", "MSISDN", "IMSI", "目标客户名称"}
|
||||
if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil {
|
||||
t.Fatalf("set headers failed: %v", err)
|
||||
}
|
||||
row := []any{
|
||||
"8986000000000000001",
|
||||
"13800008000",
|
||||
"460001234567890",
|
||||
"示例企业",
|
||||
}
|
||||
if err := f.SetSheetRow("Sheet1", "A2", &row); err != nil {
|
||||
t.Fatalf("set row failed: %v", err)
|
||||
}
|
||||
if err := f.SaveAs(filePath); err != nil {
|
||||
t.Fatalf("save excel failed: %v", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
|
||||
rows, err := ParseExcelFileRows(context.Background(), filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("parse excel failed: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("unexpected row count: %d", len(rows))
|
||||
}
|
||||
rowModel, ok := rows[0].(*Row)
|
||||
if !ok || rowModel == nil {
|
||||
t.Fatalf("unexpected row type: %#v", rows[0])
|
||||
}
|
||||
if rowModel.Iccid != "8986000000000000001" {
|
||||
t.Fatalf("unexpected iccid: %s", rowModel.Iccid)
|
||||
}
|
||||
if rowModel.Msisdn != "13800008000" {
|
||||
t.Fatalf("unexpected msisdn: %s", rowModel.Msisdn)
|
||||
}
|
||||
if rowModel.Imsi != "460001234567890" {
|
||||
t.Fatalf("unexpected imsi: %s", rowModel.Imsi)
|
||||
}
|
||||
if rowModel.TargetDeptName != "示例企业" {
|
||||
t.Fatalf("unexpected target dept name: %s", rowModel.TargetDeptName)
|
||||
}
|
||||
if rowModel.Remark != "" {
|
||||
t.Fatalf("unexpected remark: %s", rowModel.Remark)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessorConfigLimitsAllocationRows(t *testing.T) {
|
||||
config := NewProcessor().Config()
|
||||
if config.MaxRows != 10000 {
|
||||
t.Fatalf("MaxRows = %d, want 10000", config.MaxRows)
|
||||
}
|
||||
if config.MaxFileSize != 50<<20 {
|
||||
t.Fatalf("MaxFileSize = %d, want %d", config.MaxFileSize, 50<<20)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
package allocate
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/allocationunits"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"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/batchinput/excel"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"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/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAllocateChunkSize = 5000
|
||||
defaultAllocateMaxFileSize = 50 << 20
|
||||
minAllocateChunkSize = 100
|
||||
maxAllocateChunkSize = 10000
|
||||
allocateDetailInsertSize = 500
|
||||
allocateParentJobRole = "allocate_parent"
|
||||
allocateChunkJobRole = "allocate_chunk"
|
||||
)
|
||||
|
||||
type allocateSplitState struct {
|
||||
ParentBatchNo string
|
||||
ChunkSize int
|
||||
ChunkCount int
|
||||
Total int
|
||||
Valid int
|
||||
Failed int
|
||||
}
|
||||
|
||||
type allocateChunkStream func(context.Context, func([]batchimport.Row) error) (int, error)
|
||||
|
||||
func allocateChunkSize(ctx context.Context) int {
|
||||
size := g.Cfg().MustGet(ctx, "batch.allocate.chunkSize", defaultAllocateChunkSize).Int()
|
||||
if size < minAllocateChunkSize {
|
||||
return minAllocateChunkSize
|
||||
}
|
||||
if size > maxAllocateChunkSize {
|
||||
return maxAllocateChunkSize
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func allocateMaxFileSize(ctx context.Context) int64 {
|
||||
size := g.Cfg().MustGet(ctx, "batch.allocate.maxFileSize", defaultAllocateMaxFileSize).Int64()
|
||||
if size <= 0 {
|
||||
return defaultAllocateMaxFileSize
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func createPendingSplit(
|
||||
ctx context.Context,
|
||||
req batchimport.SubmitRequest,
|
||||
p batchimport.PendingProcessor,
|
||||
options batchimport.PendingOptions,
|
||||
parentBatchNo string,
|
||||
importOss *model.SysOssSaveContentModel,
|
||||
stream allocateChunkStream,
|
||||
) (*PendingResult, error) {
|
||||
units, err := allocationunits.Snapshot(ctx, req.Operator.TenantId, req.Operator.DeptId, req.TargetDeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts, ok := req.BizParams.(*SubmitOptions); ok {
|
||||
opts.Units = units
|
||||
}
|
||||
|
||||
if err := batchmetadata.ValidateCreation(req.SourceChannel, req.ExecutionMode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p == nil {
|
||||
return nil, gerror.New("allocate processor is required")
|
||||
}
|
||||
if strings.TrimSpace(parentBatchNo) == "" {
|
||||
return nil, gerror.New("parent batch number is required")
|
||||
}
|
||||
if stream == nil {
|
||||
return nil, gerror.New("allocate input stream is required")
|
||||
}
|
||||
|
||||
chunkSize := allocateChunkSize(ctx)
|
||||
state := &allocateSplitState{
|
||||
ParentBatchNo: parentBatchNo,
|
||||
ChunkSize: chunkSize,
|
||||
}
|
||||
if err := createAllocateParent(ctx, req, p, parentBatchNo, importOss, chunkSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
children := make([]*batchimport.Result, 0)
|
||||
|
||||
if _, err := stream(ctx, func(rows []batchimport.Row) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := p.ValidateRows(ctx, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
if options.PrepareRows != nil {
|
||||
if err := options.PrepareRows(ctx, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
priorFailures, err := markAllocateCrossChunkDuplicates(ctx, parentBatchNo, req.Operator.TenantId, rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.Valid -= priorFailures
|
||||
state.Failed += priorFailures
|
||||
|
||||
state.ChunkCount++
|
||||
result, err := createAllocateChunk(ctx, req, p, parentBatchNo, state.ChunkCount, rows, importOss, state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
children = append(children, result)
|
||||
return nil
|
||||
}); err != nil {
|
||||
markAllocateParentFailed(ctx, parentBatchNo, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
if state.Total == 0 {
|
||||
err := gerror.New("allocate input has no data rows")
|
||||
markAllocateParentFailed(ctx, parentBatchNo, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
if err := markAllocateSplitCompleted(ctx, parentBatchNo, state); err != nil {
|
||||
markAllocateParentFailed(ctx, parentBatchNo, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
for _, child := range children {
|
||||
if err := common.PublishBatchMessageStrict(ctx, child, consts.QueueBatchExecute, "allocate"); err != nil {
|
||||
markAllocateParentFailed(ctx, parentBatchNo, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
parentMessage := &batchimport.Result{
|
||||
BatchNo: parentBatchNo,
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
}
|
||||
if err := common.PublishBatchMessageStrict(ctx, parentMessage, consts.QueueBatchExecute, "allocate_parent"); err != nil {
|
||||
markAllocateParentFailed(ctx, parentBatchNo, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PendingResult{
|
||||
BatchNo: parentBatchNo,
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
|
||||
Total: state.Total,
|
||||
ValidCount: state.Valid,
|
||||
FailCount: state.Failed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createAllocateParent(ctx context.Context, req batchimport.SubmitRequest, p batchimport.PendingProcessor, parentBatchNo string, importOss *model.SysOssSaveContentModel, chunkSize int) error {
|
||||
result := &batchimport.Result{
|
||||
BatchNo: parentBatchNo,
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
ImportFileOssId: ossIDFromContent(importOss),
|
||||
ImportFileUrl: urlFromContent(importOss),
|
||||
}
|
||||
info := p.BuildBatchInfo(ctx, req, result, importOss, nil, 0)
|
||||
info = withAllocateInfo(info, map[string]any{
|
||||
"targetBatchType": consts.BatchTypeAllocate,
|
||||
"jobRole": allocateParentJobRole,
|
||||
"autoSplit": true,
|
||||
"chunkSize": chunkSize,
|
||||
"chunkCount": 0,
|
||||
"completedChunkCount": 0,
|
||||
"parsedCount": 0,
|
||||
})
|
||||
now := gtime.Now()
|
||||
_, err := g.DB().Model(dao.BatchMain.Table()).Data(do.BatchMain{
|
||||
BatchNo: parentBatchNo,
|
||||
OrderSn: "",
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
BatchStatus: consts.BatchStatusNormal,
|
||||
StartTime: now,
|
||||
SourceType: req.SourceType,
|
||||
SourceChannel: batchmetadata.Channel(req.SourceChannel),
|
||||
ExecutionMode: req.ExecutionMode,
|
||||
SourceOssId: ossIDFromContent(importOss),
|
||||
IsCrossDept: 0,
|
||||
TargetDeptId: req.TargetDeptId,
|
||||
DeptId: req.Operator.DeptId,
|
||||
TotalCount: 0,
|
||||
ValidCount: 0,
|
||||
SuccessCount: 0,
|
||||
FailCount: 0,
|
||||
RunResult: consts.BatchRunResultPending,
|
||||
BatchInfo: info,
|
||||
TenantId: req.Operator.TenantId,
|
||||
CreatedDept: req.Operator.DeptId,
|
||||
CreatedBy: req.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "insert allocate parent batch failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createAllocateChunk(ctx context.Context, req batchimport.SubmitRequest, p batchimport.PendingProcessor, parentBatchNo string, chunkNo int, rows []batchimport.Row, importOss *model.SysOssSaveContentModel, state *allocateSplitState) (*batchimport.Result, error) {
|
||||
childBatchNo, err := batchimport.NextRandomNo(consts.BatchNoPrefixAllocate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validCount, failCount := countBatchRows(rows)
|
||||
result := &batchimport.Result{
|
||||
ImportStatus: consts.ImportStatusSuccess,
|
||||
BatchNo: childBatchNo,
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
TotalRows: len(rows),
|
||||
ValidRows: validCount,
|
||||
ErrorRows: failCount,
|
||||
ImportFileOssId: ossIDFromContent(importOss),
|
||||
ImportFileUrl: urlFromContent(importOss),
|
||||
}
|
||||
childReq := req
|
||||
childReq.BatchNo = childBatchNo
|
||||
childReq.SourceBatchNo = parentBatchNo
|
||||
info := p.BuildBatchInfo(ctx, childReq, result, importOss, nil, 0)
|
||||
info = withAllocateInfo(info, map[string]any{
|
||||
"targetBatchType": consts.BatchTypeAllocate,
|
||||
"jobRole": allocateChunkJobRole,
|
||||
"parentBatchNo": parentBatchNo,
|
||||
"chunkNo": chunkNo,
|
||||
"chunkSize": len(rows),
|
||||
})
|
||||
now := gtime.Now()
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if _, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).Data(do.BatchMain{
|
||||
BatchNo: childBatchNo,
|
||||
OrderSn: "",
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
BatchStatus: consts.BatchStatusNormal,
|
||||
SourceType: req.SourceType,
|
||||
SourceChannel: batchmetadata.Channel(req.SourceChannel),
|
||||
ExecutionMode: req.ExecutionMode,
|
||||
SourceBatchNo: parentBatchNo,
|
||||
SourceOssId: ossIDFromContent(importOss),
|
||||
IsCrossDept: 0,
|
||||
TargetDeptId: req.TargetDeptId,
|
||||
DeptId: req.Operator.DeptId,
|
||||
TotalCount: len(rows),
|
||||
ValidCount: validCount,
|
||||
SuccessCount: 0,
|
||||
FailCount: failCount,
|
||||
RunResult: consts.BatchRunResultPending,
|
||||
BatchInfo: info,
|
||||
TenantId: req.Operator.TenantId,
|
||||
CreatedDept: req.Operator.DeptId,
|
||||
CreatedBy: req.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "insert allocate child batch failed")
|
||||
}
|
||||
|
||||
details := make([]do.BatchDetail, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
status := consts.BatchDetailStatusPending
|
||||
if len(row.GetErrorReasons()) > 0 {
|
||||
status = consts.BatchDetailStatusFailed
|
||||
}
|
||||
details = append(details, do.BatchDetail{
|
||||
BatchNo: childBatchNo,
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
DetailStatus: status,
|
||||
BlankOutStatus: consts.BlankOutStatusNo,
|
||||
RowNo: row.GetRowNo(),
|
||||
ItemId: row.GetItemID(),
|
||||
ErrorReason: strings.Join(row.GetErrorReasons(), "; "),
|
||||
Snapshot: allocationunits.Merge(row.Snapshot(), sourceUnitInfo(info)),
|
||||
TenantId: req.Operator.TenantId,
|
||||
CreatedDept: req.Operator.DeptId,
|
||||
CreatedBy: req.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
for start := 0; start < len(details); start += allocateDetailInsertSize {
|
||||
end := start + allocateDetailInsertSize
|
||||
if end > len(details) {
|
||||
end = len(details)
|
||||
}
|
||||
if _, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).Data(details[start:end]).Insert(); err != nil {
|
||||
return gerror.Wrapf(err, "insert allocate details failed, range:%d-%d", start+1, end)
|
||||
}
|
||||
}
|
||||
|
||||
parent := &entity.BatchMain{}
|
||||
columns := dao.BatchMain.Columns()
|
||||
if err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(columns.BatchNo, parentBatchNo).
|
||||
Where(columns.BatchType, consts.BatchTypeAllocate).
|
||||
LockUpdate().Scan(parent); err != nil {
|
||||
return gerror.Wrap(err, "load allocate parent batch failed")
|
||||
}
|
||||
if parent.Id == 0 {
|
||||
return gerror.New("allocate parent batch does not exist")
|
||||
}
|
||||
parentInfo := withAllocateInfo(parent.BatchInfo, map[string]any{
|
||||
"chunkCount": chunkNo,
|
||||
"parsedCount": parent.TotalCount + len(rows),
|
||||
})
|
||||
if _, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).Where(columns.Id, parent.Id).Data(do.BatchMain{
|
||||
TotalCount: parent.TotalCount + len(rows),
|
||||
ValidCount: parent.ValidCount + validCount,
|
||||
FailCount: parent.FailCount + failCount,
|
||||
BatchInfo: parentInfo,
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "update allocate parent split progress failed")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if state != nil {
|
||||
state.Total += len(rows)
|
||||
state.Valid += validCount
|
||||
state.Failed += failCount
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func markAllocateSplitCompleted(ctx context.Context, parentBatchNo string, state *allocateSplitState) error {
|
||||
columns := dao.BatchMain.Columns()
|
||||
parent := &entity.BatchMain{}
|
||||
if err := g.DB().Model(dao.BatchMain.Table()).
|
||||
Where(columns.BatchNo, parentBatchNo).
|
||||
Where(columns.BatchType, consts.BatchTypeAllocate).
|
||||
Scan(parent); err != nil {
|
||||
return err
|
||||
}
|
||||
if parent.Id == 0 {
|
||||
return gerror.New("allocate parent batch does not exist")
|
||||
}
|
||||
info := withAllocateInfo(parent.BatchInfo, map[string]any{
|
||||
"splitCompleted": true,
|
||||
"chunkCount": state.ChunkCount,
|
||||
"completedChunkCount": 0,
|
||||
"parsedCount": state.Total,
|
||||
})
|
||||
_, err := g.DB().Model(dao.BatchMain.Table()).Where(columns.Id, parent.Id).Data(do.BatchMain{
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
|
||||
RunResult: consts.BatchRunResultRunning,
|
||||
BatchInfo: info,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}).Update()
|
||||
return err
|
||||
}
|
||||
|
||||
func markAllocateParentFailed(ctx context.Context, parentBatchNo, reason string) {
|
||||
if strings.TrimSpace(parentBatchNo) == "" {
|
||||
return
|
||||
}
|
||||
now := gtime.Now()
|
||||
columns := dao.BatchMain.Columns()
|
||||
_, _ = g.DB().Model(dao.BatchMain.Table()).
|
||||
Where(columns.BatchNo, parentBatchNo).
|
||||
Where(columns.BatchType, consts.BatchTypeAllocate).
|
||||
WhereIn(columns.BatchAsyncStatus, []int{consts.BatchAsyncStatusPending, consts.BatchAsyncStatusProcessing}).
|
||||
Data(do.BatchMain{
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
|
||||
RunResult: consts.BatchRunResultFailed,
|
||||
FailReason: strings.TrimSpace(reason),
|
||||
EndTime: now,
|
||||
UpdatedAt: now,
|
||||
}).Update()
|
||||
}
|
||||
|
||||
func countBatchRows(rows []batchimport.Row) (valid, failed int) {
|
||||
for _, row := range rows {
|
||||
if len(row.GetErrorReasons()) == 0 {
|
||||
valid++
|
||||
} else {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func markAllocateCrossChunkDuplicates(ctx context.Context, parentBatchNo, tenantID string, rows []batchimport.Row) (int, error) {
|
||||
if strings.TrimSpace(parentBatchNo) == "" || len(rows) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
columns := dao.BatchDetail.Columns()
|
||||
itemIDs := make([]string, 0, len(rows))
|
||||
seen := make(map[string]struct{}, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
continue
|
||||
}
|
||||
itemID := strings.TrimSpace(row.Iccid)
|
||||
if itemID == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[itemID]; exists {
|
||||
continue
|
||||
}
|
||||
seen[itemID] = struct{}{}
|
||||
itemIDs = append(itemIDs, itemID)
|
||||
}
|
||||
if len(itemIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var existing []struct {
|
||||
Id int64 `orm:"id"`
|
||||
BatchNo string `orm:"batch_no"`
|
||||
ItemId string `orm:"item_id"`
|
||||
DetailStatus int `orm:"detail_status"`
|
||||
}
|
||||
query := g.DB().Model(dao.BatchDetail.Table()).
|
||||
Fields(columns.Id, columns.BatchNo, columns.ItemId, columns.DetailStatus).
|
||||
Where(columns.BatchType, consts.BatchTypeAllocate).
|
||||
Where(columns.TenantId, tenantID).
|
||||
WhereIn(columns.ItemId, itemIDs).
|
||||
Where("batch_no IN (SELECT batch_no FROM batch_main WHERE source_batch_no = ? AND batch_type = ? AND tenant_id = ?)",
|
||||
parentBatchNo, consts.BatchTypeAllocate, tenantID)
|
||||
if err := query.Scan(&existing); err != nil {
|
||||
return 0, gerror.Wrap(err, "check duplicate allocate ICCIDs across chunks failed")
|
||||
}
|
||||
if len(existing) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
priorFailures := 0
|
||||
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
for _, detail := range existing {
|
||||
if detail.DetailStatus != consts.BatchDetailStatusPending {
|
||||
continue
|
||||
}
|
||||
result, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(columns.Id, detail.Id).
|
||||
Where(columns.TenantId, tenantID).
|
||||
Where(columns.DetailStatus, consts.BatchDetailStatusPending).
|
||||
Data(do.BatchDetail{DetailStatus: consts.BatchDetailStatusFailed, ErrorReason: "ICCID在划拨批次内重复"}).Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "mark prior duplicate allocation detail failed")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected == 0 {
|
||||
continue
|
||||
}
|
||||
priorFailures++
|
||||
mainColumns := dao.BatchMain.Columns()
|
||||
for _, batchNo := range []string{detail.BatchNo, parentBatchNo} {
|
||||
if _, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(mainColumns.BatchNo, batchNo).
|
||||
Where(mainColumns.TenantId, tenantID).
|
||||
Data(do.BatchMain{ValidCount: gdb.Raw("valid_count - 1"), FailCount: gdb.Raw("fail_count + 1")}).Update(); err != nil {
|
||||
return gerror.Wrap(err, "update cross-chunk duplicate counts failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
duplicates := make(map[string]struct{}, len(existing))
|
||||
for _, item := range existing {
|
||||
if itemID := strings.TrimSpace(item.ItemId); itemID != "" {
|
||||
duplicates[itemID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
continue
|
||||
}
|
||||
if _, exists := duplicates[strings.TrimSpace(row.Iccid)]; exists {
|
||||
row.AddErrorReason("ICCID在划拨批次内重复")
|
||||
}
|
||||
}
|
||||
return priorFailures, nil
|
||||
}
|
||||
|
||||
func withAllocateInfo(info *gjson.Json, values map[string]any) *gjson.Json {
|
||||
data := make(map[string]any)
|
||||
if info != nil {
|
||||
for key, value := range info.Map() {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
for key, value := range values {
|
||||
data[key] = value
|
||||
}
|
||||
return gjson.New(data)
|
||||
}
|
||||
|
||||
func ossIDFromContent(content *model.SysOssSaveContentModel) int64 {
|
||||
if content == nil || content.SysOss == nil {
|
||||
return 0
|
||||
}
|
||||
return content.OssId
|
||||
}
|
||||
|
||||
func urlFromContent(content *model.SysOssSaveContentModel) string {
|
||||
if content == nil || content.SysOss == nil {
|
||||
return ""
|
||||
}
|
||||
return content.Url
|
||||
}
|
||||
|
||||
func streamAllocateExcel(ctx context.Context, filePath string, chunkSize int, emit func([]batchimport.Row) error) (int, error) {
|
||||
return streamAllocateParsedFile(ctx, chunkSize, emit, func(handler func(excel.ParsedRow) error) (int, error) {
|
||||
return excel.StreamFile(ctx, filePath, excel.ParseOptions{
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
SingleSheet: true,
|
||||
}, handler)
|
||||
})
|
||||
}
|
||||
|
||||
func streamAllocateCSV(ctx context.Context, filePath string, chunkSize int, emit func([]batchimport.Row) error) (int, error) {
|
||||
return streamAllocateParsedFile(ctx, chunkSize, emit, func(handler func(excel.ParsedRow) error) (int, error) {
|
||||
return excel.StreamCSVFile(ctx, filePath, excel.ParseOptions{
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
}, handler)
|
||||
})
|
||||
}
|
||||
|
||||
func streamAllocateParsedFile(ctx context.Context, chunkSize int, emit func([]batchimport.Row) error, stream func(func(excel.ParsedRow) error) (int, error)) (int, error) {
|
||||
rows := make([]batchimport.Row, 0, chunkSize)
|
||||
total, err := stream(func(parsed excel.ParsedRow) error {
|
||||
rows = append(rows, rowFromExcel(parsed))
|
||||
if len(rows) < chunkSize {
|
||||
return nil
|
||||
}
|
||||
if err := emit(rows); err != nil {
|
||||
return err
|
||||
}
|
||||
rows = make([]batchimport.Row, 0, chunkSize)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
if len(rows) > 0 {
|
||||
if err := emit(rows); err != nil {
|
||||
return total, err
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func streamAllocateInput(ctx context.Context, input string, targetDeptID int64, chunkSize int, emit func([]batchimport.Row) error) (int, error) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
|
||||
rows := make([]batchimport.Row, 0, chunkSize)
|
||||
total := 0
|
||||
rowNo := 0
|
||||
for scanner.Scan() {
|
||||
rowNo++
|
||||
if err := ctx.Err(); err != nil {
|
||||
return total, err
|
||||
}
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
row := &Row{RowNo: rowNo, TargetDeptId: targetDeptID}
|
||||
fields := strings.Fields(line)
|
||||
switch len(fields) {
|
||||
case 1:
|
||||
row.Iccid = fields[0]
|
||||
case 3:
|
||||
row.Iccid = fields[0]
|
||||
row.Msisdn = fields[1]
|
||||
row.Imsi = fields[2]
|
||||
default:
|
||||
row.AddErrorReason("invalid input format; expected ICCID or ICCID MSISDN IMSI")
|
||||
}
|
||||
rows = append(rows, row)
|
||||
total++
|
||||
if len(rows) == chunkSize {
|
||||
if err := emit(rows); err != nil {
|
||||
return total, err
|
||||
}
|
||||
rows = make([]batchimport.Row, 0, chunkSize)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return total, err
|
||||
}
|
||||
if total == 0 {
|
||||
return 0, gerror.New("allocate input has no data rows")
|
||||
}
|
||||
if len(rows) > 0 {
|
||||
if err := emit(rows); err != nil {
|
||||
return total, err
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func rowFromExcel(parsed excel.ParsedRow) *Row {
|
||||
row := &Row{
|
||||
RowNo: parsed.RowNo,
|
||||
Iccid: excel.Field(parsed, "ICCID"),
|
||||
Msisdn: excel.Field(parsed, "MSISDN"),
|
||||
Imsi: excel.Field(parsed, "IMSI"),
|
||||
TargetDeptKey: firstExcelField(parsed, "\u76ee\u6807\u5ba2\u6237\u7f16\u7801", "\u5ba2\u6237\u7f16\u7801", "TARGETDEPTKEY", "TARGET_CUSTOMER_CODE"),
|
||||
TargetDeptName: firstExcelField(parsed, "\u76ee\u6807\u5ba2\u6237\u540d\u79f0", "\u5ba2\u6237\u540d\u79f0", "TARGETDEPTNAME", "TARGET_CUSTOMER_NAME"),
|
||||
Remark: firstExcelField(parsed, "\u5212\u62e8\u5907\u6ce8", "\u5907\u6ce8", "REMARK"),
|
||||
}
|
||||
if value := firstExcelField(parsed, "\u76ee\u6807\u5ba2\u6237ID", "\u76ee\u6807\u90e8\u95e8ID", "TARGETDEPTID", "TARGET_CUSTOMER_ID"); value != "" {
|
||||
if targetDeptID, err := parseTargetDeptID(value); err == nil {
|
||||
row.TargetDeptId = targetDeptID
|
||||
} else {
|
||||
row.AddErrorReason("invalid target customer id")
|
||||
}
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func parseTargetDeptID(value string) (int64, error) {
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return 0, gerror.New("invalid target customer id")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func sourceUnitInfo(info *gjson.Json) *gjson.Json {
|
||||
return gjson.New(map[string]any{"sourceDeptId": info.Get("sourceDeptId").Int64(), "sourceDeptKey": info.Get("sourceDeptKey").String(), "sourceDeptName": info.Get("sourceDeptName").String()})
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package allocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type allocateMetadataSQLiteDriver struct {
|
||||
*sqlite.Driver
|
||||
}
|
||||
|
||||
func (driver *allocateMetadataSQLiteDriver) New(core *gdb.Core, _ *gdb.ConfigNode) (gdb.DB, error) {
|
||||
return &allocateMetadataSQLiteDriver{Driver: &sqlite.Driver{Core: core}}, nil
|
||||
}
|
||||
|
||||
func (driver *allocateMetadataSQLiteDriver) DoSelect(ctx context.Context, link gdb.Link, query string, args ...any) (gdb.Result, error) {
|
||||
return driver.Core.DoSelect(ctx, link, strings.TrimSuffix(query, " FOR UPDATE"), args...)
|
||||
}
|
||||
|
||||
func TestAllocateSplitInheritsRequestMetadata(t *testing.T) {
|
||||
if err := gdb.Register("allocate_metadata_sqlite", &allocateMetadataSQLiteDriver{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "allocate_metadata_sqlite", Name: ":memory:", MaxOpenConnCount: 1}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = g.DB().Close(ctx) })
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for table, record := range map[string]any{"batch_main": entity.BatchMain{}, "batch_detail": entity.BatchDetail{}} {
|
||||
recordType := reflect.TypeOf(record)
|
||||
columns := make([]string, 0, recordType.NumField())
|
||||
for fieldIndex := 0; fieldIndex < recordType.NumField(); fieldIndex++ {
|
||||
field := recordType.Field(fieldIndex)
|
||||
column := field.Tag.Get("orm")
|
||||
if column == "id" {
|
||||
columns = append(columns, "id INTEGER PRIMARY KEY AUTOINCREMENT")
|
||||
} else {
|
||||
columns = append(columns, column+" TEXT")
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().Exec(ctx, "CREATE TABLE "+table+" ("+strings.Join(columns, ",")+")"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := g.Model(table).Ctx(ctx).TableFields(table); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := g.DB().GetCore().GetTablesWithCache(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, mode := range []int{batchmetadata.Single, batchmetadata.Bulk} {
|
||||
parentNo := "META-SPLIT-SINGLE"
|
||||
if mode == batchmetadata.Bulk {
|
||||
parentNo = "META-SPLIT-BULK"
|
||||
}
|
||||
req := batchimport.SubmitRequest{SourceChannel: batchmetadata.Console, ExecutionMode: mode, SourceType: consts.BatchSourceTypeBatchInput, Operator: batchimport.OperatorContext{TenantId: "A", UserId: 7}}
|
||||
processor := NewProcessor()
|
||||
if err := createAllocateParent(ctx, req, processor, parentNo, nil, 100); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows := []batchimport.Row{&Row{RowNo: 1, Iccid: "card1"}}
|
||||
result, err := createAllocateChunk(ctx, req, processor, parentNo, 1, rows, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, batchNo := range []string{parentNo, result.BatchNo} {
|
||||
var batch *entity.BatchMain
|
||||
if err := g.Model("batch_main").Ctx(ctx).Where("batch_no", batchNo).Scan(&batch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if batch == nil || batch.SourceChannel != req.SourceChannel || batch.ExecutionMode != uint(mode) || batch.SourceType != req.SourceType || batch.TotalCount != 1 {
|
||||
t.Fatalf("metadata not inherited: %+v", batch)
|
||||
}
|
||||
if batchNo != parentNo && batch.SourceBatchNo != parentNo {
|
||||
t.Fatalf("child lost parent association: %+v", batch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package allocate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/allocationunits"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
func CreatePendingExcel(ctx context.Context, file *ghttp.UploadFile, in *model.TSimCardAllocateExcelSubmitParam) (*PendingResult, error) {
|
||||
if in == nil {
|
||||
in = &model.TSimCardAllocateExcelSubmitParam{}
|
||||
}
|
||||
if file == nil {
|
||||
return nil, gerror.New("upload file is required")
|
||||
}
|
||||
if err := service.SysOss().CheckType(ctx, consts.SysUploadFileTypeFile, file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.Size > allocateMaxFileSize(ctx) {
|
||||
return nil, gerror.Newf("uploaded file exceeds the limit of %d MB", allocateMaxFileSize(ctx)>>20)
|
||||
}
|
||||
|
||||
options := SubmitOptions{
|
||||
TargetDeptId: in.TargetDeptId,
|
||||
Remark: strings.TrimSpace(in.Remark),
|
||||
}
|
||||
processor := NewProcessor()
|
||||
parentBatchNo, err := batchimport.NextBatchNoContext(ctx, processor.Config().BatchNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extension := strings.ToLower(filepath.Ext(file.Filename))
|
||||
if extension == "" {
|
||||
extension = ".xlsx"
|
||||
}
|
||||
importOss, err := batchimport.SaveUpload(ctx, file, parentBatchNo+"-import"+extension, processor.Config().ImportDir)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "save allocate import file failed")
|
||||
}
|
||||
|
||||
req := batchimport.SubmitRequest{
|
||||
SourceChannel: batchmetadata.Console,
|
||||
ExecutionMode: batchmetadata.Bulk,
|
||||
BizParams: &options,
|
||||
SourceType: consts.BatchSourceTypeFile,
|
||||
TargetDeptId: options.TargetDeptId,
|
||||
Operator: common.CurrentOperator(ctx),
|
||||
BatchNo: parentBatchNo,
|
||||
}
|
||||
stream := func(streamCtx context.Context, emit func([]batchimport.Row) error) (int, error) {
|
||||
chunkSize := allocateChunkSize(streamCtx)
|
||||
if extension == ".csv" {
|
||||
return streamAllocateCSV(streamCtx, importOss.Path, chunkSize, emit)
|
||||
}
|
||||
return streamAllocateExcel(streamCtx, importOss.Path, chunkSize, emit)
|
||||
}
|
||||
return createPendingSplit(ctx, req, processor, prepareOptions(options), parentBatchNo, importOss, stream)
|
||||
}
|
||||
|
||||
func CreatePendingInput(ctx context.Context, in *model.TSimCardAllocatePrepareParam) (*PendingResult, error) {
|
||||
if in == nil {
|
||||
in = &model.TSimCardAllocatePrepareParam{}
|
||||
}
|
||||
if in.TargetDeptId <= 0 {
|
||||
return nil, gerror.New("target customer is required")
|
||||
}
|
||||
|
||||
options := SubmitOptions{TargetDeptId: in.TargetDeptId}
|
||||
processor := NewProcessor()
|
||||
parentBatchNo, err := batchimport.NextBatchNoContext(ctx, processor.Config().BatchNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := batchimport.SubmitRequest{
|
||||
SourceChannel: batchmetadata.Console,
|
||||
ExecutionMode: batchmetadata.Bulk,
|
||||
BizParams: &options,
|
||||
SourceType: consts.BatchSourceTypeBatchInput,
|
||||
TargetDeptId: options.TargetDeptId,
|
||||
Operator: common.CurrentOperator(ctx),
|
||||
BatchNo: parentBatchNo,
|
||||
}
|
||||
stream := func(streamCtx context.Context, emit func([]batchimport.Row) error) (int, error) {
|
||||
return streamAllocateInput(streamCtx, in.Input, options.TargetDeptId, allocateChunkSize(streamCtx), emit)
|
||||
}
|
||||
return createPendingSplit(ctx, req, processor, prepareOptions(options), parentBatchNo, nil, stream)
|
||||
}
|
||||
|
||||
func prepareOptions(options SubmitOptions) batchimport.PendingOptions {
|
||||
return batchimport.PendingOptions{
|
||||
PrepareRows: func(ctx context.Context, rows []batchimport.Row) error {
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
return gerror.New("allocate row type is invalid")
|
||||
}
|
||||
if row.TargetDeptId <= 0 && options.TargetDeptId > 0 {
|
||||
row.TargetDeptId = options.TargetDeptId
|
||||
}
|
||||
if row.Remark == "" {
|
||||
row.Remark = options.Remark
|
||||
}
|
||||
if row.TargetDeptId <= 0 && row.TargetDeptKey == "" && row.TargetDeptName == "" {
|
||||
row.AddErrorReason("target customer is required")
|
||||
}
|
||||
}
|
||||
ids := []int64{}
|
||||
for _, item := range rows {
|
||||
if row, ok := item.(*Row); ok {
|
||||
ids = append(ids, row.TargetDeptId)
|
||||
}
|
||||
}
|
||||
units, err := allocationunits.Load(ctx, common.CurrentOperator(ctx).TenantId, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range rows {
|
||||
if row, ok := item.(*Row); ok {
|
||||
if unit, exists := units[row.TargetDeptId]; exists {
|
||||
row.TargetDeptKey = unit.Key
|
||||
row.TargetDeptName = unit.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pendingResultFromBatch(result *batchimport.Result) *PendingResult {
|
||||
res := &PendingResult{
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
}
|
||||
if result == nil {
|
||||
return res
|
||||
}
|
||||
res.BatchNo = result.BatchNo
|
||||
res.BatchType = result.BatchType
|
||||
res.Total = result.TotalRows
|
||||
res.ValidCount = result.ValidRows
|
||||
res.FailCount = result.ErrorRows
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package allocate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type Row struct {
|
||||
RowNo int
|
||||
Iccid string
|
||||
Msisdn string
|
||||
Imsi string
|
||||
TargetDeptId int64
|
||||
TargetDeptKey string
|
||||
TargetDeptName string
|
||||
Remark string
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (r *Row) GetRowNo() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
return r.RowNo
|
||||
}
|
||||
|
||||
func (r *Row) GetItemID() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return r.Iccid
|
||||
}
|
||||
|
||||
func (r *Row) GetErrorReasons() []string {
|
||||
if r == nil || strings.TrimSpace(r.Reason) == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{strings.TrimSpace(r.Reason)}
|
||||
}
|
||||
|
||||
func (r *Row) AddErrorReason(reason string) {
|
||||
if r == nil || strings.TrimSpace(reason) == "" {
|
||||
return
|
||||
}
|
||||
if r.Reason != "" {
|
||||
r.Reason += ";"
|
||||
}
|
||||
r.Reason += strings.TrimSpace(reason)
|
||||
}
|
||||
|
||||
func (r *Row) Snapshot() *gjson.Json {
|
||||
if r == nil {
|
||||
return gjson.New(g.Map{})
|
||||
}
|
||||
return gjson.New(g.Map{
|
||||
"ICCID": strings.TrimSpace(r.Iccid),
|
||||
"MSISDN": strings.TrimSpace(r.Msisdn),
|
||||
"IMSI": strings.TrimSpace(r.Imsi),
|
||||
"targetDeptId": r.TargetDeptId,
|
||||
"targetDeptKey": strings.TrimSpace(r.TargetDeptKey),
|
||||
"targetDeptName": strings.TrimSpace(r.TargetDeptName),
|
||||
"remark": strings.TrimSpace(r.Remark),
|
||||
})
|
||||
}
|
||||
|
||||
type SubmitOptions struct {
|
||||
Units *gjson.Json
|
||||
TargetDeptId int64
|
||||
Remark string
|
||||
}
|
||||
|
||||
type PendingResult struct {
|
||||
BatchNo string
|
||||
BatchType int
|
||||
BatchAsyncStatus int
|
||||
Total int
|
||||
ValidCount int
|
||||
FailCount int
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package cancel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/text"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
func ParseExcelFileRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
parsed, err := excel.ParseFile(ctx, filePath, excel.ParseOptions{RequiredHeaders: []string{"ICCID"}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(parsed))
|
||||
for _, item := range parsed {
|
||||
rows = append(rows, &Row{
|
||||
RowNo: item.RowNo,
|
||||
Iccid: excel.Field(item, "ICCID"),
|
||||
Msisdn: excel.Field(item, "MSISDN"),
|
||||
Imsi: excel.Field(item, "IMSI"),
|
||||
})
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func ParseInputRows(ctx context.Context, input string) ([]batchimport.Row, error) {
|
||||
lines, err := text.ParseWhitespaceFields(ctx, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, text.ErrEmptyInput) {
|
||||
return nil, gerror.New("批量输入内容不能为空")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
row := &Row{RowNo: line.RowNo}
|
||||
switch len(line.Fields) {
|
||||
case 1:
|
||||
row.Iccid = line.Fields[0]
|
||||
case 3:
|
||||
row.Iccid = line.Fields[0]
|
||||
row.Msisdn = line.Fields[1]
|
||||
row.Imsi = line.Fields[2]
|
||||
default:
|
||||
row.AddErrorReason("格式错误,应为 ICCID 或 ICCID MSISDN IMSI")
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func validateBasicRows(rows []batchimport.Row) error {
|
||||
firstByIccid := make(map[string]*Row, len(rows))
|
||||
issues := make([]string, 0)
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
return gerror.New("销户批次行类型错误")
|
||||
}
|
||||
row.Iccid = strings.TrimSpace(row.Iccid)
|
||||
row.Msisdn = strings.TrimSpace(row.Msisdn)
|
||||
row.Imsi = strings.TrimSpace(row.Imsi)
|
||||
if row.Reason != "" {
|
||||
issues = append(issues, fmt.Sprintf("第%d行:%s", row.RowNo, row.Reason))
|
||||
continue
|
||||
}
|
||||
if row.Iccid == "" {
|
||||
row.AddErrorReason("ICCID不能为空")
|
||||
issues = append(issues, fmt.Sprintf("第%d行:ICCID不能为空", row.RowNo))
|
||||
continue
|
||||
}
|
||||
if first, exists := firstByIccid[row.Iccid]; exists {
|
||||
row.AddErrorReason(fmt.Sprintf("ICCID与第%d行重复", first.RowNo))
|
||||
first.AddErrorReason("ICCID重复")
|
||||
issues = append(issues, fmt.Sprintf("第%d行:ICCID与第%d行重复", row.RowNo, first.RowNo))
|
||||
continue
|
||||
}
|
||||
firstByIccid[row.Iccid] = row
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
return gerror.New("销户批次基础格式校验失败:" + strings.Join(issues, ";"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package cancel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"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/frame/g"
|
||||
)
|
||||
|
||||
type Processor struct{}
|
||||
|
||||
func NewProcessor() *Processor {
|
||||
return &Processor{}
|
||||
}
|
||||
|
||||
func (p *Processor) Config() batchimport.ProcessorConfig {
|
||||
return batchimport.ProcessorConfig{
|
||||
Code: "sim_card_cancel",
|
||||
BatchType: consts.BatchTypeCancel,
|
||||
BatchNoPrefix: consts.BatchNoPrefixCancel,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimCardCancel,
|
||||
ExportScene: consts.ResourceExportSceneCancel,
|
||||
ImportDir: consts.SimCardCancelSubDirImportExcel,
|
||||
ResultDir: consts.SimCardCancelSubDirResultExcel,
|
||||
MaxFileSize: consts.SimCardCancelMaxFileSize,
|
||||
TemplateHeaders: []string{"ICCID", "MSISDN", "IMSI"},
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return ParseExcelFileRows(ctx, filePath)
|
||||
}
|
||||
|
||||
func (p *Processor) ValidateRows(_ context.Context, rows []batchimport.Row) error {
|
||||
return validateBasicRows(rows)
|
||||
}
|
||||
|
||||
func (p *Processor) Execute(context.Context, gdb.TX, *batchimport.SubmitRequest, []batchimport.Row) error {
|
||||
return gerror.New("销户批次只能由异步执行器执行")
|
||||
}
|
||||
|
||||
func (p *Processor) BuildBatchInfo(_ context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadID int64) *gjson.Json {
|
||||
info := g.Map{
|
||||
"operation": "cancel",
|
||||
"mode": "internal",
|
||||
"requiresStopped": true,
|
||||
"targetBossCardStatus": consts.V3ApiSimBossCardStatusCanceled,
|
||||
"resultFile": g.Map{
|
||||
"status": "pending",
|
||||
},
|
||||
}
|
||||
if options, ok := req.BizParams.(*SubmitOptions); ok && options != nil {
|
||||
if remark := strings.TrimSpace(options.Remark); remark != "" {
|
||||
info["remark"] = remark
|
||||
}
|
||||
}
|
||||
if result != nil && result.ImportFileOssId > 0 {
|
||||
info["importFileOssId"] = result.ImportFileOssId
|
||||
} else if importOss != nil && importOss.OssId > 0 {
|
||||
info["importFileOssId"] = importOss.OssId
|
||||
}
|
||||
if resultOss != nil {
|
||||
info["resultFileOssId"] = resultOss.OssId
|
||||
}
|
||||
if downloadID > 0 {
|
||||
info["resultDownloadId"] = downloadID
|
||||
}
|
||||
return gjson.New(info)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package cancel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
|
||||
"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/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type Submitter struct{}
|
||||
|
||||
func NewSubmitter() *Submitter {
|
||||
return &Submitter{}
|
||||
}
|
||||
|
||||
func (s *Submitter) SubmitExcel(ctx context.Context, file *ghttp.UploadFile, remark string) (*SubmitResult, error) {
|
||||
processor := NewProcessor()
|
||||
req := batchimport.SubmitRequest{
|
||||
SourceChannel: batchmetadata.Console,
|
||||
ExecutionMode: batchmetadata.Bulk,
|
||||
File: file,
|
||||
BizParams: &SubmitOptions{Remark: strings.TrimSpace(remark)},
|
||||
SourceType: consts.BatchSourceTypeFile,
|
||||
Operator: currentOperator(ctx),
|
||||
}
|
||||
result, err := batchimport.CreatePendingUpload(ctx, req, processor, batchimport.PendingOptions{
|
||||
PrepareRows: s.prepareAuthorizedRows,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.publish(ctx, result)
|
||||
return pendingSubmitResult(result), nil
|
||||
}
|
||||
|
||||
func (s *Submitter) SubmitInput(ctx context.Context, input, remark string) (*SubmitResult, error) {
|
||||
rows, err := ParseInputRows(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processor := NewProcessor()
|
||||
req := batchimport.SubmitRequest{
|
||||
SourceChannel: batchmetadata.Console,
|
||||
ExecutionMode: batchmetadata.Bulk,
|
||||
BizParams: &SubmitOptions{Remark: strings.TrimSpace(remark)},
|
||||
SourceType: consts.BatchSourceTypeBatchInput,
|
||||
Operator: currentOperator(ctx),
|
||||
}
|
||||
result, err := batchimport.CreatePendingRows(ctx, req, processor, rows, batchimport.PendingOptions{
|
||||
PrepareRows: s.prepareAuthorizedRows,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.publish(ctx, result)
|
||||
return pendingSubmitResult(result), nil
|
||||
}
|
||||
|
||||
func (s *Submitter) publish(ctx context.Context, result *batchimport.Result) {
|
||||
if result == nil || strings.TrimSpace(result.BatchNo) == "" {
|
||||
return
|
||||
}
|
||||
if err := publishBatchExecute(ctx, batchimport.Message{
|
||||
BatchNo: result.BatchNo,
|
||||
BatchType: result.BatchType,
|
||||
}); err != nil {
|
||||
g.Log().Errorf(ctx, "cancel batch async publish failed, batchNo=%s, batchType=%d, tenantId=%s, error=%+v",
|
||||
result.BatchNo, result.BatchType, contexts.GetTenantId(ctx), err)
|
||||
}
|
||||
}
|
||||
|
||||
var publishBatchExecute = func(ctx context.Context, msg batchimport.Message) error {
|
||||
return rabbitmq.Publish(ctx, consts.QueueBatchExecute, msg)
|
||||
}
|
||||
|
||||
func (s *Submitter) prepareAuthorizedRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
cardRows := make([]*Row, 0, len(rows))
|
||||
iccids := make([]string, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
return gerror.New("销户批次行类型错误")
|
||||
}
|
||||
cardRows = append(cardRows, row)
|
||||
if row.Iccid != "" {
|
||||
iccids = append(iccids, row.Iccid)
|
||||
}
|
||||
}
|
||||
if len(iccids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var visible []struct {
|
||||
Iccid string `orm:"iccid"`
|
||||
}
|
||||
columns := dao.TSimCardInfo.Columns()
|
||||
err := handler.Model(dao.TSimCardInfo.Ctx(ctx), &handler.Option{FilterTenant: true, FilterAuth: true}).
|
||||
Fields(columns.Iccid, columns.TenantId, columns.DeptId, columns.CreatedDept, columns.CreatedBy).
|
||||
WhereIn(columns.Iccid, iccids).
|
||||
Scan(&visible)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "计算SIM卡数据权限失败")
|
||||
}
|
||||
authorized := make(map[string]struct{}, len(visible))
|
||||
for _, card := range visible {
|
||||
authorized[strings.TrimSpace(card.Iccid)] = struct{}{}
|
||||
}
|
||||
for _, row := range cardRows {
|
||||
_, row.Authorized = authorized[row.Iccid]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentOperator(ctx context.Context) batchimport.OperatorContext {
|
||||
return batchimport.OperatorContext{
|
||||
TenantId: contexts.GetTenantId(ctx),
|
||||
DeptId: contexts.GetDeptId(ctx),
|
||||
UserId: contexts.GetUserId(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func pendingSubmitResult(result *batchimport.Result) *SubmitResult {
|
||||
res := &SubmitResult{BatchAsyncStatus: consts.BatchAsyncStatusPending}
|
||||
if result == nil {
|
||||
return res
|
||||
}
|
||||
res.BatchNo = result.BatchNo
|
||||
res.BatchType = result.BatchType
|
||||
res.Total = result.TotalRows
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package cancel
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type Row struct {
|
||||
DetailID int64
|
||||
RowNo int
|
||||
Iccid string
|
||||
Msisdn string
|
||||
Imsi string
|
||||
Authorized bool
|
||||
Reason string
|
||||
StatusBefore int
|
||||
}
|
||||
|
||||
func (r *Row) GetRowNo() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
return r.RowNo
|
||||
}
|
||||
|
||||
func (r *Row) GetItemID() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return r.Iccid
|
||||
}
|
||||
|
||||
func (r *Row) GetErrorReasons() []string {
|
||||
if r == nil || r.Reason == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{r.Reason}
|
||||
}
|
||||
|
||||
func (r *Row) AddErrorReason(reason string) {
|
||||
if r == nil || r.Reason != "" || reason == "" {
|
||||
return
|
||||
}
|
||||
r.Reason = reason
|
||||
}
|
||||
|
||||
func (r *Row) Snapshot() *gjson.Json {
|
||||
if r == nil {
|
||||
return gjson.New(g.Map{})
|
||||
}
|
||||
return gjson.New(g.Map{
|
||||
"ICCID": r.Iccid,
|
||||
"MSISDN": r.Msisdn,
|
||||
"IMSI": r.Imsi,
|
||||
"authorized": r.Authorized,
|
||||
"statusBefore": r.StatusBefore,
|
||||
})
|
||||
}
|
||||
|
||||
type SubmitOptions struct {
|
||||
Remark string
|
||||
}
|
||||
|
||||
type SubmitResult struct {
|
||||
BatchNo string
|
||||
BatchType int
|
||||
BatchAsyncStatus int
|
||||
Total int
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Batch *entity.BatchMain
|
||||
Rows []*Row
|
||||
Operator batchimport.OperatorContext
|
||||
Remark string
|
||||
StartTime *gtime.Time
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"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/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type AsyncConfirmInput struct {
|
||||
BatchNo string
|
||||
TargetBatchType int
|
||||
NewPackageCode string
|
||||
DurationMonths int
|
||||
GiftDurationMonths int
|
||||
EffectiveMode string
|
||||
OfflineContract bool
|
||||
BuyerNote string
|
||||
AdminNote string
|
||||
Remark string
|
||||
PriceSnapshot *model.TSimCardChangePriceSnapshot
|
||||
SourceBatchNo string
|
||||
PackageBeginTimeStr string
|
||||
Operator batchimport.OperatorContext
|
||||
}
|
||||
|
||||
type AsyncConfirmResult struct {
|
||||
BatchNo string
|
||||
BatchType int
|
||||
BatchAsyncStatus int
|
||||
}
|
||||
|
||||
// ConvertPrepareToAsync 以 CAS 方式把 batch_type=4 + batch_async_status=Success + run_result=Success 的预处理批次
|
||||
// 转换为 batch_type=102 的待处理变更批次:重置主表正式执行相关字段并把明细的 BatchType 切换到 102、DetailStatus 重置为 Pending。
|
||||
func ConvertPrepareToAsync(ctx context.Context, tx gdb.TX, in *AsyncConfirmInput) (*AsyncConfirmResult, error) {
|
||||
if in == nil {
|
||||
return nil, gerror.New("变更确认参数不能为空")
|
||||
}
|
||||
batchNo := strings.TrimSpace(in.BatchNo)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("变更批次号不能为空")
|
||||
}
|
||||
targetBatchType := in.TargetBatchType
|
||||
if targetBatchType == 0 {
|
||||
targetBatchType = consts.BatchTypeChange
|
||||
}
|
||||
columns := dao.BatchMain.Columns()
|
||||
main := &entity.BatchMain{}
|
||||
err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Where(columns.TenantId, contexts.GetTenantId(ctx)).
|
||||
Where(columns.CreatedDept, contexts.GetDeptId(ctx)).
|
||||
LockUpdate().
|
||||
Scan(main)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询变更预处理批次失败")
|
||||
}
|
||||
if main.Id == 0 {
|
||||
return nil, gerror.New("变更预处理批次不存在")
|
||||
}
|
||||
if main.BatchType != consts.BatchTypePrepare {
|
||||
return nil, gerror.New("变更批次不是预处理类型")
|
||||
}
|
||||
if main.BatchAsyncStatus != consts.BatchAsyncStatusSuccess {
|
||||
return nil, gerror.New("变更预处理批次尚未完成")
|
||||
}
|
||||
if main.RunResult != consts.BatchRunResultSuccess {
|
||||
return nil, gerror.New("变更预处理批次未成功")
|
||||
}
|
||||
if main.BatchInfo == nil {
|
||||
return nil, gerror.New("变更预处理批次缺少快照")
|
||||
}
|
||||
if main.BatchInfo.Get("targetBatchType").Int() != targetBatchType {
|
||||
return nil, gerror.New("变更预处理批次类型不匹配")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
info := mergeConfirmBatchInfo(main.BatchInfo, in)
|
||||
data := do.BatchMain{
|
||||
BatchType: targetBatchType,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
RunResult: consts.BatchRunResultPending,
|
||||
StartTime: nil,
|
||||
EndTime: nil,
|
||||
FailReason: "",
|
||||
SuccessCount: 0,
|
||||
FailCount: 0,
|
||||
BatchInfo: gjson.New(info),
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
res, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(columns.Id, main.Id).
|
||||
Where(columns.BatchType, consts.BatchTypePrepare).
|
||||
Where(columns.BatchAsyncStatus, consts.BatchAsyncStatusSuccess).
|
||||
Where(columns.RunResult, consts.BatchRunResultSuccess).
|
||||
Data(data).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "更新变更批次状态失败")
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取变更批次更新结果失败")
|
||||
}
|
||||
if affected != 1 {
|
||||
return nil, gerror.New("变更批次状态已变化,请重试")
|
||||
}
|
||||
|
||||
detailColumns := dao.BatchDetail.Columns()
|
||||
if _, err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(detailColumns.BatchNo, batchNo).
|
||||
Where(detailColumns.BatchType, consts.BatchTypePrepare).
|
||||
Data(do.BatchDetail{
|
||||
BatchType: targetBatchType,
|
||||
DetailStatus: consts.BatchDetailStatusPending,
|
||||
ErrorCode: "",
|
||||
ErrorReason: "",
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return nil, gerror.Wrap(err, "更新变更批次明细类型失败")
|
||||
}
|
||||
|
||||
return &AsyncConfirmResult{
|
||||
BatchNo: batchNo,
|
||||
BatchType: targetBatchType,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mergeConfirmBatchInfo(base *gjson.Json, in *AsyncConfirmInput) g.Map {
|
||||
data := g.Map{}
|
||||
if base != nil {
|
||||
for key, value := range base.Map() {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
if in == nil {
|
||||
return data
|
||||
}
|
||||
if newPackageCode := strings.TrimSpace(in.NewPackageCode); newPackageCode != "" {
|
||||
data["newPackageCode"] = newPackageCode
|
||||
}
|
||||
if in.DurationMonths > 0 {
|
||||
data["durationMonths"] = in.DurationMonths
|
||||
}
|
||||
if in.GiftDurationMonths >= 0 {
|
||||
data["giftDurationMonths"] = in.GiftDurationMonths
|
||||
}
|
||||
if effectiveMode := strings.TrimSpace(in.EffectiveMode); effectiveMode != "" {
|
||||
data["effectiveMode"] = effectiveMode
|
||||
}
|
||||
if strings.TrimSpace(in.PackageBeginTimeStr) != "" {
|
||||
data["packageBeginTime"] = strings.TrimSpace(in.PackageBeginTimeStr)
|
||||
}
|
||||
data["offlineContractCompleted"] = in.OfflineContract
|
||||
if note := strings.TrimSpace(in.BuyerNote); note != "" {
|
||||
data["buyerNote"] = note
|
||||
}
|
||||
if note := strings.TrimSpace(in.AdminNote); note != "" {
|
||||
data["adminNote"] = note
|
||||
}
|
||||
if note := strings.TrimSpace(in.Remark); note != "" {
|
||||
data["remark"] = note
|
||||
}
|
||||
if in.PriceSnapshot != nil {
|
||||
data["changeUnitPrice"] = in.PriceSnapshot.ChangeUnitPrice
|
||||
}
|
||||
if strings.TrimSpace(in.SourceBatchNo) != "" {
|
||||
data["sourceBatchNo"] = strings.TrimSpace(in.SourceBatchNo)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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/logic/iot/cardpackage"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"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/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func (p *Processor) Complete(ctx context.Context, tx gdb.TX, in *CompleteInput) (*CompleteOutput, error) {
|
||||
if in == nil || in.SubmitParam == nil {
|
||||
return nil, gerror.New("change params cannot be empty")
|
||||
}
|
||||
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
|
||||
if strings.TrimSpace(in.BatchNo) == "" {
|
||||
return nil, gerror.New("change batchNo cannot be empty")
|
||||
}
|
||||
if len(in.Rows) == 0 {
|
||||
return nil, gerror.New("change cards cannot be empty")
|
||||
}
|
||||
if in.PriceSnapshot == nil {
|
||||
return nil, gerror.New("change price snapshot cannot be empty")
|
||||
}
|
||||
if in.PackageBeginTime == nil {
|
||||
return nil, gerror.New("change package begin time cannot be empty")
|
||||
}
|
||||
|
||||
iccids := rowsToIccids(in.Rows)
|
||||
packageSvc := cardpackage.NewService()
|
||||
termination, err := packageSvc.TerminateCurrentPackage(ctx, tx, &cardpackage.TerminateCurrentPackageInput{
|
||||
BatchNo: in.BatchNo,
|
||||
SourceBatchNo: sourceBatchNo,
|
||||
Iccids: iccids,
|
||||
Operator: in.Operator,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
voidedByIccid, err := p.voidFuturePendingBinds(ctx, tx, iccids, in.BatchNo, termination.UnsubTime, in.Operator.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quantity := len(iccids)
|
||||
totalAmount := roundAmount(in.PriceSnapshot.ChangeUnitPrice * float64(in.SubmitParam.DurationMonths) * float64(quantity))
|
||||
cards := make([]cardpackage.SubscribeCard, 0, quantity)
|
||||
for _, iccid := range iccids {
|
||||
card := termination.CardsByIccid[iccid]
|
||||
if card == nil {
|
||||
return nil, gerror.Newf("ICCID card info not found: %s", iccid)
|
||||
}
|
||||
cards = append(cards, cardpackage.SubscribeCard{
|
||||
Iccid: iccid,
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
Imsi: strings.TrimSpace(card.Imsi),
|
||||
})
|
||||
}
|
||||
|
||||
subscribe, err := packageSvc.CreatePackageSubscription(ctx, tx, &cardpackage.CreatePackageSubscriptionInput{
|
||||
BatchNo: in.BatchNo,
|
||||
Cards: cards,
|
||||
CustomerAgentId: in.PriceSnapshot.CustomerAgentId,
|
||||
PackageCode: in.PriceSnapshot.NewPackageCode,
|
||||
PackageBeginTime: in.PackageBeginTime,
|
||||
DurationMonths: in.SubmitParam.DurationMonths,
|
||||
GiftDurationMonths: in.SubmitParam.GiftDurationMonths,
|
||||
Amount: fmt.Sprintf("%.2f", totalAmount),
|
||||
OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted,
|
||||
BuyerNote: in.SubmitParam.BuyerNote,
|
||||
AdminNote: in.SubmitParam.AdminNote,
|
||||
PreviousPackageCode: termination.PackageCode,
|
||||
BusinessType: consts.PackageBusinessTypeChange,
|
||||
OrderRemark: "sim card change order",
|
||||
BindRemark: in.BatchNo,
|
||||
Operator: in.Operator,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = p.updateCardsAfterChange(ctx, tx, iccids, termination.CardsByIccid, subscribe, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
detailUpdates := make(map[string]any, len(iccids))
|
||||
for _, row := range in.Rows {
|
||||
card := termination.CardsByIccid[row.Iccid]
|
||||
oldBind := termination.BindsByIccid[row.Iccid]
|
||||
newBind := subscribe.BindsByIccid[row.Iccid]
|
||||
detailUpdates[row.Iccid] = buildDetailSnapshot(row, card, oldBind, newBind, voidedByIccid[row.Iccid], in, termination, subscribe)
|
||||
}
|
||||
if err = updateBatchDetailSnapshots(ctx, tx, in.BatchNo, detailUpdates, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = convertBatchDetailsAfterComplete(ctx, tx, in.BatchNo, in.Operator.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output := &CompleteOutput{
|
||||
BatchNo: in.BatchNo,
|
||||
BatchType: consts.BatchTypeChange,
|
||||
SourceBatchNo: sourceBatchNo,
|
||||
SourceMode: sourceMode(sourceBatchNo),
|
||||
Quantity: quantity,
|
||||
CustomerAgentId: in.PriceSnapshot.CustomerAgentId,
|
||||
CustomerName: "",
|
||||
OldPackageCode: termination.PackageCode,
|
||||
NewPackageCode: subscribe.Package.PackageCode,
|
||||
NewPackageName: subscribe.Package.PackageName,
|
||||
OrderSn: subscribe.OrderSn,
|
||||
Amount: subscribe.Amount,
|
||||
ChangeUnitPrice: in.PriceSnapshot.ChangeUnitPrice,
|
||||
DurationMonths: in.SubmitParam.DurationMonths,
|
||||
GiftDurationMonths: in.SubmitParam.GiftDurationMonths,
|
||||
EffectiveMode: strings.TrimSpace(in.SubmitParam.EffectiveMode),
|
||||
OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted,
|
||||
PackageBeginTime: subscribe.PackageBeginAt,
|
||||
PackageEndTime: subscribe.PackageEndAt,
|
||||
ChangeTime: subscribe.SubscribeTime,
|
||||
}
|
||||
if in.PrepareSnapshot != nil {
|
||||
output.CustomerName = in.PrepareSnapshot.CustomerName
|
||||
output.OldPackageName = in.PrepareSnapshot.OldPackageName
|
||||
}
|
||||
if err = updateBatchMainAfterComplete(ctx, tx, in, output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (p *Processor) voidFuturePendingBinds(ctx context.Context, tx gdb.TX, iccids []string, batchNo string, voidTime *gtime.Time, userId int64) (map[string][]string, error) {
|
||||
result := make(map[string][]string)
|
||||
pendingBinds, err := p.repo.LockPendingBindsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "lock future pending binds failed")
|
||||
}
|
||||
if len(pendingBinds) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
orderCodeSet := make(map[string]struct{})
|
||||
orderVoidCounts := make(map[string]int)
|
||||
subscriptionNos := make([]string, 0, len(pendingBinds))
|
||||
logRows := make([]do.TSimBindPackageLogs, 0, len(pendingBinds))
|
||||
for _, bind := range pendingBinds {
|
||||
if bind == nil {
|
||||
continue
|
||||
}
|
||||
subscriptionNo := strings.TrimSpace(bind.SubscriptionNo)
|
||||
if subscriptionNo == "" {
|
||||
continue
|
||||
}
|
||||
subscriptionNos = append(subscriptionNos, subscriptionNo)
|
||||
result[strings.TrimSpace(bind.Iccid)] = append(result[strings.TrimSpace(bind.Iccid)], subscriptionNo)
|
||||
logRows = append(logRows, bindToVoidedHistory(bind, voidTime, batchNo, userId))
|
||||
orderCode := strings.TrimSpace(bind.OrderCode)
|
||||
if orderCode != "" {
|
||||
orderCodeSet[orderCode] = struct{}{}
|
||||
orderVoidCounts[orderCode]++
|
||||
}
|
||||
}
|
||||
if len(logRows) > 0 {
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TSimBindPackageLogs.Table()).
|
||||
Data(logRows).
|
||||
OnConflict(dao.TSimBindPackageLogs.Columns().SubscriptionNo).
|
||||
Save(); err != nil {
|
||||
return nil, gerror.Wrap(err, "save voided bind history failed")
|
||||
}
|
||||
}
|
||||
affected, err := p.repo.DeleteBindsBySubscriptionNos(ctx, tx, subscriptionNos)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "delete future pending binds failed")
|
||||
}
|
||||
if int(affected) != len(subscriptionNos) {
|
||||
return nil, gerror.New("some future pending binds changed, please retry")
|
||||
}
|
||||
if err = p.updateOrdersAfterChangeVoid(ctx, tx, mapKeys(orderCodeSet), orderVoidCounts, batchNo, voidTime, userId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func bindToVoidedHistory(bind *entity.TSimCardBindPkg, actualExpireAt *gtime.Time, changeBatchNo string, userId int64) do.TSimBindPackageLogs {
|
||||
return do.TSimBindPackageLogs{
|
||||
SubscriptionNo: bind.SubscriptionNo,
|
||||
BatchNo: bind.BatchNo,
|
||||
Iccid: bind.Iccid,
|
||||
Msisdn: bind.Msisdn,
|
||||
DeptId: bind.DeptId,
|
||||
PackageType: bind.PackageType,
|
||||
PackageCode: bind.PackageCode,
|
||||
PreviousPackageCode: bind.PreviousPackageCode,
|
||||
BusinessType: bind.BusinessType,
|
||||
EffectiveStatus: consts.EffectiveStatusVoided,
|
||||
HandleTime: bind.HandleTime,
|
||||
EffectiveTime: bind.EffectiveTime,
|
||||
ExpectedExpireTime: bind.ExpectedExpireTime,
|
||||
ActualExpireTime: actualExpireAt,
|
||||
DurationMonths: bind.DurationMonths,
|
||||
GiftDurationMonths: bind.GiftDurationMonths,
|
||||
OrderCode: bind.OrderCode,
|
||||
Remark: appendRemark(bind.Remark, fmt.Sprintf("changeBatchNo=%s", changeBatchNo)),
|
||||
TenantId: bind.TenantId,
|
||||
CreatedDept: bind.CreatedDept,
|
||||
CreatedBy: bind.CreatedBy,
|
||||
CreatedAt: bind.CreatedAt,
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: actualExpireAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) updateOrdersAfterChangeVoid(ctx context.Context, tx gdb.TX, orderCodes []string, voidCounts map[string]int, batchNo string, voidTime *gtime.Time, userId int64) error {
|
||||
if len(orderCodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
activeCounts, err := p.repo.ActiveBindCountsByOrderCodes(ctx, tx, orderCodes)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "count active binds by order failed")
|
||||
}
|
||||
for _, orderCode := range orderCodes {
|
||||
returnStatus := consts.OrderReturnStatusSuccess
|
||||
if activeCounts[orderCode] > 0 {
|
||||
returnStatus = consts.OrderReturnStatusPartial
|
||||
}
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TOrder.Table()).
|
||||
Where(dao.TOrder.Columns().OrderSn, orderCode).
|
||||
Data(do.TOrder{
|
||||
ReturnStatus: returnStatus,
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: voidTime,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "update order return status failed")
|
||||
}
|
||||
if err = updateOrderItemChangeExtra(ctx, tx, orderCode, batchNo, voidCounts[orderCode], voidTime, userId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateOrderItemChangeExtra(ctx context.Context, tx gdb.TX, orderCode, batchNo string, voidQuantity int, voidTime *gtime.Time, userId int64) error {
|
||||
var item *entity.TOrderItem
|
||||
if err := tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
|
||||
Where(dao.TOrderItem.Columns().OrderSn, orderCode).
|
||||
Limit(1).
|
||||
Scan(&item); err != nil {
|
||||
return gerror.Wrap(err, "load order item failed")
|
||||
}
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
extra := g.Map{}
|
||||
if strings.TrimSpace(item.ExtraSkuData) != "" {
|
||||
_ = json.Unmarshal([]byte(item.ExtraSkuData), &extra)
|
||||
}
|
||||
extra["lastChangeBatchNo"] = batchNo
|
||||
extra["lastChangeTime"] = formatTime(voidTime)
|
||||
extra["voidedByChangeQuantity"] = voidQuantity
|
||||
|
||||
data, err := json.Marshal(extra)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "marshal order item extra failed")
|
||||
}
|
||||
if _, err = tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
|
||||
Where(dao.TOrderItem.Columns().ItemId, item.ItemId).
|
||||
Data(do.TOrderItem{
|
||||
ExtraSkuData: string(data),
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: voidTime,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "update order item extra failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) updateCardsAfterChange(ctx context.Context, tx gdb.TX, iccids []string, cardsByIccid map[string]*entity.TSimCardInfo, subscribe *cardpackage.CreatePackageSubscriptionOutput, userId int64) error {
|
||||
if subscribe == nil || subscribe.Package == nil {
|
||||
return gerror.New("change subscription output is empty")
|
||||
}
|
||||
now := gtime.Now()
|
||||
columns := dao.TSimCardInfo.Columns()
|
||||
for _, iccid := range iccids {
|
||||
card := cardsByIccid[iccid]
|
||||
if card == nil {
|
||||
return gerror.Newf("ICCID card info not found: %s", iccid)
|
||||
}
|
||||
result, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
|
||||
Where(columns.Iccid, iccid).
|
||||
Data(g.Map{
|
||||
columns.CurrentPackageCode: subscribe.Package.PackageCode,
|
||||
columns.PackageStatus: subscribe.PackageStatus,
|
||||
columns.FlowPkgBeginAt: subscribe.PackageBeginAt,
|
||||
columns.FlowPkgPresentAt: nil,
|
||||
columns.FlowPkgRealPresentAt: nil,
|
||||
columns.FlowPkgEndAt: subscribe.PackageEndAt,
|
||||
columns.DataServiceStatus: 1,
|
||||
columns.PackageTotalFlowMb: subscribe.Package.FlowMb,
|
||||
columns.UsedFlowMb: 0,
|
||||
columns.TrafficCutoffStatus: 0,
|
||||
columns.TrafficCutoffThreshold: 0,
|
||||
columns.SmsServiceStatus: 0,
|
||||
columns.PackageTotalSms: 0,
|
||||
columns.UsedSms: 0,
|
||||
columns.VoiceServiceStatus: 0,
|
||||
columns.PackageTotalVoice: 0,
|
||||
columns.UsedVoice: 0,
|
||||
columns.RoamServiceStatus: 0,
|
||||
columns.PackageTotalRoamFlowMb: 0,
|
||||
columns.UsedRoamFlowMb: 0,
|
||||
columns.UpdatedBy: userId,
|
||||
columns.UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "update card after change failed")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "get updated card count failed")
|
||||
}
|
||||
if affected != 1 {
|
||||
return gerror.Newf("ICCID card changed concurrently, retry: %s", iccid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateBatchDetailSnapshots(ctx context.Context, tx gdb.TX, batchNo string, snapshots map[string]any, userId int64) error {
|
||||
for iccid, snapshot := range snapshots {
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
||||
Where(dao.BatchDetail.Columns().ItemId, iccid).
|
||||
Data(do.BatchDetail{
|
||||
Snapshot: gjson.New(snapshot),
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "update change batch detail snapshot failed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertBatchDetailsAfterComplete(ctx context.Context, tx gdb.TX, batchNo string, userId int64) error {
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
||||
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare).
|
||||
Data(do.BatchDetail{
|
||||
BatchType: consts.BatchTypeChange,
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "update change batch detail type failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateBatchMainAfterComplete(ctx context.Context, tx gdb.TX, in *CompleteInput, output *CompleteOutput) error {
|
||||
info := g.Map{
|
||||
"sourceMode": output.SourceMode,
|
||||
"orderSn": output.OrderSn,
|
||||
"customerAgentId": output.CustomerAgentId,
|
||||
"customerName": output.CustomerName,
|
||||
"oldPackageCode": output.OldPackageCode,
|
||||
"oldPackageName": output.OldPackageName,
|
||||
"newPackageCode": output.NewPackageCode,
|
||||
"newPackageName": output.NewPackageName,
|
||||
"durationMonths": output.DurationMonths,
|
||||
"giftDurationMonths": output.GiftDurationMonths,
|
||||
"effectiveMode": output.EffectiveMode,
|
||||
"changeUnitPrice": output.ChangeUnitPrice,
|
||||
"amount": output.Amount,
|
||||
"quantity": output.Quantity,
|
||||
"packageBeginTime": output.PackageBeginTime,
|
||||
"packageEndTime": output.PackageEndTime,
|
||||
"offlineContractCompleted": output.OfflineContractCompleted,
|
||||
"changeTime": output.ChangeTime,
|
||||
}
|
||||
if output.SourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = output.SourceBatchNo
|
||||
}
|
||||
if in.PriceSnapshot != nil {
|
||||
info["priceSource"] = in.PriceSnapshot.PriceSource
|
||||
info["agentDefaultPriceId"] = in.PriceSnapshot.AgentDefaultPriceId
|
||||
info["targetPriceId"] = in.PriceSnapshot.TargetPriceId
|
||||
info["renewableDurationOptions"] = in.PriceSnapshot.RenewableDurationOptions
|
||||
}
|
||||
if buyerNote := strings.TrimSpace(in.SubmitParam.BuyerNote); buyerNote != "" {
|
||||
info["buyerNote"] = buyerNote
|
||||
}
|
||||
if adminNote := strings.TrimSpace(in.SubmitParam.AdminNote); adminNote != "" {
|
||||
info["adminNote"] = adminNote
|
||||
}
|
||||
if remark := strings.TrimSpace(in.SubmitParam.Remark); remark != "" {
|
||||
info["remark"] = remark
|
||||
}
|
||||
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
||||
Where(dao.BatchMain.Columns().BatchNo, in.BatchNo).
|
||||
Data(do.BatchMain{
|
||||
BatchType: consts.BatchTypeChange,
|
||||
OrderSn: output.OrderSn,
|
||||
TargetDeptId: output.CustomerAgentId,
|
||||
BatchInfo: gjson.New(info),
|
||||
UpdatedBy: in.Operator.UserId,
|
||||
UpdatedAt: output.ChangeTime,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "update change batch failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildDetailSnapshot(row *Row, card *entity.TSimCardInfo, oldBind, newBind *entity.TSimCardBindPkg, voidedSubscriptionNos []string, in *CompleteInput, termination *cardpackage.TerminateCurrentPackageOutput, subscribe *cardpackage.CreatePackageSubscriptionOutput) g.Map {
|
||||
data := g.Map{
|
||||
"ICCID": row.Iccid,
|
||||
"MSISDN": row.Msisdn,
|
||||
"IMSI": row.Imsi,
|
||||
"sourceMode": sourceMode(in.SourceBatchNo),
|
||||
"changeBatchNo": in.BatchNo,
|
||||
"oldPackageCode": termination.PackageCode,
|
||||
"newPackageCode": subscribe.Package.PackageCode,
|
||||
"newPackageName": subscribe.Package.PackageName,
|
||||
"voidedSubscriptionNos": voidedSubscriptionNos,
|
||||
"durationMonths": in.SubmitParam.DurationMonths,
|
||||
"giftDurationMonths": in.SubmitParam.GiftDurationMonths,
|
||||
"effectiveMode": strings.TrimSpace(in.SubmitParam.EffectiveMode),
|
||||
"offlineContractCompleted": in.SubmitParam.OfflineContractCompleted,
|
||||
"changeUnitPrice": in.PriceSnapshot.ChangeUnitPrice,
|
||||
"amount": roundAmount(in.PriceSnapshot.ChangeUnitPrice * float64(in.SubmitParam.DurationMonths)),
|
||||
"newEffectiveTime": formatTime(subscribe.PackageBeginAt),
|
||||
"newExpectedExpireTime": formatTime(subscribe.PackageEndAt),
|
||||
"oldActualExpireTime": formatTime(termination.UnsubTime),
|
||||
"rowNo": row.RowNo,
|
||||
"rawFields": row.RawFields,
|
||||
}
|
||||
if strings.TrimSpace(in.SourceBatchNo) != "" {
|
||||
data["sourceBatchNo"] = strings.TrimSpace(in.SourceBatchNo)
|
||||
}
|
||||
if card != nil {
|
||||
data["ICCID"] = card.Iccid
|
||||
data["MSISDN"] = card.Msisdn
|
||||
data["IMSI"] = card.Imsi
|
||||
}
|
||||
if in.PrepareSnapshot != nil {
|
||||
data["oldPackageName"] = in.PrepareSnapshot.OldPackageName
|
||||
}
|
||||
if oldBind != nil {
|
||||
data["oldSubscriptionNo"] = oldBind.SubscriptionNo
|
||||
data["oldOrderSn"] = oldBind.OrderCode
|
||||
data["oldEffectiveTime"] = formatTime(oldBind.EffectiveTime)
|
||||
data["oldExpectedExpireTime"] = formatTime(oldBind.ExpectedExpireTime)
|
||||
}
|
||||
if newBind != nil {
|
||||
data["newSubscriptionNo"] = newBind.SubscriptionNo
|
||||
data["newOrderSn"] = newBind.OrderCode
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func appendRemark(old, addition string) string {
|
||||
old = strings.TrimSpace(old)
|
||||
addition = strings.TrimSpace(addition)
|
||||
if old == "" {
|
||||
return addition
|
||||
}
|
||||
if addition == "" {
|
||||
return old
|
||||
}
|
||||
return old + "; " + addition
|
||||
}
|
||||
|
||||
func mapKeys(set map[string]struct{}) []string {
|
||||
result := make([]string, 0, len(set))
|
||||
for key := range set {
|
||||
if strings.TrimSpace(key) != "" {
|
||||
result = append(result, key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func roundAmount(value float64) float64 {
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(int(value*100+0.5)) / 100
|
||||
}
|
||||
|
||||
func sourceMode(sourceBatchNo string) int {
|
||||
if strings.TrimSpace(sourceBatchNo) != "" {
|
||||
return consts.BatchSourceTypeBatchSelect
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
)
|
||||
|
||||
type ExcelProcessor struct {
|
||||
*Processor
|
||||
options *ExcelSubmitOptions
|
||||
}
|
||||
|
||||
func NewExcelProcessor() *ExcelProcessor {
|
||||
return &ExcelProcessor{Processor: NewProcessor()}
|
||||
}
|
||||
|
||||
func (p *ExcelProcessor) Config() batchimport.ProcessorConfig {
|
||||
cfg := p.Processor.Config()
|
||||
cfg.TemplateHeaders = ExcelTemplateHeaders()
|
||||
cfg.RequiredHeaders = ExcelTemplateHeaders()
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (p *ExcelProcessor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
rows, options, err := ParseExcelFileRows(ctx, filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.options = options
|
||||
|
||||
out := make([]batchimport.Row, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *ExcelProcessor) ExcelSubmitOptions() *ExcelSubmitOptions {
|
||||
return p.options
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/common/cardsource"
|
||||
)
|
||||
|
||||
func ParseInputRows(ctx context.Context, input string) ([]batchimport.Row, error) {
|
||||
return cardsource.BuildIccidRows(ctx, input, "batch input change data is empty", func(line cardsource.IccidLine) *Row {
|
||||
return &Row{
|
||||
RowNo: line.RowNo,
|
||||
Iccid: line.Iccid,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/excel"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
)
|
||||
|
||||
type ExcelSubmitOptions struct {
|
||||
NewPackageCode string
|
||||
DurationMonths int
|
||||
GiftDurationMonths int
|
||||
EffectiveMode string
|
||||
OfflineContractCompleted bool
|
||||
}
|
||||
|
||||
func ExcelTemplateHeaders() []string {
|
||||
return []string{
|
||||
"ICCID",
|
||||
"MSISDN",
|
||||
"IMSI",
|
||||
"newPackageCode",
|
||||
"durationMonths",
|
||||
"giftDurationMonths",
|
||||
"effectiveMode",
|
||||
"offlineContractCompleted",
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ExcelSubmitOptions) ToBatchSubmitParam() *model.TSimCardChangeBatchSubmitParam {
|
||||
if o == nil {
|
||||
return &model.TSimCardChangeBatchSubmitParam{}
|
||||
}
|
||||
return &model.TSimCardChangeBatchSubmitParam{
|
||||
NewPackageCode: strings.TrimSpace(o.NewPackageCode),
|
||||
DurationMonths: o.DurationMonths,
|
||||
GiftDurationMonths: o.GiftDurationMonths,
|
||||
EffectiveMode: strings.TrimSpace(o.EffectiveMode),
|
||||
OfflineContractCompleted: o.OfflineContractCompleted,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ExcelSubmitOptions) ToBatchSubmitParamWithBatchNo(batchNo string) *model.TSimCardChangeBatchSubmitParam {
|
||||
param := o.ToBatchSubmitParam()
|
||||
param.BatchNo = strings.TrimSpace(batchNo)
|
||||
return param
|
||||
}
|
||||
|
||||
func ParseExcelFileRows(ctx context.Context, filePath string) ([]*Row, *ExcelSubmitOptions, error) {
|
||||
parsedRows, err := excel.ParseFile(ctx, filePath, excel.ParseOptions{
|
||||
RequiredHeaders: ExcelTemplateHeaders(),
|
||||
SingleSheet: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rows := make([]*Row, 0, len(parsedRows))
|
||||
var options *ExcelSubmitOptions
|
||||
inconsistent := false
|
||||
|
||||
for _, parsed := range parsedRows {
|
||||
row := &Row{
|
||||
RowNo: parsed.RowNo,
|
||||
Iccid: excel.Field(parsed, "ICCID"),
|
||||
Msisdn: excel.Field(parsed, "MSISDN"),
|
||||
Imsi: excel.Field(parsed, "IMSI"),
|
||||
RawFields: map[string]string{
|
||||
"ICCID": excel.Field(parsed, "ICCID"),
|
||||
"MSISDN": excel.Field(parsed, "MSISDN"),
|
||||
"IMSI": excel.Field(parsed, "IMSI"),
|
||||
"newPackageCode": excel.Field(parsed, "newPackageCode"),
|
||||
"durationMonths": excel.Field(parsed, "durationMonths"),
|
||||
"giftDurationMonths": excel.Field(parsed, "giftDurationMonths"),
|
||||
"effectiveMode": excel.Field(parsed, "effectiveMode"),
|
||||
"offlineContractCompleted": excel.Field(parsed, "offlineContractCompleted"),
|
||||
},
|
||||
}
|
||||
rowOptions, reasons := parseExcelSubmitOptions(parsed)
|
||||
if len(reasons) > 0 {
|
||||
row.AddReason(strings.Join(reasons, "; "))
|
||||
rows = append(rows, row)
|
||||
continue
|
||||
}
|
||||
|
||||
row.NewPackageCode = rowOptions.NewPackageCode
|
||||
row.DurationMonths = rowOptions.DurationMonths
|
||||
row.GiftDurationMonths = rowOptions.GiftDurationMonths
|
||||
row.EffectiveMode = rowOptions.EffectiveMode
|
||||
row.OfflineContractCompleted = rowOptions.OfflineContractCompleted
|
||||
|
||||
if options == nil {
|
||||
options = rowOptions
|
||||
} else if !sameExcelSubmitOptions(options, rowOptions) {
|
||||
row.AddReason("same excel batch change params must be consistent")
|
||||
inconsistent = true
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
if inconsistent {
|
||||
markRowsWithoutReason(rows, "same excel batch change params must be consistent")
|
||||
}
|
||||
return rows, options, nil
|
||||
}
|
||||
|
||||
func parseExcelSubmitOptions(row excel.ParsedRow) (*ExcelSubmitOptions, []string) {
|
||||
var reasons []string
|
||||
|
||||
newPackageCode := strings.TrimSpace(excel.Field(row, "newPackageCode"))
|
||||
if newPackageCode == "" {
|
||||
reasons = append(reasons, "newPackageCode is required")
|
||||
}
|
||||
durationMonths, err := excel.ParsePositiveInt(excel.Field(row, "durationMonths"))
|
||||
if err != nil {
|
||||
reasons = append(reasons, "durationMonths must be a positive integer")
|
||||
}
|
||||
|
||||
giftDurationValue := strings.TrimSpace(excel.Field(row, "giftDurationMonths"))
|
||||
giftDurationMonths := 0
|
||||
if giftDurationValue != "" {
|
||||
giftDurationMonths, err = excel.ParseNonNegativeInt(giftDurationValue)
|
||||
if err != nil {
|
||||
reasons = append(reasons, "giftDurationMonths must be a non-negative integer")
|
||||
}
|
||||
}
|
||||
|
||||
effectiveMode := strings.TrimSpace(excel.Field(row, "effectiveMode"))
|
||||
if effectiveMode == "" {
|
||||
reasons = append(reasons, "effectiveMode is required")
|
||||
}
|
||||
|
||||
offlineContractCompleted, err := excel.ParseBool(excel.Field(row, "offlineContractCompleted"))
|
||||
if err != nil {
|
||||
reasons = append(reasons, "offlineContractCompleted format is invalid")
|
||||
} else if !offlineContractCompleted {
|
||||
reasons = append(reasons, "offlineContractCompleted must be true")
|
||||
}
|
||||
|
||||
if len(reasons) > 0 {
|
||||
return nil, reasons
|
||||
}
|
||||
|
||||
return &ExcelSubmitOptions{
|
||||
NewPackageCode: newPackageCode,
|
||||
DurationMonths: durationMonths,
|
||||
GiftDurationMonths: giftDurationMonths,
|
||||
EffectiveMode: effectiveMode,
|
||||
OfflineContractCompleted: offlineContractCompleted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sameExcelSubmitOptions(left, right *ExcelSubmitOptions) bool {
|
||||
if left == nil || right == nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(left.NewPackageCode) == strings.TrimSpace(right.NewPackageCode) &&
|
||||
left.DurationMonths == right.DurationMonths &&
|
||||
left.GiftDurationMonths == right.GiftDurationMonths &&
|
||||
strings.TrimSpace(left.EffectiveMode) == strings.TrimSpace(right.EffectiveMode) &&
|
||||
left.OfflineContractCompleted == right.OfflineContractCompleted
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// PrepareProcessor 描述 batch_type=4 的卡变更预处理批次。
|
||||
type PrepareProcessor struct {
|
||||
inner *Processor
|
||||
repo *Repository
|
||||
config batchimport.ProcessorConfig
|
||||
}
|
||||
|
||||
func NewPrepareProcessor() *PrepareProcessor {
|
||||
inner := NewProcessor()
|
||||
cfg := inner.config
|
||||
cfg.Code = "sim_card_change_prepare"
|
||||
cfg.BatchType = consts.BatchTypePrepare
|
||||
cfg.BatchNoPrefix = consts.BatchNoPrefixChange
|
||||
return &PrepareProcessor{
|
||||
inner: inner,
|
||||
repo: inner.repo,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) Config() batchimport.ProcessorConfig {
|
||||
return p.config
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
return p.inner.ValidateRows(ctx, rows)
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
info := g.Map{}
|
||||
if result != nil && result.ImportFileOssId > 0 {
|
||||
info["importFileOssId"] = result.ImportFileOssId
|
||||
} else if importOss != nil && importOss.OssId > 0 {
|
||||
info["importFileOssId"] = importOss.OssId
|
||||
}
|
||||
if resultOss != nil {
|
||||
info["resultFileOssId"] = resultOss.OssId
|
||||
}
|
||||
if downloadId > 0 {
|
||||
info["resultDownloadId"] = downloadId
|
||||
}
|
||||
info["targetBatchType"] = consts.BatchTypeChange
|
||||
info["targetBatchName"] = "卡变更"
|
||||
|
||||
if in, ok := req.BizParams.(*model.TSimCardChangeBatchPrepareParam); ok && in != nil {
|
||||
if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
}
|
||||
if in, ok := req.BizParams.(*model.TSimCardChangeBatchSubmitParam); ok && in != nil {
|
||||
if newPackageCode := strings.TrimSpace(in.NewPackageCode); newPackageCode != "" {
|
||||
info["newPackageCode"] = newPackageCode
|
||||
}
|
||||
if in.DurationMonths > 0 {
|
||||
info["durationMonths"] = in.DurationMonths
|
||||
}
|
||||
if in.GiftDurationMonths >= 0 {
|
||||
info["giftDurationMonths"] = in.GiftDurationMonths
|
||||
}
|
||||
if effectiveMode := strings.TrimSpace(in.EffectiveMode); effectiveMode != "" {
|
||||
info["effectiveMode"] = effectiveMode
|
||||
}
|
||||
info["offlineContractCompleted"] = in.OfflineContractCompleted
|
||||
}
|
||||
if snapshot, ok := req.ExecutionResult.(*model.TSimCardChangePrepareSnapshot); ok && snapshot != nil {
|
||||
if snapshot.SourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = snapshot.SourceBatchNo
|
||||
}
|
||||
info["customerAgentId"] = snapshot.CustomerAgentId
|
||||
info["customerName"] = snapshot.CustomerName
|
||||
info["oldPackageCode"] = snapshot.OldPackageCode
|
||||
info["oldPackageName"] = snapshot.OldPackageName
|
||||
info["oldPackageType"] = snapshot.OldPackageType
|
||||
info["oldPackageFlowMb"] = snapshot.OldPackageFlowMb
|
||||
if result != nil {
|
||||
info["quantity"] = result.ValidRows
|
||||
}
|
||||
}
|
||||
return gjson.New(info)
|
||||
}
|
||||
|
||||
func (p *PrepareProcessor) ValidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardChangeBatchPrepareParam, operator batchimport.OperatorContext, rows []*Row) (*PrepareContext, error) {
|
||||
return p.inner.ValidatePrepareContext(ctx, tx, in, operator, rows)
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
|
||||
|
||||
"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/frame/g"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
repo *Repository
|
||||
config batchimport.ProcessorConfig
|
||||
}
|
||||
|
||||
func NewProcessor() *Processor {
|
||||
return &Processor{
|
||||
repo: NewRepository(),
|
||||
config: batchimport.ProcessorConfig{
|
||||
Code: "sim_card_change",
|
||||
BatchType: consts.BatchTypeChange,
|
||||
BatchNoPrefix: consts.BatchNoPrefixChange,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimCardChange,
|
||||
ExportScene: consts.ResourceExportSceneChange,
|
||||
ImportDir: consts.SimCardChangeSubDirImportExcel,
|
||||
ResultDir: consts.SimCardChangeSubDirResultExcel,
|
||||
MaxFileSize: consts.SimCardChangeMaxFileSize,
|
||||
TemplateHeaders: []string{"ICCID", "MSISDN", "IMSI"},
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) Config() batchimport.ProcessorConfig {
|
||||
return p.config
|
||||
}
|
||||
|
||||
func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return nil, gerror.New("sim card change does not support file import on base processor")
|
||||
}
|
||||
|
||||
func (p *Processor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
changeRows := make([]*Row, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
row, ok := item.(*Row)
|
||||
if !ok || row == nil {
|
||||
item.AddErrorReason("change row type is invalid")
|
||||
continue
|
||||
}
|
||||
changeRows = append(changeRows, row)
|
||||
}
|
||||
return validateRows(changeRows)
|
||||
}
|
||||
|
||||
func (p *Processor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
|
||||
return gerror.New("sim card change must be submitted from prepare batch")
|
||||
}
|
||||
|
||||
func (p *Processor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
info := g.Map{}
|
||||
if resultOss != nil {
|
||||
info["resultFileOssId"] = resultOss.OssId
|
||||
}
|
||||
if downloadId > 0 {
|
||||
info["resultDownloadId"] = downloadId
|
||||
}
|
||||
if in, ok := req.BizParams.(*model.TSimCardChangeBatchPrepareParam); ok && in != nil {
|
||||
if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
}
|
||||
if in, ok := req.BizParams.(*model.TSimCardChangeBatchSubmitParam); ok && in != nil {
|
||||
if newPackageCode := strings.TrimSpace(in.NewPackageCode); newPackageCode != "" {
|
||||
info["newPackageCode"] = newPackageCode
|
||||
}
|
||||
if in.DurationMonths > 0 {
|
||||
info["durationMonths"] = in.DurationMonths
|
||||
}
|
||||
if in.GiftDurationMonths >= 0 {
|
||||
info["giftDurationMonths"] = in.GiftDurationMonths
|
||||
}
|
||||
if effectiveMode := strings.TrimSpace(in.EffectiveMode); effectiveMode != "" {
|
||||
info["effectiveMode"] = effectiveMode
|
||||
}
|
||||
info["offlineContractCompleted"] = in.OfflineContractCompleted
|
||||
}
|
||||
if snapshot, ok := req.ExecutionResult.(*model.TSimCardChangePrepareSnapshot); ok && snapshot != nil {
|
||||
if result != nil && result.BatchType == consts.BatchTypePrepare {
|
||||
info["targetBatchType"] = consts.BatchTypeChange
|
||||
info["targetBatchName"] = "sim_card_change"
|
||||
}
|
||||
if snapshot.SourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = snapshot.SourceBatchNo
|
||||
}
|
||||
info["customerAgentId"] = snapshot.CustomerAgentId
|
||||
info["customerName"] = snapshot.CustomerName
|
||||
info["oldPackageCode"] = snapshot.OldPackageCode
|
||||
info["oldPackageName"] = snapshot.OldPackageName
|
||||
info["oldPackageType"] = snapshot.OldPackageType
|
||||
info["oldPackageFlowMb"] = snapshot.OldPackageFlowMb
|
||||
info["quantity"] = snapshot.Quantity
|
||||
}
|
||||
if output, ok := req.ExecutionResult.(*CompleteOutput); ok && output != nil {
|
||||
if output.SourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = output.SourceBatchNo
|
||||
}
|
||||
info["sourceMode"] = output.SourceMode
|
||||
info["orderSn"] = output.OrderSn
|
||||
info["customerAgentId"] = output.CustomerAgentId
|
||||
info["customerName"] = output.CustomerName
|
||||
info["oldPackageCode"] = output.OldPackageCode
|
||||
info["oldPackageName"] = output.OldPackageName
|
||||
info["newPackageCode"] = output.NewPackageCode
|
||||
info["newPackageName"] = output.NewPackageName
|
||||
info["durationMonths"] = output.DurationMonths
|
||||
info["giftDurationMonths"] = output.GiftDurationMonths
|
||||
info["effectiveMode"] = output.EffectiveMode
|
||||
info["offlineContractCompleted"] = output.OfflineContractCompleted
|
||||
info["changeUnitPrice"] = output.ChangeUnitPrice
|
||||
info["amount"] = output.Amount
|
||||
info["quantity"] = output.Quantity
|
||||
info["packageBeginTime"] = output.PackageBeginTime
|
||||
info["packageEndTime"] = output.PackageEndTime
|
||||
info["changeTime"] = output.ChangeTime
|
||||
}
|
||||
return gjson.New(info)
|
||||
}
|
||||
|
||||
func (p *Processor) ValidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardChangeBatchPrepareParam, operator batchimport.OperatorContext, rows []*Row) (*PrepareContext, error) {
|
||||
if in == nil {
|
||||
in = &model.TSimCardChangeBatchPrepareParam{}
|
||||
}
|
||||
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
|
||||
|
||||
iccids := rowsToIccids(rows)
|
||||
cardsByIccid, err := p.repo.LockCardsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "validate change cards failed")
|
||||
}
|
||||
bindsByIccid, err := p.repo.LockCurrentActiveBindsByIccids(ctx, tx, iccids)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "validate change subscriptions failed")
|
||||
}
|
||||
|
||||
packageCodes := make(map[string]struct{})
|
||||
customerAgentIds := make(map[int64]struct{})
|
||||
oldPackageCode := ""
|
||||
customerAgentId := int64(0)
|
||||
for _, row := range rows {
|
||||
if row == nil || row.Reason != "" {
|
||||
continue
|
||||
}
|
||||
card := cardsByIccid[row.Iccid]
|
||||
bind := bindsByIccid[row.Iccid]
|
||||
reason := validateChangeRow(sourceBatchNo, operator, row, card, bind)
|
||||
if reason != "" {
|
||||
row.AddReason(reason)
|
||||
continue
|
||||
}
|
||||
packageCode := strings.TrimSpace(bind.PackageCode)
|
||||
packageCodes[packageCode] = struct{}{}
|
||||
customerAgentIds[card.DeptId] = struct{}{}
|
||||
oldPackageCode = packageCode
|
||||
customerAgentId = card.DeptId
|
||||
|
||||
row.Msisdn = strings.TrimSpace(card.Msisdn)
|
||||
row.Imsi = strings.TrimSpace(card.Imsi)
|
||||
row.OldPackageCode = packageCode
|
||||
row.OldSubscriptionNo = strings.TrimSpace(bind.SubscriptionNo)
|
||||
row.OldOrderSn = strings.TrimSpace(bind.OrderCode)
|
||||
row.OldEffectiveTime = bind.EffectiveTime
|
||||
row.OldExpireTime = bind.ExpectedExpireTime
|
||||
}
|
||||
|
||||
if len(packageCodes) > 1 {
|
||||
markRowsWithoutReason(rows, "current batch contains multiple old packages and cannot be changed together")
|
||||
}
|
||||
if len(customerAgentIds) > 1 {
|
||||
markRowsWithoutReason(rows, "current batch contains multiple customers and cannot be changed together")
|
||||
}
|
||||
if hasInvalidRows(rows) {
|
||||
markRemainingRowsSkipped(rows)
|
||||
return nil, gerror.New("some cards do not meet change conditions, batch was not executed")
|
||||
}
|
||||
if oldPackageCode == "" || customerAgentId <= 0 {
|
||||
return nil, gerror.New("change prepare result is incomplete")
|
||||
}
|
||||
|
||||
pkg, err := loadPackageByCode(ctx, tx, operator.TenantId, oldPackageCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pkg.ChangeablePackageCodes == nil || pkg.ChangeablePackageCodes.IsNil() || len(pkg.ChangeablePackageCodes.Interfaces()) == 0 {
|
||||
return nil, gerror.New("current package has no changeable packages configured")
|
||||
}
|
||||
customerName, err := loadDeptName(ctx, tx, operator.TenantId, customerAgentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row != nil {
|
||||
row.OldPackageName = pkg.PackageName
|
||||
}
|
||||
}
|
||||
return &PrepareContext{
|
||||
CustomerAgentId: customerAgentId,
|
||||
CustomerName: customerName,
|
||||
OldPackage: pkg,
|
||||
Quantity: len(iccids),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateChangeRow(sourceBatchNo string, operator batchimport.OperatorContext, row *Row, card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) string {
|
||||
if card == nil {
|
||||
return "ICCID does not exist as formal card"
|
||||
}
|
||||
if bind == nil {
|
||||
return "ICCID current subscription data is missing or invalid"
|
||||
}
|
||||
if card.DeptId != operator.DeptId && card.CreatedDept != operator.DeptId {
|
||||
return "ICCID does not belong to current department"
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone {
|
||||
return "ICCID has no current package"
|
||||
}
|
||||
if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) {
|
||||
return "ICCID current package and subscription package do not match"
|
||||
}
|
||||
if sourceBatchNo != "" && strings.TrimSpace(bind.BatchNo) != sourceBatchNo {
|
||||
return "ICCID current subscription does not belong to sourceBatchNo"
|
||||
}
|
||||
if row != nil {
|
||||
if msisdn := strings.TrimSpace(row.Msisdn); msisdn != "" && msisdn != strings.TrimSpace(card.Msisdn) {
|
||||
return "MSISDN does not match card fact"
|
||||
}
|
||||
if imsi := strings.TrimSpace(row.Imsi); imsi != "" && imsi != strings.TrimSpace(card.Imsi) {
|
||||
return "IMSI does not match card fact"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadPackageByCode(ctx context.Context, tx gdb.TX, tenantId, packageCode string) (*entity.TSimFlowPackageInfo, error) {
|
||||
pkg := &entity.TSimFlowPackageInfo{}
|
||||
columns := dao.TSimFlowPackageInfo.Columns()
|
||||
err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()).
|
||||
Where(columns.TenantId, tenantId).
|
||||
Where(columns.PackageCode, strings.TrimSpace(packageCode)).
|
||||
Scan(pkg)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "load package failed")
|
||||
}
|
||||
if pkg.Id == 0 {
|
||||
return nil, gerror.New("package not found")
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func loadDeptName(ctx context.Context, tx gdb.TX, tenantId string, deptId int64) (string, error) {
|
||||
dept := &entity.SysDept{}
|
||||
columns := dao.SysDept.Columns()
|
||||
err := tx.Ctx(ctx).Model(dao.SysDept.Table()).
|
||||
Where(columns.TenantId, tenantId).
|
||||
Where(columns.DeptId, deptId).
|
||||
Scan(dept)
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "load customer failed")
|
||||
}
|
||||
if dept.DeptId == 0 {
|
||||
return "", gerror.New("customer not found")
|
||||
}
|
||||
return strings.TrimSpace(dept.DeptName), nil
|
||||
}
|
||||
|
||||
func hasInvalidRows(rows []*Row) bool {
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Reason != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func markRowsWithoutReason(rows []*Row, reason string) {
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Reason == "" {
|
||||
row.AddReason(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markRemainingRowsSkipped(rows []*Row) {
|
||||
for _, row := range rows {
|
||||
if row != nil && row.Reason == "" {
|
||||
row.AddReason("same batch has rows failing change conditions, this row was skipped")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateRows(rows []*Row) error {
|
||||
firstByIccid := make(map[string]*Row, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
row.Iccid = strings.TrimSpace(row.Iccid)
|
||||
row.Msisdn = strings.TrimSpace(row.Msisdn)
|
||||
row.Imsi = strings.TrimSpace(row.Imsi)
|
||||
if row.Iccid == "" {
|
||||
row.AddReason("ICCID cannot be empty")
|
||||
continue
|
||||
}
|
||||
if first, ok := firstByIccid[row.Iccid]; ok {
|
||||
row.AddReason("ICCID duplicated")
|
||||
first.AddReason("ICCID duplicated")
|
||||
continue
|
||||
}
|
||||
firstByIccid[row.Iccid] = row
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user