chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。 - app/:uni-app 小程序前端(基于 unibest,MIT) - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID - 内网后端地址 → admin.iotlcm.example.com - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明) - server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0) - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件) - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化 - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位) - 依赖号码尾部的断言同步更新 - 补 server/LICENSE(Apache-2.0) - 移除内部资产:CI 触发器 - 补根 README,LF 行尾 + .gitattributes 已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type appFeatureAccessRule struct {
|
||||
Feature string
|
||||
Permissions []string
|
||||
Routes []string
|
||||
}
|
||||
|
||||
// appFeatureAccessRules maps mini-program business features to existing console
|
||||
// menu permissions in sys_menu.perms. It deliberately avoids role names so the
|
||||
// app follows the same role-menu binding model as the admin console.
|
||||
var appFeatureAccessRules = []appFeatureAccessRule{
|
||||
{
|
||||
Feature: "card:list",
|
||||
Permissions: []string{"cpa:terminal:user", "cpm:iot:tSimCardInfo:list"},
|
||||
Routes: []string{"/pages/card/index"},
|
||||
},
|
||||
{
|
||||
Feature: "card:detail",
|
||||
Permissions: []string{"cpm:iot:tSimCardInfo:view", "cpm:iot:tSimCardInfo:list"},
|
||||
Routes: []string{"/pages/card/views/detail/index"},
|
||||
},
|
||||
{
|
||||
Feature: "package:view",
|
||||
Permissions: []string{
|
||||
"cpm:iot:tSimCardBindPkg:list",
|
||||
"cpm:iot:tSimFlowPackageInfo:list",
|
||||
"cpm:iot:tSimFlowPackageInfo:view",
|
||||
"cpm:iot:tSimCardBindPkg:getPkgInfo",
|
||||
},
|
||||
Routes: []string{"/pages/card/views/packageDetail/index"},
|
||||
},
|
||||
{
|
||||
Feature: "card:bind",
|
||||
Permissions: []string{consts.SysCheckPermissionCurrentUser},
|
||||
},
|
||||
{
|
||||
Feature: "card:renew",
|
||||
Permissions: []string{"cpm:iot:tSimCardRenew:batch:confirm"},
|
||||
Routes: []string{
|
||||
"/pages/business/views/renew/index",
|
||||
"/pages/business/views/cardRenew/index",
|
||||
},
|
||||
},
|
||||
{
|
||||
Feature: "card:renew:submit",
|
||||
Permissions: []string{"cpm:iot:tSimCardRenew:batch:confirm"},
|
||||
},
|
||||
{
|
||||
Feature: "card:addon",
|
||||
Permissions: []string{
|
||||
"cpm:iot:tSimCardBindPkg:list",
|
||||
"cpm:iot:tSimFlowPackageInfo:view",
|
||||
"cpm:iot:tSimCardBindPkg:getPkgInfo",
|
||||
},
|
||||
Routes: []string{"/pages/business/views/addon/index"},
|
||||
},
|
||||
{
|
||||
Feature: "card:change",
|
||||
Permissions: []string{"cpm:iot:tSimCardChange:batch:submit"},
|
||||
Routes: []string{"/pages/business/views/change/index"},
|
||||
},
|
||||
{
|
||||
Feature: "card:change:submit",
|
||||
Permissions: []string{"cpm:iot:tSimCardChange:batch:submit"},
|
||||
},
|
||||
{
|
||||
Feature: "card:allocate",
|
||||
Permissions: []string{
|
||||
"cpm:iot:tSimCardAllocate:targets&cpm:iot:tSimCardAllocate:input:prepare&cpm:iot:tSimCardAllocate:batch:submit",
|
||||
},
|
||||
Routes: []string{"/pages/business/views/allocate/index"},
|
||||
},
|
||||
{
|
||||
Feature: "card:stop",
|
||||
Permissions: []string{"cpm:iot:tSimCardStop:input:submit"},
|
||||
},
|
||||
{
|
||||
Feature: "card:start",
|
||||
Permissions: []string{"cpm:iot:tSimCardStart:input:submit"},
|
||||
},
|
||||
{
|
||||
Feature: "card:restart",
|
||||
Permissions: []string{"cpm:iot:tSimCardRestart:input:submit"},
|
||||
},
|
||||
{
|
||||
Feature: "card:dataStart",
|
||||
Permissions: []string{"cpm:iot:tSimCardDataStart:input:submit"},
|
||||
},
|
||||
{
|
||||
Feature: "card:dataStop",
|
||||
Permissions: []string{"cpm:iot:tSimCardDataStop:input:submit"},
|
||||
},
|
||||
{
|
||||
Feature: "card:diagnosis",
|
||||
Permissions: []string{
|
||||
"cpm:iot:tSimdiagnose:info",
|
||||
"cpm:iot:tSimdiagnose:flow",
|
||||
"cpm:iot:tSimCardInfo:querySimSession",
|
||||
"cpm:iot:tSimCardInfo:querySimStatus",
|
||||
},
|
||||
Routes: []string{"/pages/business/views/diagnosis/index"},
|
||||
},
|
||||
{
|
||||
Feature: "warn:view",
|
||||
Permissions: []string{
|
||||
"cpm:iot:simWarningConfig:list",
|
||||
"cpm:iot:simWarningConfig:view",
|
||||
"cpm:iot:simWarningConfig:save",
|
||||
"cpm:iot:tSimWarningConfig:input:submit",
|
||||
"cpm:iot:tSimWarningConfig:range:submit",
|
||||
"cpm:iot:tSimWarningConfig:excel:submit",
|
||||
},
|
||||
Routes: []string{"/pages/business/views/warning/index"},
|
||||
},
|
||||
{
|
||||
Feature: "order:list",
|
||||
Permissions: []string{
|
||||
"cpm:common:tBalanceDetail:list",
|
||||
"cpm:common:tFinanceApply:list",
|
||||
"cpm:gen:tFinanceApply:list",
|
||||
},
|
||||
Routes: []string{"/pages/business/views/order/index"},
|
||||
},
|
||||
{
|
||||
Feature: "order:detail",
|
||||
Permissions: []string{
|
||||
"cpm:common:tBalanceDetail:view",
|
||||
"cpm:common:tBalanceDetail:list",
|
||||
"cpm:common:tFinanceApply:view",
|
||||
"cpm:common:tFinanceApply:list",
|
||||
"cpm:gen:tFinanceApply:list",
|
||||
},
|
||||
},
|
||||
{
|
||||
Feature: "customer:list",
|
||||
Permissions: []string{"cpm:agent:dept:list", "cpm:agent:tAgentEnterprise:list"},
|
||||
Routes: []string{"/pages/business/views/customer/index"},
|
||||
},
|
||||
{
|
||||
Feature: "customer:detail",
|
||||
Permissions: []string{"cpm:agent:tAgentEnterprise:view", "cpm:agent:tAgentEnterprise:list"},
|
||||
Routes: []string{"/pages/business/views/customerDetail/index"},
|
||||
},
|
||||
{
|
||||
Feature: "message:view",
|
||||
Permissions: []string{consts.SysCheckPermissionCurrentUser, "cpm:system:notice:list"},
|
||||
Routes: []string{"/pages/message/index", "/pages/message/views/cardDetail/index", "/pages/message/views/systemDetail/index"},
|
||||
},
|
||||
{
|
||||
Feature: "workorder:list",
|
||||
Permissions: []string{"cpm:gen:tWoMain:list"},
|
||||
Routes: []string{"/pages/business/views/workOrder/index"},
|
||||
},
|
||||
{
|
||||
Feature: "workorder:create",
|
||||
Permissions: []string{"cpm:gen:tWoMain:edit"},
|
||||
Routes: []string{"/pages/business/views/workOrder/index", "/pages/business/views/workOrderCreate/index"},
|
||||
},
|
||||
{
|
||||
Feature: "workorder:detail",
|
||||
Permissions: []string{"cpm:gen:tWoMain:view", "cpm:gen:tWoMain:list"},
|
||||
Routes: []string{"/pages/business/views/workOrderDetail/index"},
|
||||
},
|
||||
}
|
||||
|
||||
func appEnsureFeatureAccess(ctx context.Context, feature string) error {
|
||||
feature = strings.TrimSpace(feature)
|
||||
if feature == "" {
|
||||
return nil
|
||||
}
|
||||
configured, err := appAccessConfigured(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !configured || contexts.IsSuperAdmin(ctx) {
|
||||
return nil
|
||||
}
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId == 0 {
|
||||
return gerror.New("登录已过期")
|
||||
}
|
||||
requiredCodes := appFeaturePermissionCandidates(feature)
|
||||
if len(requiredCodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
accessCodes, _, err := mcache.GetUserAccessCodeList(ctx, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, requiredCode := range requiredCodes {
|
||||
if appAccessCodesMatchExpression(accessCodes, requiredCode) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return gerror.New("暂无操作权限")
|
||||
}
|
||||
|
||||
func appAccessCodesMatchExpression(accessCodes []string, expression string) bool {
|
||||
parts := appPermissionExpressionParts(expression)
|
||||
if len(parts) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, part := range parts {
|
||||
matched := false
|
||||
for _, accessCode := range accessCodes {
|
||||
if appMatchAccessCode(accessCode, part) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func appMatchAccessCode(pattern string, code string) bool {
|
||||
pattern = appNormalizePermissionCode(pattern)
|
||||
code = appNormalizePermissionCode(code)
|
||||
if pattern == "" || code == "" {
|
||||
return false
|
||||
}
|
||||
if pattern == code || pattern == "*" || pattern == "cpm:*" {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(pattern, "*") {
|
||||
return strings.HasPrefix(code, pattern[:len(pattern)-1])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appFeaturePermissionCandidates(feature string) []string {
|
||||
feature = appNormalizeFeatureKey(feature)
|
||||
if feature == "" {
|
||||
return nil
|
||||
}
|
||||
if feature == "*" {
|
||||
return []string{"*"}
|
||||
}
|
||||
codes := make([]string, 0)
|
||||
for _, rule := range appFeatureAccessRules {
|
||||
if rule.Feature != feature {
|
||||
continue
|
||||
}
|
||||
for _, permission := range rule.Permissions {
|
||||
codes = appendUniqueString(codes, appNormalizePermissionExpression(permission))
|
||||
}
|
||||
}
|
||||
if len(codes) == 0 && strings.HasPrefix(feature, consts.SysCheckPermissionPrefix) {
|
||||
codes = appendUniqueString(codes, appNormalizePermissionCode(feature))
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
func appFeaturesByPermission(permission string) []string {
|
||||
permission = appNormalizePermissionCode(permission)
|
||||
if permission == "" {
|
||||
return nil
|
||||
}
|
||||
features := make([]string, 0)
|
||||
for _, rule := range appFeatureAccessRules {
|
||||
for _, rulePermission := range rule.Permissions {
|
||||
if strings.Contains(rulePermission, "&") {
|
||||
continue
|
||||
}
|
||||
if appMatchAccessCode(permission, rulePermission) || appMatchAccessCode(rulePermission, permission) {
|
||||
features = appendUniqueString(features, rule.Feature)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return features
|
||||
}
|
||||
|
||||
func appRoutesByFeature(feature string) []string {
|
||||
feature = appNormalizeFeatureKey(feature)
|
||||
routes := make([]string, 0)
|
||||
for _, rule := range appFeatureAccessRules {
|
||||
if rule.Feature != feature {
|
||||
continue
|
||||
}
|
||||
for _, route := range rule.Routes {
|
||||
routes = appendUniqueString(routes, route)
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func appKnownPermissionCodes() []string {
|
||||
codes := make([]string, 0)
|
||||
for _, rule := range appFeatureAccessRules {
|
||||
for _, permission := range rule.Permissions {
|
||||
for _, part := range appPermissionExpressionParts(permission) {
|
||||
codes = appendUniqueString(codes, appNormalizePermissionCode(part))
|
||||
}
|
||||
}
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
func appAppendFeaturesByPermissionSet(accessPermissions []string, appendFeature func(string)) {
|
||||
if len(accessPermissions) == 0 || appendFeature == nil {
|
||||
return
|
||||
}
|
||||
for _, rule := range appFeatureAccessRules {
|
||||
for _, permission := range rule.Permissions {
|
||||
if appAccessCodesMatchExpression(accessPermissions, permission) {
|
||||
appendFeature(rule.Feature)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appMenuPermCandidates() []string {
|
||||
values := make([]string, 0)
|
||||
for _, permission := range appKnownPermissionCodes() {
|
||||
if permission == "" || permission == consts.SysCheckPermissionCurrentUser {
|
||||
continue
|
||||
}
|
||||
values = appendUniqueString(values, permission)
|
||||
if strings.HasPrefix(permission, consts.SysCheckPermissionMenuPrefix) {
|
||||
values = appendUniqueString(values, strings.TrimPrefix(permission, consts.SysCheckPermissionMenuPrefix))
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func appNormalizeFeatureKey(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if value == "*" || value == "cpm:*" {
|
||||
return "*"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func appNormalizePermissionCode(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
for strings.HasPrefix(value, consts.SysCheckPermissionMenuPrefix+consts.SysCheckPermissionMenuPrefix) {
|
||||
value = strings.TrimPrefix(value, consts.SysCheckPermissionMenuPrefix)
|
||||
}
|
||||
if value == "*" ||
|
||||
value == consts.SysCheckPermissionCurrentUser ||
|
||||
value == consts.SysCheckPermissionTerminalUser ||
|
||||
strings.HasPrefix(value, consts.SysCheckPermissionMenuPrefix) ||
|
||||
strings.HasPrefix(value, "cpa:") ||
|
||||
strings.HasPrefix(value, consts.SysCheckPermissionRolePrefix) ||
|
||||
strings.HasPrefix(value, consts.SysCheckPermissionDeptPrefix) ||
|
||||
strings.HasPrefix(value, consts.SysCheckPermissionPostPrefix) ||
|
||||
strings.HasPrefix(value, consts.SysCheckPermissionUserPrefix) {
|
||||
return value
|
||||
}
|
||||
return consts.SysCheckPermissionMenuPrefix + value
|
||||
}
|
||||
|
||||
func appNormalizePermissionExpression(value string) string {
|
||||
parts := appPermissionExpressionParts(value)
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(parts, "&")
|
||||
}
|
||||
|
||||
func appPermissionExpressionParts(value string) []string {
|
||||
rawParts := strings.Split(strings.TrimSpace(value), "&")
|
||||
parts := make([]string, 0, len(rawParts))
|
||||
for _, part := range rawParts {
|
||||
part = appNormalizePermissionCode(part)
|
||||
if part != "" {
|
||||
parts = appendUniqueString(parts, part)
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func appAccessConfigured(ctx context.Context) (bool, error) {
|
||||
return appAccessConfiguredForTenant(ctx, contexts.GetTenantId(ctx))
|
||||
}
|
||||
|
||||
func appAccessConfiguredForTenant(ctx context.Context, tenantId string) (bool, error) {
|
||||
perms := appMenuPermCandidates()
|
||||
if len(perms) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
mod := dao.SysMenu.Ctx(ctx).
|
||||
Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
|
||||
WhereIn(dao.SysMenu.Columns().Perms, perms)
|
||||
count, err := mod.Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "appAccessConfiguredForTenant count failed: %v", err)
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func appendUniqueString(values []string, value string) []string {
|
||||
if value == "" {
|
||||
return values
|
||||
}
|
||||
for _, existing := range values {
|
||||
if existing == value {
|
||||
return values
|
||||
}
|
||||
}
|
||||
return append(values, value)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package app
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAppAccessCodesMatchExpression(t *testing.T) {
|
||||
accessCodes := []string{
|
||||
"cpm:iot:tSimCardInfo:edit",
|
||||
"cpm:agent:dept:list",
|
||||
}
|
||||
|
||||
if !appAccessCodesMatchExpression(accessCodes, "cpm:iot:tSimCardInfo:edit&cpm:agent:dept:list") {
|
||||
t.Fatal("expected AND expression to match when all permissions are granted")
|
||||
}
|
||||
|
||||
if appAccessCodesMatchExpression(accessCodes, "cpm:iot:tSimCardInfo:edit&cpm:agent:tAgentEnterprise:list") {
|
||||
t.Fatal("expected AND expression to fail when one permission is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeFeatureDoesNotGrantLegacyAppPermission(t *testing.T) {
|
||||
if got := appNormalizeFeatureKey("cpm:app:*"); got == "*" {
|
||||
t.Fatal("legacy cpm:app:* must not be normalized to wildcard access")
|
||||
}
|
||||
if got := appNormalizeFeatureKey("app:card:list"); got == "card:list" {
|
||||
t.Fatal("legacy app: feature prefix must not be stripped")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartFeatureUsesDedicatedConsolePermission(t *testing.T) {
|
||||
if appAccessCodesMatchExpression(
|
||||
[]string{"cpm:iot:tSimCardInfo:edit"},
|
||||
appFeaturePermissionCandidates("card:restart")[0],
|
||||
) {
|
||||
t.Fatal("card edit permission must not grant restart feature")
|
||||
}
|
||||
|
||||
if !appAccessCodesMatchExpression(
|
||||
[]string{"cpm:iot:tSimCardRestart:input:submit"},
|
||||
appFeaturePermissionCandidates("card:restart")[0],
|
||||
) {
|
||||
t.Fatal("restart submit permission should grant restart feature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewAndChangeFeaturesUseSubmitPermissions(t *testing.T) {
|
||||
if appAccessCodesMatchExpression(
|
||||
[]string{"cpm:iot:tSimCardRenew:input:prepare"},
|
||||
appFeaturePermissionCandidates("card:renew")[0],
|
||||
) {
|
||||
t.Fatal("renew prepare permission must not grant mini app renew entry")
|
||||
}
|
||||
if !appAccessCodesMatchExpression(
|
||||
[]string{"cpm:iot:tSimCardRenew:batch:confirm"},
|
||||
appFeaturePermissionCandidates("card:renew")[0],
|
||||
) {
|
||||
t.Fatal("renew batch confirm permission should grant renew entry")
|
||||
}
|
||||
if appAccessCodesMatchExpression(
|
||||
[]string{"cpm:iot:tSimCardChange:input:prepare", "cpm:iot:tSimCardBindPkg:getChangePkg"},
|
||||
appFeaturePermissionCandidates("card:change")[0],
|
||||
) {
|
||||
t.Fatal("change prepare permission must not grant mini app change entry")
|
||||
}
|
||||
if !appAccessCodesMatchExpression(
|
||||
[]string{"cpm:iot:tSimCardChange:batch:submit"},
|
||||
appFeaturePermissionCandidates("card:change")[0],
|
||||
) {
|
||||
t.Fatal("change batch submit permission should grant change entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocateFeatureUsesDedicatedConsolePermissions(t *testing.T) {
|
||||
if appAccessCodesMatchExpression(
|
||||
[]string{"cpm:iot:tSimCardInfo:edit", "cpm:agent:dept:list"},
|
||||
appFeaturePermissionCandidates("card:allocate")[0],
|
||||
) {
|
||||
t.Fatal("card edit and customer list permissions must not grant mini app allocate entry")
|
||||
}
|
||||
if !appAccessCodesMatchExpression(
|
||||
[]string{
|
||||
"cpm:iot:tSimCardAllocate:targets",
|
||||
"cpm:iot:tSimCardAllocate:input:prepare",
|
||||
"cpm:iot:tSimCardAllocate:batch:submit",
|
||||
},
|
||||
appFeaturePermissionCandidates("card:allocate")[0],
|
||||
) {
|
||||
t.Fatal("complete allocate permissions should grant allocate entry")
|
||||
}
|
||||
if appAccessCodesMatchExpression(
|
||||
[]string{"cpm:iot:tSimCardAllocate:batch:submit"},
|
||||
appFeaturePermissionCandidates("card:allocate")[0],
|
||||
) {
|
||||
t.Fatal("single allocate submit permission must not grant full allocate entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataServiceFeaturesUseConsolePermissions(t *testing.T) {
|
||||
tests := []struct {
|
||||
feature string
|
||||
permission string
|
||||
}{
|
||||
{feature: "card:dataStart", permission: "cpm:iot:tSimCardDataStart:input:submit"},
|
||||
{feature: "card:dataStop", permission: "cpm:iot:tSimCardDataStop:input:submit"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
candidates := appFeaturePermissionCandidates(tt.feature)
|
||||
if len(candidates) != 1 || !appAccessCodesMatchExpression([]string{tt.permission}, candidates[0]) {
|
||||
t.Fatalf("%s should use %s", tt.feature, tt.permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestPhoneRegisterParamAllowsEmptySmsCode(t *testing.T) {
|
||||
err := g.Validator().Data(&model.AppPhoneRegisterParam{
|
||||
TenantId: "000000",
|
||||
Phone: "13800008000",
|
||||
Password: "123456",
|
||||
NickName: "测试用户",
|
||||
}).Run(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("AppPhoneRegisterParam should allow empty smsCode, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyOptionalAppSmsCodeSkipsEmptyCode(t *testing.T) {
|
||||
if err := verifyOptionalAppSmsCode(context.Background(), "000000", "13800008000", "register", ""); err != nil {
|
||||
t.Fatalf("empty smsCode should skip sms verification, got: %v", err)
|
||||
}
|
||||
if err := verifyOptionalAppSmsCode(context.Background(), "000000", "13800008000", "register", " "); err != nil {
|
||||
t.Fatalf("blank smsCode should skip sms verification, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletedRegisterUserNameFitsSysUserUniqueColumn(t *testing.T) {
|
||||
name := deletedRegisterUserName(88, "18800004698")
|
||||
if name != "del88_004698" {
|
||||
t.Fatalf("unexpected deleted register username: %s", name)
|
||||
}
|
||||
if len(name) > 30 {
|
||||
t.Fatalf("deleted register username length = %d, want <= 30", len(name))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSysUserNameUniqueKeyError(t *testing.T) {
|
||||
if !isSysUserNameUniqueKeyError(gerror.New("Error 1062: Duplicate entry '000000-18800004698' for key 'sys_user.uk_user_name'")) {
|
||||
t.Fatal("expected sys_user.uk_user_name duplicate error to be recognized")
|
||||
}
|
||||
if isSysUserNameUniqueKeyError(gerror.New("Error 1062: Duplicate entry 'x' for key 'other_key'")) {
|
||||
t.Fatal("unexpected duplicate key match for other indexes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppWechatMiniSecretMissing(t *testing.T) {
|
||||
if !appWechatMiniSecretMissing("") {
|
||||
t.Fatal("empty mini app secret should be treated as missing")
|
||||
}
|
||||
if !appWechatMiniSecretMissing(appWechatMiniAppSecretPlaceholder) {
|
||||
t.Fatal("placeholder mini app secret should be treated as missing")
|
||||
}
|
||||
if appWechatMiniSecretMissing("real-secret") {
|
||||
t.Fatal("non-placeholder mini app secret should not be treated as missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppEnsureWechatBindPhoneVerifiedSkipsVerifiedPhone(t *testing.T) {
|
||||
err := appEnsureWechatBindPhoneVerified(
|
||||
context.Background(),
|
||||
&model.AppWechatBindPhoneParam{},
|
||||
&appWechatBindTicketCache{VerifiedPhone: "13800008000"},
|
||||
"13800008000",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("verified phone should skip captcha verification, got: %v", err)
|
||||
}
|
||||
|
||||
err = appEnsureWechatBindPhoneVerified(
|
||||
context.Background(),
|
||||
&model.AppWechatBindPhoneParam{},
|
||||
&appWechatBindTicketCache{VerifiedPhone: "13800008001"},
|
||||
"13800008000",
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("different phone should require captcha verification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableAppLoginUsersByPasswordFiltersCandidates(t *testing.T) {
|
||||
svc := &sAppAuth{}
|
||||
users := []*entity.SysUser{
|
||||
appAuthTestUser(1, "phone-a", "right-password"),
|
||||
appAuthTestUser(2, "phone-b", "other-password"),
|
||||
}
|
||||
|
||||
matched, err := svc.availableAppLoginUsersByPassword(users, "wrong-password")
|
||||
if err != nil {
|
||||
t.Fatalf("availableAppLoginUsersByPassword returned error: %v", err)
|
||||
}
|
||||
if len(matched) != 0 {
|
||||
t.Fatalf("wrong password matched %d users, want 0", len(matched))
|
||||
}
|
||||
|
||||
matched, err = svc.availableAppLoginUsersByPassword(users, "right-password")
|
||||
if err != nil {
|
||||
t.Fatalf("availableAppLoginUsersByPassword returned error: %v", err)
|
||||
}
|
||||
if len(matched) != 1 || matched[0].UserId != 1 {
|
||||
t.Fatalf("right password matched %#v, want only user 1", matched)
|
||||
}
|
||||
}
|
||||
|
||||
func appAuthTestUser(userId int64, username string, password string) *entity.SysUser {
|
||||
salt := "abcde"
|
||||
return &entity.SysUser{
|
||||
UserId: userId,
|
||||
UserName: username,
|
||||
Salt: salt,
|
||||
Password: appPasswordEncryptForStore(password, salt),
|
||||
Status: consts.SysUserStatusNormal,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type appBalanceAccount struct {
|
||||
AccountId int64
|
||||
CreatedDept int64
|
||||
Balance float64
|
||||
AccountLabel string
|
||||
Status string
|
||||
}
|
||||
|
||||
func (s *sAppCard) deductAppBalance(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string, amount float64, remark string, now *gtime.Time) (*appBalanceAccount, error) {
|
||||
return s.deductAppBalanceForDept(ctx, tx, user, orderSn, amount, remark, 0, now)
|
||||
}
|
||||
|
||||
func (s *sAppCard) deductAppBalanceForDept(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string, amount float64, remark string, payerDeptId int64, now *gtime.Time) (*appBalanceAccount, error) {
|
||||
amount = appRoundAmount(amount)
|
||||
if amount <= 0 {
|
||||
return nil, gerror.New("扣款金额必须大于0")
|
||||
}
|
||||
var account *appBalanceAccount
|
||||
var err error
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
account, err = s.lockAppBalanceAccount(ctx, tx, user)
|
||||
} else {
|
||||
account, err = s.lockAppAgentBalanceAccount(ctx, tx, user, payerDeptId)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.ensureAppBalanceAccountConsumable(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if account.Balance < amount {
|
||||
return nil, gerror.Newf("账户余额不足,当前余额 %.2f 元,应付 %.2f 元", account.Balance, amount)
|
||||
}
|
||||
|
||||
afterBalance := appRoundAmount(account.Balance - amount)
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
result, err := dao.TTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TTerminal.Columns().TerminalId, user.UserId).
|
||||
Data(do.TTerminal{
|
||||
Balance: afterBalance,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "扣减终端账户余额失败")
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err != nil {
|
||||
return nil, gerror.Wrap(err, "读取终端账户余额扣减数量失败")
|
||||
} else if affected != 1 {
|
||||
return nil, gerror.New("终端账户余额已变化,请刷新后重试")
|
||||
}
|
||||
} else {
|
||||
result, err := dao.TAgentBalance.Ctx(ctx).TX(tx).
|
||||
Where(dao.TAgentBalance.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TAgentBalance.Columns().DeptId, account.AccountId).
|
||||
Data(do.TAgentBalance{
|
||||
Balance: afterBalance,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "扣减企业账户余额失败")
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err != nil {
|
||||
return nil, gerror.Wrap(err, "读取企业账户余额扣减数量失败")
|
||||
} else if affected != 1 {
|
||||
return nil, gerror.New("企业账户余额已变化,请刷新后重试")
|
||||
}
|
||||
}
|
||||
|
||||
account.Balance = afterBalance
|
||||
if err = s.insertAppBalanceDetail(ctx, tx, user, account, orderSn, amount, account.Balance+amount, afterBalance, consts.BalanceExpense, consts.BalanceTransactionTypeConsume, remark, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) creditAppBalance(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string, amount float64, remark string, channelSerialNo string, now *gtime.Time) (*appBalanceAccount, error) {
|
||||
return s.creditAppBalanceWithChannel(ctx, tx, user, orderSn, amount, remark, consts.BalanceTransactionChannelWechat, channelSerialNo, now)
|
||||
}
|
||||
|
||||
func (s *sAppCard) creditAppBalanceWithChannel(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string, amount float64, remark string, transactionChannel int, channelSerialNo string, now *gtime.Time) (*appBalanceAccount, error) {
|
||||
amount = appRoundAmount(amount)
|
||||
if amount <= 0 {
|
||||
return nil, gerror.New("入账金额必须大于0")
|
||||
}
|
||||
account, err := s.lockAppBalanceAccount(ctx, tx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
beforeBalance := account.Balance
|
||||
afterBalance := appRoundAmount(beforeBalance + amount)
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
result, err := dao.TTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TTerminal.Columns().TerminalId, user.UserId).
|
||||
Data(do.TTerminal{
|
||||
Balance: afterBalance,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "增加终端账户余额失败")
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err != nil {
|
||||
return nil, gerror.Wrap(err, "读取终端账户余额入账数量失败")
|
||||
} else if affected != 1 {
|
||||
return nil, gerror.New("终端账户余额已变化,请刷新后重试")
|
||||
}
|
||||
} else {
|
||||
result, err := dao.TAgentBalance.Ctx(ctx).TX(tx).
|
||||
Where(dao.TAgentBalance.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TAgentBalance.Columns().DeptId, user.DeptId).
|
||||
Data(do.TAgentBalance{
|
||||
Balance: afterBalance,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "增加企业账户余额失败")
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err != nil {
|
||||
return nil, gerror.Wrap(err, "读取企业账户余额入账数量失败")
|
||||
} else if affected != 1 {
|
||||
return nil, gerror.New("企业账户余额已变化,请刷新后重试")
|
||||
}
|
||||
}
|
||||
|
||||
account.Balance = afterBalance
|
||||
if err = s.insertAppBalanceDetailWithChannel(ctx, tx, user, account, orderSn, amount, beforeBalance, afterBalance, consts.BalanceIncome, consts.BalanceTransactionTypeRecharge, transactionChannel, channelSerialNo, remark, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) lockAppBalanceAccount(ctx context.Context, tx gdb.TX, user *entity.SysUser) (*appBalanceAccount, error) {
|
||||
if user == nil {
|
||||
return nil, gerror.New("当前用户不存在")
|
||||
}
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
var terminal *entity.TTerminal
|
||||
if err := dao.TTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TTerminal.Columns().TerminalId, user.UserId).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&terminal); err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定终端账户余额失败")
|
||||
}
|
||||
if terminal == nil || terminal.TerminalId == 0 {
|
||||
return nil, gerror.New("终端账户不存在,无法余额扣款")
|
||||
}
|
||||
return &appBalanceAccount{
|
||||
AccountId: user.UserId,
|
||||
CreatedDept: appUserDeptOrDefault(user),
|
||||
Balance: terminal.Balance,
|
||||
AccountLabel: "terminal",
|
||||
Status: strings.TrimSpace(terminal.Status),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return s.lockAppAgentBalanceAccount(ctx, tx, user, 0)
|
||||
}
|
||||
|
||||
func (s *sAppCard) lockAppAgentBalanceAccount(ctx context.Context, tx gdb.TX, user *entity.SysUser, payerDeptId int64) (*appBalanceAccount, error) {
|
||||
if user == nil {
|
||||
return nil, gerror.New("当前用户不存在")
|
||||
}
|
||||
payerDeptId = appBalancePayerDeptId(user, payerDeptId)
|
||||
if payerDeptId <= 0 {
|
||||
return nil, gerror.New("当前用户未绑定机构,无法余额扣款")
|
||||
}
|
||||
var agentBalance *entity.TAgentBalance
|
||||
if err := dao.TAgentBalance.Ctx(ctx).TX(tx).
|
||||
Where(dao.TAgentBalance.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TAgentBalance.Columns().DeptId, payerDeptId).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&agentBalance); err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定企业账户余额失败")
|
||||
}
|
||||
if agentBalance == nil || agentBalance.Id == 0 {
|
||||
createdBalance, createErr := s.ensureAppAgentBalanceForDept(ctx, tx, user, payerDeptId, gtime.Now())
|
||||
if createErr != nil {
|
||||
return nil, createErr
|
||||
}
|
||||
agentBalance = createdBalance
|
||||
}
|
||||
if agentBalance.Status != 0 {
|
||||
return nil, gerror.New("企业账户已停用,无法余额扣款")
|
||||
}
|
||||
return &appBalanceAccount{
|
||||
AccountId: payerDeptId,
|
||||
CreatedDept: payerDeptId,
|
||||
Balance: agentBalance.Balance,
|
||||
AccountLabel: "agent",
|
||||
Status: fmt.Sprintf("%d", agentBalance.Status),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureAppBalanceAccountConsumable(account *appBalanceAccount) error {
|
||||
if account == nil {
|
||||
return gerror.New("余额账户信息不完整")
|
||||
}
|
||||
switch account.AccountLabel {
|
||||
case "terminal":
|
||||
if status := strings.TrimSpace(account.Status); status != "" && status != consts.SysUserStatusNormal {
|
||||
return gerror.New("终端账户已停用,无法扣减余额")
|
||||
}
|
||||
case "agent":
|
||||
if status := strings.TrimSpace(account.Status); status != "" && status != "0" {
|
||||
return gerror.New("企业账户已停用,无法扣减余额")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureAppAgentBalance(ctx context.Context, tx gdb.TX, user *entity.SysUser, now *gtime.Time) (*entity.TAgentBalance, error) {
|
||||
return s.ensureAppAgentBalanceForDept(ctx, tx, user, 0, now)
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureAppAgentBalanceForDept(ctx context.Context, tx gdb.TX, user *entity.SysUser, payerDeptId int64, now *gtime.Time) (*entity.TAgentBalance, error) {
|
||||
if user == nil {
|
||||
return nil, gerror.New("当前用户未绑定企业,无法初始化企业账户")
|
||||
}
|
||||
payerDeptId = appBalancePayerDeptId(user, payerDeptId)
|
||||
if payerDeptId <= 0 {
|
||||
return nil, gerror.New("当前用户未绑定企业,无法初始化企业账户")
|
||||
}
|
||||
if now == nil {
|
||||
now = gtime.Now()
|
||||
}
|
||||
|
||||
var dept *entity.SysDept
|
||||
deptMod := dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, user.TenantId).
|
||||
Where(dao.SysDept.Columns().DeptId, payerDeptId).
|
||||
Limit(1)
|
||||
if tx != nil {
|
||||
deptMod = deptMod.TX(tx).LockUpdate()
|
||||
}
|
||||
if err := deptMod.Scan(&dept); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业机构失败")
|
||||
}
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
return nil, gerror.New("企业机构不存在,无法初始化余额账户")
|
||||
}
|
||||
|
||||
balanceModel := func() *gdb.Model {
|
||||
mod := dao.TAgentBalance.Ctx(ctx)
|
||||
if tx != nil {
|
||||
mod = mod.TX(tx)
|
||||
}
|
||||
return mod
|
||||
}
|
||||
var existing *entity.TAgentBalance
|
||||
if err := balanceModel().
|
||||
Where(dao.TAgentBalance.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TAgentBalance.Columns().DeptId, payerDeptId).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&existing); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业余额账户失败")
|
||||
}
|
||||
if existing != nil && existing.Id > 0 {
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
id, err := balanceModel().Data(do.TAgentBalance{
|
||||
DeptId: payerDeptId,
|
||||
Balance: 0,
|
||||
WithdrawAmount: 0,
|
||||
WithdrawAmountStatus: 1,
|
||||
InvoiceAmount: 0,
|
||||
CanInvoiceAmount: 0,
|
||||
CreditLimit: 0,
|
||||
UsedCredit: 0,
|
||||
PendingRepayment: 0,
|
||||
Status: 0,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: payerDeptId,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
var created *entity.TAgentBalance
|
||||
if queryErr := balanceModel().
|
||||
Where(dao.TAgentBalance.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TAgentBalance.Columns().DeptId, payerDeptId).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&created); queryErr == nil && created != nil && created.Id > 0 {
|
||||
return created, nil
|
||||
}
|
||||
return nil, gerror.Wrap(err, "初始化企业余额账户失败")
|
||||
}
|
||||
return &entity.TAgentBalance{
|
||||
Id: id,
|
||||
DeptId: payerDeptId,
|
||||
Balance: 0,
|
||||
WithdrawAmountStatus: 1,
|
||||
Status: 0,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: payerDeptId,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appBalancePayerDeptId(user *entity.SysUser, payerDeptId int64) int64 {
|
||||
if payerDeptId > 0 {
|
||||
return payerDeptId
|
||||
}
|
||||
if user != nil {
|
||||
return user.DeptId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *sAppCard) insertAppBalanceDetail(ctx context.Context, tx gdb.TX, user *entity.SysUser, account *appBalanceAccount, orderSn string, amount, beforeBalance, afterBalance float64, incomeExpenseType int, transactionType int, remark string, now *gtime.Time) error {
|
||||
return s.insertAppBalanceDetailWithChannel(ctx, tx, user, account, orderSn, amount, beforeBalance, afterBalance, incomeExpenseType, transactionType, consts.BalanceTransactionChannelBalance, "", remark, now)
|
||||
}
|
||||
|
||||
func (s *sAppCard) insertAppBalanceDetailWithChannel(ctx context.Context, tx gdb.TX, user *entity.SysUser, account *appBalanceAccount, orderSn string, amount, beforeBalance, afterBalance float64, incomeExpenseType int, transactionType int, transactionChannel int, channelSerialNo string, remark string, now *gtime.Time) error {
|
||||
if user == nil || account == nil {
|
||||
return gerror.New("余额流水账户信息不完整")
|
||||
}
|
||||
serialNo := appCardBatchNo("BAL", user.UserId, now)
|
||||
if _, err := dao.TBalanceDetail.Ctx(ctx).TX(tx).Data(do.TBalanceDetail{
|
||||
DeptId: account.AccountId,
|
||||
SerialNo: serialNo,
|
||||
TransactionTime: now,
|
||||
IncomeExpenseType: incomeExpenseType,
|
||||
TransactionType: transactionType,
|
||||
TransactionChannel: transactionChannel,
|
||||
Amount: appRoundAmount(amount),
|
||||
BeforeBalance: appRoundAmount(beforeBalance),
|
||||
BalanceAfter: appRoundAmount(afterBalance),
|
||||
ChannelSerialNo: channelSerialNo,
|
||||
OrderSn: orderSn,
|
||||
Remark: fmt.Sprintf("%s:%s", account.AccountLabel, remark),
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: account.CreatedDept,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "写入余额流水失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"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 appBalanceRechargeAccount struct {
|
||||
AccountType string
|
||||
AccountId int64
|
||||
CustomerType int
|
||||
CustomerAgentId int64
|
||||
EndUserId int64
|
||||
AgentId int64
|
||||
CreatedDept int64
|
||||
}
|
||||
|
||||
// BalanceRechargeSubmit 创建余额充值待支付订单,并返回前端可使用的支付信息。
|
||||
// 余额入账和余额流水只在支付回调或模拟支付确认成功后写入。
|
||||
func (s *sAppCard) BalanceRechargeSubmit(ctx context.Context, param *model.AppBalanceRechargeSubmitParam) (res *model.AppBalanceRechargeSubmitModel, err error) {
|
||||
if !appBalanceRechargeEnabled(ctx) {
|
||||
return nil, gerror.New("充值功能暂未开放")
|
||||
}
|
||||
if param == nil {
|
||||
param = &model.AppBalanceRechargeSubmitParam{}
|
||||
}
|
||||
amount := appRoundAmount(param.Amount)
|
||||
if amount <= 0 {
|
||||
return nil, gerror.New("充值金额必须大于0")
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
orderSn := appOrderNo("SO", user.UserId, now)
|
||||
payProvider := strings.ToLower(strings.TrimSpace(param.PayProvider))
|
||||
if payProvider == "" {
|
||||
payProvider = "wechat"
|
||||
}
|
||||
switch payProvider {
|
||||
case "mock":
|
||||
if !appMockPaymentEnabled(ctx) {
|
||||
return nil, gerror.New("模拟支付未启用")
|
||||
}
|
||||
case "wechat":
|
||||
if !appWechatEnabled(ctx) {
|
||||
return nil, gerror.New("微信支付通道未配置")
|
||||
}
|
||||
default:
|
||||
return nil, gerror.New("暂不支持的支付方式")
|
||||
}
|
||||
|
||||
var orderId int64
|
||||
err = dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
account, err := s.ensureAppBalanceRechargeAccount(ctx, tx, user, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
orderId, err = s.insertAppBalanceRechargeOrder(ctx, tx, user, account, orderSn, strings.TrimSpace(param.BuyerNote), amount, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.insertAppBalanceRechargeOrderItem(ctx, tx, user, account, orderId, orderSn, amount, now)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var payRequest *appWechatPayRequest
|
||||
if payProvider == "wechat" {
|
||||
if appNormalizeWechatPayScene(param.PayScene) == "mini" {
|
||||
if err = service.AppAuth().BindWechatOpenIdByLoginCode(ctx, user, param.WxCode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
payRequest, err = appBuildWechatPayRequest(ctx, &entity.TOrder{
|
||||
Id: orderId,
|
||||
OrderSn: orderSn,
|
||||
OrderType: consts.OrderTypeBalanceRecharge,
|
||||
PayStatus: consts.PayStatusUnpaid,
|
||||
TotalAmount: amount,
|
||||
CreatedAt: now,
|
||||
}, param.PayScene, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
paymentExpireAt, paymentRemainingSeconds, paymentExpired := appBalanceRechargePaymentTiming(ctx, &entity.TOrder{
|
||||
OrderType: consts.OrderTypeBalanceRecharge,
|
||||
PayStatus: consts.PayStatusUnpaid,
|
||||
CreatedAt: now,
|
||||
})
|
||||
|
||||
return &model.AppBalanceRechargeSubmitModel{
|
||||
OrderId: orderId,
|
||||
OrderSn: orderSn,
|
||||
TotalAmount: amount,
|
||||
PriceText: appFormatMoney(amount),
|
||||
OrderStatus: consts.OrderStatusPendingPay,
|
||||
PayStatus: consts.PayStatusUnpaid,
|
||||
Status: "pending_pay",
|
||||
PaymentExpireAt: paymentExpireAt,
|
||||
PaymentRemainingSeconds: paymentRemainingSeconds,
|
||||
PaymentExpired: paymentExpired,
|
||||
PayProvider: payProvider,
|
||||
PayUrl: appWechatPayUrl(payRequest),
|
||||
PayMethod: appWechatPayMethod(payRequest),
|
||||
PayGateway: "",
|
||||
PayParams: appWechatPayParams(payRequest),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureAppBalanceRechargeAccount(ctx context.Context, tx gdb.TX, user *entity.SysUser, now *gtime.Time) (*appBalanceRechargeAccount, error) {
|
||||
if user == nil {
|
||||
return nil, gerror.New("当前用户不存在")
|
||||
}
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
if err := syncTerminalUserProfile(ctx, tx, user, &model.AppUpdateUserInfoParam{
|
||||
NickName: user.NickName,
|
||||
TerminalName: user.NickName,
|
||||
Phone: user.Phonenumber,
|
||||
Email: user.Email,
|
||||
Sex: user.Sex,
|
||||
}, now); err != nil {
|
||||
return nil, gerror.Wrap(err, "初始化终端余额账户失败")
|
||||
}
|
||||
var terminal *entity.TTerminal
|
||||
if err := dao.TTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TTerminal.Columns().TerminalId, user.UserId).
|
||||
Limit(1).
|
||||
Scan(&terminal); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询终端余额账户失败")
|
||||
}
|
||||
if terminal == nil || terminal.TerminalId == 0 {
|
||||
return nil, gerror.New("终端余额账户不存在,无法充值")
|
||||
}
|
||||
if strings.TrimSpace(terminal.Status) != "" && strings.TrimSpace(terminal.Status) != consts.SysUserStatusNormal {
|
||||
return nil, gerror.New("终端账户已停用,无法充值")
|
||||
}
|
||||
return &appBalanceRechargeAccount{
|
||||
AccountType: "terminal",
|
||||
AccountId: user.UserId,
|
||||
CustomerType: consts.CustomerTypeTerminal,
|
||||
CustomerAgentId: appUserDeptOrDefault(user),
|
||||
EndUserId: user.UserId,
|
||||
AgentId: 0,
|
||||
CreatedDept: appUserDeptOrDefault(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if user.DeptId <= 0 {
|
||||
return nil, gerror.New("当前用户未绑定机构,无法充值")
|
||||
}
|
||||
var agentBalance *entity.TAgentBalance
|
||||
if err := dao.TAgentBalance.Ctx(ctx).TX(tx).
|
||||
Where(dao.TAgentBalance.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TAgentBalance.Columns().DeptId, user.DeptId).
|
||||
Limit(1).
|
||||
Scan(&agentBalance); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业余额账户失败")
|
||||
}
|
||||
if agentBalance == nil || agentBalance.Id == 0 {
|
||||
createdBalance, createErr := s.ensureAppAgentBalance(ctx, tx, user, now)
|
||||
if createErr != nil {
|
||||
return nil, createErr
|
||||
}
|
||||
agentBalance = createdBalance
|
||||
}
|
||||
if agentBalance.Status != 0 {
|
||||
return nil, gerror.New("企业账户已停用,无法充值")
|
||||
}
|
||||
|
||||
sellerDeptId, err := s.renewSellerDeptId(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sellerDeptId <= 0 {
|
||||
sellerDeptId = user.DeptId
|
||||
}
|
||||
return &appBalanceRechargeAccount{
|
||||
AccountType: "agent",
|
||||
AccountId: user.DeptId,
|
||||
CustomerType: consts.CustomerTypeAgent,
|
||||
CustomerAgentId: user.DeptId,
|
||||
EndUserId: 0,
|
||||
AgentId: sellerDeptId,
|
||||
CreatedDept: user.DeptId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) insertAppBalanceRechargeOrder(ctx context.Context, tx gdb.TX, user *entity.SysUser, account *appBalanceRechargeAccount, orderSn string, buyerNote string, amount float64, now *gtime.Time) (int64, error) {
|
||||
orderId, err := dao.TOrder.Ctx(ctx).TX(tx).Data(do.TOrder{
|
||||
OrderSn: orderSn,
|
||||
ParentOrderId: 0,
|
||||
ParentOrderSn: "",
|
||||
CustomerType: account.CustomerType,
|
||||
CustomerAgentId: account.CustomerAgentId,
|
||||
EndUserId: account.EndUserId,
|
||||
AgentId: account.AgentId,
|
||||
OrderType: consts.OrderTypeBalanceRecharge,
|
||||
OrderStatus: consts.OrderStatusPendingPay,
|
||||
ShippingStatus: 0,
|
||||
ReturnStatus: 0,
|
||||
Consignee: "",
|
||||
Mobile: "",
|
||||
Email: "",
|
||||
Address: "",
|
||||
AreaCode: "",
|
||||
BuyerNote: strings.TrimSpace(buyerNote),
|
||||
AdminNote: "",
|
||||
Remark: "小程序余额充值待支付订单",
|
||||
ShippingMethod: consts.ShippingMethodNone,
|
||||
LogisticsId: 0,
|
||||
LogisticsName: "",
|
||||
ShippingTypeId: 0,
|
||||
ShippingTypeName: "",
|
||||
TrackingNo: "",
|
||||
PayType: consts.PayTypeOnline,
|
||||
PayStatus: consts.PayStatusUnpaid,
|
||||
PayOrderSn: "",
|
||||
TotalAmount: amount,
|
||||
ProductAmount: amount,
|
||||
ShippingFee: 0,
|
||||
ServiceFee: 0,
|
||||
InvoiceFee: 0,
|
||||
CouponAmount: 0,
|
||||
DiscountAmount: 0,
|
||||
Balance: 0,
|
||||
PaidAmount: 0,
|
||||
OnlinePaidAmount: 0,
|
||||
OfflinePaidAmount: 0,
|
||||
UnpaidAmount: amount,
|
||||
UnrefundAmount: 0,
|
||||
IsNeedCommission: 0,
|
||||
DistributionStatus: 0,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: account.CreatedDept,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "创建余额充值订单失败")
|
||||
}
|
||||
return orderId, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) insertAppBalanceRechargeOrderItem(ctx context.Context, tx gdb.TX, user *entity.SysUser, account *appBalanceRechargeAccount, orderId int64, orderSn string, amount float64, now *gtime.Time) error {
|
||||
skuData := appRenewMarshalJSON([]map[string]any{
|
||||
{"skuName": "业务类型", "skuValue": "余额充值"},
|
||||
{"skuName": "充值金额", "skuValue": appFormatMoney(amount), "amount": amount},
|
||||
})
|
||||
extraSkuData := appRenewMarshalJSON(map[string]any{
|
||||
"business": "miniapp_balance_recharge",
|
||||
"accountType": account.AccountType,
|
||||
"accountId": account.AccountId,
|
||||
"amount": amount,
|
||||
"payChannel": "pending",
|
||||
})
|
||||
if _, err := dao.TOrderItem.Ctx(ctx).TX(tx).Data(do.TOrderItem{
|
||||
OrderId: orderId,
|
||||
OrderSn: orderSn,
|
||||
CustomerType: account.CustomerType,
|
||||
CustomerAgentId: account.CustomerAgentId,
|
||||
EndUserId: account.EndUserId,
|
||||
Price: amount,
|
||||
Quantity: 1,
|
||||
ProductId: 0,
|
||||
ProductName: "余额充值",
|
||||
ProductSn: "BALANCE_RECHARGE",
|
||||
PicThumb: "",
|
||||
SkuId: 0,
|
||||
SkuData: skuData,
|
||||
DeliveryQuantity: 0,
|
||||
ProductType: consts.ProductTypeBalance,
|
||||
IsGift: 0,
|
||||
AgentId: account.AgentId,
|
||||
PrepayPrice: 0,
|
||||
Commission: "",
|
||||
OriginPrice: amount,
|
||||
PromotionData: "",
|
||||
ExtraSkuData: extraSkuData,
|
||||
PayMode: 0,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: account.CreatedDept,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "创建余额充值订单项失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appFormatMoney(value float64) string {
|
||||
return fmt.Sprintf("¥%.2f", appRoundAmount(value))
|
||||
}
|
||||
|
||||
func appBalanceRechargeEnabled(ctx context.Context) bool {
|
||||
return g.Cfg().MustGet(ctx, "payment.balanceRecharge.enabled", false).Bool()
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type appBalanceRechargePaymentResult struct {
|
||||
Order *entity.TOrder
|
||||
BalanceAfter float64
|
||||
PayTradeNo string
|
||||
PayProvider string
|
||||
}
|
||||
|
||||
func (s *sAppCard) completeAppBalanceRechargePayment(ctx context.Context, orderSn string, loginUser *entity.SysUser, payTradeNo string, payProvider string, transactionChannel int, expectedAmount float64, paidAt *gtime.Time) (*appBalanceRechargePaymentResult, error) {
|
||||
orderSn = strings.TrimSpace(orderSn)
|
||||
if orderSn == "" {
|
||||
return nil, gerror.New("订单号不能为空")
|
||||
}
|
||||
payTradeNo = strings.TrimSpace(payTradeNo)
|
||||
payProvider = strings.TrimSpace(payProvider)
|
||||
if payProvider == "" {
|
||||
payProvider = "online"
|
||||
}
|
||||
if paidAt == nil {
|
||||
paidAt = gtime.Now()
|
||||
}
|
||||
|
||||
var result *appBalanceRechargePaymentResult
|
||||
err := dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
query := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().OrderSn, orderSn).
|
||||
LockUpdate().
|
||||
Limit(1)
|
||||
if loginUser != nil {
|
||||
query = query.Where(dao.TOrder.Columns().TenantId, loginUser.TenantId)
|
||||
}
|
||||
|
||||
var lockedOrder *entity.TOrder
|
||||
if err := query.Scan(&lockedOrder); err != nil {
|
||||
return gerror.Wrap(err, "锁定充值订单失败")
|
||||
}
|
||||
if lockedOrder == nil || lockedOrder.Id == 0 {
|
||||
return gerror.New("充值订单不存在")
|
||||
}
|
||||
if lockedOrder.OrderType != consts.OrderTypeBalanceRecharge {
|
||||
return gerror.New("订单不是余额充值订单")
|
||||
}
|
||||
|
||||
user := loginUser
|
||||
if user == nil {
|
||||
var err error
|
||||
user, err = s.loadAppBalanceRechargeOrderUser(ctx, tx, lockedOrder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, lockedOrder); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amount := appRoundAmount(lockedOrder.TotalAmount)
|
||||
if amount <= 0 {
|
||||
return gerror.New("充值订单金额不正确")
|
||||
}
|
||||
if expectedAmount > 0 && math.Abs(appRoundAmount(expectedAmount)-amount) >= 0.01 {
|
||||
return gerror.Newf("支付金额与订单金额不一致,订单 %.2f 元,支付 %.2f 元", amount, expectedAmount)
|
||||
}
|
||||
|
||||
account, err := s.lockAppBalanceAccount(ctx, tx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if lockedOrder.PayStatus == consts.PayStatusPaid {
|
||||
if payTradeNo == "" {
|
||||
payTradeNo = strings.TrimSpace(lockedOrder.PayOrderSn)
|
||||
}
|
||||
result = &appBalanceRechargePaymentResult{
|
||||
Order: lockedOrder,
|
||||
BalanceAfter: appRoundAmount(account.Balance),
|
||||
PayTradeNo: payTradeNo,
|
||||
PayProvider: payProvider,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if lockedOrder.PayStatus != consts.PayStatusUnpaid || lockedOrder.OrderStatus != consts.OrderStatusPendingPay {
|
||||
return gerror.New("充值订单状态不允许支付")
|
||||
}
|
||||
|
||||
creditedAccount, err := s.creditAppBalanceWithChannel(ctx, tx, user, lockedOrder.OrderSn, amount, fmt.Sprintf("%s充值", payProvider), transactionChannel, payTradeNo, paidAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().Id, lockedOrder.Id).
|
||||
Data(do.TOrder{
|
||||
OrderStatus: consts.OrderStatusCompleted,
|
||||
PayType: consts.PayTypeOnline,
|
||||
PayStatus: consts.PayStatusPaid,
|
||||
PayOrderSn: payTradeNo,
|
||||
PayTime: paidAt,
|
||||
PaidAmount: amount,
|
||||
OnlinePaidAmount: amount,
|
||||
UnpaidAmount: 0,
|
||||
Remark: fmt.Sprintf("小程序余额充值%s支付成功", payProvider),
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: paidAt,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新充值订单支付状态失败")
|
||||
}
|
||||
if err = s.updateAppBalanceRechargeOrderItemPayment(ctx, tx, user, lockedOrder, creditedAccount, payProvider, payTradeNo, paidAt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lockedOrder.OrderStatus = consts.OrderStatusCompleted
|
||||
lockedOrder.PayType = consts.PayTypeOnline
|
||||
lockedOrder.PayStatus = consts.PayStatusPaid
|
||||
lockedOrder.PayOrderSn = payTradeNo
|
||||
lockedOrder.PayTime = paidAt
|
||||
lockedOrder.PaidAmount = amount
|
||||
lockedOrder.OnlinePaidAmount = amount
|
||||
lockedOrder.UnpaidAmount = 0
|
||||
result = &appBalanceRechargePaymentResult{
|
||||
Order: lockedOrder,
|
||||
BalanceAfter: appRoundAmount(creditedAccount.Balance),
|
||||
PayTradeNo: payTradeNo,
|
||||
PayProvider: payProvider,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil || result.Order == nil {
|
||||
return nil, gerror.New("支付结果不完整")
|
||||
}
|
||||
_ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{
|
||||
EventType: appAsyncEventBalanceRechargePaid,
|
||||
OrderSn: result.Order.OrderSn,
|
||||
PayProvider: result.PayProvider,
|
||||
PayTradeNo: result.PayTradeNo,
|
||||
TenantId: result.Order.TenantId,
|
||||
UserId: result.Order.CreatedBy,
|
||||
DeptId: result.Order.CreatedDept,
|
||||
Trigger: "payment_complete",
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) BalanceRechargeMockPay(ctx context.Context, param *model.AppBalanceRechargeMockPayParam) (*model.AppBalanceRechargeStatusModel, error) {
|
||||
if param == nil {
|
||||
param = &model.AppBalanceRechargeMockPayParam{}
|
||||
}
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !appMockPaymentEnabled(ctx) {
|
||||
return nil, gerror.New("模拟支付未启用")
|
||||
}
|
||||
orderSn := strings.TrimSpace(param.OrderSn)
|
||||
if orderSn == "" {
|
||||
return nil, gerror.New("订单号不能为空")
|
||||
}
|
||||
now := gtime.Now()
|
||||
payTradeNo := appCardBatchNo("MOCK", user.UserId, now)
|
||||
if _, err = s.completeAppBalanceRechargePayment(ctx, orderSn, user, payTradeNo, "mock", consts.BalanceTransactionChannelMock, 0, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.BalanceRechargeStatus(ctx, &model.AppBalanceRechargeStatusParam{OrderSn: orderSn})
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadAppBalanceRechargeOrderUser(ctx context.Context, tx gdb.TX, order *entity.TOrder) (*entity.SysUser, error) {
|
||||
if order == nil || order.CreatedBy <= 0 {
|
||||
return nil, gerror.New("充值订单创建人信息不完整")
|
||||
}
|
||||
var user *entity.SysUser
|
||||
if err := dao.SysUser.Ctx(ctx).TX(tx).
|
||||
Where(dao.SysUser.Columns().TenantId, order.TenantId).
|
||||
Where(dao.SysUser.Columns().UserId, order.CreatedBy).
|
||||
Limit(1).
|
||||
Scan(&user); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询充值订单用户失败")
|
||||
}
|
||||
if user == nil || user.UserId == 0 {
|
||||
return nil, gerror.New("充值订单用户不存在")
|
||||
}
|
||||
if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) validateAppBalanceRechargeOrderOwner(ctx context.Context, user *entity.SysUser, order *entity.TOrder) error {
|
||||
if user == nil || order == nil {
|
||||
return gerror.New("充值订单信息不完整")
|
||||
}
|
||||
if order.CreatedBy != user.UserId {
|
||||
if order.CustomerType != consts.CustomerTypeTerminal || order.EndUserId != user.UserId {
|
||||
return gerror.New("无权操作该充值订单")
|
||||
}
|
||||
}
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
if order.CustomerType != consts.CustomerTypeTerminal || order.EndUserId != user.UserId {
|
||||
return gerror.New("无权操作该充值订单")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if user.DeptId <= 0 {
|
||||
return gerror.New("当前用户未绑定机构,无法操作充值订单")
|
||||
}
|
||||
if order.CustomerType != consts.CustomerTypeAgent || order.CustomerAgentId != user.DeptId {
|
||||
return gerror.New("无权操作该充值订单")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) updateAppBalanceRechargeOrderItemPayment(ctx context.Context, tx gdb.TX, user *entity.SysUser, order *entity.TOrder, account *appBalanceAccount, payProvider string, payTradeNo string, paidAt *gtime.Time) error {
|
||||
if user == nil || order == nil || account == nil {
|
||||
return gerror.New("充值订单项支付快照信息不完整")
|
||||
}
|
||||
extraSkuData := appRenewMarshalJSON(map[string]any{
|
||||
"business": "miniapp_balance_recharge",
|
||||
"accountType": account.AccountLabel,
|
||||
"accountId": account.AccountId,
|
||||
"amount": appRoundAmount(order.TotalAmount),
|
||||
"payChannel": payProvider,
|
||||
"payTradeNo": payTradeNo,
|
||||
"paidAt": appRenewFormatTime(paidAt),
|
||||
})
|
||||
if _, err := dao.TOrderItem.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrderItem.Columns().OrderSn, order.OrderSn).
|
||||
Data(do.TOrderItem{
|
||||
ExtraSkuData: extraSkuData,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: paidAt,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新充值订单项支付快照失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) BalanceRechargeStatus(ctx context.Context, param *model.AppBalanceRechargeStatusParam) (*model.AppBalanceRechargeStatusModel, error) {
|
||||
if param == nil {
|
||||
param = &model.AppBalanceRechargeStatusParam{}
|
||||
}
|
||||
orderSn := strings.TrimSpace(param.OrderSn)
|
||||
if orderSn == "" {
|
||||
return nil, gerror.New("订单号不能为空")
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var order *entity.TOrder
|
||||
var balanceAfter float64
|
||||
err = dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TOrder.Columns().OrderSn, orderSn).
|
||||
Limit(1).
|
||||
Scan(&order); err != nil {
|
||||
return gerror.Wrap(err, "查询充值订单失败")
|
||||
}
|
||||
if order == nil || order.Id == 0 {
|
||||
return gerror.New("充值订单不存在")
|
||||
}
|
||||
if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, order); err != nil {
|
||||
return err
|
||||
}
|
||||
account, err := s.lockAppBalanceAccount(ctx, tx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
balanceAfter = appRoundAmount(account.Balance)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status := "pending_pay"
|
||||
if order.PayStatus == consts.PayStatusPaid {
|
||||
status = "paid"
|
||||
}
|
||||
paymentExpireAt, paymentRemainingSeconds, paymentExpired := appBalanceRechargePaymentTiming(ctx, order)
|
||||
if paymentExpired {
|
||||
status = "expired"
|
||||
}
|
||||
var payRequest *appWechatPayRequest
|
||||
payProvider := appBalancePayProviderFromTradeNo(order.PayOrderSn)
|
||||
if order.PayStatus == consts.PayStatusUnpaid && !paymentExpired && appWechatEnabled(ctx) {
|
||||
payProvider = "wechat"
|
||||
if appNormalizeWechatPayScene(param.PayScene) == "mini" && strings.TrimSpace(param.WxCode) != "" {
|
||||
if err = service.AppAuth().BindWechatOpenIdByLoginCode(ctx, user, param.WxCode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
payRequest, err = appBuildWechatPayRequest(ctx, order, param.PayScene, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &model.AppBalanceRechargeStatusModel{
|
||||
OrderId: order.Id,
|
||||
OrderSn: order.OrderSn,
|
||||
TotalAmount: appRoundAmount(order.TotalAmount),
|
||||
PriceText: appFormatMoney(order.TotalAmount),
|
||||
OrderStatus: order.OrderStatus,
|
||||
PayStatus: order.PayStatus,
|
||||
Status: status,
|
||||
PaymentExpireAt: paymentExpireAt,
|
||||
PaymentRemainingSeconds: paymentRemainingSeconds,
|
||||
PaymentExpired: paymentExpired,
|
||||
BalanceAfter: balanceAfter,
|
||||
BalanceAfterText: appFormatMoney(balanceAfter),
|
||||
PayProvider: payProvider,
|
||||
PayTradeNo: order.PayOrderSn,
|
||||
PayUrl: appWechatPayUrl(payRequest),
|
||||
PayMethod: appWechatPayMethod(payRequest),
|
||||
PayGateway: "",
|
||||
PayParams: appWechatPayParams(payRequest),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appBalancePayProviderFromTradeNo(tradeNo string) string {
|
||||
if strings.TrimSpace(tradeNo) == "" {
|
||||
return ""
|
||||
}
|
||||
upper := strings.ToUpper(tradeNo)
|
||||
if strings.HasPrefix(upper, "MOCK") {
|
||||
return "mock"
|
||||
}
|
||||
if strings.HasPrefix(upper, "WX") {
|
||||
return "wechat"
|
||||
}
|
||||
return "wechat"
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"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/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||||
)
|
||||
|
||||
const (
|
||||
appRefundStatusSuccess = "SUCCESS"
|
||||
appRefundStatusProcessing = "PROCESSING"
|
||||
appRefundStatusAbnormal = "ABNORMAL"
|
||||
appRefundStatusClosed = "CLOSED"
|
||||
)
|
||||
|
||||
func (s *sAppCard) BalanceRechargeRefund(ctx context.Context, param *model.AppBalanceRechargeRefundParam) (*model.AppOrderModel, error) {
|
||||
if param == nil {
|
||||
param = &model.AppBalanceRechargeRefundParam{}
|
||||
}
|
||||
orderSn := strings.TrimSpace(param.OrderSn)
|
||||
if orderSn == "" {
|
||||
return nil, gerror.New("订单号不能为空")
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := appLoadWechatConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
return nil, gerror.New("微信支付未启用")
|
||||
}
|
||||
if err = appValidateWechatCommonConfig(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(cfg.RefundNotifyUrl) == "" {
|
||||
return nil, gerror.New("微信退款回调地址未配置")
|
||||
}
|
||||
client, err := appWechatClient(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refundSvc := refunddomestic.RefundsApiService{Client: client}
|
||||
|
||||
now := gtime.Now()
|
||||
refundReason := strings.TrimSpace(param.RefundReason)
|
||||
if refundReason == "" {
|
||||
refundReason = "用户发起余额充值退款"
|
||||
}
|
||||
outRefundNo := appBalanceRechargeRefundNo(orderSn)
|
||||
|
||||
var payOrderSn string
|
||||
var refundAmount float64
|
||||
var refundAmountFen int64
|
||||
err = dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
order, err := s.lockAppBalanceRechargeRefundOrder(ctx, tx, user, orderSn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_no") == outRefundNo {
|
||||
return gerror.New("该订单已提交过退款申请,请等待处理结果")
|
||||
}
|
||||
|
||||
refundAmount = appRoundAmount(order.TotalAmount)
|
||||
if refundAmount <= 0 {
|
||||
return gerror.New("订单退款金额不正确")
|
||||
}
|
||||
refundAmountFen = appWechatAmountFen(refundAmount)
|
||||
if refundAmountFen <= 0 {
|
||||
return gerror.New("订单退款金额不正确")
|
||||
}
|
||||
payOrderSn = strings.TrimSpace(order.PayOrderSn)
|
||||
|
||||
if _, err = s.deductAppBalance(
|
||||
ctx,
|
||||
tx,
|
||||
user,
|
||||
order.OrderSn,
|
||||
refundAmount,
|
||||
"微信充值退款扣回",
|
||||
now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().Id, order.Id).
|
||||
Data(do.TOrder{
|
||||
ReturnStatus: consts.OrderReturnStatusPending,
|
||||
UnrefundAmount: refundAmount,
|
||||
Remark: "小程序余额充值退款申请中",
|
||||
AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, appRefundStatusProcessing, "", refundReason, true),
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新退款申请状态失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refundResp, _, err := refundSvc.Create(ctx, refunddomestic.CreateRequest{
|
||||
TransactionId: core.String(payOrderSn),
|
||||
OutTradeNo: core.String(orderSn),
|
||||
OutRefundNo: core.String(outRefundNo),
|
||||
Reason: core.String(refundReason),
|
||||
NotifyUrl: core.String(cfg.RefundNotifyUrl),
|
||||
Amount: &refunddomestic.AmountReq{
|
||||
Refund: core.Int64(refundAmountFen),
|
||||
Total: core.Int64(refundAmountFen),
|
||||
Currency: core.String("CNY"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if rollbackErr := s.rollbackAppBalanceRechargeRefund(ctx, orderSn, outRefundNo, "FAILED", "", refundReason, refundAmount, gtime.Now()); rollbackErr != nil {
|
||||
g.Log().Errorf(ctx, "余额充值退款申请失败后回滚余额失败: order=%s refundNo=%s error=%v", orderSn, outRefundNo, rollbackErr)
|
||||
}
|
||||
return nil, gerror.Wrap(err, "提交微信退款申请失败")
|
||||
}
|
||||
|
||||
refundStatus := appWechatRefundStatus(refundResp)
|
||||
switch refundStatus {
|
||||
case "", appRefundStatusProcessing, appRefundStatusSuccess:
|
||||
if err = s.applyAppBalanceRechargeRefundAccepted(ctx, orderSn, outRefundNo, refundStatus, appWechatRefundId(refundResp), refundReason, refundAmount, gtime.Now()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
if rollbackErr := s.rollbackAppBalanceRechargeRefund(ctx, orderSn, outRefundNo, refundStatus, appWechatRefundId(refundResp), refundReason, refundAmount, gtime.Now()); rollbackErr != nil {
|
||||
g.Log().Errorf(ctx, "微信退款申请未受理后回滚余额失败: order=%s refundNo=%s status=%s error=%v", orderSn, outRefundNo, refundStatus, rollbackErr)
|
||||
}
|
||||
return nil, gerror.Newf("微信退款申请未受理:%s", refundStatus)
|
||||
}
|
||||
return service.AppOrder().Detail(ctx, &model.AppOrderDetailParam{OrderSn: orderSn})
|
||||
}
|
||||
|
||||
func (s *sAppCard) applyAppBalanceRechargeRefundAccepted(ctx context.Context, orderSn string, outRefundNo string, refundStatus string, refundId string, refundReason string, refundAmount float64, now *gtime.Time) error {
|
||||
return dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
var order *entity.TOrder
|
||||
if err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().OrderSn, orderSn).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&order); err != nil {
|
||||
return gerror.Wrap(err, "锁定退款订单失败")
|
||||
}
|
||||
if order == nil || order.Id == 0 {
|
||||
return gerror.New("退款订单不存在")
|
||||
}
|
||||
if order.ReturnStatus != consts.OrderReturnStatusPending && order.ReturnStatus != consts.OrderReturnStatusRunning {
|
||||
return nil
|
||||
}
|
||||
metaRefundNo := appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_no")
|
||||
if metaRefundNo != "" && outRefundNo != "" && metaRefundNo != outRefundNo {
|
||||
return nil
|
||||
}
|
||||
|
||||
returnStatus := consts.OrderReturnStatusPending
|
||||
if refundStatus == appRefundStatusProcessing {
|
||||
returnStatus = consts.OrderReturnStatusRunning
|
||||
} else if refundStatus == appRefundStatusSuccess {
|
||||
returnStatus = consts.OrderReturnStatusSuccess
|
||||
}
|
||||
|
||||
unrefundAmount := refundAmount
|
||||
remark := "小程序余额充值退款申请中"
|
||||
if returnStatus == consts.OrderReturnStatusSuccess {
|
||||
unrefundAmount = 0
|
||||
remark = "小程序余额充值已退款"
|
||||
}
|
||||
if unrefundAmount <= 0 && returnStatus != consts.OrderReturnStatusSuccess {
|
||||
unrefundAmount = appRoundAmount(order.UnrefundAmount)
|
||||
}
|
||||
|
||||
if _, err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().Id, order.Id).
|
||||
Data(do.TOrder{
|
||||
ReturnStatus: returnStatus,
|
||||
UnrefundAmount: unrefundAmount,
|
||||
Remark: remark,
|
||||
AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, refundStatus, refundId, refundReason, true),
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新退款申请状态失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sAppCard) rollbackAppBalanceRechargeRefund(ctx context.Context, orderSn string, outRefundNo string, refundStatus string, refundId string, refundReason string, refundAmount float64, now *gtime.Time) error {
|
||||
return dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
var order *entity.TOrder
|
||||
if err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().OrderSn, orderSn).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&order); err != nil {
|
||||
return gerror.Wrap(err, "锁定退款订单失败")
|
||||
}
|
||||
if order == nil || order.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
if order.ReturnStatus != consts.OrderReturnStatusPending && order.ReturnStatus != consts.OrderReturnStatusRunning {
|
||||
return nil
|
||||
}
|
||||
metaRefundNo := appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_no")
|
||||
if metaRefundNo != "" && outRefundNo != "" && metaRefundNo != outRefundNo {
|
||||
return nil
|
||||
}
|
||||
|
||||
if appBalanceRechargeRefundBalanceDeducted(order.AdminNote) {
|
||||
orderUser, err := s.loadAppBalanceRechargeOrderUser(ctx, tx, order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if refundAmount <= 0 {
|
||||
refundAmount = appRoundAmount(order.UnrefundAmount)
|
||||
}
|
||||
if refundAmount <= 0 {
|
||||
refundAmount = appRoundAmount(order.TotalAmount)
|
||||
}
|
||||
if _, err = s.creditAppBalanceWithChannel(
|
||||
ctx,
|
||||
tx,
|
||||
orderUser,
|
||||
order.OrderSn,
|
||||
refundAmount,
|
||||
"微信退款失败余额回退",
|
||||
consts.BalanceTransactionChannelWechat,
|
||||
outRefundNo,
|
||||
now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().Id, order.Id).
|
||||
Data(do.TOrder{
|
||||
ReturnStatus: consts.OrderReturnStatusPartial,
|
||||
UnrefundAmount: 0,
|
||||
Remark: "小程序余额充值退款异常,余额已回退",
|
||||
AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, refundStatus, refundId, refundReason, false),
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新退款异常状态失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sAppCard) lockAppBalanceRechargeRefundOrder(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string) (*entity.TOrder, error) {
|
||||
var order *entity.TOrder
|
||||
if err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TOrder.Columns().OrderSn, orderSn).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&order); err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定充值订单失败")
|
||||
}
|
||||
if order == nil || order.Id == 0 {
|
||||
return nil, gerror.New("充值订单不存在")
|
||||
}
|
||||
if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if order.OrderType != consts.OrderTypeBalanceRecharge {
|
||||
return nil, gerror.New("仅余额充值订单支持退款")
|
||||
}
|
||||
if order.PayStatus != consts.PayStatusPaid || order.OrderStatus != consts.OrderStatusCompleted {
|
||||
return nil, gerror.New("仅已支付完成的充值订单支持退款")
|
||||
}
|
||||
if order.PayType != consts.PayTypeOnline || !strings.HasPrefix(strings.ToUpper(strings.TrimSpace(order.PayOrderSn)), "WX") {
|
||||
return nil, gerror.New("仅微信支付充值订单支持退款")
|
||||
}
|
||||
switch order.ReturnStatus {
|
||||
case consts.OrderReturnStatusPending, consts.OrderReturnStatusRunning:
|
||||
return nil, gerror.New("退款申请正在处理中")
|
||||
case consts.OrderReturnStatusSuccess:
|
||||
return nil, gerror.New("该订单已退款")
|
||||
case consts.OrderReturnStatusPartial:
|
||||
return nil, gerror.New("该订单退款异常,请联系管理员处理")
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) applyAppBalanceRechargeRefundNotify(ctx context.Context, refund *refunddomestic.Refund) error {
|
||||
if refund == nil {
|
||||
return nil
|
||||
}
|
||||
orderSn := strings.TrimSpace(appStringValue(refund.OutTradeNo))
|
||||
if orderSn == "" {
|
||||
return gerror.New("微信退款通知订单号为空")
|
||||
}
|
||||
|
||||
outRefundNo := strings.TrimSpace(appStringValue(refund.OutRefundNo))
|
||||
if outRefundNo == "" {
|
||||
outRefundNo = appBalanceRechargeRefundNo(orderSn)
|
||||
}
|
||||
refundStatus := appWechatRefundStatus(refund)
|
||||
refundId := appStringValue(refund.RefundId)
|
||||
refundAmount := appRoundAmount(float64(appWechatRefundAmountFen(refund)) / 100)
|
||||
now := gtime.Now()
|
||||
|
||||
return dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
var order *entity.TOrder
|
||||
if err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().OrderSn, orderSn).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&order); err != nil {
|
||||
return gerror.Wrap(err, "锁定退款订单失败")
|
||||
}
|
||||
if order == nil || order.Id == 0 {
|
||||
g.Log().Warningf(ctx, "微信退款通知订单不存在: order=%s refundNo=%s", orderSn, outRefundNo)
|
||||
return nil
|
||||
}
|
||||
if order.OrderType != consts.OrderTypeBalanceRecharge {
|
||||
return nil
|
||||
}
|
||||
if order.ReturnStatus != consts.OrderReturnStatusPending && order.ReturnStatus != consts.OrderReturnStatusRunning {
|
||||
return nil
|
||||
}
|
||||
metaRefundNo := appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_no")
|
||||
if metaRefundNo != "" && outRefundNo != "" && metaRefundNo != outRefundNo {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch refundStatus {
|
||||
case appRefundStatusSuccess:
|
||||
if !appBalanceRechargeRefundBalanceDeducted(order.AdminNote) {
|
||||
user, err := s.loadAppBalanceRechargeOrderUser(ctx, tx, order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if refundAmount <= 0 {
|
||||
refundAmount = appRoundAmount(order.UnrefundAmount)
|
||||
}
|
||||
if refundAmount <= 0 {
|
||||
refundAmount = appRoundAmount(order.TotalAmount)
|
||||
}
|
||||
if _, err = s.deductAppBalance(
|
||||
ctx,
|
||||
tx,
|
||||
user,
|
||||
order.OrderSn,
|
||||
refundAmount,
|
||||
"微信充值退款扣回",
|
||||
now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().Id, order.Id).
|
||||
Data(do.TOrder{
|
||||
ReturnStatus: consts.OrderReturnStatusSuccess,
|
||||
UnrefundAmount: 0,
|
||||
Remark: "小程序余额充值已退款",
|
||||
AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, refundStatus, refundId, appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_reason"), true),
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新退款成功状态失败")
|
||||
}
|
||||
case "", appRefundStatusProcessing:
|
||||
if _, err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().Id, order.Id).
|
||||
Data(do.TOrder{
|
||||
ReturnStatus: consts.OrderReturnStatusRunning,
|
||||
AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, appRefundStatusProcessing, refundId, appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_reason"), appBalanceRechargeRefundBalanceDeducted(order.AdminNote)),
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新退款处理中状态失败")
|
||||
}
|
||||
case appRefundStatusAbnormal, appRefundStatusClosed:
|
||||
if appBalanceRechargeRefundBalanceDeducted(order.AdminNote) {
|
||||
user, err := s.loadAppBalanceRechargeOrderUser(ctx, tx, order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if refundAmount <= 0 {
|
||||
refundAmount = appRoundAmount(order.UnrefundAmount)
|
||||
}
|
||||
if refundAmount <= 0 {
|
||||
refundAmount = appRoundAmount(order.TotalAmount)
|
||||
}
|
||||
if _, err = s.creditAppBalanceWithChannel(
|
||||
ctx,
|
||||
tx,
|
||||
user,
|
||||
order.OrderSn,
|
||||
refundAmount,
|
||||
"微信退款失败余额回退",
|
||||
consts.BalanceTransactionChannelWechat,
|
||||
outRefundNo,
|
||||
now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := dao.TOrder.Ctx(ctx).TX(tx).
|
||||
Where(dao.TOrder.Columns().Id, order.Id).
|
||||
Data(do.TOrder{
|
||||
ReturnStatus: consts.OrderReturnStatusPartial,
|
||||
UnrefundAmount: 0,
|
||||
Remark: "小程序余额充值退款异常,余额已回退",
|
||||
AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, refundStatus, refundId, appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_reason"), false),
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新退款异常状态失败")
|
||||
}
|
||||
default:
|
||||
g.Log().Warningf(ctx, "微信退款通知状态暂不处理: order=%s refundNo=%s status=%s", orderSn, outRefundNo, refundStatus)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func appBalanceRechargeRefundNo(orderSn string) string {
|
||||
orderSn = strings.TrimSpace(orderSn)
|
||||
if strings.HasPrefix(orderSn, "RF") {
|
||||
return orderSn
|
||||
}
|
||||
return "RF" + orderSn
|
||||
}
|
||||
|
||||
func appWechatRefundStatus(refund *refunddomestic.Refund) string {
|
||||
if refund == nil || refund.Status == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.ToUpper(strings.TrimSpace(string(*refund.Status)))
|
||||
}
|
||||
|
||||
func appWechatRefundId(refund *refunddomestic.Refund) string {
|
||||
if refund == nil {
|
||||
return ""
|
||||
}
|
||||
return appStringValue(refund.RefundId)
|
||||
}
|
||||
|
||||
func appWechatRefundAmountFen(refund *refunddomestic.Refund) int64 {
|
||||
if refund == nil || refund.Amount == nil {
|
||||
return 0
|
||||
}
|
||||
if refund.Amount.PayerRefund != nil && *refund.Amount.PayerRefund > 0 {
|
||||
return *refund.Amount.PayerRefund
|
||||
}
|
||||
if refund.Amount.Refund != nil && *refund.Amount.Refund > 0 {
|
||||
return *refund.Amount.Refund
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func appBalanceRechargeRefundNote(outRefundNo string, status string, refundId string, reason string) string {
|
||||
return fmt.Sprintf(
|
||||
"refund_no=%s;refund_status=%s;refund_id=%s;refund_reason=%s",
|
||||
strings.TrimSpace(outRefundNo),
|
||||
strings.ToLower(strings.TrimSpace(status)),
|
||||
strings.TrimSpace(refundId),
|
||||
strings.ReplaceAll(strings.TrimSpace(reason), ";", " "),
|
||||
)
|
||||
}
|
||||
|
||||
func appBalanceRechargeRefundNoteWithBalance(outRefundNo string, status string, refundId string, reason string, balanceDeducted bool) string {
|
||||
note := appBalanceRechargeRefundNote(outRefundNo, status, refundId, reason)
|
||||
if balanceDeducted {
|
||||
return note + ";refund_balance_deducted=1"
|
||||
}
|
||||
return note + ";refund_balance_deducted=0"
|
||||
}
|
||||
|
||||
func appBalanceRechargeRefundBalanceDeducted(note string) bool {
|
||||
value := strings.ToLower(appBalanceRechargeRefundNoteValue(note, "refund_balance_deducted"))
|
||||
return value == "1" || value == "true" || value == "yes"
|
||||
}
|
||||
|
||||
func appBalanceRechargeRefundNoteValue(note string, key string) string {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return ""
|
||||
}
|
||||
prefix := key + "="
|
||||
for _, part := range strings.Split(note, ";") {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(part, prefix) {
|
||||
return strings.TrimSpace(strings.TrimPrefix(part, prefix))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
)
|
||||
|
||||
func TestAppBalancePayerDeptIdPrefersExplicitDepartment(t *testing.T) {
|
||||
user := &entity.SysUser{DeptId: 175}
|
||||
|
||||
if got := appBalancePayerDeptId(user, 185); got != 185 {
|
||||
t.Fatalf("appBalancePayerDeptId() = %d, want 185", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppBalancePayerDeptIdFallsBackToUserDepartment(t *testing.T) {
|
||||
user := &entity.SysUser{DeptId: 175}
|
||||
|
||||
if got := appBalancePayerDeptId(user, 0); got != 175 {
|
||||
t.Fatalf("appBalancePayerDeptId() = %d, want 175", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppBalancePayerDeptIdReturnsZeroForNilUser(t *testing.T) {
|
||||
if got := appBalancePayerDeptId(nil, 0); got != 0 {
|
||||
t.Fatalf("appBalancePayerDeptId() = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/rabbitmq"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const (
|
||||
appAsyncEventBalanceRechargePaid = "balance_recharge_paid"
|
||||
appAsyncEventRenewCompleted = "card_renew_completed"
|
||||
appAsyncEventChangeCompleted = "card_change_completed"
|
||||
appAsyncEventAllocateCompleted = "card_allocate_completed"
|
||||
)
|
||||
|
||||
type appBusinessAsyncEvent struct {
|
||||
EventType string `json:"eventType"`
|
||||
OrderSn string `json:"orderSn,omitempty"`
|
||||
BatchNo string `json:"batchNo,omitempty"`
|
||||
BatchType int `json:"batchType,omitempty"`
|
||||
PayProvider string `json:"payProvider,omitempty"`
|
||||
PayTradeNo string `json:"payTradeNo,omitempty"`
|
||||
TenantId string `json:"tenantId,omitempty"`
|
||||
UserId int64 `json:"userId,omitempty"`
|
||||
DeptId int64 `json:"deptId,omitempty"`
|
||||
Trigger string `json:"trigger,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (s *sAppCard) publishAppBusinessAsyncEvent(ctx context.Context, event appBusinessAsyncEvent) error {
|
||||
event.EventType = strings.TrimSpace(event.EventType)
|
||||
if event.EventType == "" {
|
||||
return nil
|
||||
}
|
||||
if event.CreatedAt == "" {
|
||||
event.CreatedAt = gtime.Now().Format("Y-m-d H:i:s")
|
||||
}
|
||||
if !g.Cfg().MustGet(ctx, "appBusinessAsync.enabled", false).Bool() {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "序列化小程序业务异步事件失败")
|
||||
}
|
||||
|
||||
provider := strings.ToLower(strings.TrimSpace(g.Cfg().MustGet(ctx, "appBusinessAsync.provider", "rabbitmq").String()))
|
||||
queueName := strings.TrimSpace(g.Cfg().MustGet(ctx, "appBusinessAsync.queueName", "iotlcm.app.business").String())
|
||||
failOnPublish := g.Cfg().MustGet(ctx, "appBusinessAsync.failOnPublish", false).Bool()
|
||||
|
||||
var publishErr error
|
||||
switch provider {
|
||||
case "log", "stdout":
|
||||
g.Log().Infof(ctx, "小程序业务异步事件: queue=%s payload=%s", queueName, string(payload))
|
||||
case "rabbitmq":
|
||||
publishErr = rabbitmq.PublishBytes(ctx, queueName, payload)
|
||||
if publishErr != nil {
|
||||
g.Log().Warningf(ctx, "小程序业务异步事件 RabbitMQ 发布失败: queue=%s payload=%s err:%+v", queueName, string(payload), publishErr)
|
||||
} else {
|
||||
g.Log().Infof(ctx, "小程序业务异步事件 RabbitMQ 发布成功: queue=%s eventType=%s orderSn=%s batchNo=%s", queueName, event.EventType, event.OrderSn, event.BatchNo)
|
||||
}
|
||||
default:
|
||||
publishErr = gerror.Newf("不支持的小程序业务异步事件发布器: %s", provider)
|
||||
g.Log().Warningf(ctx, "%v: queue=%s payload=%s", publishErr, queueName, string(payload))
|
||||
}
|
||||
|
||||
if publishErr != nil && failOnPublish {
|
||||
return publishErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
)
|
||||
|
||||
const (
|
||||
appCardOwnerPublic = "public"
|
||||
appCardOwnerTerminal = "terminal"
|
||||
appCardOwnerAgent = "agent"
|
||||
|
||||
appSimTerminalDeptIdColumn = "dept_id"
|
||||
appSimTerminalCurrentPackageCodeColumn = "current_package_code"
|
||||
)
|
||||
|
||||
// sAppCard 是小程序卡片域服务入口,具体业务按文件拆分:
|
||||
// 查询、套餐、绑卡、重启、字段映射和通用工具分别维护。
|
||||
type sAppCard struct{}
|
||||
|
||||
// 承接卡主表和展示关联表的查询结果。
|
||||
type appCardQueryRow struct {
|
||||
entity.TSimCardInfo
|
||||
PackageName string `json:"packageName"`
|
||||
PackageType int `json:"packageType"`
|
||||
CategoryCode string `json:"categoryCode"`
|
||||
CategoryName string `json:"categoryName"`
|
||||
OwnerDeptName string `json:"ownerDeptName"`
|
||||
OwnerDeptKey string `json:"ownerDeptKey"`
|
||||
ChanName string `json:"chanName"`
|
||||
ChanAlias string `json:"chanAlias"`
|
||||
TempKey string `json:"tempKey"`
|
||||
TerminalName string `json:"terminalName"`
|
||||
TerminalBindId int64 `json:"terminalBindId" orm:"terminal_bind_id"`
|
||||
TerminalUserId int64 `json:"terminalUserId" orm:"terminal_user_id"`
|
||||
TerminalDeptId int64 `json:"terminalDeptId" orm:"terminal_dept_id"`
|
||||
TerminalCurrentPackageCode string `json:"terminalCurrentPackageCode" orm:"terminal_current_package_code"`
|
||||
TerminalPackageName string `json:"terminalPackageName" orm:"terminal_package_name"`
|
||||
TerminalPackageType int `json:"terminalPackageType" orm:"terminal_package_type"`
|
||||
TerminalCategoryCode string `json:"terminalCategoryCode" orm:"terminal_category_code"`
|
||||
TerminalCategoryName string `json:"terminalCategoryName" orm:"terminal_category_name"`
|
||||
}
|
||||
|
||||
// 承接用户可见套餐订购记录的查询结果。
|
||||
type appPackageQueryRow struct {
|
||||
entity.TSimCardBindPkg
|
||||
PackageName string `json:"packageName"`
|
||||
CategoryCode string `json:"categoryCode"`
|
||||
CategoryName string `json:"categoryName"`
|
||||
FlowMb float64 `json:"flowMb"`
|
||||
CardId int64 `json:"cardId"`
|
||||
CardDeptId int64 `json:"cardDeptId"`
|
||||
CardRemark string `json:"cardRemark"`
|
||||
TerminalName string `json:"terminalName"`
|
||||
}
|
||||
|
||||
// 承接公共查卡场景下当前套餐详情查询结果。
|
||||
type appPublicPackageQueryRow struct {
|
||||
entity.TSimFlowPackageInfo
|
||||
CategoryName string `json:"categoryName"`
|
||||
}
|
||||
|
||||
func NewAppCard() *sAppCard {
|
||||
return &sAppCard{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAppCard(NewAppCard())
|
||||
}
|
||||
@@ -0,0 +1,941 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type appAllocateBatchContext struct {
|
||||
Target *entity.SysDept
|
||||
Cards []*appAllocateCardContext
|
||||
TotalCount int
|
||||
ValidCount int
|
||||
FailCount int
|
||||
FailReason string
|
||||
}
|
||||
|
||||
type appAllocateCardContext struct {
|
||||
Card *appCardQueryRow
|
||||
ItemId string
|
||||
RowNo int
|
||||
SourceDeptId int64
|
||||
ErrorReason string
|
||||
}
|
||||
|
||||
// AllocateTargets 查询当前企业用户可划拨的目标客户。
|
||||
func (s *sAppCard) AllocateTargets(ctx context.Context, param *model.AppCardAllocateTargetListParam) (items []*model.AppCardAllocateTargetModel, total int, err error) {
|
||||
if param == nil {
|
||||
param = &model.AppCardAllocateTargetListParam{}
|
||||
}
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
return nil, 0, gerror.New("终端用户不能使用卡划拨功能")
|
||||
}
|
||||
if user.DeptId <= 0 {
|
||||
return nil, 0, gerror.New("当前账号未绑定企业,不能使用卡划拨功能")
|
||||
}
|
||||
if param.Page <= 0 {
|
||||
param.Page = 1
|
||||
}
|
||||
if param.PageSize <= 0 {
|
||||
param.PageSize = 50
|
||||
}
|
||||
if param.PageSize > 1000 {
|
||||
param.PageSize = 1000
|
||||
}
|
||||
|
||||
var rows []*entity.SysDept
|
||||
rows, total, err = s.loadAllocateTargetDepts(ctx, user, strings.TrimSpace(param.Keyword), param.Page, param.PageSize)
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "查询可划拨目标客户失败")
|
||||
}
|
||||
items = make([]*model.AppCardAllocateTargetModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, appAllocateTargetModel(row, user.DeptId))
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadAllocateTargetDepts(ctx context.Context, user *entity.SysUser, keyword string, page int, pageSize int) ([]*entity.SysDept, int, error) {
|
||||
if user == nil || user.DeptId <= 0 {
|
||||
return []*entity.SysDept{}, 0, nil
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 50
|
||||
}
|
||||
|
||||
var children []*entity.SysDept
|
||||
err := dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, user.TenantId).
|
||||
Where(dao.SysDept.Columns().ParentId, user.DeptId).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
OrderAsc(dao.SysDept.Columns().OrderNum).
|
||||
OrderAsc(dao.SysDept.Columns().DeptId).
|
||||
Scan(&children)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows := make([]*entity.SysDept, 0, len(children))
|
||||
for _, child := range children {
|
||||
if child == nil || child.DeptId <= 0 || !appAllocateDeptMatchesKeyword(child, keyword) {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, child)
|
||||
}
|
||||
rows, err = appFilterDeptsByEnterpriseDataScope(ctx, user, rows)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
total := len(rows)
|
||||
start := (page - 1) * pageSize
|
||||
if start >= total {
|
||||
return []*entity.SysDept{}, total, nil
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
return rows[start:end], total, nil
|
||||
}
|
||||
|
||||
func appAllocateDeptMatchesKeyword(dept *entity.SysDept, keyword string) bool {
|
||||
keyword = strings.TrimSpace(keyword)
|
||||
if keyword == "" {
|
||||
return true
|
||||
}
|
||||
if dept == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(strings.TrimSpace(dept.DeptName), keyword) ||
|
||||
strings.Contains(strings.TrimSpace(dept.DeptKey), keyword) ||
|
||||
strings.Contains(fmt.Sprintf("%d", dept.DeptId), keyword)
|
||||
}
|
||||
|
||||
// AllocateBatchPrepare 校验待划拨卡片,并写入划拨预处理批次。
|
||||
func (s *sAppCard) AllocateBatchPrepare(ctx context.Context, param *model.AppCardAllocateBatchPrepareParam) (res *model.AppCardAllocateBatchModel, err error) {
|
||||
if param == nil {
|
||||
param = &model.AppCardAllocateBatchPrepareParam{}
|
||||
}
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
batch, err := s.buildAllocateBatchContext(ctx, user, param.CardValues, param.TargetDeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx = appContextWithUser(ctx, user)
|
||||
processor := newAppAllocateBatchProcessor(batch)
|
||||
rows := appAllocateBatchRows(batch)
|
||||
req := batchimport.SubmitRequest{
|
||||
BizParams: param,
|
||||
SourceType: consts.BatchSourceTypeBatchSelect,
|
||||
BatchType: consts.BatchTypePrepare,
|
||||
FailureBatchType: consts.BatchTypeAllocate,
|
||||
SkipExecute: true,
|
||||
TargetDeptId: batch.Target.DeptId,
|
||||
Operator: batchimport.OperatorContext{
|
||||
TenantId: user.TenantId,
|
||||
DeptId: user.DeptId,
|
||||
UserId: user.UserId,
|
||||
},
|
||||
}
|
||||
result, err := batchimport.RunRows(ctx, req, processor, rows, "miniapp-allocate-prepare")
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "写入划拨预处理批次失败")
|
||||
}
|
||||
|
||||
status := "prepared"
|
||||
batchNo := ""
|
||||
if result != nil {
|
||||
batchNo = result.BatchNo
|
||||
}
|
||||
if batch.FailCount > 0 || result == nil || result.ImportStatus != consts.ImportStatusSuccess {
|
||||
status = "failed"
|
||||
}
|
||||
return s.buildAllocateBatchModel(ctx, user, batch, batchNo, "", consts.BatchTypePrepare, status), nil
|
||||
}
|
||||
|
||||
// AllocateBatchSubmit 确认划拨预处理批次,并更新卡片归属;划拨后原企业的可见和办理权限仍保留。
|
||||
func (s *sAppCard) AllocateBatchSubmit(ctx context.Context, param *model.AppCardAllocateBatchSubmitParam) (res *model.AppCardAllocateBatchModel, err error) {
|
||||
if param == nil {
|
||||
param = &model.AppCardAllocateBatchSubmitParam{}
|
||||
}
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
return nil, gerror.New("终端用户不能使用卡划拨功能")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
var batch *appAllocateBatchContext
|
||||
var sourceBatchNo string
|
||||
batchNo := strings.TrimSpace(param.BatchNo)
|
||||
err = dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
prepareBatch, err := s.lockAppAllocatePrepareBatch(ctx, tx, user, batchNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceBatchNo = appAllocateSourceBatchNoFromBatch(prepareBatch)
|
||||
targetDeptId := prepareBatch.TargetDeptId
|
||||
if param.TargetDeptId > 0 && param.TargetDeptId != targetDeptId {
|
||||
return gerror.New("提交目标客户与预处理批次不一致,请重新校验")
|
||||
}
|
||||
target, err := s.loadAllocateTarget(ctx, user, targetDeptId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
details, err := s.loadAppAllocatePrepareBatchDetails(ctx, tx, prepareBatch.BatchNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(details) == 0 {
|
||||
return gerror.New("划拨预处理批次没有可确认的卡")
|
||||
}
|
||||
cardValues := make([]string, 0, len(details))
|
||||
for _, detail := range details {
|
||||
cardValues = append(cardValues, strings.TrimSpace(detail.ItemId))
|
||||
}
|
||||
if len(param.CardValues) > 0 && !appSameStringSet(appNormalizeAllocateCardValues(param.CardValues), cardValues) {
|
||||
return gerror.New("提交卡片与预处理批次不一致,请重新校验")
|
||||
}
|
||||
|
||||
batch = &appAllocateBatchContext{
|
||||
Target: target,
|
||||
Cards: make([]*appAllocateCardContext, 0, len(details)),
|
||||
}
|
||||
for _, detail := range details {
|
||||
card, err := s.lockAllocateCardForSubmit(ctx, tx, user, detail, target.DeptId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceDeptId := card.DeptId
|
||||
if detail.Snapshot != nil && detail.Snapshot.Get("sourceDeptId").Int64() > 0 {
|
||||
sourceDeptId = detail.Snapshot.Get("sourceDeptId").Int64()
|
||||
}
|
||||
batch.Cards = append(batch.Cards, &appAllocateCardContext{
|
||||
Card: card,
|
||||
ItemId: card.Iccid,
|
||||
RowNo: detail.RowNo,
|
||||
SourceDeptId: sourceDeptId,
|
||||
})
|
||||
}
|
||||
batch.refreshCounts()
|
||||
if err = s.updateAllocateCards(ctx, tx, user, batch, prepareBatch.BatchNo, now); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.markAppAllocatePrepareBatchConfirmed(ctx, tx, user, prepareBatch, batch, sourceBatchNo, strings.TrimSpace(param.Remark), now); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if batch == nil {
|
||||
return nil, gerror.New("划拨提交结果不完整")
|
||||
}
|
||||
res = s.buildAllocateBatchModel(ctx, user, batch, batchNo, sourceBatchNo, consts.BatchTypeAllocate, "success")
|
||||
_ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{
|
||||
EventType: appAsyncEventAllocateCompleted,
|
||||
BatchNo: batchNo,
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
TenantId: user.TenantId,
|
||||
UserId: user.UserId,
|
||||
DeptId: user.DeptId,
|
||||
Trigger: "allocate_submit",
|
||||
})
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) buildAllocateBatchContext(ctx context.Context, user *entity.SysUser, cardValues []string, targetDeptId int64) (*appAllocateBatchContext, error) {
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
return nil, gerror.New("终端用户不能使用卡划拨功能")
|
||||
}
|
||||
if user.DeptId <= 0 {
|
||||
return nil, gerror.New("当前账号未绑定企业,不能使用卡划拨功能")
|
||||
}
|
||||
values := appNormalizeAllocateCardValues(cardValues)
|
||||
if len(values) == 0 {
|
||||
return nil, gerror.New("请选择需要划拨的卡")
|
||||
}
|
||||
if len(values) > 100 {
|
||||
return nil, gerror.New("单次批量划拨最多支持100张卡")
|
||||
}
|
||||
target, err := s.loadAllocateTarget(ctx, user, targetDeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
batch := &appAllocateBatchContext{
|
||||
Target: target,
|
||||
Cards: make([]*appAllocateCardContext, 0, len(values)),
|
||||
TotalCount: len(values),
|
||||
}
|
||||
seenIccid := make(map[string]struct{}, len(values))
|
||||
for index, value := range values {
|
||||
item := &appAllocateCardContext{
|
||||
ItemId: strings.TrimSpace(value),
|
||||
RowNo: index + 2,
|
||||
}
|
||||
card, err := s.findVisibleCard(ctx, user, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if card == nil {
|
||||
item.ErrorReason = fmt.Sprintf("第%d张卡不存在或无权划拨:%s", index+1, value)
|
||||
batch.Cards = append(batch.Cards, item)
|
||||
continue
|
||||
}
|
||||
item.Card = card
|
||||
item.ItemId = strings.TrimSpace(card.Iccid)
|
||||
item.SourceDeptId = card.DeptId
|
||||
iccid := strings.TrimSpace(card.Iccid)
|
||||
if _, exists := seenIccid[iccid]; exists {
|
||||
item.ErrorReason = fmt.Sprintf("批量划拨卡重复:%s", iccid)
|
||||
batch.Cards = append(batch.Cards, item)
|
||||
continue
|
||||
}
|
||||
seenIccid[iccid] = struct{}{}
|
||||
if err = s.ensureAllocateCardOwnedByOperator(ctx, nil, user, card); err != nil {
|
||||
item.ErrorReason = err.Error()
|
||||
} else if err = s.validateAllocateCardBusinessRules(ctx, nil, card); err != nil {
|
||||
item.ErrorReason = err.Error()
|
||||
} else if card.DeptId == target.DeptId {
|
||||
item.ErrorReason = fmt.Sprintf("卡%s已属于目标客户,无需重复划拨", appFirstNotEmpty(card.Iccid, value))
|
||||
}
|
||||
batch.Cards = append(batch.Cards, item)
|
||||
}
|
||||
batch.refreshCounts()
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
func (batch *appAllocateBatchContext) refreshCounts() {
|
||||
if batch == nil {
|
||||
return
|
||||
}
|
||||
batch.TotalCount = len(batch.Cards)
|
||||
batch.ValidCount = 0
|
||||
batch.FailCount = 0
|
||||
batch.FailReason = ""
|
||||
for _, item := range batch.Cards {
|
||||
if item == nil {
|
||||
batch.FailCount++
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(item.ErrorReason) != "" {
|
||||
batch.FailCount++
|
||||
continue
|
||||
}
|
||||
batch.ValidCount++
|
||||
}
|
||||
if batch.FailCount > 0 {
|
||||
batch.FailReason = "存在校验失败数据,本次未写入"
|
||||
}
|
||||
}
|
||||
|
||||
func (item *appAllocateCardContext) allocateItemId() string {
|
||||
if item == nil {
|
||||
return ""
|
||||
}
|
||||
if item.Card != nil && strings.TrimSpace(item.Card.Iccid) != "" {
|
||||
return strings.TrimSpace(item.Card.Iccid)
|
||||
}
|
||||
return strings.TrimSpace(item.ItemId)
|
||||
}
|
||||
|
||||
func (s *sAppCard) insertAppAllocatePrepareBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appAllocateBatchContext, batchNo string, now *gtime.Time) error {
|
||||
batch.refreshCounts()
|
||||
batchStatus := consts.BatchAsyncStatusSuccess
|
||||
runResult := consts.BatchRunResultSuccess
|
||||
successCount := batch.ValidCount
|
||||
if batch.FailCount > 0 {
|
||||
batchStatus = consts.BatchAsyncStatusFailed
|
||||
runResult = consts.BatchRunResultFailed
|
||||
successCount = 0
|
||||
}
|
||||
_, err := dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{
|
||||
BatchNo: batchNo,
|
||||
BatchType: consts.BatchTypePrepare,
|
||||
BatchStatus: batchStatus,
|
||||
BatchAsyncStatus: batchStatus,
|
||||
SourceType: consts.BatchSourceTypeBatchSelect,
|
||||
SourceBatchNo: batchNo,
|
||||
IsCrossDept: 0,
|
||||
TargetDeptId: batch.Target.DeptId,
|
||||
DeptId: user.DeptId,
|
||||
TotalCount: batch.TotalCount,
|
||||
ValidCount: batch.ValidCount,
|
||||
SuccessCount: successCount,
|
||||
FailCount: batch.FailCount,
|
||||
RunResult: runResult,
|
||||
FailReason: batch.FailReason,
|
||||
BatchInfo: appAllocatePrepareBatchInfo(batch, batchNo),
|
||||
StartTime: now,
|
||||
EndTime: now,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: user.DeptId,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "写入划拨预处理主表失败")
|
||||
}
|
||||
|
||||
details := make([]do.BatchDetail, 0, len(batch.Cards))
|
||||
for _, item := range batch.Cards {
|
||||
detailStatus := consts.BatchDetailStatusSuccess
|
||||
if strings.TrimSpace(item.ErrorReason) != "" {
|
||||
detailStatus = consts.BatchDetailStatusFailed
|
||||
}
|
||||
details = append(details, do.BatchDetail{
|
||||
BatchNo: batchNo,
|
||||
BatchType: consts.BatchTypePrepare,
|
||||
DetailStatus: detailStatus,
|
||||
BlankOutStatus: consts.BlankOutStatusNo,
|
||||
RowNo: item.RowNo,
|
||||
ItemId: item.allocateItemId(),
|
||||
ErrorReason: strings.TrimSpace(item.ErrorReason),
|
||||
Snapshot: appAllocatePrepareDetailSnapshot(batch, item),
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: user.DeptId,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
if _, err = dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "写入划拨预处理明细失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) lockAppAllocatePrepareBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batchNo string) (*entity.BatchMain, error) {
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("预处理批次号不能为空")
|
||||
}
|
||||
var batch *entity.BatchMain
|
||||
columns := dao.BatchMain.Columns()
|
||||
err := dao.BatchMain.Ctx(ctx).TX(tx).
|
||||
Where(columns.BatchNo, batchNo).
|
||||
Where(columns.BatchType, consts.BatchTypePrepare).
|
||||
Where(columns.TenantId, user.TenantId).
|
||||
Where(columns.CreatedBy, user.UserId).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&batch)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询划拨预处理批次失败")
|
||||
}
|
||||
if batch == nil || batch.Id == 0 {
|
||||
return nil, gerror.New("划拨预处理批次不存在或无权确认")
|
||||
}
|
||||
if batch.BatchInfo == nil || batch.BatchInfo.Get("targetBatchType").Int() != consts.BatchTypeAllocate {
|
||||
return nil, gerror.New("批次不是卡划拨预处理批次")
|
||||
}
|
||||
if batch.RunResult != consts.BatchRunResultSuccess || batch.FailCount > 0 {
|
||||
return nil, gerror.New("划拨预处理批次存在校验失败数据,请重新校验后再提交")
|
||||
}
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadAppAllocatePrepareBatchDetails(ctx context.Context, tx gdb.TX, batchNo string) ([]*entity.BatchDetail, error) {
|
||||
var details []*entity.BatchDetail
|
||||
columns := dao.BatchDetail.Columns()
|
||||
err := dao.BatchDetail.Ctx(ctx).TX(tx).
|
||||
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
|
||||
Where(columns.BatchType, consts.BatchTypePrepare).
|
||||
Where(columns.DetailStatus, consts.BatchDetailStatusSuccess).
|
||||
Where(columns.BlankOutStatus, consts.BlankOutStatusNo).
|
||||
OrderAsc(columns.RowNo).
|
||||
Scan(&details)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询划拨预处理明细失败")
|
||||
}
|
||||
return details, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) lockAllocateCardForSubmit(ctx context.Context, tx gdb.TX, user *entity.SysUser, detail *entity.BatchDetail, targetDeptId int64) (*appCardQueryRow, error) {
|
||||
if detail == nil || strings.TrimSpace(detail.ItemId) == "" {
|
||||
return nil, gerror.New("划拨批次明细缺少卡号")
|
||||
}
|
||||
expectedSourceDeptId := int64(0)
|
||||
if detail.Snapshot != nil {
|
||||
expectedSourceDeptId = detail.Snapshot.Get("sourceDeptId").Int64()
|
||||
}
|
||||
var card *appCardQueryRow
|
||||
err := s.cardQueryBase(ctx, false).TX(tx).
|
||||
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Iccid, strings.TrimSpace(detail.ItemId)).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&card)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrapf(err, "锁定划拨卡%s失败", detail.ItemId)
|
||||
}
|
||||
if card == nil || card.Id == 0 {
|
||||
return nil, gerror.Newf("卡%s不存在,不能划拨", detail.ItemId)
|
||||
}
|
||||
if err = s.attachCardChannelInfo(ctx, card); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expectedSourceDeptId > 0 && card.DeptId != expectedSourceDeptId {
|
||||
return nil, gerror.Newf("卡%s归属已变化,请重新校验", card.Iccid)
|
||||
}
|
||||
if err = s.ensureAllocateCardVisible(ctx, tx, user, card); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.ensureAllocateCardOwnedByOperator(ctx, tx, user, card); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.validateAllocateCardBusinessRules(ctx, tx, card); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if card.DeptId == targetDeptId {
|
||||
return nil, gerror.Newf("卡%s已属于目标客户,无需重复划拨", card.Iccid)
|
||||
}
|
||||
return card, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) validateAllocateCardBusinessRules(ctx context.Context, tx gdb.TX, card *appCardQueryRow) error {
|
||||
if card == nil || strings.TrimSpace(card.Iccid) == "" {
|
||||
return gerror.New("划拨卡信息不完整")
|
||||
}
|
||||
cardLabel := appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei)
|
||||
if card.BossCardStatus == 4 {
|
||||
return gerror.Newf("卡%s当前为停机状态,不能划拨", cardLabel)
|
||||
}
|
||||
if card.PackageStatus == consts.PackageStatusExpired ||
|
||||
(card.FlowPkgEndAt != nil && !card.FlowPkgEndAt.IsZero() && card.FlowPkgEndAt.Before(gtime.Now())) {
|
||||
return gerror.Newf("卡%s套餐已过期,不能划拨", cardLabel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) updateAllocateCards(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appAllocateBatchContext, batchNo string, now *gtime.Time) error {
|
||||
for _, item := range batch.Cards {
|
||||
result, err := dao.TSimCardInfo.Ctx(ctx).TX(tx).
|
||||
Where(dao.TSimCardInfo.Columns().Id, item.Card.Id).
|
||||
Where(dao.TSimCardInfo.Columns().DeptId, item.Card.DeptId).
|
||||
Data(do.TSimCardInfo{
|
||||
DeptId: batch.Target.DeptId,
|
||||
CreatedDept: user.DeptId,
|
||||
OutboundBatchNo: batchNo,
|
||||
AllocationBatchNo: batchNo,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrapf(err, "更新卡%s归属失败", item.Card.Iccid)
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err != nil {
|
||||
return gerror.Wrap(err, "读取划拨更新数量失败")
|
||||
} else if affected != 1 {
|
||||
return gerror.Newf("卡%s归属已变化,请重新校验", item.Card.Iccid)
|
||||
}
|
||||
if item.SourceDeptId <= 0 {
|
||||
item.SourceDeptId = item.Card.DeptId
|
||||
}
|
||||
item.Card.DeptId = batch.Target.DeptId
|
||||
item.Card.CreatedDept = user.DeptId
|
||||
item.Card.OutboundBatchNo = batchNo
|
||||
item.Card.AllocationBatchNo = batchNo
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) markAppAllocatePrepareBatchConfirmed(ctx context.Context, tx gdb.TX, user *entity.SysUser, prepareBatch *entity.BatchMain, batch *appAllocateBatchContext, sourceBatchNo string, remark string, now *gtime.Time) error {
|
||||
batchNo := strings.TrimSpace(prepareBatch.BatchNo)
|
||||
for _, item := range batch.Cards {
|
||||
_, err := dao.BatchDetail.Ctx(ctx).TX(tx).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
||||
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare).
|
||||
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusSuccess).
|
||||
Where(dao.BatchDetail.Columns().ItemId, item.Card.Iccid).
|
||||
Data(do.BatchDetail{
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
Snapshot: appAllocateSubmitDetailSnapshot(batch, item, batchNo, sourceBatchNo),
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新划拨批次明细失败")
|
||||
}
|
||||
}
|
||||
if _, err := dao.BatchDetail.Ctx(ctx).TX(tx).
|
||||
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
||||
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare).
|
||||
Data(do.BatchDetail{
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新划拨批次明细类型失败")
|
||||
}
|
||||
|
||||
logFiles, err := s.ensureAppAllocateBatchLogFiles(ctx, tx, user, batch, prepareBatch.BatchInfo, batchNo, sourceBatchNo, remark, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
batchInfo := appAllocateBatchInfoWithLogFiles(
|
||||
appAllocateSubmitBatchInfo(batch, batchNo, sourceBatchNo, remark, now),
|
||||
logFiles,
|
||||
)
|
||||
|
||||
_, err = dao.BatchMain.Ctx(ctx).TX(tx).
|
||||
Where(dao.BatchMain.Columns().Id, prepareBatch.Id).
|
||||
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypePrepare).
|
||||
Data(do.BatchMain{
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
BatchStatus: consts.BatchStatusSuccess,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusSuccess,
|
||||
TargetDeptId: batch.Target.DeptId,
|
||||
TotalCount: len(batch.Cards),
|
||||
ValidCount: len(batch.Cards),
|
||||
SuccessCount: len(batch.Cards),
|
||||
FailCount: 0,
|
||||
RunResult: consts.BatchRunResultSuccess,
|
||||
BatchInfo: batchInfo,
|
||||
EndTime: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新划拨批次主表失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadAllocateTarget(ctx context.Context, user *entity.SysUser, targetDeptId int64) (*entity.SysDept, error) {
|
||||
if targetDeptId <= 0 {
|
||||
return nil, gerror.New("请选择划拨目标客户")
|
||||
}
|
||||
target, err := s.loadDeptById(ctx, user.TenantId, targetDeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if target == nil || target.DeptId == 0 {
|
||||
return nil, gerror.New("划拨目标客户不存在")
|
||||
}
|
||||
if target.DeptId == user.DeptId {
|
||||
return nil, gerror.New("划拨目标不能选择当前企业")
|
||||
}
|
||||
if target.ParentId != user.DeptId {
|
||||
return nil, gerror.New("划拨目标客户不在当前企业直属下级范围内")
|
||||
}
|
||||
visible, err := appEnterpriseDeptVisibleByDataScope(ctx, user, target.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !visible {
|
||||
return nil, gerror.New("划拨目标客户不在当前角色可操作范围内")
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureAllocateCardVisible(ctx context.Context, tx gdb.TX, user *entity.SysUser, card *appCardQueryRow) error {
|
||||
if card == nil || card.Id <= 0 || strings.TrimSpace(card.Iccid) == "" {
|
||||
return gerror.New("划拨卡信息不完整")
|
||||
}
|
||||
var visible *appCardQueryRow
|
||||
if err := s.visibleCardModel(ctx, user).TX(tx).
|
||||
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Id, card.Id).
|
||||
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Iccid, card.Iccid).
|
||||
Limit(1).
|
||||
Scan(&visible); err != nil {
|
||||
return gerror.Wrapf(err, "校验卡%s可操作范围失败", card.Iccid)
|
||||
}
|
||||
if visible == nil || visible.Id == 0 {
|
||||
return gerror.Newf("卡%s不在当前企业可操作范围内", card.Iccid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureAllocateCardOwnedByOperator(ctx context.Context, tx gdb.TX, user *entity.SysUser, card *appCardQueryRow) error {
|
||||
if user == nil || user.DeptId <= 0 {
|
||||
return gerror.New("当前用户未绑定机构,不能划拨卡片")
|
||||
}
|
||||
if card == nil || strings.TrimSpace(card.Iccid) == "" {
|
||||
return gerror.New("划拨卡信息不完整")
|
||||
}
|
||||
if card.DeptId != user.DeptId {
|
||||
cardLabel := appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei)
|
||||
return gerror.Newf("卡%s当前归属不在本企业名下,划拨权已归当前归属客户", cardLabel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadDeptById(ctx context.Context, tenantId string, deptId int64) (*entity.SysDept, error) {
|
||||
var dept *entity.SysDept
|
||||
err := dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, tenantId).
|
||||
Where(dao.SysDept.Columns().DeptId, deptId).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
Limit(1).
|
||||
Scan(&dept)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询客户信息失败")
|
||||
}
|
||||
return dept, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) buildAllocateBatchModel(ctx context.Context, user *entity.SysUser, batch *appAllocateBatchContext, batchNo string, sourceBatchNo string, batchType int, status string) *model.AppCardAllocateBatchModel {
|
||||
batch.refreshCounts()
|
||||
cards := make([]*model.AppCardModel, 0, len(batch.Cards))
|
||||
details := make([]*model.AppCardAllocateBatchDetailModel, 0, len(batch.Cards))
|
||||
for _, item := range batch.Cards {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if item.Card != nil && strings.TrimSpace(item.ErrorReason) == "" {
|
||||
cards = append(cards, s.cardRowToModel(ctx, item.Card, user, appCardOwnerAgent, true))
|
||||
}
|
||||
details = append(details, &model.AppCardAllocateBatchDetailModel{
|
||||
RowNo: item.RowNo,
|
||||
ItemId: item.allocateItemId(),
|
||||
Status: appAllocateDetailStatusText(item.ErrorReason),
|
||||
ErrorReason: strings.TrimSpace(item.ErrorReason),
|
||||
})
|
||||
}
|
||||
return &model.AppCardAllocateBatchModel{
|
||||
BatchNo: strings.TrimSpace(batchNo),
|
||||
SourceBatchNo: strings.TrimSpace(sourceBatchNo),
|
||||
BatchType: batchType,
|
||||
Quantity: batch.ValidCount,
|
||||
TotalCount: batch.TotalCount,
|
||||
ValidCount: batch.ValidCount,
|
||||
FailCount: batch.FailCount,
|
||||
FailReason: batch.FailReason,
|
||||
Target: appAllocateTargetModel(batch.Target, user.DeptId),
|
||||
Cards: cards,
|
||||
Details: details,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
func appAllocateTargetModel(dept *entity.SysDept, currentDeptId int64) *model.AppCardAllocateTargetModel {
|
||||
if dept == nil {
|
||||
return nil
|
||||
}
|
||||
label := strings.TrimSpace(dept.DeptName)
|
||||
if strings.TrimSpace(dept.DeptKey) != "" {
|
||||
label = fmt.Sprintf("%s(%s)", label, strings.TrimSpace(dept.DeptKey))
|
||||
}
|
||||
scope := "self"
|
||||
scopeName := "当前机构"
|
||||
if currentDeptId > 0 && dept.DeptId != currentDeptId && dept.ParentId == currentDeptId {
|
||||
scope = "direct_child"
|
||||
scopeName = "直属下级"
|
||||
}
|
||||
return &model.AppCardAllocateTargetModel{
|
||||
DeptId: dept.DeptId,
|
||||
ParentId: dept.ParentId,
|
||||
DeptKey: strings.TrimSpace(dept.DeptKey),
|
||||
DeptName: strings.TrimSpace(dept.DeptName),
|
||||
DeptCategory: strings.TrimSpace(dept.DeptCategory),
|
||||
TargetScope: scope,
|
||||
TargetScopeName: scopeName,
|
||||
Label: label,
|
||||
}
|
||||
}
|
||||
|
||||
func appAllocatePrepareBatchInfo(batch *appAllocateBatchContext, batchNo string) *gjson.Json {
|
||||
return gjson.New(map[string]any{
|
||||
"source": "app",
|
||||
"batchNo": strings.TrimSpace(batchNo),
|
||||
"sourceBatchNo": strings.TrimSpace(batchNo),
|
||||
"targetBatchType": consts.BatchTypeAllocate,
|
||||
"targetBatchName": "卡划拨",
|
||||
"targetDeptId": batch.Target.DeptId,
|
||||
"targetDeptName": batch.Target.DeptName,
|
||||
"quantity": batch.ValidCount,
|
||||
"totalCount": batch.TotalCount,
|
||||
"validCount": batch.ValidCount,
|
||||
"failCount": batch.FailCount,
|
||||
"failReason": batch.FailReason,
|
||||
"cards": appAllocateCardSnapshotList(batch),
|
||||
})
|
||||
}
|
||||
|
||||
func appAllocateSubmitBatchInfo(batch *appAllocateBatchContext, batchNo string, sourceBatchNo string, remark string, now *gtime.Time) *gjson.Json {
|
||||
return gjson.New(map[string]any{
|
||||
"source": "app",
|
||||
"batchNo": strings.TrimSpace(batchNo),
|
||||
"sourceBatchNo": strings.TrimSpace(sourceBatchNo),
|
||||
"targetBatchType": consts.BatchTypeAllocate,
|
||||
"targetBatchName": "卡划拨",
|
||||
"targetDeptId": batch.Target.DeptId,
|
||||
"targetDeptName": batch.Target.DeptName,
|
||||
"quantity": len(batch.Cards),
|
||||
"remark": strings.TrimSpace(remark),
|
||||
"allocateTime": now,
|
||||
"cards": appAllocateCardSnapshotList(batch),
|
||||
})
|
||||
}
|
||||
|
||||
func appAllocatePrepareDetailSnapshot(batch *appAllocateBatchContext, item *appAllocateCardContext) *gjson.Json {
|
||||
if item == nil {
|
||||
return gjson.New(map[string]any{})
|
||||
}
|
||||
card := item.Card
|
||||
cardId := int64(0)
|
||||
if card != nil {
|
||||
cardId = card.Id
|
||||
}
|
||||
return gjson.New(map[string]any{
|
||||
"ICCID": appAllocateSnapshotICCID(item),
|
||||
"MSISDN": appAllocateSnapshotField(card, func(c *appCardQueryRow) string { return c.Msisdn }),
|
||||
"IMSI": appAllocateSnapshotField(card, func(c *appCardQueryRow) string { return c.Imsi }),
|
||||
"IMEI": appAllocateSnapshotField(card, func(c *appCardQueryRow) string { return c.Imei }),
|
||||
"rowNo": item.RowNo,
|
||||
"cardId": cardId,
|
||||
"sourceDeptId": item.SourceDeptId,
|
||||
"targetDeptId": batch.Target.DeptId,
|
||||
"targetDeptName": batch.Target.DeptName,
|
||||
"currentPackageCode": appCardCurrentPackageCode(card),
|
||||
"packageName": appCardPackageName(card),
|
||||
"errorReason": strings.TrimSpace(item.ErrorReason),
|
||||
})
|
||||
}
|
||||
|
||||
func appAllocateSnapshotICCID(item *appAllocateCardContext) string {
|
||||
if item == nil {
|
||||
return ""
|
||||
}
|
||||
if item.Card != nil && strings.TrimSpace(item.Card.Iccid) != "" {
|
||||
return strings.TrimSpace(item.Card.Iccid)
|
||||
}
|
||||
return strings.TrimSpace(item.ItemId)
|
||||
}
|
||||
|
||||
func appAllocateSnapshotField(card *appCardQueryRow, getter func(*appCardQueryRow) string) string {
|
||||
if card == nil || getter == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(getter(card))
|
||||
}
|
||||
|
||||
func appAllocateDetailStatusText(reason string) string {
|
||||
if strings.TrimSpace(reason) != "" {
|
||||
return "failed"
|
||||
}
|
||||
return "success"
|
||||
}
|
||||
|
||||
func appAllocateSubmitDetailSnapshot(batch *appAllocateBatchContext, item *appAllocateCardContext, batchNo string, sourceBatchNo string) *gjson.Json {
|
||||
card := item.Card
|
||||
return gjson.New(map[string]any{
|
||||
"ICCID": card.Iccid,
|
||||
"MSISDN": card.Msisdn,
|
||||
"IMSI": card.Imsi,
|
||||
"IMEI": card.Imei,
|
||||
"rowNo": item.RowNo,
|
||||
"cardId": card.Id,
|
||||
"allocateBatchNo": strings.TrimSpace(batchNo),
|
||||
"sourceBatchNo": strings.TrimSpace(sourceBatchNo),
|
||||
"sourceDeptId": item.SourceDeptId,
|
||||
"targetDeptId": batch.Target.DeptId,
|
||||
"targetDeptName": batch.Target.DeptName,
|
||||
"currentPackageCode": appCardCurrentPackageCode(card),
|
||||
"packageName": appCardPackageName(card),
|
||||
})
|
||||
}
|
||||
|
||||
func appAllocateCardSnapshotList(batch *appAllocateBatchContext) []map[string]any {
|
||||
cards := make([]map[string]any, 0, len(batch.Cards))
|
||||
for _, item := range batch.Cards {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
cardId := int64(0)
|
||||
iccid := item.allocateItemId()
|
||||
msisdn := ""
|
||||
currentPackageCode := ""
|
||||
if item.Card != nil {
|
||||
cardId = item.Card.Id
|
||||
msisdn = item.Card.Msisdn
|
||||
currentPackageCode = appCardCurrentPackageCode(item.Card)
|
||||
}
|
||||
cards = append(cards, map[string]any{
|
||||
"cardId": cardId,
|
||||
"iccid": iccid,
|
||||
"msisdn": msisdn,
|
||||
"sourceDeptId": item.SourceDeptId,
|
||||
"targetDeptId": batch.Target.DeptId,
|
||||
"currentPackageCode": currentPackageCode,
|
||||
"errorReason": strings.TrimSpace(item.ErrorReason),
|
||||
})
|
||||
}
|
||||
return cards
|
||||
}
|
||||
|
||||
func appAllocateSourceBatchNoFromBatch(batch *entity.BatchMain) string {
|
||||
if batch == nil {
|
||||
return ""
|
||||
}
|
||||
if batch.BatchInfo != nil {
|
||||
if sourceBatchNo := strings.TrimSpace(batch.BatchInfo.Get("sourceBatchNo").String()); sourceBatchNo != "" {
|
||||
return sourceBatchNo
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(batch.SourceBatchNo)
|
||||
}
|
||||
|
||||
func appNormalizeAllocateCardValues(values []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
result := make([]string, 0, len(values))
|
||||
for _, raw := range values {
|
||||
parts := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
switch r {
|
||||
case ',', ',', ';', ';', '\n', '\r', '\t', ' ':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
for _, part := range parts {
|
||||
value := strings.TrimSpace(part)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type appAllocateBatchProcessor struct {
|
||||
batch *appAllocateBatchContext
|
||||
}
|
||||
|
||||
type appAllocateBatchRow struct {
|
||||
rowNo int
|
||||
itemID string
|
||||
snapshot *gjson.Json
|
||||
errorReasons []string
|
||||
}
|
||||
|
||||
func newAppAllocateBatchProcessor(batch *appAllocateBatchContext) *appAllocateBatchProcessor {
|
||||
return &appAllocateBatchProcessor{batch: batch}
|
||||
}
|
||||
|
||||
func appAllocateBatchRows(batch *appAllocateBatchContext) []batchimport.Row {
|
||||
if batch == nil {
|
||||
return nil
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(batch.Cards))
|
||||
for _, item := range batch.Cards {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
row := &appAllocateBatchRow{
|
||||
rowNo: item.RowNo,
|
||||
itemID: item.allocateItemId(),
|
||||
snapshot: appAllocatePrepareDetailSnapshot(batch, item),
|
||||
}
|
||||
if reason := strings.TrimSpace(item.ErrorReason); reason != "" {
|
||||
row.AddErrorReason(reason)
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (p *appAllocateBatchProcessor) Config() batchimport.ProcessorConfig {
|
||||
return batchimport.ProcessorConfig{
|
||||
Code: "miniapp_card_allocate",
|
||||
BatchType: consts.BatchTypeAllocate,
|
||||
BatchNoPrefix: consts.BatchNoPrefixAllocate,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimCardAllocate,
|
||||
ExportScene: consts.ResourceExportSceneAllocate,
|
||||
ImportDir: consts.SimCardAllocateSubDirImportExcel,
|
||||
ResultDir: consts.SimCardAllocateSubDirResultExcel,
|
||||
MaxFileSize: 50 << 20,
|
||||
TemplateHeaders: []string{"ICCID", "MSISDN", "IMSI"},
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *appAllocateBatchProcessor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return nil, gerror.New("小程序划拨暂不支持文件导入")
|
||||
}
|
||||
|
||||
func (p *appAllocateBatchProcessor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
seen := make(map[string]batchimport.Row, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
itemID := strings.TrimSpace(row.GetItemID())
|
||||
if itemID == "" {
|
||||
row.AddErrorReason("ICCID不能为空")
|
||||
continue
|
||||
}
|
||||
if first, ok := seen[itemID]; ok {
|
||||
row.AddErrorReason("ICCID重复")
|
||||
first.AddErrorReason("ICCID重复")
|
||||
continue
|
||||
}
|
||||
seen[itemID] = row
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *appAllocateBatchProcessor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
|
||||
return gerror.New("小程序划拨批次确认由提交接口触发")
|
||||
}
|
||||
|
||||
func (p *appAllocateBatchProcessor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
info := map[string]any{}
|
||||
if p.batch != nil {
|
||||
if base := appAllocatePrepareBatchInfo(p.batch, req.BatchNo); base != nil {
|
||||
for key, value := range base.Map() {
|
||||
info[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if sourceBatchNo := strings.TrimSpace(req.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
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
|
||||
}
|
||||
if result != nil && strings.TrimSpace(result.FailReason) != "" {
|
||||
info["failReason"] = strings.TrimSpace(result.FailReason)
|
||||
}
|
||||
return gjson.New(info)
|
||||
}
|
||||
|
||||
func (r *appAllocateBatchRow) GetRowNo() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
return r.rowNo
|
||||
}
|
||||
|
||||
func (r *appAllocateBatchRow) GetItemID() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(r.itemID)
|
||||
}
|
||||
|
||||
func (r *appAllocateBatchRow) GetErrorReasons() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return r.errorReasons
|
||||
}
|
||||
|
||||
func (r *appAllocateBatchRow) AddErrorReason(reason string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
reason = strings.TrimSpace(reason)
|
||||
if reason == "" {
|
||||
return
|
||||
}
|
||||
for _, item := range r.errorReasons {
|
||||
if item == reason {
|
||||
return
|
||||
}
|
||||
}
|
||||
r.errorReasons = append(r.errorReasons, reason)
|
||||
}
|
||||
|
||||
func (r *appAllocateBatchRow) Snapshot() *gjson.Json {
|
||||
if r == nil || r.snapshot == nil {
|
||||
return gjson.New(map[string]any{})
|
||||
}
|
||||
return r.snapshot
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/hash"
|
||||
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type appAllocateBatchLogFiles struct {
|
||||
ImportOssId int64
|
||||
ImportFileName string
|
||||
ResultOssId int64
|
||||
DownloadId int64
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureAppAllocateBatchLogFiles(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appAllocateBatchContext, existingInfo *gjson.Json, batchNo string, sourceBatchNo string, remark string, now *gtime.Time) (*appAllocateBatchLogFiles, error) {
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("划拨批次号不能为空")
|
||||
}
|
||||
if user == nil {
|
||||
return nil, gerror.New("当前用户不存在")
|
||||
}
|
||||
if batch == nil || batch.Target == nil {
|
||||
return nil, gerror.New("划拨批次日志文件信息不完整")
|
||||
}
|
||||
|
||||
files := appAllocateBatchLogFilesFromInfo(existingInfo)
|
||||
existingDownload, err := appAllocateBatchResultDownload(ctx, tx, batchNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingDownload != nil {
|
||||
files.ResultOssId = existingDownload.ResultOssId
|
||||
files.DownloadId = existingDownload.DownloadId
|
||||
}
|
||||
|
||||
if files.ImportOssId <= 0 {
|
||||
importContent, err := appBuildAllocateBatchImportExcel(batch, sourceBatchNo, remark)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "生成划拨批次源文件失败")
|
||||
}
|
||||
importUpload, importOssId, err := appSaveAllocateBatchOss(ctx, tx, user, user.DeptId, importContent, batchNo+"-import.xlsx", consts.SimCardAllocateSubDirImportExcel, now)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "保存划拨批次源文件失败")
|
||||
}
|
||||
files.ImportOssId = importOssId
|
||||
files.ImportFileName = strings.TrimSpace(importUpload.Name)
|
||||
}
|
||||
if strings.TrimSpace(files.ImportFileName) == "" {
|
||||
files.ImportFileName = appAllocateBatchImportFileName(ctx, tx, files.ImportOssId, batchNo+"-import.xlsx")
|
||||
}
|
||||
if existingDownload != nil {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
resultContent, err := appBuildAllocateBatchResultExcel(batch, files, batchNo, sourceBatchNo, now, user.UserId, 0)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "生成划拨批次结果文件失败")
|
||||
}
|
||||
resultUpload, resultOssId, err := appSaveAllocateBatchOss(ctx, tx, user, user.DeptId, resultContent, batchNo+"-result.xlsx", consts.SimCardAllocateSubDirResultExcel, now)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "保存划拨批次结果文件失败")
|
||||
}
|
||||
files.ResultOssId = resultOssId
|
||||
|
||||
if err = appFillAllocateResultOssId(ctx, tx, resultOssId, resultUpload.Path, user.UserId, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refreshedResult, err := appLoadAllocateOssUpload(ctx, tx, resultOssId, resultUpload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quantity := len(batch.Cards)
|
||||
downloadId, err := dao.TResourceBatchDownload.Ctx(ctx).TX(tx).Data(do.TResourceBatchDownload{
|
||||
BatchNo: batchNo,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimCardAllocate,
|
||||
ResourceType: consts.ResourceTypeExcel,
|
||||
ExportScene: consts.ResourceExportSceneAllocate,
|
||||
TotalCount: quantity,
|
||||
SuccessCount: quantity,
|
||||
FailCount: 0,
|
||||
DownloadStatus: consts.DownloadStatusGenerated,
|
||||
OssId: resultOssId,
|
||||
FileName: refreshedResult.Name,
|
||||
FileExt: refreshedResult.Ext,
|
||||
FileUrl: refreshedResult.FullPath,
|
||||
FilePath: refreshedResult.Path,
|
||||
FileSize: refreshedResult.Size,
|
||||
FileMd5: refreshedResult.Md5,
|
||||
DownloadCount: 0,
|
||||
MaxDownloadCount: 0,
|
||||
DeptId: user.DeptId,
|
||||
StartTime: now,
|
||||
FinishTime: now,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: user.DeptId,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "写入划拨批次结果下载记录失败")
|
||||
}
|
||||
files.DownloadId = downloadId
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func appAllocateBatchLogFilesFromInfo(info *gjson.Json) *appAllocateBatchLogFiles {
|
||||
files := &appAllocateBatchLogFiles{}
|
||||
if info == nil {
|
||||
return files
|
||||
}
|
||||
files.ImportOssId = info.Get("importFileOssId").Int64()
|
||||
files.ResultOssId = info.Get("resultFileOssId").Int64()
|
||||
files.DownloadId = info.Get("resultDownloadId").Int64()
|
||||
return files
|
||||
}
|
||||
|
||||
func appAllocateBatchResultDownload(ctx context.Context, tx gdb.TX, batchNo string) (*appAllocateBatchLogFiles, error) {
|
||||
var downloads []*entity.TResourceBatchDownload
|
||||
columns := dao.TResourceBatchDownload.Columns()
|
||||
err := dao.TResourceBatchDownload.Ctx(ctx).TX(tx).
|
||||
Fields(columns.Id, columns.OssId).
|
||||
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
|
||||
OrderDesc(columns.Id).
|
||||
Limit(1).
|
||||
Scan(&downloads)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询划拨批次结果下载记录失败")
|
||||
}
|
||||
if len(downloads) == 0 || downloads[0] == nil || downloads[0].Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &appAllocateBatchLogFiles{
|
||||
DownloadId: downloads[0].Id,
|
||||
ResultOssId: downloads[0].OssId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appAllocateBatchInfoWithLogFiles(info *gjson.Json, files *appAllocateBatchLogFiles) *gjson.Json {
|
||||
data := map[string]any{}
|
||||
if info != nil {
|
||||
for key, value := range info.Map() {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
if files != nil {
|
||||
if files.ImportOssId > 0 {
|
||||
data["importFileOssId"] = files.ImportOssId
|
||||
}
|
||||
if files.ResultOssId > 0 {
|
||||
data["resultFileOssId"] = files.ResultOssId
|
||||
}
|
||||
if files.DownloadId > 0 {
|
||||
data["resultDownloadId"] = files.DownloadId
|
||||
}
|
||||
}
|
||||
return gjson.New(data)
|
||||
}
|
||||
|
||||
func appBuildAllocateBatchImportExcel(batch *appAllocateBatchContext, sourceBatchNo string, remark string) ([]byte, error) {
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
sheet := "Sheet1"
|
||||
headers := []string{"ICCID", "MSISDN", "IMSI", "sourceBatchNo", "sourceDeptId", "targetDeptId", "targetDeptName", "remark"}
|
||||
if err := f.SetSheetRow(sheet, "A1", &headers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := appApplyHeaderColumnsTextStyle(f, sheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index, item := range batch.Cards {
|
||||
row := []interface{}{
|
||||
strings.TrimSpace(item.Card.Iccid),
|
||||
strings.TrimSpace(item.Card.Msisdn),
|
||||
strings.TrimSpace(item.Card.Imsi),
|
||||
strings.TrimSpace(sourceBatchNo),
|
||||
item.SourceDeptId,
|
||||
batch.Target.DeptId,
|
||||
strings.TrimSpace(batch.Target.DeptName),
|
||||
strings.TrimSpace(remark),
|
||||
}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(sheet, cell, &row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_ = f.SetColWidth(sheet, "A", "A", 24)
|
||||
_ = f.SetColWidth(sheet, "B", "C", 18)
|
||||
_ = f.SetColWidth(sheet, "D", "H", 18)
|
||||
return appExcelBytes(f)
|
||||
}
|
||||
|
||||
func appBuildAllocateBatchResultExcel(batch *appAllocateBatchContext, files *appAllocateBatchLogFiles, batchNo string, sourceBatchNo string, now *gtime.Time, userId int64, resultOssId int64) ([]byte, error) {
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
leftStyleID, err := f.NewStyle(&excelize.Style{
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "left",
|
||||
Vertical: "center",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultSheet := "导入结果"
|
||||
if err = f.SetSheetName("Sheet1", resultSheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers := []string{"源文件行号", "item_id", "处理状态", "异常原因"}
|
||||
if err = f.SetSheetRow(resultSheet, "A1", &headers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index, item := range batch.Cards {
|
||||
row := []interface{}{
|
||||
item.RowNo,
|
||||
strings.TrimSpace(item.Card.Iccid),
|
||||
"成功",
|
||||
"",
|
||||
}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(resultSheet, cell, &row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resultEndCell, err := excelize.CoordinatesToCellName(4, len(batch.Cards)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetCellStyle(resultSheet, "A1", resultEndCell, leftStyleID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infoSheet := "批次信息"
|
||||
if _, err = f.NewSheet(infoSheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoHeaders := []string{"字段", "值"}
|
||||
if err = f.SetSheetRow(infoSheet, "A1", &infoHeaders); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoRows := appAllocateBatchResultInfoRows(batch, files, batchNo, sourceBatchNo, now, userId, resultOssId)
|
||||
for index, item := range infoRows {
|
||||
row := []interface{}{item[0], item[1]}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(infoSheet, cell, &row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
infoEndCell, err := excelize.CoordinatesToCellName(2, len(infoRows)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetCellStyle(infoSheet, "A1", infoEndCell, leftStyleID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = f.SetColWidth(resultSheet, "A", "A", 14)
|
||||
_ = f.SetColWidth(resultSheet, "B", "B", 24)
|
||||
_ = f.SetColWidth(resultSheet, "C", "C", 12)
|
||||
_ = f.SetColWidth(resultSheet, "D", "D", 40)
|
||||
_ = f.SetColWidth(infoSheet, "A", "B", 24)
|
||||
f.SetActiveSheet(0)
|
||||
return appExcelBytes(f)
|
||||
}
|
||||
|
||||
func appAllocateBatchResultInfoRows(batch *appAllocateBatchContext, files *appAllocateBatchLogFiles, batchNo string, sourceBatchNo string, now *gtime.Time, userId int64, resultOssId int64) [][2]string {
|
||||
importFileName := ""
|
||||
importOssId := ""
|
||||
if files != nil {
|
||||
importFileName = strings.TrimSpace(files.ImportFileName)
|
||||
if files.ImportOssId > 0 {
|
||||
importOssId = fmt.Sprintf("%d", files.ImportOssId)
|
||||
}
|
||||
}
|
||||
if importFileName == "" {
|
||||
importFileName = strings.TrimSpace(batchNo) + "-import.xlsx"
|
||||
}
|
||||
resultFileOssId := ""
|
||||
if resultOssId > 0 {
|
||||
resultFileOssId = fmt.Sprintf("%d", resultOssId)
|
||||
}
|
||||
return [][2]string{
|
||||
{"批次号", strings.TrimSpace(batchNo)},
|
||||
{"批次类型", "卡划拨"},
|
||||
{"处理结果", "成功"},
|
||||
{"总行数", fmt.Sprintf("%d", len(batch.Cards))},
|
||||
{"校验通过行数", fmt.Sprintf("%d", len(batch.Cards))},
|
||||
{"失败行数", "0"},
|
||||
{"实际写入行数", fmt.Sprintf("%d", len(batch.Cards))},
|
||||
{"源批次号", strings.TrimSpace(sourceBatchNo)},
|
||||
{"目标机构", fmt.Sprintf("%d %s", batch.Target.DeptId, strings.TrimSpace(batch.Target.DeptName))},
|
||||
{"原始文件名", importFileName},
|
||||
{"原始文件OSS ID", importOssId},
|
||||
{"结果文件OSS ID", resultFileOssId},
|
||||
{"操作人", fmt.Sprintf("%d", userId)},
|
||||
{"开始时间", appRenewFormatTime(now)},
|
||||
{"结束时间", appRenewFormatTime(now)},
|
||||
{"备注", "导入成功"},
|
||||
}
|
||||
}
|
||||
|
||||
func appSaveAllocateBatchOss(ctx context.Context, tx gdb.TX, user *entity.SysUser, deptId int64, content []byte, fileName string, subDirName string, now *gtime.Time) (model.UploadResponse, int64, error) {
|
||||
upload, err := service.SysOss().SaveContentLocal(ctx, content, consts.SysUploadNewFileTypeUserFile, fileName, subDirName, 0)
|
||||
if err != nil {
|
||||
return upload, 0, err
|
||||
}
|
||||
ossId, err := dao.SysOss.Ctx(ctx).TX(tx).Data(do.SysOss{
|
||||
TenantId: user.TenantId,
|
||||
FileName: upload.Name,
|
||||
OriginalName: upload.OriginalName,
|
||||
FileSuffix: upload.Ext,
|
||||
Path: upload.Path,
|
||||
Url: upload.FullPath,
|
||||
CreatedDept: deptId,
|
||||
CreatedAt: now,
|
||||
CreatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
Service: "local",
|
||||
Md5: upload.Md5,
|
||||
FileSize: upload.Size,
|
||||
FileCrc16: upload.Crc16,
|
||||
FileSum: upload.Sum16,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return upload, 0, gerror.Wrap(err, "写入划拨批次OSS记录失败")
|
||||
}
|
||||
return upload, ossId, nil
|
||||
}
|
||||
|
||||
func appFillAllocateResultOssId(ctx context.Context, tx gdb.TX, ossId int64, filePath string, userId int64, now *gtime.Time) error {
|
||||
if ossId <= 0 || strings.TrimSpace(filePath) == "" {
|
||||
return gerror.New("划拨结果文件OSS信息不完整")
|
||||
}
|
||||
f, err := excelize.OpenFile(filePath)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "打开划拨结果文件失败")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
rows, err := f.GetRows("批次信息")
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取划拨结果批次信息失败")
|
||||
}
|
||||
found := false
|
||||
for index, row := range rows {
|
||||
if len(row) == 0 || strings.TrimSpace(row[0]) != "结果文件OSS ID" {
|
||||
continue
|
||||
}
|
||||
cell, err := excelize.CoordinatesToCellName(2, index+1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetCellStr("批次信息", cell, fmt.Sprintf("%d", ossId)); err != nil {
|
||||
return err
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
return gerror.New("划拨结果文件缺少结果文件OSS ID字段")
|
||||
}
|
||||
if err = f.SaveAs(filePath); err != nil {
|
||||
return gerror.Wrap(err, "回写划拨结果文件OSS ID失败")
|
||||
}
|
||||
return appRefreshAllocateOssFileInfo(ctx, tx, ossId, filePath, userId, now)
|
||||
}
|
||||
|
||||
func appRefreshAllocateOssFileInfo(ctx context.Context, tx gdb.TX, ossId int64, filePath string, userId int64, now *gtime.Time) error {
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取划拨结果文件信息失败")
|
||||
}
|
||||
md5, _ := gmd5.EncryptFile(filePath)
|
||||
crc16 := hash.Crc16ChecksumFile(filePath)
|
||||
sum16 := hash.Sum16ChecksumFile(filePath)
|
||||
_, err = dao.SysOss.Ctx(ctx).TX(tx).
|
||||
Where(dao.SysOss.Columns().OssId, ossId).
|
||||
Data(do.SysOss{
|
||||
Md5: md5,
|
||||
FileSize: fileInfo.Size(),
|
||||
FileCrc16: crc16,
|
||||
FileSum: sum16,
|
||||
UpdatedAt: now,
|
||||
UpdatedBy: userId,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新划拨结果文件信息失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appLoadAllocateOssUpload(ctx context.Context, tx gdb.TX, ossId int64, fallback model.UploadResponse) (model.UploadResponse, error) {
|
||||
if ossId <= 0 {
|
||||
return fallback, nil
|
||||
}
|
||||
var oss *entity.SysOss
|
||||
err := dao.SysOss.Ctx(ctx).TX(tx).
|
||||
Where(dao.SysOss.Columns().OssId, ossId).
|
||||
Limit(1).
|
||||
Scan(&oss)
|
||||
if err != nil {
|
||||
return fallback, gerror.Wrap(err, "读取划拨结果文件OSS记录失败")
|
||||
}
|
||||
if oss == nil || oss.OssId == 0 {
|
||||
return fallback, nil
|
||||
}
|
||||
fallback.Name = oss.FileName
|
||||
fallback.OriginalName = oss.OriginalName
|
||||
fallback.Ext = oss.FileSuffix
|
||||
fallback.Path = oss.Path
|
||||
fallback.FullPath = oss.Url
|
||||
fallback.Size = int64(oss.FileSize)
|
||||
fallback.Md5 = oss.Md5
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
func appAllocateBatchImportFileName(ctx context.Context, tx gdb.TX, ossId int64, sourceName string) string {
|
||||
sourceName = strings.TrimSpace(sourceName)
|
||||
if ossId <= 0 {
|
||||
return sourceName
|
||||
}
|
||||
var oss *entity.SysOss
|
||||
if err := dao.SysOss.Ctx(ctx).TX(tx).
|
||||
Fields(dao.SysOss.Columns().FileName).
|
||||
Where(dao.SysOss.Columns().OssId, ossId).
|
||||
Limit(1).
|
||||
Scan(&oss); err != nil {
|
||||
return sourceName
|
||||
}
|
||||
if oss != nil && strings.TrimSpace(oss.FileName) != "" {
|
||||
return strings.TrimSpace(oss.FileName)
|
||||
}
|
||||
return sourceName
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// 只处理终端用户绑卡。企业用户的持卡范围由管理平台权限逻辑决定,不写终端卡关联表。
|
||||
type appSimTerminalBindRow struct {
|
||||
Id int64 `orm:"id"`
|
||||
TerminalId int64 `orm:"terminal_id"`
|
||||
DeptId int64 `orm:"dept_id"`
|
||||
CurrentPackageCode string `orm:"current_package_code"`
|
||||
}
|
||||
|
||||
func (s *sAppCard) Bind(ctx context.Context, param *model.AppCardBindParam) (res *model.AppCardModel, err error) {
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !s.isTerminalUser(ctx, user) {
|
||||
return nil, gerror.New("企业用户无需绑定卡,请在我的卡片中查询当前可见卡")
|
||||
}
|
||||
|
||||
card, err := s.findCardByValue(ctx, user.TenantId, param.CardValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if card == nil {
|
||||
return nil, gerror.New("卡信息不存在,请确认查询号码是否正确")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
alreadyBound := false
|
||||
err = dao.TSimTerminal.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
terminal, err := s.ensureTerminal(ctx, tx, user, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var bind *appSimTerminalBindRow
|
||||
err = dao.TSimTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TSimTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TSimTerminal.Columns().Iccid, card.Iccid).
|
||||
Where(dao.TSimTerminal.Columns().TerminalId, user.UserId).
|
||||
Scan(&bind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bind != nil {
|
||||
alreadyBound = true
|
||||
if bind.DeptId != card.DeptId || strings.TrimSpace(bind.CurrentPackageCode) != strings.TrimSpace(card.CurrentPackageCode) {
|
||||
_, err = dao.TSimTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TSimTerminal.Columns().Id, bind.Id).
|
||||
Data(do.TSimTerminal{
|
||||
DeptId: card.DeptId,
|
||||
CurrentPackageCode: strings.TrimSpace(card.CurrentPackageCode),
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
}
|
||||
return err
|
||||
}
|
||||
_, err = dao.TSimTerminal.Ctx(ctx).TX(tx).Data(do.TSimTerminal{
|
||||
Iccid: card.Iccid,
|
||||
Msisdn: card.Msisdn,
|
||||
Imsi: card.Imsi,
|
||||
TerminalId: user.UserId,
|
||||
TerminalName: terminal.TerminalName,
|
||||
DeptId: card.DeptId,
|
||||
CurrentPackageCode: strings.TrimSpace(card.CurrentPackageCode),
|
||||
Remark: param.Remark,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: appUserDeptOrDefault(user),
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
if isOldSimTerminalUniqueKeyError(err) {
|
||||
return nil, gerror.New("终端卡关联表唯一索引未更新,当前数据库仍限制一个终端用户只能绑定一张卡")
|
||||
}
|
||||
if isSimTerminalCardUniqueKeyError(err) {
|
||||
return nil, gerror.New("该卡已绑定当前用户,请刷新后重试")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if alreadyBound {
|
||||
return nil, gerror.New("该卡已绑定当前用户")
|
||||
}
|
||||
|
||||
cardRow, err := s.findVisibleCard(ctx, user, card.Iccid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cardRow == nil {
|
||||
return nil, gerror.New("绑卡成功,但刷新卡信息失败")
|
||||
}
|
||||
return s.cardRowToModel(ctx, cardRow, user, appCardOwnerTerminal, true), nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureTerminal(ctx context.Context, tx gdb.TX, user *entity.SysUser, now *gtime.Time) (*entity.TTerminal, error) {
|
||||
var terminal *entity.TTerminal
|
||||
err := dao.TTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TTerminal.Columns().TerminalId, user.UserId).
|
||||
Scan(&terminal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if terminal != nil {
|
||||
if strings.TrimSpace(terminal.TerminalName) == "" {
|
||||
terminal.TerminalName = user.NickName
|
||||
}
|
||||
return terminal, nil
|
||||
}
|
||||
name := user.NickName
|
||||
if name == "" {
|
||||
name = user.UserName
|
||||
}
|
||||
_, err = dao.TTerminal.Ctx(ctx).TX(tx).Data(do.TTerminal{
|
||||
TerminalId: user.UserId,
|
||||
TerminalName: name,
|
||||
Phone: user.Phonenumber,
|
||||
Status: consts.SysUserStatusNormal,
|
||||
FullName: name,
|
||||
LegalPersonName: name,
|
||||
LegalPersonIdCard: "000000000000000000",
|
||||
Balance: 0,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: appUserDeptOrDefault(user),
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &entity.TTerminal{
|
||||
TerminalId: user.UserId,
|
||||
TerminalName: name,
|
||||
Phone: user.Phonenumber,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: appUserDeptOrDefault(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) isCardBoundToUser(ctx context.Context, iccid string, user *entity.SysUser) bool {
|
||||
count, err := dao.TSimTerminal.Ctx(ctx).
|
||||
Where(dao.TSimTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TSimTerminal.Columns().TerminalId, user.UserId).
|
||||
Where(dao.TSimTerminal.Columns().Iccid, iccid).
|
||||
Count()
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
func isOldSimTerminalUniqueKeyError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
errText := err.Error()
|
||||
return strings.Contains(errText, "Duplicate entry") &&
|
||||
strings.Contains(errText, "uk_package_terminal")
|
||||
}
|
||||
|
||||
func isSimTerminalCardUniqueKeyError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
errText := err.Error()
|
||||
return strings.Contains(errText, "Duplicate entry") &&
|
||||
strings.Contains(errText, "uk_sim_terminal_card")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,282 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type appCardChannelInfo struct {
|
||||
ChanKey string
|
||||
ChanName string
|
||||
ChanAlias string
|
||||
TempKey string
|
||||
}
|
||||
|
||||
type appCardChannelAssocRecord struct {
|
||||
Id int64 `orm:"id"`
|
||||
ChannelID 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"`
|
||||
}
|
||||
|
||||
func (s *sAppCard) attachCardChannelInfo(ctx context.Context, row *appCardQueryRow) error {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
info, err := s.loadCardChannelInfo(ctx, row.TenantId, row.Iccid, row.ChanKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
row.ChanKey = info.ChanKey
|
||||
row.ChanName = info.ChanName
|
||||
row.ChanAlias = info.ChanAlias
|
||||
row.TempKey = info.TempKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) attachDiagnosisCardChannelInfo(ctx context.Context, row *appCardQueryRow) error {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
info, err := s.loadDiagnosisCardChannelInfo(ctx, row.TenantId, row.Iccid, row.ChanKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
row.ChanKey = info.ChanKey
|
||||
row.ChanName = info.ChanName
|
||||
row.ChanAlias = info.ChanAlias
|
||||
row.TempKey = info.TempKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) attachCardChannelInfoRows(ctx context.Context, rows []*appCardQueryRow) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
cache := make(map[string]*appCardChannelInfo, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
cacheKey := strings.Join([]string{
|
||||
strings.TrimSpace(row.TenantId),
|
||||
strings.TrimSpace(row.Iccid),
|
||||
strings.TrimSpace(row.ChanKey),
|
||||
}, "|")
|
||||
info, ok := cache[cacheKey]
|
||||
if !ok {
|
||||
var err error
|
||||
info, err = s.loadCardChannelInfo(ctx, row.TenantId, row.Iccid, row.ChanKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cache[cacheKey] = info
|
||||
}
|
||||
if info == nil {
|
||||
continue
|
||||
}
|
||||
row.ChanKey = info.ChanKey
|
||||
row.ChanName = info.ChanName
|
||||
row.ChanAlias = info.ChanAlias
|
||||
row.TempKey = info.TempKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadCardChannelInfo(ctx context.Context, tenantId string, iccid string, legacyChanKey string) (*appCardChannelInfo, error) {
|
||||
tenantId = strings.TrimSpace(tenantId)
|
||||
legacyChanKey = strings.TrimSpace(legacyChanKey)
|
||||
if legacyChanKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return s.findLegacyCardChannelInfo(ctx, tenantId, legacyChanKey)
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadDiagnosisCardChannelInfo(ctx context.Context, tenantId string, iccid string, legacyChanKey string) (*appCardChannelInfo, error) {
|
||||
iccid = strings.TrimSpace(iccid)
|
||||
tenantId = strings.TrimSpace(tenantId)
|
||||
legacyChanKey = strings.TrimSpace(legacyChanKey)
|
||||
if iccid != "" {
|
||||
info, err := s.findAssocCardChannelInfo(ctx, tenantId, iccid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info != nil {
|
||||
return info, nil
|
||||
}
|
||||
}
|
||||
if legacyChanKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return s.findLegacyCardChannelInfo(ctx, tenantId, legacyChanKey)
|
||||
}
|
||||
|
||||
func (s *sAppCard) findAssocCardChannelInfo(ctx context.Context, tenantId string, iccid string) (*appCardChannelInfo, error) {
|
||||
if strings.TrimSpace(iccid) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
assoc, err := s.resolveCardChannelAssocByIccid(ctx, iccid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if assoc == nil || strings.TrimSpace(assoc.ChannelID) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
info, err := s.findCardChannelInfoByRef(ctx, tenantId, assoc.ChannelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info == nil || strings.TrimSpace(info.ChanKey) == "" {
|
||||
return nil, gerror.New("卡通道关联已匹配,但未找到对应通道配置")
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) resolveCardChannelAssocByIccid(ctx context.Context, iccid string) (*appCardChannelAssocRecord, error) {
|
||||
iccid = strings.TrimSpace(iccid)
|
||||
if iccid == "" {
|
||||
return nil, nil
|
||||
}
|
||||
columns := dao.CardChannelAssoc.Columns()
|
||||
|
||||
var exact *appCardChannelAssocRecord
|
||||
err := dao.CardChannelAssoc.Ctx(ctx).
|
||||
Fields(columns.Id, columns.ChannelId, columns.MatchType, columns.Iccid, columns.IccidStart, columns.IccidEnd, columns.Status).
|
||||
Where(columns.Status, 1).
|
||||
Where(columns.MatchType, 1).
|
||||
Where(columns.Iccid+" COLLATE utf8mb4_general_ci = ?", iccid).
|
||||
OrderDesc(columns.Id).
|
||||
Limit(1).
|
||||
Scan(&exact)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询卡通道单卡关联失败")
|
||||
}
|
||||
if exact != nil && strings.TrimSpace(exact.ChannelID) != "" {
|
||||
return exact, nil
|
||||
}
|
||||
|
||||
var ranged *appCardChannelAssocRecord
|
||||
err = dao.CardChannelAssoc.Ctx(ctx).
|
||||
Fields(columns.Id, columns.ChannelId, columns.MatchType, columns.Iccid, columns.IccidStart, columns.IccidEnd, columns.Status).
|
||||
Where(columns.Status, 1).
|
||||
Where(columns.MatchType, 2).
|
||||
Where(columns.IccidStart+" COLLATE utf8mb4_general_ci <= ? AND "+columns.IccidEnd+" COLLATE utf8mb4_general_ci >= ?", iccid, iccid).
|
||||
OrderDesc(columns.Id).
|
||||
Limit(1).
|
||||
Scan(&ranged)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询卡通道号段关联失败")
|
||||
}
|
||||
if ranged != nil && strings.TrimSpace(ranged.ChannelID) != "" {
|
||||
return ranged, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) findCardChannelInfoByRef(ctx context.Context, tenantId string, channelRef string) (*appCardChannelInfo, error) {
|
||||
channelRef = strings.TrimSpace(channelRef)
|
||||
if channelRef == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if id, err := strconv.ParseInt(channelRef, 10, 64); err == nil && id > 0 {
|
||||
if info, err := s.findCardChannelInfoByID(ctx, tenantId, id); err != nil || info != nil {
|
||||
return info, err
|
||||
}
|
||||
}
|
||||
return s.findLegacyCardChannelInfo(ctx, tenantId, channelRef)
|
||||
}
|
||||
|
||||
func (s *sAppCard) findCardChannelInfoByID(ctx context.Context, tenantId string, channelID int64) (*appCardChannelInfo, error) {
|
||||
if channelID <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if tenantId = strings.TrimSpace(tenantId); tenantId == "" {
|
||||
tenantId = contexts.GetTenantId(ctx)
|
||||
}
|
||||
if tenantId != "" {
|
||||
channel, err := s.findCardChannelEntityByID(ctx, tenantId, channelID)
|
||||
if err != nil || channel != nil {
|
||||
return appCardChannelInfoFromEntity(channel), err
|
||||
}
|
||||
}
|
||||
channel, err := s.findCardChannelEntityByID(ctx, "", channelID)
|
||||
return appCardChannelInfoFromEntity(channel), err
|
||||
}
|
||||
|
||||
func (s *sAppCard) findCardChannelEntityByID(ctx context.Context, tenantId string, channelID int64) (*entity.TSimChannels, error) {
|
||||
var channel *entity.TSimChannels
|
||||
mod := dao.TSimChannels.Ctx(ctx).
|
||||
Where(dao.TSimChannels.Columns().Id, channelID)
|
||||
if tenantId = strings.TrimSpace(tenantId); tenantId != "" {
|
||||
mod = mod.Where(dao.TSimChannels.Columns().TenantId, tenantId)
|
||||
}
|
||||
err := mod.Limit(1).Scan(&channel)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询通道信息失败")
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) findLegacyCardChannelInfo(ctx context.Context, tenantId string, chanKey string) (*appCardChannelInfo, error) {
|
||||
chanKey = strings.TrimSpace(chanKey)
|
||||
if chanKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if tenantId = strings.TrimSpace(tenantId); tenantId == "" {
|
||||
tenantId = contexts.GetTenantId(ctx)
|
||||
}
|
||||
if tenantId != "" {
|
||||
channel, err := s.findCardChannelEntityByKey(ctx, tenantId, chanKey)
|
||||
if err != nil || channel != nil {
|
||||
return appCardChannelInfoFromEntity(channel), err
|
||||
}
|
||||
}
|
||||
channel, err := s.findCardChannelEntityByKey(ctx, "", chanKey)
|
||||
return appCardChannelInfoFromEntity(channel), err
|
||||
}
|
||||
|
||||
func (s *sAppCard) findCardChannelEntityByKey(ctx context.Context, tenantId string, chanKey string) (*entity.TSimChannels, error) {
|
||||
var channel *entity.TSimChannels
|
||||
mod := dao.TSimChannels.Ctx(ctx).
|
||||
Where(dao.TSimChannels.Columns().ChanKey, chanKey).
|
||||
Fields(dao.TSimChannels.Columns().Id, dao.TSimChannels.Columns().ChanKey, dao.TSimChannels.Columns().ChanName, dao.TSimChannels.Columns().ChanAlias, dao.TSimChannels.Columns().TempKey)
|
||||
if tenantId = strings.TrimSpace(tenantId); tenantId != "" {
|
||||
mod = mod.Where(dao.TSimChannels.Columns().TenantId, tenantId)
|
||||
}
|
||||
err := mod.Limit(1).Scan(&channel)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询通道信息失败")
|
||||
}
|
||||
if channel == nil || channel.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func appCardChannelInfoFromEntity(channel *entity.TSimChannels) *appCardChannelInfo {
|
||||
if channel == nil || channel.Id == 0 || strings.TrimSpace(channel.ChanKey) == "" {
|
||||
return nil
|
||||
}
|
||||
return &appCardChannelInfo{
|
||||
ChanKey: strings.TrimSpace(channel.ChanKey),
|
||||
ChanName: strings.TrimSpace(channel.ChanName),
|
||||
ChanAlias: strings.TrimSpace(channel.ChanAlias),
|
||||
TempKey: strings.TrimSpace(channel.TempKey),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
func (s *sAppCard) cardRowToModel(ctx context.Context, row *appCardQueryRow, user *entity.SysUser, ownerType string, canBusiness bool) *model.AppCardModel {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
canBusiness = canBusiness && s.canOperateCard(ctx, user, row)
|
||||
remain := row.PackageTotalFlowMb - row.UsedFlowMb
|
||||
if remain < 0 {
|
||||
remain = 0
|
||||
}
|
||||
usedPercent := float64(0)
|
||||
if row.PackageTotalFlowMb > 0 {
|
||||
usedPercent = row.UsedFlowMb / row.PackageTotalFlowMb * 100
|
||||
}
|
||||
isBound := false
|
||||
if user != nil && s.isTerminalUser(ctx, user) && ownerType == appCardOwnerTerminal {
|
||||
isBound = true
|
||||
} else if user != nil {
|
||||
isBound = s.isCardBoundToUser(ctx, row.Iccid, user)
|
||||
}
|
||||
currentPackageCode := appCardCurrentPackageCode(row)
|
||||
packageName := appCardPackageName(row)
|
||||
packageType := appCardPackageType(row)
|
||||
categoryCode := appCardCategoryCode(row)
|
||||
categoryName := appCardCategoryName(row)
|
||||
abnormalReasons := appCardAbnormalReasons(row.BossCardStatus, row.PackageStatus)
|
||||
deptName := ""
|
||||
deptKey := ""
|
||||
deptId := int64(0)
|
||||
tenantId := ""
|
||||
chanName := ""
|
||||
chanAlias := ""
|
||||
if ownerType == appCardOwnerAgent {
|
||||
actualDeptId := appCardOwnerDeptId(row)
|
||||
if user != nil && !s.isTerminalUser(ctx, user) && user.DeptId > 0 && actualDeptId > 0 {
|
||||
if displayDept, err := appEnterpriseDisplayDept(ctx, row.TenantId, user.DeptId, actualDeptId); err == nil && displayDept != nil && displayDept.DeptId > 0 {
|
||||
deptId = displayDept.DeptId
|
||||
deptName = strings.TrimSpace(displayDept.DeptName)
|
||||
deptKey = strings.TrimSpace(displayDept.DeptKey)
|
||||
}
|
||||
} else {
|
||||
deptId = actualDeptId
|
||||
deptName = appCardOwnerDeptName(row)
|
||||
deptKey = appCardOwnerDeptKey(row)
|
||||
}
|
||||
tenantId = row.TenantId
|
||||
chanName = row.ChanName
|
||||
chanAlias = row.ChanAlias
|
||||
}
|
||||
return &model.AppCardModel{
|
||||
Id: row.Id,
|
||||
Iccid: row.Iccid,
|
||||
Msisdn: row.Msisdn,
|
||||
Imsi: row.Imsi,
|
||||
Imei: row.Imei,
|
||||
RegionCode: row.RegionCode,
|
||||
DeptId: deptId,
|
||||
DeptName: deptName,
|
||||
DeptKey: deptKey,
|
||||
CurrentPackageCode: currentPackageCode,
|
||||
PackageName: packageName,
|
||||
PackageType: packageType,
|
||||
CategoryCode: categoryCode,
|
||||
CategoryName: categoryName,
|
||||
PackageStatus: row.PackageStatus,
|
||||
AccessType: row.AccessType,
|
||||
BossCardStatus: row.BossCardStatus,
|
||||
AbnormalReason: strings.Join(abnormalReasons, "、"),
|
||||
AbnormalReasons: abnormalReasons,
|
||||
CardType: row.CardType,
|
||||
NetModel: row.NetModel,
|
||||
PackageTotalFlowMb: row.PackageTotalFlowMb,
|
||||
UsedFlowMb: row.UsedFlowMb,
|
||||
RemainFlowMb: remain,
|
||||
UsedFlowPercent: usedPercent,
|
||||
FlowPkgBeginAt: row.FlowPkgBeginAt,
|
||||
FlowPkgEndAt: row.FlowPkgEndAt,
|
||||
DataServiceStatus: row.DataServiceStatus,
|
||||
PackageTotalSms: row.PackageTotalSms,
|
||||
UsedSms: row.UsedSms,
|
||||
SmsServiceStatus: row.SmsServiceStatus,
|
||||
PackageTotalVoice: row.PackageTotalVoice,
|
||||
UsedVoice: row.UsedVoice,
|
||||
VoiceServiceStatus: row.VoiceServiceStatus,
|
||||
RoamServiceStatus: row.RoamServiceStatus,
|
||||
StatusSyncTime: row.StatusSyncTime,
|
||||
FlowSyncTime: row.FlowSyncTime,
|
||||
ChanKey: "",
|
||||
ChanName: chanName,
|
||||
ChanAlias: chanAlias,
|
||||
Remark: row.Remark,
|
||||
TenantId: tenantId,
|
||||
TerminalName: row.TerminalName,
|
||||
IsBound: isBound,
|
||||
CanBusiness: canBusiness,
|
||||
CanAllocate: s.canAllocateCard(ctx, user, row, ownerType, canBusiness),
|
||||
OwnerType: ownerType,
|
||||
DataSource: "local",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sAppCard) canAllocateCard(ctx context.Context, user *entity.SysUser, row *appCardQueryRow, ownerType string, canBusiness bool) bool {
|
||||
return user != nil &&
|
||||
row != nil &&
|
||||
ownerType == appCardOwnerAgent &&
|
||||
!s.isTerminalUser(ctx, user) &&
|
||||
canBusiness &&
|
||||
user.DeptId > 0 &&
|
||||
row.DeptId == user.DeptId
|
||||
}
|
||||
|
||||
func (s *sAppCard) publicCardRowToModel(ctx context.Context, row *appCardQueryRow, cardValue string) *model.AppCardModel {
|
||||
card := s.cardRowToModel(ctx, row, nil, appCardOwnerPublic, false)
|
||||
if card == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
card.Id = 0
|
||||
card.DeptId = 0
|
||||
card.DeptName = ""
|
||||
card.DeptKey = ""
|
||||
card.ChanKey = ""
|
||||
card.ChanName = ""
|
||||
card.ChanAlias = ""
|
||||
card.Remark = ""
|
||||
card.TenantId = ""
|
||||
card.TerminalName = ""
|
||||
card.IsBound = false
|
||||
card.CanBusiness = false
|
||||
card.CanAllocate = false
|
||||
card.OwnerType = appCardOwnerPublic
|
||||
|
||||
return card
|
||||
}
|
||||
|
||||
func (s *sAppCard) upstreamPublicCardToModel(info *appSimBasicInfoOutput, cardValue string) *model.AppCardModel {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
cardStatus := info.CardStatus
|
||||
if cardStatus == 0 {
|
||||
cardStatus = consts.V3ApiSimBossCardStatusActivated
|
||||
}
|
||||
packageCode, packageName := appOneLinkMockPackageFromRemark(info.Remark)
|
||||
abnormalReasons := appCardAbnormalReasons(cardStatus, 0)
|
||||
return &model.AppCardModel{
|
||||
Iccid: appFirstNotEmpty(info.Iccid, strings.TrimSpace(cardValue)),
|
||||
Msisdn: info.Msisdn,
|
||||
Imsi: info.Imsi,
|
||||
Imei: info.Imei,
|
||||
CurrentPackageCode: packageCode,
|
||||
PackageName: packageName,
|
||||
BossCardStatus: cardStatus,
|
||||
AbnormalReason: strings.Join(abnormalReasons, "、"),
|
||||
AbnormalReasons: abnormalReasons,
|
||||
StatusSyncTime: appParseOneLinkTime(info.LastChangeDate),
|
||||
FlowPkgBeginAt: appParseOneLinkTime(info.ActiveDate),
|
||||
OwnerType: appCardOwnerPublic,
|
||||
DataSource: info.Provider,
|
||||
UpstreamProvider: info.Provider,
|
||||
IsBound: false,
|
||||
CanBusiness: false,
|
||||
CanAllocate: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sAppCard) packageRowToModel(row *appPackageQueryRow, ownerType string, canBusiness bool) *model.AppPackageModel {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
status, statusType := appPackageStatus(row.EffectiveStatus, row.ExpectedExpireTime)
|
||||
return &model.AppPackageModel{
|
||||
Id: row.Id,
|
||||
SubscriptionNo: row.SubscriptionNo,
|
||||
PackageCode: row.PackageCode,
|
||||
PackageName: row.PackageName,
|
||||
PackageType: row.PackageType,
|
||||
PackageTypeName: appPackageTypeName(row.PackageType),
|
||||
CategoryCode: row.CategoryCode,
|
||||
CategoryName: row.CategoryName,
|
||||
Iccid: row.Iccid,
|
||||
Msisdn: row.Msisdn,
|
||||
CardId: row.CardId,
|
||||
CardValue: appFirstNotEmpty(row.Msisdn, row.Iccid),
|
||||
CardRemark: row.CardRemark,
|
||||
TerminalName: row.TerminalName,
|
||||
BusinessType: row.BusinessType,
|
||||
BusinessTypeName: appBusinessTypeName(row.BusinessType),
|
||||
EffectiveStatus: row.EffectiveStatus,
|
||||
Status: status,
|
||||
StatusType: statusType,
|
||||
FilterValue: row.PackageCode,
|
||||
EffectiveTime: row.EffectiveTime,
|
||||
ExpireTime: row.ExpectedExpireTime,
|
||||
ValidText: appPackageValidText(row.EffectiveTime, row.ExpectedExpireTime),
|
||||
EffectText: appPackageEffectText(row.EffectiveStatus),
|
||||
FlowMb: row.FlowMb,
|
||||
FlowText: appFlowText(row.FlowMb),
|
||||
OwnerType: ownerType,
|
||||
CanBusiness: canBusiness,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sAppCard) publicPackageRowToModel(card *appCardQueryRow, pkg *appPublicPackageQueryRow) *model.AppPackageModel {
|
||||
if card == nil || pkg == nil {
|
||||
return nil
|
||||
}
|
||||
effectiveStatus := appEffectiveStatusFromCardPackageStatus(card.PackageStatus)
|
||||
status, statusType := appPackageStatus(effectiveStatus, card.FlowPkgEndAt)
|
||||
flowMb := pkg.FlowMb
|
||||
if flowMb <= 0 {
|
||||
flowMb = card.PackageTotalFlowMb
|
||||
}
|
||||
return &model.AppPackageModel{
|
||||
Id: pkg.Id,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageType: pkg.PackageType,
|
||||
PackageTypeName: appPackageTypeName(pkg.PackageType),
|
||||
CategoryCode: pkg.CategoryCode,
|
||||
CategoryName: pkg.CategoryName,
|
||||
Iccid: card.Iccid,
|
||||
Msisdn: card.Msisdn,
|
||||
CardId: card.Id,
|
||||
CardValue: appFirstNotEmpty(card.Msisdn, card.Iccid),
|
||||
CardRemark: card.Remark,
|
||||
TerminalName: card.TerminalName,
|
||||
BusinessTypeName: "当前套餐",
|
||||
EffectiveStatus: effectiveStatus,
|
||||
Status: status,
|
||||
StatusType: statusType,
|
||||
FilterValue: pkg.PackageCode,
|
||||
EffectiveTime: card.FlowPkgBeginAt,
|
||||
ExpireTime: card.FlowPkgEndAt,
|
||||
ValidText: appPackageValidText(card.FlowPkgBeginAt, card.FlowPkgEndAt),
|
||||
EffectText: appPackageEffectText(effectiveStatus),
|
||||
FlowMb: flowMb,
|
||||
FlowText: appFlowText(flowMb),
|
||||
OwnerType: appCardOwnerPublic,
|
||||
CanBusiness: false,
|
||||
}
|
||||
}
|
||||
|
||||
func appSanitizePublicPackageModel(pkg *model.AppPackageModel, cardValue string) *model.AppPackageModel {
|
||||
if pkg == nil {
|
||||
return nil
|
||||
}
|
||||
pkg.Id = 0
|
||||
pkg.SubscriptionNo = ""
|
||||
pkg.CardId = 0
|
||||
pkg.CardValue = appFirstNotEmpty(pkg.Msisdn, pkg.Iccid, strings.TrimSpace(cardValue))
|
||||
pkg.CardRemark = ""
|
||||
pkg.TerminalName = ""
|
||||
pkg.BusinessType = 0
|
||||
pkg.BusinessTypeName = "当前套餐"
|
||||
pkg.OwnerType = appCardOwnerPublic
|
||||
pkg.CanBusiness = false
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func appPackageStatus(status int, expireAt *gtime.Time) (string, string) {
|
||||
if expireAt != nil && !expireAt.IsZero() && expireAt.Before(gtime.Now()) {
|
||||
return "已过期", "danger"
|
||||
}
|
||||
if status == 1 && expireAt != nil && !expireAt.IsZero() && expireAt.Before(gtime.Now().AddDate(0, 0, 30)) {
|
||||
return "即将到期", "warning"
|
||||
}
|
||||
switch status {
|
||||
case 0:
|
||||
return "待生效", "primary"
|
||||
case 1:
|
||||
return "生效中", "success"
|
||||
case 2:
|
||||
return "已过期", "danger"
|
||||
case 3:
|
||||
return "已终止", "danger"
|
||||
case 4:
|
||||
return "已作废", "danger"
|
||||
default:
|
||||
return "未知", "primary"
|
||||
}
|
||||
}
|
||||
|
||||
func appCardAbnormalReasons(bossCardStatus int, packageStatus int) []string {
|
||||
reasons := make([]string, 0, 2)
|
||||
if packageStatus == consts.PackageStatusExpired {
|
||||
reasons = append(reasons, "套餐已过期")
|
||||
}
|
||||
switch bossCardStatus {
|
||||
case 0:
|
||||
reasons = append(reasons, "卡状态未知")
|
||||
case consts.V3ApiSimBossCardStatusStopped:
|
||||
reasons = append(reasons, "卡状态停机")
|
||||
case 8:
|
||||
reasons = append(reasons, "卡状态预销户")
|
||||
case 9:
|
||||
reasons = append(reasons, "卡状态已销户")
|
||||
case consts.V3ApiSimBossCardStatusPendingActivate,
|
||||
consts.V3ApiSimBossCardStatusActivated,
|
||||
6,
|
||||
7:
|
||||
default:
|
||||
reasons = append(reasons, fmt.Sprintf("未知卡状态:%d", bossCardStatus))
|
||||
}
|
||||
return reasons
|
||||
}
|
||||
|
||||
func appEffectiveStatusFromCardPackageStatus(status int) int {
|
||||
switch status {
|
||||
case 1:
|
||||
return 0
|
||||
case 2:
|
||||
return 1
|
||||
case 3:
|
||||
return 2
|
||||
default:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
func appPackageTypeName(packageType int) string {
|
||||
switch packageType {
|
||||
case 101:
|
||||
return "单卡套餐"
|
||||
case 102:
|
||||
return "池套餐"
|
||||
case 103:
|
||||
return "单卡加油包"
|
||||
case 104:
|
||||
return "池卡加油包"
|
||||
default:
|
||||
return gconv.String(packageType)
|
||||
}
|
||||
}
|
||||
|
||||
func appBusinessTypeName(businessType int) string {
|
||||
switch businessType {
|
||||
case 0:
|
||||
return "订购"
|
||||
case 1:
|
||||
return "变更"
|
||||
case 2:
|
||||
return "续费"
|
||||
default:
|
||||
return gconv.String(businessType)
|
||||
}
|
||||
}
|
||||
|
||||
func appPackageValidText(beginAt, expireAt *gtime.Time) string {
|
||||
if beginAt == nil || beginAt.IsZero() || expireAt == nil || expireAt.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return beginAt.Format("Y-m-d") + " 至 " + expireAt.Format("Y-m-d")
|
||||
}
|
||||
|
||||
func appPackageEffectText(status int) string {
|
||||
switch status {
|
||||
case 0:
|
||||
return "套餐待生效"
|
||||
case 1:
|
||||
return "套餐生效中"
|
||||
case 2:
|
||||
return "套餐已过期"
|
||||
case 3:
|
||||
return "套餐已终止"
|
||||
case 4:
|
||||
return "套餐已作废"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func appFlowText(flowMb float64) string {
|
||||
if flowMb <= 0 {
|
||||
return ""
|
||||
}
|
||||
if flowMb >= 1024 {
|
||||
return fmt.Sprintf("%.2fGB", flowMb/1024)
|
||||
}
|
||||
return fmt.Sprintf("%.0fMB", flowMb)
|
||||
}
|
||||
|
||||
func appFirstNotEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func appCardOwnerDeptId(row *appCardQueryRow) int64 {
|
||||
if row == nil {
|
||||
return 0
|
||||
}
|
||||
if row.TerminalDeptId > 0 {
|
||||
return row.TerminalDeptId
|
||||
}
|
||||
return row.DeptId
|
||||
}
|
||||
|
||||
func appCardOwnerDeptName(row *appCardQueryRow) string {
|
||||
if row == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(row.OwnerDeptName)
|
||||
}
|
||||
|
||||
func appCardOwnerDeptKey(row *appCardQueryRow) string {
|
||||
if row == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(row.OwnerDeptKey)
|
||||
}
|
||||
|
||||
func appCardCurrentPackageCode(row *appCardQueryRow) string {
|
||||
if row == nil {
|
||||
return ""
|
||||
}
|
||||
if strings.TrimSpace(row.TerminalCurrentPackageCode) != "" {
|
||||
return strings.TrimSpace(row.TerminalCurrentPackageCode)
|
||||
}
|
||||
return strings.TrimSpace(row.CurrentPackageCode)
|
||||
}
|
||||
|
||||
func appCardPackageName(row *appCardQueryRow) string {
|
||||
if row == nil {
|
||||
return ""
|
||||
}
|
||||
terminalPackageCode := strings.TrimSpace(row.TerminalCurrentPackageCode)
|
||||
if terminalPackageCode != "" {
|
||||
if strings.TrimSpace(row.TerminalPackageName) != "" {
|
||||
return row.TerminalPackageName
|
||||
}
|
||||
if terminalPackageCode != strings.TrimSpace(row.CurrentPackageCode) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return row.PackageName
|
||||
}
|
||||
|
||||
func appCardPackageType(row *appCardQueryRow) int {
|
||||
if row == nil {
|
||||
return 0
|
||||
}
|
||||
terminalPackageCode := strings.TrimSpace(row.TerminalCurrentPackageCode)
|
||||
if terminalPackageCode != "" {
|
||||
if row.TerminalPackageType > 0 {
|
||||
return row.TerminalPackageType
|
||||
}
|
||||
if terminalPackageCode != strings.TrimSpace(row.CurrentPackageCode) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return row.PackageType
|
||||
}
|
||||
|
||||
func appCardCategoryCode(row *appCardQueryRow) string {
|
||||
if row == nil {
|
||||
return ""
|
||||
}
|
||||
terminalPackageCode := strings.TrimSpace(row.TerminalCurrentPackageCode)
|
||||
if terminalPackageCode != "" {
|
||||
if strings.TrimSpace(row.TerminalCategoryCode) != "" {
|
||||
return row.TerminalCategoryCode
|
||||
}
|
||||
if terminalPackageCode != strings.TrimSpace(row.CurrentPackageCode) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return row.CategoryCode
|
||||
}
|
||||
|
||||
func appCardCategoryName(row *appCardQueryRow) string {
|
||||
if row == nil {
|
||||
return ""
|
||||
}
|
||||
terminalPackageCode := strings.TrimSpace(row.TerminalCurrentPackageCode)
|
||||
if terminalPackageCode != "" {
|
||||
if strings.TrimSpace(row.TerminalCategoryName) != "" {
|
||||
return row.TerminalCategoryName
|
||||
}
|
||||
if terminalPackageCode != strings.TrimSpace(row.CurrentPackageCode) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return row.CategoryName
|
||||
}
|
||||
|
||||
func appOneLinkMockPackageFromRemark(remark string) (packageCode string, packageName string) {
|
||||
for _, part := range strings.Split(remark, ";") {
|
||||
item := strings.TrimSpace(part)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
key, value, ok := strings.Cut(item, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(key) {
|
||||
case "package":
|
||||
packageCode = strings.TrimSpace(value)
|
||||
case "name":
|
||||
packageName = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
if packageName == "" {
|
||||
packageName = packageCode
|
||||
}
|
||||
return packageCode, packageName
|
||||
}
|
||||
|
||||
func appParseOneLinkTime(value string) *gtime.Time {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
parsed := gtime.NewFromStr(value)
|
||||
if parsed == nil || parsed.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
)
|
||||
|
||||
func TestPublicCardRowToModelKeepsCardIdentifiers(t *testing.T) {
|
||||
svc := &sAppCard{}
|
||||
row := &appCardQueryRow{
|
||||
TSimCardInfo: entity.TSimCardInfo{
|
||||
Id: 100,
|
||||
Iccid: "89860123456789012345",
|
||||
Msisdn: "14400005678",
|
||||
Imsi: "460001234567890",
|
||||
Imei: "861234567890123",
|
||||
DeptId: 10,
|
||||
TenantId: "000000",
|
||||
},
|
||||
}
|
||||
|
||||
card := svc.publicCardRowToModel(context.Background(), row, "14400005678")
|
||||
if card == nil {
|
||||
t.Fatal("publicCardRowToModel returned nil")
|
||||
}
|
||||
if card.Iccid != row.Iccid || card.Msisdn != row.Msisdn || card.Imsi != row.Imsi || card.Imei != row.Imei {
|
||||
t.Fatalf("card identifiers were not preserved: %#v", card)
|
||||
}
|
||||
if card.Id != 0 || card.DeptId != 0 || card.TenantId != "" {
|
||||
t.Fatalf("private card fields should still be hidden: %#v", card)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppCardAbnormalReasons(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bossStatus int
|
||||
packageStatus int
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "expired package on activated card",
|
||||
bossStatus: consts.V3ApiSimBossCardStatusActivated,
|
||||
packageStatus: consts.PackageStatusExpired,
|
||||
want: []string{"套餐已过期"},
|
||||
},
|
||||
{
|
||||
name: "stopped card",
|
||||
bossStatus: consts.V3ApiSimBossCardStatusStopped,
|
||||
packageStatus: consts.PackageStatusEffective,
|
||||
want: []string{"卡状态停机"},
|
||||
},
|
||||
{
|
||||
name: "unknown boss status",
|
||||
bossStatus: 99,
|
||||
packageStatus: consts.PackageStatusEffective,
|
||||
want: []string{"未知卡状态:99"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := appCardAbnormalReasons(tt.bossStatus, tt.packageStatus)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("appCardAbnormalReasons() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/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 *sAppCard) PackageList(ctx context.Context, param *model.AppPackageListParam) (items []*model.AppPackageModel, total int, err error) {
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
mod := s.visiblePackageModel(ctx, user)
|
||||
mod = s.applyPackageFilters(mod, param)
|
||||
if param.Page <= 0 {
|
||||
param.Page = 1
|
||||
}
|
||||
if param.PageSize <= 0 {
|
||||
param.PageSize = 10
|
||||
}
|
||||
|
||||
var rows []*appPackageQueryRow
|
||||
err = mod.OrderDesc(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().Id).
|
||||
Page(param.Page, param.PageSize).
|
||||
ScanAndCount(&rows, &total, false)
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "查询套餐列表失败,请稍后重试")
|
||||
}
|
||||
|
||||
ownerType := s.ownerType(ctx, user)
|
||||
items = make([]*model.AppPackageModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, s.packageRowToModel(row, ownerType, s.canOperatePackageRow(ctx, user, row)))
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
// 查询卡片当前套餐详情。
|
||||
// 核心规则是“卡主表 current_package_code -> 套餐表 package_code”,订购记录只做详情兜底,
|
||||
// 避免卡主表与订购表不同步时前端报“套餐信息不存在”。
|
||||
func (s *sAppCard) PublicPackageDetail(ctx context.Context, param *model.AppPackagePublicDetailParam) (res *model.AppPackageModel, err error) {
|
||||
if param == nil {
|
||||
param = &model.AppPackagePublicDetailParam{}
|
||||
}
|
||||
card, err := s.findPublicCard(ctx, param.TenantId, param.CardValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if card == nil {
|
||||
return nil, gerror.New("卡信息不存在,请确认查询号码是否正确")
|
||||
}
|
||||
|
||||
ownerType := appCardOwnerPublic
|
||||
canBusiness := false
|
||||
displayCard := card
|
||||
if userId := contexts.GetUserId(ctx); userId > 0 {
|
||||
user, userErr := appCurrentUser(ctx)
|
||||
if userErr == nil && user != nil {
|
||||
visibleCard, visibleErr := s.findVisibleCard(ctx, user, param.CardValue)
|
||||
if visibleErr == nil && visibleCard != nil &&
|
||||
strings.TrimSpace(visibleCard.Iccid) == strings.TrimSpace(card.Iccid) &&
|
||||
strings.TrimSpace(visibleCard.TenantId) == strings.TrimSpace(card.TenantId) {
|
||||
displayCard = visibleCard
|
||||
ownerType = s.ownerType(ctx, user)
|
||||
canBusiness = s.canOperateCard(ctx, user, visibleCard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var subscriptionBind *entity.TSimCardBindPkg
|
||||
if strings.TrimSpace(param.SubscriptionNo) != "" {
|
||||
subscriptionBind, err = s.loadRenewSubscription(ctx, card, param.SubscriptionNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
latestBind, err := s.loadLatestRenewBind(ctx, card)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidates := appPublicPackageCandidates(displayCard, subscriptionBind, latestBind, param.PackageCode)
|
||||
if len(candidates) == 0 {
|
||||
return nil, gerror.New("当前卡片没有套餐信息")
|
||||
}
|
||||
|
||||
for _, packageCode := range candidates {
|
||||
pkg, err := s.loadPublicPackageInfo(ctx, card.TenantId, packageCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pkg == nil {
|
||||
continue
|
||||
}
|
||||
result := s.publicPackageRowToModel(displayCard, pkg)
|
||||
result.OwnerType = ownerType
|
||||
result.CanBusiness = canBusiness
|
||||
if subscriptionBind != nil && strings.TrimSpace(subscriptionBind.PackageCode) == strings.TrimSpace(pkg.PackageCode) {
|
||||
appFillPublicPackageFromBind(result, subscriptionBind)
|
||||
} else if latestBind != nil && strings.TrimSpace(latestBind.PackageCode) == strings.TrimSpace(pkg.PackageCode) {
|
||||
appFillPublicPackageFromBind(result, latestBind)
|
||||
}
|
||||
if ownerType == appCardOwnerPublic {
|
||||
result = appSanitizePublicPackageModel(result, param.CardValue)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, gerror.Newf("当前卡片套餐编码 %s 未配置套餐信息", strings.Join(candidates, "、"))
|
||||
}
|
||||
|
||||
func appPublicPackageCandidates(card *appCardQueryRow, subscriptionBind *entity.TSimCardBindPkg, latestBind *entity.TSimCardBindPkg, requestedPackageCode string) []string {
|
||||
var candidates []string
|
||||
addCandidate := func(value string) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
for _, item := range candidates {
|
||||
if item == value {
|
||||
return
|
||||
}
|
||||
}
|
||||
candidates = append(candidates, value)
|
||||
}
|
||||
if subscriptionBind != nil {
|
||||
addCandidate(subscriptionBind.PackageCode)
|
||||
}
|
||||
currentPackageCode := ""
|
||||
if card != nil {
|
||||
currentPackageCode = appCardCurrentPackageCode(card)
|
||||
addCandidate(currentPackageCode)
|
||||
}
|
||||
if latestBind != nil && currentPackageCode == "" {
|
||||
addCandidate(latestBind.PackageCode)
|
||||
}
|
||||
if appPublicPackageCodeAllowed(requestedPackageCode, card, subscriptionBind, latestBind) {
|
||||
addCandidate(requestedPackageCode)
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func appPublicPackageCodeAllowed(packageCode string, card *appCardQueryRow, subscriptionBind *entity.TSimCardBindPkg, latestBind *entity.TSimCardBindPkg) bool {
|
||||
packageCode = strings.TrimSpace(packageCode)
|
||||
if packageCode == "" {
|
||||
return false
|
||||
}
|
||||
if card != nil {
|
||||
currentPackageCode := appCardCurrentPackageCode(card)
|
||||
if currentPackageCode != "" {
|
||||
return currentPackageCode == packageCode
|
||||
}
|
||||
}
|
||||
if subscriptionBind != nil && strings.TrimSpace(subscriptionBind.PackageCode) == packageCode {
|
||||
return true
|
||||
}
|
||||
return latestBind != nil && strings.TrimSpace(latestBind.PackageCode) == packageCode
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadPublicPackageInfo(ctx context.Context, tenantId string, packageCode string) (*appPublicPackageQueryRow, error) {
|
||||
packageTable := dao.TSimFlowPackageInfo.Table()
|
||||
categoryTable := dao.TSimPackageCategory.Table()
|
||||
packageColumns := dao.TSimFlowPackageInfo.Columns()
|
||||
categoryColumns := dao.TSimPackageCategory.Columns()
|
||||
|
||||
var pkg *appPublicPackageQueryRow
|
||||
err := dao.TSimFlowPackageInfo.Ctx(ctx).
|
||||
FieldsPrefix(packageTable, entity.TSimFlowPackageInfo{}).
|
||||
Fields(categoryTable+"."+categoryColumns.CategoryName+" as categoryName").
|
||||
LeftJoin(categoryTable, categoryTable+"."+categoryColumns.CategoryCode+"="+packageTable+"."+packageColumns.CategoryCode+" AND "+categoryTable+"."+categoryColumns.TenantId+"="+packageTable+"."+packageColumns.TenantId).
|
||||
Where(packageTable+"."+packageColumns.TenantId, strings.TrimSpace(tenantId)).
|
||||
Where(packageTable+"."+packageColumns.PackageCode, strings.TrimSpace(packageCode)).
|
||||
Limit(1).
|
||||
Scan(&pkg)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询套餐详情失败")
|
||||
}
|
||||
if pkg == nil || pkg.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func appFillPublicPackageFromBind(pkg *model.AppPackageModel, bind *entity.TSimCardBindPkg) {
|
||||
if pkg == nil || bind == nil {
|
||||
return
|
||||
}
|
||||
pkg.Id = bind.Id
|
||||
pkg.SubscriptionNo = bind.SubscriptionNo
|
||||
pkg.BusinessType = bind.BusinessType
|
||||
pkg.BusinessTypeName = appBusinessTypeName(bind.BusinessType)
|
||||
pkg.EffectiveStatus = bind.EffectiveStatus
|
||||
pkg.EffectiveTime = bind.EffectiveTime
|
||||
pkg.ExpireTime = bind.ExpectedExpireTime
|
||||
pkg.ValidText = appPackageValidText(bind.EffectiveTime, bind.ExpectedExpireTime)
|
||||
pkg.EffectText = appPackageEffectText(bind.EffectiveStatus)
|
||||
}
|
||||
|
||||
func (s *sAppCard) visiblePackageModel(ctx context.Context, user *entity.SysUser) *gdb.Model {
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
bindPkgTable := dao.TSimCardBindPkg.Table()
|
||||
packageTable := dao.TSimFlowPackageInfo.Table()
|
||||
categoryTable := dao.TSimPackageCategory.Table()
|
||||
terminalBindTable := dao.TSimTerminal.Table()
|
||||
|
||||
mod := dao.TSimCardBindPkg.Ctx(ctx).
|
||||
FieldsPrefix(bindPkgTable, entity.TSimCardBindPkg{}).
|
||||
Fields(cardTable+"."+dao.TSimCardInfo.Columns().Id+" as cardId").
|
||||
Fields(cardTable+"."+dao.TSimCardInfo.Columns().DeptId+" as cardDeptId").
|
||||
Fields(cardTable+"."+dao.TSimCardInfo.Columns().Remark+" as cardRemark").
|
||||
Fields(packageTable+"."+dao.TSimFlowPackageInfo.Columns().PackageName+" as packageName").
|
||||
Fields(packageTable+"."+dao.TSimFlowPackageInfo.Columns().CategoryCode+" as categoryCode").
|
||||
Fields(packageTable+"."+dao.TSimFlowPackageInfo.Columns().FlowMb+" as flowMb").
|
||||
Fields(categoryTable+"."+dao.TSimPackageCategory.Columns().CategoryName+" as categoryName").
|
||||
LeftJoin(cardTable, cardTable+"."+dao.TSimCardInfo.Columns().Iccid+"="+bindPkgTable+"."+dao.TSimCardBindPkg.Columns().Iccid+" AND "+cardTable+"."+dao.TSimCardInfo.Columns().TenantId+"="+bindPkgTable+"."+dao.TSimCardBindPkg.Columns().TenantId).
|
||||
LeftJoin(packageTable, packageTable+"."+dao.TSimFlowPackageInfo.Columns().PackageCode+"="+bindPkgTable+"."+dao.TSimCardBindPkg.Columns().PackageCode+" AND "+packageTable+"."+dao.TSimFlowPackageInfo.Columns().TenantId+"="+bindPkgTable+"."+dao.TSimCardBindPkg.Columns().TenantId).
|
||||
LeftJoin(categoryTable, categoryTable+"."+dao.TSimPackageCategory.Columns().CategoryCode+"="+packageTable+"."+dao.TSimFlowPackageInfo.Columns().CategoryCode+" AND "+categoryTable+"."+dao.TSimPackageCategory.Columns().TenantId+"="+packageTable+"."+dao.TSimFlowPackageInfo.Columns().TenantId)
|
||||
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
return mod.
|
||||
Fields(terminalBindTable+"."+dao.TSimTerminal.Columns().TerminalName+" as terminalName").
|
||||
InnerJoin(terminalBindTable, terminalBindTable+"."+dao.TSimTerminal.Columns().Iccid+"="+cardTable+"."+dao.TSimCardInfo.Columns().Iccid+" AND "+terminalBindTable+"."+dao.TSimTerminal.Columns().TenantId+"="+cardTable+"."+dao.TSimCardInfo.Columns().TenantId).
|
||||
Where(terminalBindTable+"."+dao.TSimTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(terminalBindTable+"."+dao.TSimTerminal.Columns().TerminalId, user.UserId).
|
||||
Where(bindPkgTable+"."+dao.TSimCardBindPkg.Columns().TenantId, user.TenantId)
|
||||
}
|
||||
mod = mod.Where(bindPkgTable+"."+dao.TSimCardBindPkg.Columns().TenantId, user.TenantId)
|
||||
return appApplyEnterpriseDataScope(
|
||||
ctx,
|
||||
user,
|
||||
mod,
|
||||
cardTable+"."+dao.TSimCardInfo.Columns().DeptId,
|
||||
cardTable+"."+dao.TSimCardInfo.Columns().CreatedBy,
|
||||
func(legacyMod *gdb.Model) *gdb.Model {
|
||||
return appLegacyEnterpriseDeptAndSubScope(ctx, user, legacyMod, cardTable+"."+dao.TSimCardInfo.Columns().DeptId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sAppCard) applyPackageFilters(mod *gdb.Model, param *model.AppPackageListParam) *gdb.Model {
|
||||
if strings.TrimSpace(param.Keyword) != "" {
|
||||
keyword := "%" + strings.TrimSpace(param.Keyword) + "%"
|
||||
mod = mod.Where(
|
||||
"("+dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().Iccid+" LIKE ? OR "+
|
||||
dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().Msisdn+" LIKE ? OR "+
|
||||
dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Imsi+" LIKE ? OR "+
|
||||
dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Imei+" LIKE ? OR "+
|
||||
dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().PackageCode+" LIKE ? OR "+
|
||||
dao.TSimFlowPackageInfo.Table()+"."+dao.TSimFlowPackageInfo.Columns().PackageName+" LIKE ? OR "+
|
||||
dao.TSimPackageCategory.Table()+"."+dao.TSimPackageCategory.Columns().CategoryName+" LIKE ? OR "+
|
||||
dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().Remark+" LIKE ?)",
|
||||
keyword, keyword, keyword, keyword, keyword, keyword, keyword, keyword,
|
||||
)
|
||||
}
|
||||
if strings.TrimSpace(param.CardValue) != "" {
|
||||
value := strings.TrimSpace(param.CardValue)
|
||||
mod = mod.Where(
|
||||
"("+dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().Iccid+" = ? OR "+
|
||||
dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().Msisdn+" = ? OR "+
|
||||
dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Imsi+" = ? OR "+
|
||||
dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Imei+" = ?)",
|
||||
value, value, value, value,
|
||||
)
|
||||
}
|
||||
if strings.TrimSpace(param.SubscriptionNo) != "" {
|
||||
mod = mod.Where(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().SubscriptionNo, strings.TrimSpace(param.SubscriptionNo))
|
||||
}
|
||||
if strings.TrimSpace(param.PackageCode) != "" {
|
||||
mod = mod.Where(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().PackageCode, strings.TrimSpace(param.PackageCode))
|
||||
}
|
||||
if strings.TrimSpace(param.CategoryCode) != "" {
|
||||
mod = mod.Where(dao.TSimFlowPackageInfo.Table()+"."+dao.TSimFlowPackageInfo.Columns().CategoryCode, strings.TrimSpace(param.CategoryCode))
|
||||
}
|
||||
switch strings.TrimSpace(param.Status) {
|
||||
case "active":
|
||||
mod = mod.Where(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().EffectiveStatus, 1)
|
||||
case "expiring":
|
||||
mod = mod.Where(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().ExpectedExpireTime+" IS NOT NULL").
|
||||
Where(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().ExpectedExpireTime+" >= ?", gtime.Now()).
|
||||
Where(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().ExpectedExpireTime+" <= ?", gtime.Now().AddDate(0, 0, 30))
|
||||
case "expired":
|
||||
mod = mod.Where(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().EffectiveStatus, 2)
|
||||
case "pending":
|
||||
mod = mod.Where(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().EffectiveStatus, 0)
|
||||
case "addon":
|
||||
mod = mod.WhereIn(dao.TSimCardBindPkg.Table()+"."+dao.TSimCardBindPkg.Columns().PackageType, []int{103, 104})
|
||||
}
|
||||
return mod
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const appCardListMaxPageSize = 100
|
||||
|
||||
// 处理首页免登录公共查卡,只返回卡基础信息,不开放绑卡、重启等业务能力。
|
||||
func (s *sAppCard) PublicQuery(ctx context.Context, param *model.AppCardQueryParam) (res *model.AppCardModel, err error) {
|
||||
card, err := s.findPublicCard(ctx, param.TenantId, param.CardValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if card == nil {
|
||||
return s.publicQueryUpstream(ctx, param.CardValue)
|
||||
}
|
||||
return s.publicCardRowToModel(ctx, card, param.CardValue), nil
|
||||
}
|
||||
|
||||
// 查询当前登录用户可见卡片。终端用户走 t_sim_terminal 关联表,
|
||||
// 企业用户沿用管理平台数据权限查询持卡列表。
|
||||
func (s *sAppCard) List(ctx context.Context, param *model.AppCardListParam) (items []*model.AppCardModel, total int, err error) {
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
mod := s.visibleCardModel(ctx, user)
|
||||
mod = s.applyCardFilters(mod, param)
|
||||
if param.Page <= 0 {
|
||||
param.Page = 1
|
||||
}
|
||||
if param.PageSize <= 0 {
|
||||
param.PageSize = 10
|
||||
}
|
||||
if param.PageSize > appCardListMaxPageSize {
|
||||
param.PageSize = appCardListMaxPageSize
|
||||
}
|
||||
|
||||
var rows []*appCardQueryRow
|
||||
includeTotal := param.IncludeTotal == nil || *param.IncludeTotal
|
||||
queryPageSize := param.PageSize
|
||||
if !includeTotal {
|
||||
queryPageSize++
|
||||
}
|
||||
query := mod.OrderDesc(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Id).
|
||||
Page(param.Page, queryPageSize)
|
||||
if includeTotal {
|
||||
err = query.ScanAndCount(&rows, &total, false)
|
||||
} else {
|
||||
err = query.Scan(&rows)
|
||||
if len(rows) > param.PageSize {
|
||||
rows = rows[:param.PageSize]
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "查询卡列表失败,请稍后重试")
|
||||
}
|
||||
|
||||
if err = s.attachCardChannelInfoRows(ctx, rows); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
ownerType := s.ownerType(ctx, user)
|
||||
items = make([]*model.AppCardModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, s.cardRowToModel(ctx, row, user, ownerType, true))
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
// 登录后先查用户可见卡;查不到时回退公共查卡,保证登录后仍能查看公共卡详情。
|
||||
func (s *sAppCard) Detail(ctx context.Context, param *model.AppCardDetailParam) (res *model.AppCardModel, err error) {
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
card, err := s.findVisibleCard(ctx, user, param.CardValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if card != nil {
|
||||
model := s.attachYesterdayDeptFlow(ctx, s.cardRowToModel(ctx, card, user, s.ownerType(ctx, user), true))
|
||||
return s.attachAppCardRealtimeSession(ctx, card, model), nil
|
||||
}
|
||||
|
||||
card, err = s.findPublicCard(ctx, user.TenantId, param.CardValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if card == nil {
|
||||
return s.publicQueryUpstream(ctx, param.CardValue)
|
||||
}
|
||||
if err = s.attachCardChannelInfo(ctx, card); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.publicCardRowToModel(ctx, card, param.CardValue), nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) publicQueryUpstream(ctx context.Context, cardValue string) (*model.AppCardModel, error) {
|
||||
provider, err := newAppSimUpstreamProvider(ctx)
|
||||
if err != nil {
|
||||
return nil, appErrUpstreamUnavailable(err)
|
||||
}
|
||||
info, err := provider.QuerySimBasicInfo(ctx, appSimBasicInfoInputFromCardValue(cardValue))
|
||||
if err != nil {
|
||||
return nil, appErrUpstreamUnavailable(err)
|
||||
}
|
||||
card := s.upstreamPublicCardToModel(info, cardValue)
|
||||
if card == nil {
|
||||
return nil, gerror.New("卡信息不存在,请确认查询号码是否正确")
|
||||
}
|
||||
return card, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) attachYesterdayDeptFlow(ctx context.Context, card *model.AppCardModel) *model.AppCardModel {
|
||||
if card == nil || card.DeptId <= 0 || strings.TrimSpace(card.TenantId) == "" {
|
||||
return card
|
||||
}
|
||||
statDate := gtime.Now().AddDate(0, 0, -1).Format("Y-m-d")
|
||||
var daily *entity.TDeptFlowDaily
|
||||
_ = dao.TDeptFlowDaily.Ctx(ctx).
|
||||
Where(dao.TDeptFlowDaily.Columns().TenantId, card.TenantId).
|
||||
Where(dao.TDeptFlowDaily.Columns().DeptId, card.DeptId).
|
||||
Where(dao.TDeptFlowDaily.Columns().StatDate, statDate).
|
||||
Limit(1).
|
||||
Scan(&daily)
|
||||
if daily == nil || daily.Id == 0 {
|
||||
return card
|
||||
}
|
||||
card.YesterdayFlowStatDate = statDate
|
||||
if daily.StatDate != nil && !daily.StatDate.IsZero() {
|
||||
card.YesterdayFlowStatDate = daily.StatDate.Format("Y-m-d")
|
||||
}
|
||||
card.YesterdayDeptUsedFlowMb = daily.TotalUsedFlowMb
|
||||
card.YesterdayDeptSms = daily.TotalSms
|
||||
card.YesterdayDeptVoiceMin = daily.TotalVoiceMin
|
||||
return card
|
||||
}
|
||||
|
||||
func (s *sAppCard) findPublicCard(ctx context.Context, tenantId string, cardValue string) (*appCardQueryRow, error) {
|
||||
mod := s.cardQueryBase(ctx, false)
|
||||
tenantId = strings.TrimSpace(tenantId)
|
||||
if tenantId == "" {
|
||||
tenantId = consts.DefaultSystemTenantCode
|
||||
}
|
||||
mod = mod.Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().TenantId, tenantId)
|
||||
var row *appCardQueryRow
|
||||
err := mod.
|
||||
Where(appCardValueCondition(), appCardValueArgs(cardValue)...).
|
||||
Limit(1).
|
||||
Scan(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.attachCardChannelInfo(ctx, row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) findOpenCard(ctx context.Context, tenantId string, cardValue string) (*appCardQueryRow, error) {
|
||||
if strings.TrimSpace(cardValue) == "" {
|
||||
return nil, gerror.New("请输入ICCID、MSISDN、IMSI或IMEI")
|
||||
}
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
mod := s.cardQueryBase(ctx, false)
|
||||
if tenantId = strings.TrimSpace(tenantId); tenantId != "" {
|
||||
mod = mod.Where(cardTable+"."+dao.TSimCardInfo.Columns().TenantId, tenantId)
|
||||
}
|
||||
var row *appCardQueryRow
|
||||
err := mod.
|
||||
Where(appCardValueCondition(), appCardValueArgs(cardValue)...).
|
||||
OrderDesc(cardTable + "." + dao.TSimCardInfo.Columns().Id).
|
||||
Limit(1).
|
||||
Scan(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.attachCardChannelInfo(ctx, row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) findCardByValue(ctx context.Context, tenantId string, cardValue string) (*entity.TSimCardInfo, error) {
|
||||
if strings.TrimSpace(cardValue) == "" {
|
||||
return nil, gerror.New("请输入ICCID、MSISDN、IMSI或IMEI")
|
||||
}
|
||||
var card *entity.TSimCardInfo
|
||||
err := dao.TSimCardInfo.Ctx(ctx).
|
||||
Where(dao.TSimCardInfo.Columns().TenantId, tenantId).
|
||||
Where(appCardValueCondition(), appCardValueArgs(cardValue)...).
|
||||
Limit(1).
|
||||
Scan(&card)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return card, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) findVisibleCard(ctx context.Context, user *entity.SysUser, cardValue string) (*appCardQueryRow, error) {
|
||||
var row *appCardQueryRow
|
||||
err := s.visibleCardModel(ctx, user).
|
||||
Where(appCardValueCondition(), appCardValueArgs(cardValue)...).
|
||||
Limit(1).
|
||||
Scan(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.attachCardChannelInfo(ctx, row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
// findDiagnosisCard 查询诊断卡片。企业用户诊断只隔离租户,不套用部门数据范围;
|
||||
// 终端用户仍沿用已绑定卡范围,避免终端账号读取其他用户的卡片。
|
||||
func (s *sAppCard) findDiagnosisCard(ctx context.Context, user *entity.SysUser, cardValue string) (*appCardQueryRow, error) {
|
||||
if user == nil || strings.TrimSpace(user.TenantId) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
return s.findVisibleCard(ctx, user, cardValue)
|
||||
}
|
||||
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
var row *appCardQueryRow
|
||||
err := s.cardQueryBase(ctx, false).
|
||||
Where(cardTable+"."+dao.TSimCardInfo.Columns().TenantId, strings.TrimSpace(user.TenantId)).
|
||||
Where(appCardValueCondition(), appCardValueArgs(cardValue)...).
|
||||
OrderDesc(cardTable + "." + dao.TSimCardInfo.Columns().Id).
|
||||
Limit(1).
|
||||
Scan(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = s.attachCardChannelInfo(ctx, row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) visibleCardModel(ctx context.Context, user *entity.SysUser) *gdb.Model {
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
bindTable := dao.TSimTerminal.Table()
|
||||
packageAlias := "terminal_package"
|
||||
categoryAlias := "terminal_category"
|
||||
packageJoinTable := dao.TSimFlowPackageInfo.Table() + " " + packageAlias
|
||||
categoryJoinTable := dao.TSimPackageCategory.Table() + " " + categoryAlias
|
||||
return s.cardQueryBase(ctx, false).
|
||||
InnerJoin(bindTable, bindTable+"."+dao.TSimTerminal.Columns().Iccid+"="+cardTable+"."+dao.TSimCardInfo.Columns().Iccid).
|
||||
LeftJoin(packageJoinTable, packageAlias+"."+dao.TSimFlowPackageInfo.Columns().PackageCode+"="+bindTable+"."+appSimTerminalCurrentPackageCodeColumn+" AND "+packageAlias+"."+dao.TSimFlowPackageInfo.Columns().TenantId+"="+bindTable+"."+dao.TSimTerminal.Columns().TenantId).
|
||||
LeftJoin(categoryJoinTable, categoryAlias+"."+dao.TSimPackageCategory.Columns().CategoryCode+"="+packageAlias+"."+dao.TSimFlowPackageInfo.Columns().CategoryCode+" AND "+categoryAlias+"."+dao.TSimPackageCategory.Columns().TenantId+"="+packageAlias+"."+dao.TSimFlowPackageInfo.Columns().TenantId).
|
||||
Where(cardTable+"."+dao.TSimCardInfo.Columns().TenantId, user.TenantId).
|
||||
Where(bindTable+"."+dao.TSimTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(bindTable+"."+dao.TSimTerminal.Columns().TerminalId, user.UserId).
|
||||
Fields(bindTable + "." + dao.TSimTerminal.Columns().Id + " as terminal_bind_id").
|
||||
Fields(bindTable + "." + dao.TSimTerminal.Columns().TerminalId + " as terminal_user_id").
|
||||
Fields(bindTable + "." + dao.TSimTerminal.Columns().TerminalName + " as terminalName").
|
||||
Fields(bindTable + "." + appSimTerminalDeptIdColumn + " as terminal_dept_id").
|
||||
Fields(bindTable + "." + appSimTerminalCurrentPackageCodeColumn + " as terminal_current_package_code").
|
||||
Fields(packageAlias + "." + dao.TSimFlowPackageInfo.Columns().PackageName + " as terminal_package_name").
|
||||
Fields(packageAlias + "." + dao.TSimFlowPackageInfo.Columns().PackageType + " as terminal_package_type").
|
||||
Fields(packageAlias + "." + dao.TSimFlowPackageInfo.Columns().CategoryCode + " as terminal_category_code").
|
||||
Fields(categoryAlias + "." + dao.TSimPackageCategory.Columns().CategoryName + " as terminal_category_name")
|
||||
}
|
||||
return s.enterpriseVisibleCardModel(ctx, user)
|
||||
}
|
||||
|
||||
func (s *sAppCard) enterpriseVisibleCardModel(ctx context.Context, user *entity.SysUser) *gdb.Model {
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
if user == nil || user.DeptId <= 0 {
|
||||
return s.cardQueryBase(ctx, false).Where("1=0")
|
||||
}
|
||||
mod := s.cardQueryBase(ctx, false).
|
||||
Where(cardTable+"."+dao.TSimCardInfo.Columns().TenantId, user.TenantId)
|
||||
return appApplyEnterpriseDataScope(
|
||||
ctx,
|
||||
user,
|
||||
mod,
|
||||
cardTable+"."+dao.TSimCardInfo.Columns().DeptId,
|
||||
cardTable+"."+dao.TSimCardInfo.Columns().CreatedBy,
|
||||
func(legacyMod *gdb.Model) *gdb.Model {
|
||||
return appLegacyEnterpriseDeptAndSubScope(ctx, user, legacyMod, cardTable+"."+dao.TSimCardInfo.Columns().DeptId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 统一卡查询基础 JOIN,其中 current_package_code 对应套餐表 package_code。
|
||||
// 卡片展示和公共当前套餐详情都依赖这个关联关系。
|
||||
func (s *sAppCard) cardQueryBase(ctx context.Context, useAuth bool) *gdb.Model {
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
packageTable := dao.TSimFlowPackageInfo.Table()
|
||||
categoryTable := dao.TSimPackageCategory.Table()
|
||||
ownerDeptAlias := "owner_dept"
|
||||
ownerDeptJoinTable := dao.SysDept.Table() + " " + ownerDeptAlias
|
||||
|
||||
mod := dao.TSimCardInfo.Ctx(ctx).
|
||||
FieldsPrefix(cardTable, entity.TSimCardInfo{}).
|
||||
Fields(packageTable+"."+dao.TSimFlowPackageInfo.Columns().PackageName+" as packageName").
|
||||
Fields(packageTable+"."+dao.TSimFlowPackageInfo.Columns().PackageType+" as packageType").
|
||||
Fields(packageTable+"."+dao.TSimFlowPackageInfo.Columns().CategoryCode+" as categoryCode").
|
||||
Fields(categoryTable+"."+dao.TSimPackageCategory.Columns().CategoryName+" as categoryName").
|
||||
Fields(ownerDeptAlias+"."+dao.SysDept.Columns().DeptName+" as ownerDeptName").
|
||||
Fields(ownerDeptAlias+"."+dao.SysDept.Columns().DeptKey+" as ownerDeptKey").
|
||||
LeftJoin(packageTable, packageTable+"."+dao.TSimFlowPackageInfo.Columns().PackageCode+"="+cardTable+"."+dao.TSimCardInfo.Columns().CurrentPackageCode+" AND "+packageTable+"."+dao.TSimFlowPackageInfo.Columns().TenantId+"="+cardTable+"."+dao.TSimCardInfo.Columns().TenantId).
|
||||
LeftJoin(categoryTable, categoryTable+"."+dao.TSimPackageCategory.Columns().CategoryCode+"="+packageTable+"."+dao.TSimFlowPackageInfo.Columns().CategoryCode+" AND "+categoryTable+"."+dao.TSimPackageCategory.Columns().TenantId+"="+packageTable+"."+dao.TSimFlowPackageInfo.Columns().TenantId).
|
||||
LeftJoin(ownerDeptJoinTable, ownerDeptAlias+"."+dao.SysDept.Columns().DeptId+"="+cardTable+"."+dao.TSimCardInfo.Columns().DeptId+" AND "+ownerDeptAlias+"."+dao.SysDept.Columns().TenantId+"="+cardTable+"."+dao.TSimCardInfo.Columns().TenantId)
|
||||
if useAuth {
|
||||
mod = handler.Model(mod, &handler.Option{FilterAuth: true, FilterTenant: true})
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
func (s *sAppCard) applyCardFilters(mod *gdb.Model, param *model.AppCardListParam) *gdb.Model {
|
||||
if strings.TrimSpace(param.CardValue) != "" {
|
||||
mod = mod.Where(appCardValueCondition(), appCardValueArgs(param.CardValue)...)
|
||||
}
|
||||
if strings.TrimSpace(param.CategoryCode) != "" {
|
||||
mod = mod.Where(dao.TSimFlowPackageInfo.Table()+"."+dao.TSimFlowPackageInfo.Columns().CategoryCode, strings.TrimSpace(param.CategoryCode))
|
||||
}
|
||||
switch strings.TrimSpace(param.Status) {
|
||||
case "normal":
|
||||
mod = mod.Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().BossCardStatus, 2).
|
||||
WhereNot(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().PackageStatus, 3)
|
||||
case "low-flow":
|
||||
mod = mod.Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().PackageTotalFlowMb+">?", 0).
|
||||
Where("("+dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().PackageTotalFlowMb+"-"+dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().UsedFlowMb+")/"+dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().PackageTotalFlowMb+" <= ?", 0.2)
|
||||
case "expiring":
|
||||
mod = mod.Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().FlowPkgEndAt+" IS NOT NULL").
|
||||
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().FlowPkgEndAt+" >= ?", gtime.Now()).
|
||||
Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().FlowPkgEndAt+" <= ?", gtime.Now().AddDate(0, 0, 30))
|
||||
case "error":
|
||||
mod = mod.Where(
|
||||
"("+dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().PackageStatus+" = ? OR "+dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().BossCardStatus+" IN(?, ?, ?, ?) OR "+dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().BossCardStatus+" NOT IN(?, ?, ?, ?, ?, ?, ?, ?))",
|
||||
consts.PackageStatusExpired,
|
||||
0,
|
||||
consts.V3ApiSimBossCardStatusStopped,
|
||||
8,
|
||||
9,
|
||||
0,
|
||||
consts.V3ApiSimBossCardStatusPendingActivate,
|
||||
consts.V3ApiSimBossCardStatusActivated,
|
||||
consts.V3ApiSimBossCardStatusStopped,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
)
|
||||
}
|
||||
return mod
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
internalai "git.5iot.com/iotlcm-public/iotlcm-app/internal/ai"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
const (
|
||||
cardRecognitionCodeRecognized = "CARD_RECOGNIZED"
|
||||
cardRecognitionCodeAmbiguous = "CARD_AMBIGUOUS"
|
||||
cardRecognitionCodeUnreadable = "CARD_UNREADABLE"
|
||||
cardRecognitionCodeTimeout = "RECOGNITION_TIMEOUT"
|
||||
cardRecognitionCodeFailed = "RECOGNITION_FAILED"
|
||||
cardRecognitionCodeDisabled = "RECOGNITION_DISABLED"
|
||||
cardRecognitionCodeRateLimited = "RECOGNITION_RATE_LIMITED"
|
||||
|
||||
cardRecognitionDefaultBaseURL = "https://xueyangai.com/v1"
|
||||
cardRecognitionDefaultModel = "gpt-4o"
|
||||
cardRecognitionDefaultMaxImageSize = int64(8 * 1024 * 1024)
|
||||
cardRecognitionDefaultMaxTokens = 256
|
||||
cardRecognitionDefaultRateCount = 20
|
||||
cardRecognitionDefaultRatePeriod = time.Minute
|
||||
cardRecognitionRateLimitRedisKey = "iotlcm:app:card-recognition:rate:"
|
||||
)
|
||||
|
||||
var cardRecognitionCandidatePattern = regexp.MustCompile(`(^|[^0-9])(89([-[:space:]]*[0-9]){17,18})($|[^0-9])`)
|
||||
|
||||
type cardRecognitionConfig struct {
|
||||
Enabled bool
|
||||
Provider string
|
||||
APIKey string
|
||||
BaseURL string
|
||||
Endpoint string
|
||||
Model string
|
||||
Timeout time.Duration
|
||||
MaxImageBytes int64
|
||||
MaxTokens int
|
||||
RateLimitEnabled bool
|
||||
RateLimitCount int
|
||||
RateLimitPeriod time.Duration
|
||||
}
|
||||
|
||||
type cardRecognitionModelOutput struct {
|
||||
Code string `json:"code"`
|
||||
CardType string `json:"cardType"`
|
||||
Message string `json:"message"`
|
||||
ICCID string `json:"iccid"`
|
||||
CardValue string `json:"cardValue"`
|
||||
Candidates []string `json:"candidates"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
type cardRecognitionParsedResult struct {
|
||||
Code string
|
||||
Candidates []string
|
||||
Confidence float64
|
||||
}
|
||||
|
||||
func (s *sAppCard) RecognizeCard(ctx context.Context) (*model.AppCardRecognitionModel, error) {
|
||||
result := &model.AppCardRecognitionModel{
|
||||
Code: cardRecognitionCodeFailed,
|
||||
CardType: "iccid",
|
||||
Candidates: []string{},
|
||||
}
|
||||
|
||||
req := g.RequestFromCtx(ctx)
|
||||
if req == nil {
|
||||
result.Message = "无法读取上传请求,请稍后重试"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
file := req.GetUploadFile("file")
|
||||
if file == nil {
|
||||
result.Code = cardRecognitionCodeUnreadable
|
||||
result.Message = "请上传一张包含 ICCID 的图片"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
config := loadCardRecognitionConfig(ctx)
|
||||
if !config.Enabled || config.APIKey == "" {
|
||||
result.Code = cardRecognitionCodeDisabled
|
||||
result.Message = "图片识别服务暂未配置"
|
||||
return result, nil
|
||||
}
|
||||
allowed, err := checkCardRecognitionRateLimit(ctx, config, req)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "card recognition rate limit check failed: %v", err)
|
||||
} else if !allowed {
|
||||
result.Code = cardRecognitionCodeRateLimited
|
||||
result.Message = "图片识别请求过于频繁,请稍后再试"
|
||||
return result, nil
|
||||
}
|
||||
content, contentType, err := readCardRecognitionImage(file, config.MaxImageBytes)
|
||||
if err != nil {
|
||||
result.Code = cardRecognitionCodeUnreadable
|
||||
result.Message = err.Error()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rawContent, err := requestCardRecognitionModel(ctx, config, content, contentType)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "card recognition model request failed: %v", err)
|
||||
if isCardRecognitionTimeoutError(ctx, err) {
|
||||
result.Code = cardRecognitionCodeTimeout
|
||||
result.Message = "图片识别超时,请稍后重试"
|
||||
} else {
|
||||
result.Code = cardRecognitionCodeFailed
|
||||
result.Message = "图片识别服务暂时不可用,请稍后重试"
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
parsed := parseCardRecognitionResult(rawContent)
|
||||
result.Confidence = parsed.Confidence
|
||||
result.Candidates = parsed.Candidates
|
||||
switch {
|
||||
case parsed.Code == cardRecognitionCodeUnreadable && len(parsed.Candidates) == 0:
|
||||
result.Code = cardRecognitionCodeUnreadable
|
||||
result.Message = "未能从图片中识别出清晰的 ICCID,请重新拍摄"
|
||||
return result, nil
|
||||
case parsed.Code == cardRecognitionCodeAmbiguous && len(parsed.Candidates) > 1:
|
||||
result.Code = cardRecognitionCodeAmbiguous
|
||||
result.Message = "图片中存在多个可能的 ICCID,请重新拍摄单张卡片"
|
||||
return result, nil
|
||||
}
|
||||
switch len(parsed.Candidates) {
|
||||
case 0:
|
||||
result.Code = cardRecognitionCodeUnreadable
|
||||
result.Message = "未能从图片中识别出清晰的 ICCID,请重新拍摄"
|
||||
return result, nil
|
||||
case 1:
|
||||
result.CardValue = parsed.Candidates[0]
|
||||
result.Code = cardRecognitionCodeRecognized
|
||||
result.Message = "已识别出 ICCID"
|
||||
return result, nil
|
||||
default:
|
||||
result.Code = cardRecognitionCodeAmbiguous
|
||||
result.Message = "图片中存在多个可能的 ICCID,请重新拍摄单张卡片"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func loadCardRecognitionConfig(ctx context.Context) cardRecognitionConfig {
|
||||
timeout := 25 * time.Second
|
||||
if value := strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.timeout", "25s").String()); value != "" {
|
||||
if parsed, err := time.ParseDuration(value); err == nil && parsed > 0 {
|
||||
timeout = parsed
|
||||
}
|
||||
}
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_TIMEOUT"); value != "" {
|
||||
if parsed, err := time.ParseDuration(value); err == nil && parsed > 0 {
|
||||
timeout = parsed
|
||||
}
|
||||
}
|
||||
|
||||
maxImageBytes := g.Cfg().MustGet(ctx, "appCardRecognition.maxImageBytes", cardRecognitionDefaultMaxImageSize).Int64()
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_MAX_IMAGE_BYTES"); value != "" {
|
||||
if parsed, err := strconv.ParseInt(value, 10, 64); err == nil && parsed > 0 {
|
||||
maxImageBytes = parsed
|
||||
}
|
||||
}
|
||||
if maxImageBytes <= 0 {
|
||||
maxImageBytes = cardRecognitionDefaultMaxImageSize
|
||||
}
|
||||
|
||||
maxTokens := g.Cfg().MustGet(ctx, "appCardRecognition.maxTokens", cardRecognitionDefaultMaxTokens).Int()
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_MAX_TOKENS"); value != "" {
|
||||
if parsed, err := strconv.Atoi(value); err == nil && parsed > 0 {
|
||||
maxTokens = parsed
|
||||
}
|
||||
}
|
||||
if maxTokens <= 0 {
|
||||
maxTokens = cardRecognitionDefaultMaxTokens
|
||||
}
|
||||
|
||||
rateLimitPeriod := cardRecognitionDefaultRatePeriod
|
||||
if value := strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.rateLimit.period", rateLimitPeriod.String()).String()); value != "" {
|
||||
if parsed, err := time.ParseDuration(value); err == nil && parsed > 0 {
|
||||
rateLimitPeriod = parsed
|
||||
}
|
||||
}
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_RATE_LIMIT_PERIOD"); value != "" {
|
||||
if parsed, err := time.ParseDuration(value); err == nil && parsed > 0 {
|
||||
rateLimitPeriod = parsed
|
||||
}
|
||||
}
|
||||
|
||||
rateLimitCount := g.Cfg().MustGet(ctx, "appCardRecognition.rateLimit.count", cardRecognitionDefaultRateCount).Int()
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_RATE_LIMIT_COUNT"); value != "" {
|
||||
if parsed, err := strconv.Atoi(value); err == nil && parsed > 0 {
|
||||
rateLimitCount = parsed
|
||||
}
|
||||
}
|
||||
if rateLimitCount <= 0 {
|
||||
rateLimitCount = cardRecognitionDefaultRateCount
|
||||
}
|
||||
baseURL := appEnvFirst("APP_CARD_RECOGNITION_BASE_URL")
|
||||
endpoint := appEnvFirst("APP_CARD_RECOGNITION_ENDPOINT")
|
||||
if baseURL == "" && endpoint == "" {
|
||||
baseURL = strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.baseUrl", "").String())
|
||||
endpoint = strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.endpoint", "").String())
|
||||
}
|
||||
if baseURL == "" && endpoint == "" {
|
||||
baseURL = cardRecognitionDefaultBaseURL
|
||||
}
|
||||
modelName := strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.model", cardRecognitionDefaultModel).String())
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_MODEL"); value != "" {
|
||||
modelName = value
|
||||
}
|
||||
if modelName == "" {
|
||||
modelName = cardRecognitionDefaultModel
|
||||
}
|
||||
apiKey := strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.apiKey", "").String())
|
||||
if value := appEnvFirst("APP_CARD_RECOGNITION_API_KEY", "XUEYANGAI_API_KEY"); value != "" {
|
||||
apiKey = value
|
||||
}
|
||||
|
||||
provider := appEnvFirst("APP_CARD_RECOGNITION_PROVIDER")
|
||||
if provider == "" {
|
||||
provider = strings.TrimSpace(g.Cfg().MustGet(ctx, "appCardRecognition.provider", internalai.ProviderOpenAICompatible).String())
|
||||
}
|
||||
|
||||
return cardRecognitionConfig{
|
||||
Enabled: appEnvBool("APP_CARD_RECOGNITION_ENABLED", g.Cfg().MustGet(ctx, "appCardRecognition.enabled", false).Bool()),
|
||||
Provider: provider,
|
||||
APIKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
Endpoint: endpoint,
|
||||
Model: modelName,
|
||||
Timeout: timeout,
|
||||
MaxImageBytes: maxImageBytes,
|
||||
MaxTokens: maxTokens,
|
||||
RateLimitEnabled: appEnvBool("APP_CARD_RECOGNITION_RATE_LIMIT_ENABLED", g.Cfg().MustGet(ctx, "appCardRecognition.rateLimit.enabled", true).Bool()),
|
||||
RateLimitCount: rateLimitCount,
|
||||
RateLimitPeriod: rateLimitPeriod,
|
||||
}
|
||||
}
|
||||
|
||||
func readCardRecognitionImage(file *ghttp.UploadFile, maxImageBytes int64) ([]byte, string, error) {
|
||||
if file == nil || file.FileHeader == nil {
|
||||
return nil, "", gerror.New("上传图片无效")
|
||||
}
|
||||
if maxImageBytes <= 0 {
|
||||
maxImageBytes = cardRecognitionDefaultMaxImageSize
|
||||
}
|
||||
if file.Size > maxImageBytes {
|
||||
return nil, "", gerror.Newf("图片不能超过 %d MB", maxImageBytes/(1024*1024))
|
||||
}
|
||||
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, "", gerror.New("无法读取图片")
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
content, err := io.ReadAll(io.LimitReader(reader, maxImageBytes+1))
|
||||
if err != nil {
|
||||
return nil, "", gerror.New("无法读取图片")
|
||||
}
|
||||
if int64(len(content)) > maxImageBytes {
|
||||
return nil, "", gerror.Newf("图片不能超过 %d MB", maxImageBytes/(1024*1024))
|
||||
}
|
||||
|
||||
contentType := strings.ToLower(strings.TrimSpace(file.Header.Get("Content-Type")))
|
||||
if contentType == "" || contentType == "application/octet-stream" {
|
||||
contentType = http.DetectContentType(content)
|
||||
}
|
||||
switch contentType {
|
||||
case "image/jpeg", "image/png", "image/webp":
|
||||
default:
|
||||
return nil, "", gerror.New("仅支持 JPG、PNG 或 WEBP 图片")
|
||||
}
|
||||
return content, contentType, nil
|
||||
}
|
||||
|
||||
func checkCardRecognitionRateLimit(ctx context.Context, config cardRecognitionConfig, req *ghttp.Request) (bool, error) {
|
||||
if !config.RateLimitEnabled || config.RateLimitCount <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
periodSeconds := int(config.RateLimitPeriod.Seconds())
|
||||
if periodSeconds <= 0 {
|
||||
periodSeconds = int(cardRecognitionDefaultRatePeriod.Seconds())
|
||||
}
|
||||
clientIP := "unknown"
|
||||
if req != nil {
|
||||
if value := strings.TrimSpace(req.GetClientIp()); value != "" {
|
||||
clientIP = value
|
||||
}
|
||||
}
|
||||
key := cardRecognitionRateLimitRedisKey + cleanCardRecognitionRateLimitKey(clientIP)
|
||||
script := `
|
||||
local v = redis.call("INCR", KEYS[1])
|
||||
if v == 1 then
|
||||
redis.call("EXPIRE", KEYS[1], ARGV[1])
|
||||
end
|
||||
return v`
|
||||
res, err := g.Redis().Do(ctx, "EVAL", script, 1, key, periodSeconds)
|
||||
if err != nil {
|
||||
return true, gerror.Wrap(err, "检查图片识别限流失败")
|
||||
}
|
||||
return res.Int() <= config.RateLimitCount, nil
|
||||
}
|
||||
|
||||
func cleanCardRecognitionRateLimitKey(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}, value)
|
||||
}
|
||||
|
||||
func requestCardRecognitionModel(ctx context.Context, config cardRecognitionConfig, content []byte, contentType string) (string, error) {
|
||||
requestCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
chatModel, err := internalai.NewChatModel(requestCtx, internalai.ChatModelConfig{
|
||||
Provider: config.Provider,
|
||||
APIKey: config.APIKey,
|
||||
BaseURL: config.BaseURL,
|
||||
Endpoint: config.Endpoint,
|
||||
Model: config.Model,
|
||||
Timeout: config.Timeout,
|
||||
MaxTokens: config.MaxTokens,
|
||||
JSONOutput: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
imageBase64 := base64.StdEncoding.EncodeToString(content)
|
||||
response, err := chatModel.Generate(requestCtx, []*schema.Message{
|
||||
{
|
||||
Role: schema.User,
|
||||
UserInputMultiContent: []schema.MessageInputPart{
|
||||
{
|
||||
Type: schema.ChatMessagePartTypeText,
|
||||
Text: "请只识别图片中物联网卡的 ICCID。忽略 MSISDN、IMSI、IMEI、订单号和其他数字。请仅返回 JSON,不要返回 Markdown 或解释:{\"code\":\"CARD_RECOGNIZED|CARD_AMBIGUOUS|CARD_UNREADABLE\",\"cardType\":\"iccid\",\"iccid\":\"19或20位数字,没有则为空\",\"candidates\":[\"候选ICCID\"],\"confidence\":0到1之间的小数}。图片模糊、遮挡、反光或无法确定时,iccid 置空并返回 CARD_UNREADABLE;有多个卡号时返回 CARD_AMBIGUOUS,并把全部候选 ICCID 放入 candidates。",
|
||||
},
|
||||
{
|
||||
Type: schema.ChatMessagePartTypeImageURL,
|
||||
Image: &schema.MessageInputImage{
|
||||
MessagePartCommon: schema.MessagePartCommon{
|
||||
Base64Data: &imageBase64,
|
||||
MIMEType: contentType,
|
||||
},
|
||||
Detail: schema.ImageURLDetailHigh,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "Eino调用图片识别模型失败")
|
||||
}
|
||||
if response == nil || strings.TrimSpace(response.Content) == "" {
|
||||
return "", gerror.New("图片识别响应为空")
|
||||
}
|
||||
return response.Content, nil
|
||||
}
|
||||
|
||||
func parseCardRecognitionResult(raw string) cardRecognitionParsedResult {
|
||||
raw = trimCardRecognitionJSON(raw)
|
||||
|
||||
output := cardRecognitionModelOutput{}
|
||||
if json.Unmarshal([]byte(raw), &output) == nil {
|
||||
rawValues := append([]string{output.ICCID, output.CardValue}, output.Candidates...)
|
||||
candidates := normalizeCardRecognitionCandidates(rawValues)
|
||||
code := normalizeCardRecognitionCode(output.Code)
|
||||
if code == "" {
|
||||
switch len(candidates) {
|
||||
case 0:
|
||||
code = cardRecognitionCodeUnreadable
|
||||
case 1:
|
||||
code = cardRecognitionCodeRecognized
|
||||
default:
|
||||
code = cardRecognitionCodeAmbiguous
|
||||
}
|
||||
}
|
||||
return cardRecognitionParsedResult{
|
||||
Code: code,
|
||||
Candidates: candidates,
|
||||
Confidence: clampCardRecognitionConfidence(output.Confidence),
|
||||
}
|
||||
}
|
||||
candidates := normalizeCardRecognitionCandidates([]string{raw})
|
||||
code := cardRecognitionCodeUnreadable
|
||||
if len(candidates) == 1 {
|
||||
code = cardRecognitionCodeRecognized
|
||||
} else if len(candidates) > 1 {
|
||||
code = cardRecognitionCodeAmbiguous
|
||||
}
|
||||
return cardRecognitionParsedResult{
|
||||
Code: code,
|
||||
Candidates: candidates,
|
||||
Confidence: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCardRecognitionCandidates(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
for _, candidate := range extractCardRecognitionCandidates(value) {
|
||||
if _, ok := seen[candidate]; ok {
|
||||
continue
|
||||
}
|
||||
seen[candidate] = struct{}{}
|
||||
result = append(result, candidate)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func extractCardRecognitionCandidates(value string) []string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]string, 0, 1)
|
||||
if cleaned := cleanCardRecognitionCandidate(value); isValidCardRecognitionCandidate(cleaned) {
|
||||
result = append(result, cleaned)
|
||||
}
|
||||
for _, match := range cardRecognitionCandidatePattern.FindAllStringSubmatch(value, -1) {
|
||||
if len(match) < 3 {
|
||||
continue
|
||||
}
|
||||
if cleaned := cleanCardRecognitionCandidate(match[2]); isValidCardRecognitionCandidate(cleaned) {
|
||||
result = append(result, cleaned)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func cleanCardRecognitionCandidate(value string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r >= '0' && r <= '9' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func isValidCardRecognitionCandidate(value string) bool {
|
||||
return (len(value) == 19 || len(value) == 20) && strings.HasPrefix(value, "89")
|
||||
}
|
||||
|
||||
func clampCardRecognitionConfidence(value float64) float64 {
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
if value > 1 {
|
||||
return 1
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func trimCardRecognitionJSON(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
for _, prefix := range []string{"```json", "```JSON", "```"} {
|
||||
if strings.HasPrefix(raw, prefix) {
|
||||
raw = strings.TrimSpace(strings.TrimPrefix(raw, prefix))
|
||||
break
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(raw, "```") {
|
||||
raw = strings.TrimSpace(strings.TrimSuffix(raw, "```"))
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func normalizeCardRecognitionCode(code string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(code)) {
|
||||
case cardRecognitionCodeRecognized:
|
||||
return cardRecognitionCodeRecognized
|
||||
case cardRecognitionCodeAmbiguous:
|
||||
return cardRecognitionCodeAmbiguous
|
||||
case cardRecognitionCodeUnreadable:
|
||||
return cardRecognitionCodeUnreadable
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func isCardRecognitionTimeoutError(ctx context.Context, err error) bool {
|
||||
if ctx != nil && errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(strings.ToLower(err.Error()), "timeout")
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCardRecognitionLive(t *testing.T) {
|
||||
if os.Getenv("RUN_CARD_RECOGNITION_LIVE") != "1" {
|
||||
t.Skip("explicit opt-in required: calls configured model service")
|
||||
}
|
||||
ctx := context.Background()
|
||||
cfg := loadCardRecognitionConfig(ctx)
|
||||
if !cfg.Enabled || cfg.APIKey == "" {
|
||||
t.Fatal("disabled or missing API key")
|
||||
}
|
||||
img := image.NewRGBA(image.Rect(0, 0, 256, 128))
|
||||
draw.Draw(img, img.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("provider=%s model=%s timeout=%s", cfg.Provider, cfg.Model, cfg.Timeout)
|
||||
raw, err := requestCardRecognitionModel(ctx, cfg, buf.Bytes(), "image/png")
|
||||
if err != nil {
|
||||
t.Fatal(strings.ReplaceAll(err.Error(), cfg.APIKey, "[REDACTED]"))
|
||||
}
|
||||
result := parseCardRecognitionResult(raw)
|
||||
t.Logf("result code=%s candidates=%d", result.Code, len(result.Candidates))
|
||||
if result.Code != cardRecognitionCodeUnreadable || len(result.Candidates) != 0 {
|
||||
t.Fatal("blank image must not produce an ICCID")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type timeoutNetError struct{}
|
||||
|
||||
func (timeoutNetError) Error() string { return "timeout" }
|
||||
func (timeoutNetError) Timeout() bool { return true }
|
||||
func (timeoutNetError) Temporary() bool { return true }
|
||||
|
||||
func TestNormalizeCardRecognitionCandidates(t *testing.T) {
|
||||
got := normalizeCardRecognitionCandidates([]string{
|
||||
"ICCID: 8986-0401-7854-7796-048",
|
||||
"8986040178547796048",
|
||||
"89860401785477960480",
|
||||
"7986040178547796048",
|
||||
"898604017854779604",
|
||||
"898604017854779604800",
|
||||
})
|
||||
want := []string{
|
||||
"8986040178547796048",
|
||||
"89860401785477960480",
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("len(got) = %d, want %d, got=%v", len(got), len(want), got)
|
||||
}
|
||||
for index := range want {
|
||||
if got[index] != want[index] {
|
||||
t.Fatalf("got[%d] = %s, want %s", index, got[index], want[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestCardRecognitionModelUsesEinoMultimodalMessage(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path != "/v1/chat/completions" {
|
||||
t.Errorf("path = %s, want /v1/chat/completions", request.URL.Path)
|
||||
}
|
||||
var body struct {
|
||||
Messages []struct {
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
ImageURL struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"image_url"`
|
||||
} `json:"content"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
|
||||
t.Errorf("decode request: %v", err)
|
||||
}
|
||||
if len(body.Messages) != 1 || len(body.Messages[0].Content) != 2 {
|
||||
t.Errorf("unexpected messages: %+v", body.Messages)
|
||||
} else if !strings.HasPrefix(body.Messages[0].Content[1].ImageURL.URL, "data:image/png;base64,") {
|
||||
t.Errorf("image url = %s", body.Messages[0].Content[1].ImageURL.URL)
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
_, _ = writer.Write([]byte(`{"id":"chatcmpl-test","object":"chat.completion","created":1,"model":"test-model","choices":[{"index":0,"message":{"role":"assistant","content":"{\"code\":\"CARD_RECOGNIZED\",\"iccid\":\"8986040178547796048\"}"},"finish_reason":"stop"}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
content, err := requestCardRecognitionModel(context.Background(), cardRecognitionConfig{
|
||||
APIKey: "test-key",
|
||||
BaseURL: server.URL + "/v1",
|
||||
Model: "test-model",
|
||||
Timeout: 3 * time.Second,
|
||||
MaxTokens: 64,
|
||||
}, []byte("image"), "image/png")
|
||||
if err != nil {
|
||||
t.Fatalf("requestCardRecognitionModel() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(content, "8986040178547796048") {
|
||||
t.Fatalf("content = %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCardRecognitionResultMarkdownJSON(t *testing.T) {
|
||||
got := parseCardRecognitionResult("```json\n{\"code\":\"CARD_RECOGNIZED\",\"cardType\":\"iccid\",\"iccid\":\"8986040178547796048\",\"candidates\":[\"8986040178547796048\"],\"confidence\":1.2}\n```")
|
||||
if got.Code != cardRecognitionCodeRecognized {
|
||||
t.Fatalf("Code = %s, want %s", got.Code, cardRecognitionCodeRecognized)
|
||||
}
|
||||
if got.Confidence != 1 {
|
||||
t.Fatalf("Confidence = %v, want 1", got.Confidence)
|
||||
}
|
||||
if len(got.Candidates) != 1 || got.Candidates[0] != "8986040178547796048" {
|
||||
t.Fatalf("Candidates = %v", got.Candidates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCardRecognitionResultAmbiguous(t *testing.T) {
|
||||
got := parseCardRecognitionResult(`{
|
||||
"code": "CARD_AMBIGUOUS",
|
||||
"cardType": "iccid",
|
||||
"candidates": ["8986040178547796048", "89860401785477960480"],
|
||||
"confidence": 0.84
|
||||
}`)
|
||||
if got.Code != cardRecognitionCodeAmbiguous {
|
||||
t.Fatalf("Code = %s, want %s", got.Code, cardRecognitionCodeAmbiguous)
|
||||
}
|
||||
if len(got.Candidates) != 2 {
|
||||
t.Fatalf("Candidates = %v", got.Candidates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCardRecognitionResultFiltersInvalidCandidates(t *testing.T) {
|
||||
got := parseCardRecognitionResult(`{
|
||||
"code": "CARD_RECOGNIZED",
|
||||
"iccid": "13800008000",
|
||||
"candidates": ["898604017854779604", "898604017854779604800", "7986040178547796048"],
|
||||
"confidence": -0.4
|
||||
}`)
|
||||
if got.Confidence != 0 {
|
||||
t.Fatalf("Confidence = %v, want 0", got.Confidence)
|
||||
}
|
||||
if len(got.Candidates) != 0 {
|
||||
t.Fatalf("Candidates = %v, want empty", got.Candidates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCardRecognitionResultFallbackFromText(t *testing.T) {
|
||||
got := parseCardRecognitionResult("识别到 ICCID 8986040178547796048,另一个候选是 89860401785477960480。")
|
||||
if got.Code != cardRecognitionCodeAmbiguous {
|
||||
t.Fatalf("Code = %s, want %s", got.Code, cardRecognitionCodeAmbiguous)
|
||||
}
|
||||
if len(got.Candidates) != 2 {
|
||||
t.Fatalf("Candidates = %v", got.Candidates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampCardRecognitionConfidence(t *testing.T) {
|
||||
if got := clampCardRecognitionConfidence(-0.1); got != 0 {
|
||||
t.Fatalf("negative clamp = %v, want 0", got)
|
||||
}
|
||||
if got := clampCardRecognitionConfidence(1.1); got != 1 {
|
||||
t.Fatalf("over one clamp = %v, want 1", got)
|
||||
}
|
||||
if got := clampCardRecognitionConfidence(0.5); got != 0.5 {
|
||||
t.Fatalf("normal clamp = %v, want 0.5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCardRecognitionTimeoutError(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
||||
defer cancel()
|
||||
<-ctx.Done()
|
||||
if !isCardRecognitionTimeoutError(ctx, errors.New("other")) {
|
||||
t.Fatal("deadline exceeded context should be treated as timeout")
|
||||
}
|
||||
if !isCardRecognitionTimeoutError(context.Background(), timeoutNetError{}) {
|
||||
t.Fatal("net timeout error should be treated as timeout")
|
||||
}
|
||||
if isCardRecognitionTimeoutError(context.Background(), errors.New("temporary unavailable")) {
|
||||
t.Fatal("non-timeout error should not be treated as timeout")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,306 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type appRenewExecutionContext struct {
|
||||
CardPeriods []map[string]any
|
||||
LatestBindsByIccid map[string]*entity.TSimCardBindPkg
|
||||
NewBindsByIccid map[string]*entity.TSimCardBindPkg
|
||||
BeginAtByIccid map[string]*gtime.Time
|
||||
EndAtByIccid map[string]*gtime.Time
|
||||
MinBeginAt *gtime.Time
|
||||
MaxEndAt *gtime.Time
|
||||
}
|
||||
|
||||
func (s *sAppCard) prepareAppRenewExecution(ctx context.Context, tx gdb.TX, batch *appRenewBatchContext, durationMonths int) (*appRenewExecutionContext, error) {
|
||||
if batch == nil || len(batch.Items) == 0 {
|
||||
return nil, gerror.New("续费卡不能为空")
|
||||
}
|
||||
if durationMonths <= 0 {
|
||||
return nil, gerror.New("续费时长必须大于0")
|
||||
}
|
||||
|
||||
exec := &appRenewExecutionContext{
|
||||
CardPeriods: make([]map[string]any, 0, len(batch.Items)),
|
||||
LatestBindsByIccid: make(map[string]*entity.TSimCardBindPkg, len(batch.Items)),
|
||||
NewBindsByIccid: make(map[string]*entity.TSimCardBindPkg, len(batch.Items)),
|
||||
BeginAtByIccid: make(map[string]*gtime.Time, len(batch.Items)),
|
||||
EndAtByIccid: make(map[string]*gtime.Time, len(batch.Items)),
|
||||
}
|
||||
for index, item := range batch.Items {
|
||||
if item == nil || item.Card == nil {
|
||||
return nil, gerror.New("续费卡信息不完整")
|
||||
}
|
||||
lockedCard, err := s.lockRenewCardForSubmit(ctx, tx, item.Card, item.ExpectedCurrentPackageCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
latestBind, err := s.lockLatestRenewBindForSubmit(ctx, tx, lockedCard, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseExpireAt := appRenewBaseExpireTimeFromEntity(lockedCard, latestBind)
|
||||
if baseExpireAt == nil || baseExpireAt.IsZero() {
|
||||
return nil, gerror.Newf("ICCID最新订购记录没有预计到期时间,不能续费:%s", lockedCard.Iccid)
|
||||
}
|
||||
if appRenewExpireAfterAllowedWindow(baseExpireAt) {
|
||||
return nil, gerror.New("距离到期超过6个月,不能续费")
|
||||
}
|
||||
beginAt := appRenewBeginDateFromExpire(baseExpireAt)
|
||||
endAt := beginAt.AddDate(0, durationMonths, 0)
|
||||
exec.MinBeginAt = appEarlierTime(exec.MinBeginAt, beginAt)
|
||||
exec.MaxEndAt = appLaterTime(exec.MaxEndAt, endAt)
|
||||
exec.LatestBindsByIccid[item.Card.Iccid] = latestBind
|
||||
exec.BeginAtByIccid[item.Card.Iccid] = beginAt
|
||||
exec.EndAtByIccid[item.Card.Iccid] = endAt
|
||||
item.LatestBind = latestBind
|
||||
exec.CardPeriods = append(exec.CardPeriods, map[string]any{
|
||||
"rowNo": appRenewBatchSourceRowNo(index),
|
||||
"cardId": item.Card.Id,
|
||||
"iccid": item.Card.Iccid,
|
||||
"msisdn": item.Card.Msisdn,
|
||||
"oldSubscriptionNo": latestBind.SubscriptionNo,
|
||||
"serviceBeginAt": appRenewFormatTime(beginAt),
|
||||
"serviceEndAt": appRenewFormatTime(endAt),
|
||||
"oldExpectedExpireTime": appRenewFormatTime(latestBind.ExpectedExpireTime),
|
||||
})
|
||||
}
|
||||
return exec, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) lockLatestRenewBindForSubmit(ctx context.Context, tx gdb.TX, lockedCard *entity.TSimCardInfo, item *appRenewPackageContext) (*entity.TSimCardBindPkg, error) {
|
||||
if lockedCard == nil || item == nil || item.Card == nil {
|
||||
return nil, gerror.New("续费卡信息不完整")
|
||||
}
|
||||
expectedPackageCode := strings.TrimSpace(item.ExpectedCurrentPackageCode)
|
||||
if expectedPackageCode == "" {
|
||||
return nil, gerror.New("ICCID没有当前套餐,不能续费")
|
||||
}
|
||||
|
||||
var bind *entity.TSimCardBindPkg
|
||||
err := dao.TSimCardBindPkg.Ctx(ctx).TX(tx).
|
||||
Where(dao.TSimCardBindPkg.Columns().Iccid, lockedCard.Iccid).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().ExpectedExpireTime).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().Id).
|
||||
LockUpdate().
|
||||
Limit(1).
|
||||
Scan(&bind)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "锁定最新订购记录失败")
|
||||
}
|
||||
if bind == nil || bind.Id == 0 {
|
||||
return nil, gerror.Newf("ICCID没有订购记录,不能续费:%s", lockedCard.Iccid)
|
||||
}
|
||||
if strings.TrimSpace(bind.PackageCode) != expectedPackageCode {
|
||||
return nil, gerror.Newf("ICCID当前套餐与最新订购记录不一致,不能续费:%s", lockedCard.Iccid)
|
||||
}
|
||||
return bind, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) insertAppRenewSubscriptionBinds(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appRenewBatchContext, exec *appRenewExecutionContext, batchNo string, orderSn string, durationMonths int, now *gtime.Time) error {
|
||||
if user == nil || batch == nil || batch.Package == nil || batch.Party == nil || exec == nil {
|
||||
return gerror.New("续费订购记录信息不完整")
|
||||
}
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
orderSn = strings.TrimSpace(orderSn)
|
||||
if batchNo == "" || orderSn == "" {
|
||||
return gerror.New("续费批次号或订单号不能为空")
|
||||
}
|
||||
|
||||
periodByIccid := make(map[string]map[string]any, len(exec.CardPeriods))
|
||||
for _, period := range exec.CardPeriods {
|
||||
iccid := strings.TrimSpace(fmt.Sprint(period["iccid"]))
|
||||
if iccid != "" {
|
||||
periodByIccid[iccid] = period
|
||||
}
|
||||
}
|
||||
|
||||
rows := make([]do.TSimCardBindPkg, 0, len(batch.Items))
|
||||
for _, item := range batch.Items {
|
||||
card := item.Card
|
||||
if card == nil {
|
||||
return gerror.New("续费卡信息不完整")
|
||||
}
|
||||
latestBind := exec.LatestBindsByIccid[card.Iccid]
|
||||
if latestBind == nil {
|
||||
return gerror.Newf("ICCID最新订购记录不完整:%s", card.Iccid)
|
||||
}
|
||||
periodBegin := exec.BeginAtByIccid[card.Iccid]
|
||||
periodEnd := exec.EndAtByIccid[card.Iccid]
|
||||
if periodBegin == nil || periodBegin.IsZero() || periodEnd == nil || periodEnd.IsZero() {
|
||||
return gerror.Newf("ICCID续费服务周期不完整:%s", card.Iccid)
|
||||
}
|
||||
subscriptionNo := appOrderNo("SUB", user.UserId, now)
|
||||
row := do.TSimCardBindPkg{
|
||||
SubscriptionNo: subscriptionNo,
|
||||
BatchNo: batchNo,
|
||||
Iccid: card.Iccid,
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
DeptId: fmt.Sprintf("%d", batch.Party.CustomerAgentId),
|
||||
PackageType: batch.Package.PackageType,
|
||||
PackageCode: batch.Package.PackageCode,
|
||||
PreviousPackageCode: item.ExpectedCurrentPackageCode,
|
||||
BusinessType: consts.PackageBusinessTypeRenew,
|
||||
EffectiveStatus: consts.EffectiveStatusPending,
|
||||
HandleTime: now,
|
||||
EffectiveTime: periodBegin,
|
||||
ExpectedExpireTime: periodEnd,
|
||||
DurationMonths: durationMonths,
|
||||
GiftDurationMonths: 0,
|
||||
OrderCode: orderSn,
|
||||
Remark: batchNo,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: batch.Party.CreatedDept,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
rows = append(rows, row)
|
||||
newBind := &entity.TSimCardBindPkg{
|
||||
SubscriptionNo: subscriptionNo,
|
||||
BatchNo: batchNo,
|
||||
Iccid: card.Iccid,
|
||||
Msisdn: strings.TrimSpace(card.Msisdn),
|
||||
DeptId: fmt.Sprintf("%d", batch.Party.CustomerAgentId),
|
||||
PackageType: batch.Package.PackageType,
|
||||
PackageCode: batch.Package.PackageCode,
|
||||
PreviousPackageCode: item.ExpectedCurrentPackageCode,
|
||||
BusinessType: consts.PackageBusinessTypeRenew,
|
||||
EffectiveStatus: consts.EffectiveStatusPending,
|
||||
HandleTime: now,
|
||||
EffectiveTime: periodBegin,
|
||||
ExpectedExpireTime: periodEnd,
|
||||
DurationMonths: durationMonths,
|
||||
GiftDurationMonths: 0,
|
||||
OrderCode: orderSn,
|
||||
Remark: batchNo,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: batch.Party.CreatedDept,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
exec.NewBindsByIccid[card.Iccid] = newBind
|
||||
if period := periodByIccid[card.Iccid]; period != nil {
|
||||
period["newSubscriptionNo"] = subscriptionNo
|
||||
period["renewEffectiveTime"] = appRenewFormatTime(periodBegin)
|
||||
period["renewExpectedExpireTime"] = appRenewFormatTime(periodEnd)
|
||||
period["cardFlowPkgEndAt"] = appRenewFormatTime(periodEnd)
|
||||
}
|
||||
}
|
||||
if len(rows) > 0 {
|
||||
if _, err := dao.TSimCardBindPkg.Ctx(ctx).TX(tx).Data(rows).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "写入续费订购记录失败")
|
||||
}
|
||||
}
|
||||
return s.updateCardsAfterAppRenew(ctx, tx, user, batch, exec, now)
|
||||
}
|
||||
|
||||
func (s *sAppCard) updateCardsAfterAppRenew(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appRenewBatchContext, exec *appRenewExecutionContext, now *gtime.Time) error {
|
||||
for _, item := range batch.Items {
|
||||
if item == nil || item.Card == nil {
|
||||
return gerror.New("续费卡信息不完整")
|
||||
}
|
||||
newBind := exec.NewBindsByIccid[item.Card.Iccid]
|
||||
if newBind == nil || newBind.ExpectedExpireTime == nil {
|
||||
return gerror.Newf("ICCID续费订购记录不完整:%s", item.Card.Iccid)
|
||||
}
|
||||
packageStatus := appCalcPackageStatus(now, item.Card.FlowPkgBeginAt, newBind.ExpectedExpireTime)
|
||||
result, err := dao.TSimCardInfo.Ctx(ctx).TX(tx).
|
||||
Where(dao.TSimCardInfo.Columns().TenantId, item.Card.TenantId).
|
||||
Where(dao.TSimCardInfo.Columns().Iccid, item.Card.Iccid).
|
||||
Data(do.TSimCardInfo{
|
||||
CurrentPackageCode: batch.Package.PackageCode,
|
||||
FlowPkgEndAt: newBind.ExpectedExpireTime,
|
||||
PackageStatus: packageStatus,
|
||||
UpdatedBy: user.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", item.Card.Iccid)
|
||||
}
|
||||
item.Card.FlowPkgEndAt = newBind.ExpectedExpireTime
|
||||
item.Card.PackageStatus = packageStatus
|
||||
item.Card.CurrentPackageCode = batch.Package.PackageCode
|
||||
if err = s.updateTerminalBindAfterAppRenew(ctx, tx, user, batch, item, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) updateTerminalBindAfterAppRenew(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appRenewBatchContext, item *appRenewPackageContext, now *gtime.Time) error {
|
||||
if item == nil || item.Card == nil || batch == nil || batch.Party == nil || !s.isTerminalUser(ctx, user) {
|
||||
return nil
|
||||
}
|
||||
card := item.Card
|
||||
if card.TerminalBindId <= 0 && card.TerminalUserId <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mod := dao.TSimTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TSimTerminal.Columns().TenantId, card.TenantId).
|
||||
Where(dao.TSimTerminal.Columns().Iccid, card.Iccid)
|
||||
if card.TerminalBindId > 0 {
|
||||
mod = mod.Where(dao.TSimTerminal.Columns().Id, card.TerminalBindId)
|
||||
} else {
|
||||
mod = mod.Where(dao.TSimTerminal.Columns().TerminalId, user.UserId)
|
||||
}
|
||||
|
||||
result, err := mod.Data(do.TSimTerminal{
|
||||
DeptId: batch.Party.CustomerAgentId,
|
||||
CurrentPackageCode: strings.TrimSpace(batch.Package.PackageCode),
|
||||
UpdatedBy: user.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", card.Iccid)
|
||||
}
|
||||
card.TerminalDeptId = batch.Party.CustomerAgentId
|
||||
card.TerminalCurrentPackageCode = strings.TrimSpace(batch.Package.PackageCode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func appCalcPackageStatus(now, beginAt, endAt *gtime.Time) int {
|
||||
if now == nil {
|
||||
now = gtime.Now()
|
||||
}
|
||||
if beginAt != nil && beginAt.After(now) {
|
||||
return consts.PackageStatusPending
|
||||
}
|
||||
if endAt != nil && !endAt.After(now) {
|
||||
return consts.PackageStatusExpired
|
||||
}
|
||||
return consts.PackageStatusEffective
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func TestAppRenewBeginDateFromExpireUsesPreviousMonthEnd(t *testing.T) {
|
||||
expireAt := gtime.NewFromStr("2026-08-15 00:00:00")
|
||||
|
||||
got := appRenewBeginDateFromExpire(expireAt)
|
||||
if got == nil {
|
||||
t.Fatal("expected renew begin time")
|
||||
}
|
||||
if want := "2026-07-31 23:59:59"; got.Format("Y-m-d H:i:s") != want {
|
||||
t.Fatalf("expected %s, got %s", want, got.Format("Y-m-d H:i:s"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRenewExpireAfterUsesPreviousMonthEnd(t *testing.T) {
|
||||
expireAt := gtime.NewFromStr("2026-08-15 00:00:00")
|
||||
|
||||
got := appRenewExpireAfter(expireAt, 3)
|
||||
if got == nil {
|
||||
t.Fatal("expected renew expire after time")
|
||||
}
|
||||
if want := "2026-10-31 23:59:59"; got.Format("Y-m-d H:i:s") != want {
|
||||
t.Fatalf("expected %s, got %s", want, got.Format("Y-m-d H:i:s"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRenewableDurationYearOptionsUsesYearConfig(t *testing.T) {
|
||||
pkg := &entity.TSimFlowPackageInfo{
|
||||
RenewableDurationConfig: gjson.New([]int{1, 2, 3}),
|
||||
}
|
||||
|
||||
got, err := appRenewableDurationYearOptions(pkg)
|
||||
if err != nil {
|
||||
t.Fatalf("appRenewableDurationYearOptions() error = %v", err)
|
||||
}
|
||||
if want := []int{1, 2, 3}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("appRenewableDurationYearOptions() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRenewableDurationYearOptionsCompatibleWithLegacyMonthConfig(t *testing.T) {
|
||||
pkg := &entity.TSimFlowPackageInfo{
|
||||
RenewableDurationConfig: gjson.New([]int{12, 24, 36}),
|
||||
}
|
||||
|
||||
got, err := appRenewableDurationYearOptions(pkg)
|
||||
if err != nil {
|
||||
t.Fatalf("appRenewableDurationYearOptions() error = %v", err)
|
||||
}
|
||||
if want := []int{1, 2, 3}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("appRenewableDurationYearOptions() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRenewApplyPriceRuleNormalizesPercentRatio(t *testing.T) {
|
||||
if got, want := appRenewApplyPriceRule(100, 85, 5), 90.0; got != want {
|
||||
t.Fatalf("appRenewApplyPriceRule() = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := appRenewApplyPriceRule(100, 0.85, 5), 90.0; got != want {
|
||||
t.Fatalf("appRenewApplyPriceRule() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRenewEffectiveSalePriceUsesPriceOwnerBaseCost(t *testing.T) {
|
||||
salePrice := appRenewEffectiveSalePriceFromBaseCosts(
|
||||
appRenewEffectiveBaseCosts{
|
||||
AgentCost: 80,
|
||||
TerminalCost: 120,
|
||||
},
|
||||
appRenewPriceRule{
|
||||
AgentCostRatio: 1.2,
|
||||
AgentCostProfit: 3,
|
||||
TerminalCostRatio: 1.5,
|
||||
TerminalCostProfit: 6,
|
||||
},
|
||||
)
|
||||
|
||||
if got, want := salePrice.AgentShipmentPrice, 99.0; got != want {
|
||||
t.Fatalf("agent shipment price = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := salePrice.TerminalSalePrice, 186.0; got != want {
|
||||
t.Fatalf("terminal sale price = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRenewPriceRuleUsesTargetAndDefaultSnapshots(t *testing.T) {
|
||||
target := appRenewPriceRuleFromOverride("target", &entity.TSimFlowPackagePrice{
|
||||
Id: 11,
|
||||
AgentCostRatio: 0.9,
|
||||
AgentCostProfit: 3,
|
||||
UserCostRatio: 1.2,
|
||||
UserCostProfit: 5,
|
||||
})
|
||||
if target.PriceSource != "target" || target.TargetPriceId != 11 || target.AgentDefaultPriceId != 0 {
|
||||
t.Fatalf("target rule snapshot = %#v", target)
|
||||
}
|
||||
if got, want := appRenewEffectiveSalePriceFromBaseCosts(appRenewEffectiveBaseCosts{
|
||||
AgentCost: 100,
|
||||
TerminalCost: 100,
|
||||
}, target).AgentShipmentPrice, 93.0; got != want {
|
||||
t.Fatalf("target agent sale price = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
defaultRule := appRenewPriceRuleFromOverride("agentDefault", &entity.TSimFlowPackagePrice{
|
||||
Id: 12,
|
||||
AgentCostRatio: 80,
|
||||
AgentCostProfit: 1,
|
||||
UserCostRatio: 150,
|
||||
UserCostProfit: 2,
|
||||
})
|
||||
if defaultRule.PriceSource != "agentDefault" || defaultRule.AgentDefaultPriceId != 12 || defaultRule.TargetPriceId != 0 {
|
||||
t.Fatalf("default rule snapshot = %#v", defaultRule)
|
||||
}
|
||||
if got, want := appRenewEffectiveSalePriceFromBaseCosts(appRenewEffectiveBaseCosts{
|
||||
AgentCost: 100,
|
||||
TerminalCost: 100,
|
||||
}, defaultRule).TerminalSalePrice, 152.0; got != want {
|
||||
t.Fatalf("default terminal sale price = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRenewSystemBaseCostsFallsBackToCostPriceForTerminal(t *testing.T) {
|
||||
pkg := &entity.TSimFlowPackageInfo{
|
||||
CostPrice: 80,
|
||||
TerminalCost: 0,
|
||||
}
|
||||
|
||||
got := appRenewSystemBaseCosts(pkg)
|
||||
if got.AgentCost != 80 || got.TerminalCost != 80 {
|
||||
t.Fatalf("appRenewSystemBaseCosts() = %#v, want agent=80 terminal=80", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type appRenewBatchProcessor struct {
|
||||
batch *appRenewBatchContext
|
||||
}
|
||||
|
||||
type appRenewBatchRow struct {
|
||||
rowNo int
|
||||
itemID string
|
||||
snapshot *gjson.Json
|
||||
errorReasons []string
|
||||
}
|
||||
|
||||
func newAppRenewBatchProcessor(batch *appRenewBatchContext) *appRenewBatchProcessor {
|
||||
return &appRenewBatchProcessor{batch: batch}
|
||||
}
|
||||
|
||||
func appRenewBatchRows(batch *appRenewBatchContext) []batchimport.Row {
|
||||
if batch == nil {
|
||||
return nil
|
||||
}
|
||||
rows := make([]batchimport.Row, 0, len(batch.Items))
|
||||
for index, item := range batch.Items {
|
||||
rowNo := appRenewBatchSourceRowNo(index)
|
||||
itemID := ""
|
||||
var snapshot *gjson.Json
|
||||
if item != nil && item.Card != nil {
|
||||
itemID = strings.TrimSpace(item.Card.Iccid)
|
||||
snapshot = appRenewPrepareDetailSnapshot(batch, item, rowNo)
|
||||
}
|
||||
rows = append(rows, &appRenewBatchRow{
|
||||
rowNo: rowNo,
|
||||
itemID: itemID,
|
||||
snapshot: snapshot,
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (p *appRenewBatchProcessor) Config() batchimport.ProcessorConfig {
|
||||
return batchimport.ProcessorConfig{
|
||||
Code: "miniapp_card_renew",
|
||||
BatchType: consts.BatchTypeRenew,
|
||||
BatchNoPrefix: consts.BatchNoPrefixRenew,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimCardRenew,
|
||||
ExportScene: consts.ResourceExportSceneRenew,
|
||||
ImportDir: consts.SimCardRenewSubDirImportExcel,
|
||||
ResultDir: consts.SimCardRenewSubDirResultExcel,
|
||||
MaxFileSize: 50 << 20,
|
||||
TemplateHeaders: []string{"ICCID", "MSISDN", "IMSI"},
|
||||
RequiredHeaders: []string{"ICCID"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *appRenewBatchProcessor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) {
|
||||
return nil, gerror.New("小程序续费暂不支持文件导入")
|
||||
}
|
||||
|
||||
func (p *appRenewBatchProcessor) ValidateRows(ctx context.Context, rows []batchimport.Row) error {
|
||||
seen := make(map[string]batchimport.Row, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
itemID := strings.TrimSpace(row.GetItemID())
|
||||
if itemID == "" {
|
||||
row.AddErrorReason("ICCID不能为空")
|
||||
continue
|
||||
}
|
||||
if first, ok := seen[itemID]; ok {
|
||||
row.AddErrorReason("ICCID重复")
|
||||
first.AddErrorReason("ICCID重复")
|
||||
continue
|
||||
}
|
||||
seen[itemID] = row
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *appRenewBatchProcessor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error {
|
||||
return gerror.New("小程序续费批次确认由余额支付流程触发")
|
||||
}
|
||||
|
||||
func (p *appRenewBatchProcessor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json {
|
||||
info := map[string]any{}
|
||||
if p.batch != nil {
|
||||
if base := appRenewPrepareBatchInfo(p.batch, "", "", 0, 0); base != nil {
|
||||
for key, value := range base.Map() {
|
||||
info[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if sourceBatchNo := strings.TrimSpace(req.SourceBatchNo); sourceBatchNo != "" {
|
||||
info["sourceBatchNo"] = sourceBatchNo
|
||||
}
|
||||
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
|
||||
}
|
||||
if result != nil && result.FailReason != "" {
|
||||
info["failReason"] = result.FailReason
|
||||
}
|
||||
return gjson.New(info)
|
||||
}
|
||||
|
||||
func (r *appRenewBatchRow) GetRowNo() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
return r.rowNo
|
||||
}
|
||||
|
||||
func (r *appRenewBatchRow) GetItemID() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(r.itemID)
|
||||
}
|
||||
|
||||
func (r *appRenewBatchRow) GetErrorReasons() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return r.errorReasons
|
||||
}
|
||||
|
||||
func (r *appRenewBatchRow) AddErrorReason(reason string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
reason = strings.TrimSpace(reason)
|
||||
if reason == "" {
|
||||
return
|
||||
}
|
||||
for _, item := range r.errorReasons {
|
||||
if item == reason {
|
||||
return
|
||||
}
|
||||
}
|
||||
r.errorReasons = append(r.errorReasons, reason)
|
||||
}
|
||||
|
||||
func (r *appRenewBatchRow) Snapshot() *gjson.Json {
|
||||
if r == nil || r.snapshot == nil {
|
||||
return gjson.New(map[string]any{})
|
||||
}
|
||||
return r.snapshot
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type appRenewPackageContext struct {
|
||||
Card *appCardQueryRow
|
||||
Package *entity.TSimFlowPackageInfo
|
||||
PackageCode string
|
||||
ExpectedCurrentPackageCode string
|
||||
Subscription *entity.TSimCardBindPkg
|
||||
LatestBind *entity.TSimCardBindPkg
|
||||
}
|
||||
|
||||
type appRenewTargetContext struct {
|
||||
Package *entity.TSimFlowPackageInfo
|
||||
Price *appRenewPriceSnapshot
|
||||
Durations []int
|
||||
}
|
||||
|
||||
// resolveRenewPackageContext 统一解析“当前可续费套餐”。
|
||||
// 当前套餐只用于锁定卡片和读取 renewable_package_codes,续费目标套餐由后续步骤单独解析。
|
||||
func (s *sAppCard) resolveRenewPackageContext(ctx context.Context, card *appCardQueryRow, subscriptionNo string) (*appRenewPackageContext, error) {
|
||||
if card == nil || strings.TrimSpace(card.Iccid) == "" {
|
||||
return nil, gerror.New("卡信息不存在或无权续费")
|
||||
}
|
||||
|
||||
currentPackageCode := appCardCurrentPackageCode(card)
|
||||
renewCtx := &appRenewPackageContext{
|
||||
Card: card,
|
||||
ExpectedCurrentPackageCode: currentPackageCode,
|
||||
}
|
||||
|
||||
subscriptionNo = strings.TrimSpace(subscriptionNo)
|
||||
if subscriptionNo != "" {
|
||||
bind, err := s.loadRenewSubscription(ctx, card, subscriptionNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if currentPackageCode != "" && strings.TrimSpace(bind.PackageCode) != currentPackageCode {
|
||||
return nil, gerror.New("订购记录不是当前卡片套餐,请刷新后重试")
|
||||
}
|
||||
renewCtx.Subscription = bind
|
||||
}
|
||||
|
||||
latestBind, err := s.loadLatestRenewBind(ctx, card)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
renewCtx.LatestBind = latestBind
|
||||
if err = appValidateRenewLatestBind(card, currentPackageCode, latestBind); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkg, err := s.findRenewPackageOrNil(ctx, card.TenantId, currentPackageCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pkg == nil {
|
||||
return nil, gerror.Newf("当前卡片套餐编码 %s 未配置套餐信息", currentPackageCode)
|
||||
}
|
||||
renewCtx.Package = pkg
|
||||
renewCtx.PackageCode = pkg.PackageCode
|
||||
return renewCtx, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) resolveRenewTargetPackages(ctx context.Context, user *entity.SysUser, renewCtx *appRenewPackageContext, requestedPackageCode string) ([]string, []*appRenewTargetContext, *appRenewTargetContext, error) {
|
||||
if renewCtx == nil || renewCtx.Card == nil || renewCtx.Package == nil {
|
||||
return nil, nil, nil, gerror.New("当前套餐信息不完整")
|
||||
}
|
||||
codes := appChangePackageCodes(renewCtx.Package.RenewablePackageCodes)
|
||||
if len(codes) == 0 {
|
||||
return nil, nil, nil, gerror.New("当前套餐未配置可续费套餐")
|
||||
}
|
||||
requestedPackageCode = appRenewRequestedTargetPackageCode(renewCtx, requestedPackageCode, codes)
|
||||
|
||||
packages, err := s.loadRenewPackagesByCodes(ctx, renewCtx.Card.TenantId, codes)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
targets := make([]*appRenewTargetContext, 0, len(codes))
|
||||
var selected *appRenewTargetContext
|
||||
for _, code := range codes {
|
||||
pkg := packages[strings.TrimSpace(code)]
|
||||
if pkg == nil {
|
||||
continue
|
||||
}
|
||||
durations, err := appRenewableDurationYearOptions(pkg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, gerror.Newf("续费套餐%s:%v", pkg.PackageName, err)
|
||||
}
|
||||
price, err := s.resolveRenewUnitPrice(ctx, user, renewCtx.Card, pkg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if price.UnitPrice < 0 {
|
||||
return nil, nil, nil, gerror.Newf("续费套餐%s价格配置不正确", pkg.PackageName)
|
||||
}
|
||||
target := &appRenewTargetContext{
|
||||
Package: pkg,
|
||||
Price: price,
|
||||
Durations: durations,
|
||||
}
|
||||
targets = append(targets, target)
|
||||
if requestedPackageCode != "" && strings.TrimSpace(pkg.PackageCode) == requestedPackageCode {
|
||||
selected = target
|
||||
}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return nil, nil, nil, gerror.New("当前套餐可续费套餐不存在或未配置续费年限")
|
||||
}
|
||||
if requestedPackageCode != "" && selected == nil {
|
||||
return nil, nil, nil, gerror.New("续费套餐不存在")
|
||||
}
|
||||
if selected == nil {
|
||||
selected = targets[0]
|
||||
}
|
||||
return codes, targets, selected, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadRenewPackagesByCodes(ctx context.Context, tenantId string, codes []string) (map[string]*entity.TSimFlowPackageInfo, error) {
|
||||
result := make(map[string]*entity.TSimFlowPackageInfo, len(codes))
|
||||
codes = appNormalizePackageCodes(codes)
|
||||
if len(codes) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var rows []*entity.TSimFlowPackageInfo
|
||||
err := dao.TSimFlowPackageInfo.Ctx(ctx).
|
||||
Where(dao.TSimFlowPackageInfo.Columns().TenantId, strings.TrimSpace(tenantId)).
|
||||
WhereIn(dao.TSimFlowPackageInfo.Columns().PackageCode, codes).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询可续费套餐列表失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row != nil && strings.TrimSpace(row.PackageCode) != "" {
|
||||
result[strings.TrimSpace(row.PackageCode)] = row
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func appRenewRequestedTargetPackageCode(renewCtx *appRenewPackageContext, requestedPackageCode string, renewableCodes []string) string {
|
||||
requestedPackageCode = strings.TrimSpace(requestedPackageCode)
|
||||
if requestedPackageCode == "" {
|
||||
return ""
|
||||
}
|
||||
if appContainsString(renewableCodes, requestedPackageCode) {
|
||||
return requestedPackageCode
|
||||
}
|
||||
if renewCtx != nil && strings.TrimSpace(renewCtx.PackageCode) == requestedPackageCode {
|
||||
return ""
|
||||
}
|
||||
return requestedPackageCode
|
||||
}
|
||||
|
||||
func appContainsString(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 appValidateRenewLatestBind(card *appCardQueryRow, currentPackageCode string, latestBind *entity.TSimCardBindPkg) error {
|
||||
if card == nil {
|
||||
return gerror.New("卡信息不存在或无权续费")
|
||||
}
|
||||
currentPackageCode = strings.TrimSpace(currentPackageCode)
|
||||
if currentPackageCode == "" || card.PackageStatus == consts.PackageStatusNone {
|
||||
return gerror.Newf("ICCID没有当前套餐,不能续费:%s", card.Iccid)
|
||||
}
|
||||
if latestBind == nil || latestBind.Id == 0 {
|
||||
return gerror.Newf("ICCID没有订购记录,不能续费:%s", card.Iccid)
|
||||
}
|
||||
if strings.TrimSpace(latestBind.PackageCode) != currentPackageCode {
|
||||
return gerror.Newf("ICCID当前套餐与最新订购记录不一致,不能续费:%s", card.Iccid)
|
||||
}
|
||||
baseExpireAt := appRenewBaseExpireTimeFromRow(card, latestBind)
|
||||
if baseExpireAt == nil || baseExpireAt.IsZero() {
|
||||
return gerror.Newf("ICCID最新订购记录没有预计到期时间,不能续费:%s", card.Iccid)
|
||||
}
|
||||
if appRenewExpireAfterAllowedWindow(baseExpireAt) {
|
||||
return gerror.New("距离到期超过6个月,不能续费")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadRenewSubscription(ctx context.Context, card *appCardQueryRow, subscriptionNo string) (*entity.TSimCardBindPkg, error) {
|
||||
var bind *entity.TSimCardBindPkg
|
||||
err := dao.TSimCardBindPkg.Ctx(ctx).
|
||||
Where(dao.TSimCardBindPkg.Columns().SubscriptionNo, strings.TrimSpace(subscriptionNo)).
|
||||
Limit(1).
|
||||
Scan(&bind)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询订购记录失败")
|
||||
}
|
||||
if bind == nil || bind.Id == 0 || strings.TrimSpace(bind.Iccid) != strings.TrimSpace(card.Iccid) {
|
||||
return nil, gerror.New("订购记录不存在或不属于当前卡片")
|
||||
}
|
||||
return bind, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) loadLatestRenewBind(ctx context.Context, card *appCardQueryRow) (*entity.TSimCardBindPkg, error) {
|
||||
var bind *entity.TSimCardBindPkg
|
||||
err := dao.TSimCardBindPkg.Ctx(ctx).
|
||||
Where(dao.TSimCardBindPkg.Columns().Iccid, card.Iccid).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().ExpectedExpireTime).
|
||||
OrderDesc(dao.TSimCardBindPkg.Columns().Id).
|
||||
Limit(1).
|
||||
Scan(&bind)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询最新订购记录失败")
|
||||
}
|
||||
if bind == nil || bind.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return bind, nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) findRenewPackageOrNil(ctx context.Context, tenantId string, packageCode string) (*entity.TSimFlowPackageInfo, error) {
|
||||
packageCode = strings.TrimSpace(packageCode)
|
||||
if packageCode == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var pkg *entity.TSimFlowPackageInfo
|
||||
err := dao.TSimFlowPackageInfo.Ctx(ctx).
|
||||
Where(dao.TSimFlowPackageInfo.Columns().TenantId, strings.TrimSpace(tenantId)).
|
||||
Where(dao.TSimFlowPackageInfo.Columns().PackageCode, packageCode).
|
||||
Limit(1).
|
||||
Scan(&pkg)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询续费套餐失败")
|
||||
}
|
||||
if pkg == nil || pkg.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (renewCtx *appRenewPackageContext) expireAt() *gtime.Time {
|
||||
if renewCtx == nil {
|
||||
return nil
|
||||
}
|
||||
expireAt := appRenewBaseExpireTimeFromRow(renewCtx.Card, renewCtx.LatestBind)
|
||||
if renewCtx.Subscription != nil &&
|
||||
renewCtx.Subscription.ExpectedExpireTime != nil &&
|
||||
!renewCtx.Subscription.ExpectedExpireTime.IsZero() &&
|
||||
(expireAt == nil || renewCtx.Subscription.ExpectedExpireTime.After(expireAt)) {
|
||||
return renewCtx.Subscription.ExpectedExpireTime
|
||||
}
|
||||
return expireAt
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/hash"
|
||||
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type appRenewBatchLogFiles struct {
|
||||
ImportOssId int64
|
||||
ImportFileName string
|
||||
ResultOssId int64
|
||||
DownloadId int64
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureAppRenewBatchLogFiles(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appRenewBatchContext, existingInfo *gjson.Json, batchNo string, sourceBatchNo string, orderSn string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, cardPeriods []map[string]any, buyerNote string, now *gtime.Time) (*appRenewBatchLogFiles, error) {
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
if batchNo == "" {
|
||||
return nil, gerror.New("续费批次号不能为空")
|
||||
}
|
||||
if user == nil {
|
||||
return nil, gerror.New("当前用户不存在")
|
||||
}
|
||||
if batch == nil || batch.Party == nil {
|
||||
return nil, gerror.New("续费批次日志文件信息不完整")
|
||||
}
|
||||
|
||||
files := appRenewBatchLogFilesFromInfo(existingInfo)
|
||||
existingDownload, err := appRenewBatchResultDownload(ctx, tx, batchNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingDownload != nil {
|
||||
files.ResultOssId = existingDownload.ResultOssId
|
||||
files.DownloadId = existingDownload.DownloadId
|
||||
}
|
||||
|
||||
if files.ImportOssId <= 0 {
|
||||
importContent, err := appBuildRenewBatchImportExcel(batch, sourceBatchNo, durationMonths, buyerNote, now)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "生成续费批次源文件失败")
|
||||
}
|
||||
importFileName := appRenewBatchImportFileNameByBatchNo(batchNo)
|
||||
importUpload, importOssId, err := appSaveRenewBatchOss(ctx, tx, user, batch.Party.CreatedDept, importContent, importFileName, consts.SimCardRenewSubDirImportExcel, now)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "保存续费批次源文件失败")
|
||||
}
|
||||
files.ImportOssId = importOssId
|
||||
files.ImportFileName = strings.TrimSpace(importUpload.Name)
|
||||
}
|
||||
if strings.TrimSpace(files.ImportFileName) == "" {
|
||||
files.ImportFileName = appRenewBatchImportFileName(ctx, tx, files.ImportOssId, appRenewBatchImportFileNameByBatchNo(batchNo))
|
||||
}
|
||||
if existingDownload != nil {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
resultContent, err := appBuildRenewBatchResultExcel(batch, files, batchNo, orderSn, durationMonths, singleCardAmount, totalAmount, beginAt, endAt, cardPeriods, now, user.UserId, 0)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "生成续费批次结果文件失败")
|
||||
}
|
||||
resultUpload, resultOssId, err := appSaveRenewBatchOss(ctx, tx, user, batch.Party.CreatedDept, resultContent, batchNo+"-result.xlsx", consts.SimCardRenewSubDirResultExcel, now)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "保存续费批次结果文件失败")
|
||||
}
|
||||
files.ResultOssId = resultOssId
|
||||
|
||||
if err = appFillRenewResultOssId(ctx, tx, resultOssId, resultUpload.Path, user.UserId, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refreshedResult, err := appLoadRenewOssUpload(ctx, tx, resultOssId, resultUpload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quantity := len(batch.Items)
|
||||
downloadId, err := dao.TResourceBatchDownload.Ctx(ctx).TX(tx).Data(do.TResourceBatchDownload{
|
||||
BatchNo: batchNo,
|
||||
TaskName: consts.ResourceBatchDownloadTaskSimCardRenew,
|
||||
ResourceType: consts.ResourceTypeExcel,
|
||||
ExportScene: consts.ResourceExportSceneRenew,
|
||||
TotalCount: quantity,
|
||||
SuccessCount: quantity,
|
||||
FailCount: 0,
|
||||
DownloadStatus: consts.DownloadStatusGenerated,
|
||||
OssId: resultOssId,
|
||||
FileName: refreshedResult.Name,
|
||||
FileExt: refreshedResult.Ext,
|
||||
FileUrl: refreshedResult.FullPath,
|
||||
FilePath: refreshedResult.Path,
|
||||
FileSize: refreshedResult.Size,
|
||||
FileMd5: refreshedResult.Md5,
|
||||
DownloadCount: 0,
|
||||
MaxDownloadCount: 0,
|
||||
DeptId: batch.Party.CreatedDept,
|
||||
StartTime: now,
|
||||
FinishTime: now,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: batch.Party.CreatedDept,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "写入续费批次结果下载记录失败")
|
||||
}
|
||||
files.DownloadId = downloadId
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func appRenewBatchLogFilesFromInfo(info *gjson.Json) *appRenewBatchLogFiles {
|
||||
files := &appRenewBatchLogFiles{}
|
||||
if info == nil {
|
||||
return files
|
||||
}
|
||||
files.ImportOssId = info.Get("importFileOssId").Int64()
|
||||
files.ResultOssId = info.Get("resultFileOssId").Int64()
|
||||
files.DownloadId = info.Get("resultDownloadId").Int64()
|
||||
return files
|
||||
}
|
||||
|
||||
func appRenewBatchResultDownload(ctx context.Context, tx gdb.TX, batchNo string) (*appRenewBatchLogFiles, error) {
|
||||
var downloads []*entity.TResourceBatchDownload
|
||||
columns := dao.TResourceBatchDownload.Columns()
|
||||
err := dao.TResourceBatchDownload.Ctx(ctx).TX(tx).
|
||||
Fields(columns.Id, columns.OssId).
|
||||
Where(columns.BatchNo, strings.TrimSpace(batchNo)).
|
||||
OrderDesc(columns.Id).
|
||||
Limit(1).
|
||||
Scan(&downloads)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询续费批次结果下载记录失败")
|
||||
}
|
||||
if len(downloads) == 0 || downloads[0] == nil || downloads[0].Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &appRenewBatchLogFiles{
|
||||
DownloadId: downloads[0].Id,
|
||||
ResultOssId: downloads[0].OssId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appRenewBatchInfoWithLogFiles(info *gjson.Json, files *appRenewBatchLogFiles) *gjson.Json {
|
||||
data := map[string]any{}
|
||||
if info != nil {
|
||||
for key, value := range info.Map() {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
if files != nil {
|
||||
if files.ImportOssId > 0 {
|
||||
data["importFileOssId"] = files.ImportOssId
|
||||
}
|
||||
if files.ResultOssId > 0 {
|
||||
data["resultFileOssId"] = files.ResultOssId
|
||||
}
|
||||
if files.DownloadId > 0 {
|
||||
data["resultDownloadId"] = files.DownloadId
|
||||
}
|
||||
}
|
||||
return gjson.New(data)
|
||||
}
|
||||
|
||||
func appSaveRenewBatchOss(ctx context.Context, tx gdb.TX, user *entity.SysUser, deptId int64, content []byte, fileName string, subDirName string, now *gtime.Time) (model.UploadResponse, int64, error) {
|
||||
upload, err := service.SysOss().SaveContentLocal(ctx, content, consts.SysUploadNewFileTypeUserFile, fileName, subDirName, 0)
|
||||
if err != nil {
|
||||
return upload, 0, err
|
||||
}
|
||||
ossId, err := dao.SysOss.Ctx(ctx).TX(tx).Data(do.SysOss{
|
||||
TenantId: user.TenantId,
|
||||
FileName: upload.Name,
|
||||
OriginalName: upload.OriginalName,
|
||||
FileSuffix: upload.Ext,
|
||||
Path: upload.Path,
|
||||
Url: upload.FullPath,
|
||||
CreatedDept: deptId,
|
||||
CreatedAt: now,
|
||||
CreatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
Service: "local",
|
||||
Md5: upload.Md5,
|
||||
FileSize: upload.Size,
|
||||
FileCrc16: upload.Crc16,
|
||||
FileSum: upload.Sum16,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return upload, 0, gerror.Wrap(err, "写入续费批次OSS记录失败")
|
||||
}
|
||||
return upload, ossId, nil
|
||||
}
|
||||
|
||||
func appRenewBatchImportHeaders() []string {
|
||||
return []string{
|
||||
"ICCID",
|
||||
"MSISDN",
|
||||
"IMSI",
|
||||
"续费年限",
|
||||
"是否走线下",
|
||||
}
|
||||
}
|
||||
|
||||
func appBuildRenewBatchImportExcel(batch *appRenewBatchContext, sourceBatchNo string, durationMonths int, buyerNote string, now *gtime.Time) ([]byte, error) {
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
sheet := "Sheet1"
|
||||
headers := appRenewBatchImportHeaders()
|
||||
if err := f.SetSheetRow(sheet, "A1", &headers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 源文件保持管理平台续费导入模板格式,方便批次日志统一下载和排查。
|
||||
if err := appApplyHeaderColumnsTextStyle(f, sheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
durationYears := appRenewDurationYearsFromLegacy(durationMonths, batch.Durations)
|
||||
for index, item := range batch.Items {
|
||||
row := []interface{}{
|
||||
strings.TrimSpace(item.Card.Iccid),
|
||||
strings.TrimSpace(item.Card.Msisdn),
|
||||
strings.TrimSpace(item.Card.Imsi),
|
||||
durationYears,
|
||||
true,
|
||||
}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(sheet, cell, &row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_ = f.SetColWidth(sheet, "A", "A", 24)
|
||||
_ = f.SetColWidth(sheet, "B", "B", 14)
|
||||
_ = f.SetColWidth(sheet, "C", "C", 18)
|
||||
_ = f.SetColWidth(sheet, "D", "D", 18)
|
||||
_ = f.SetColWidth(sheet, "E", "E", 18)
|
||||
return appExcelBytes(f)
|
||||
}
|
||||
|
||||
func appBuildRenewBatchResultExcel(batch *appRenewBatchContext, files *appRenewBatchLogFiles, batchNo string, orderSn string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, cardPeriods []map[string]any, now *gtime.Time, userId int64, resultOssId int64) ([]byte, error) {
|
||||
f := excelize.NewFile()
|
||||
defer f.Close()
|
||||
|
||||
leftStyleID, err := f.NewStyle(&excelize.Style{
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "left",
|
||||
Vertical: "center",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultSheet := "导入结果"
|
||||
if err = f.SetSheetName("Sheet1", resultSheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers := []string{"源文件行号", "item_id", "处理状态", "异常原因"}
|
||||
if err = f.SetSheetRow(resultSheet, "A1", &headers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index, item := range batch.Items {
|
||||
row := []interface{}{
|
||||
appRenewBatchSourceRowNo(index),
|
||||
strings.TrimSpace(item.Card.Iccid),
|
||||
"成功",
|
||||
"",
|
||||
}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(resultSheet, cell, &row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resultEndCell, err := excelize.CoordinatesToCellName(4, len(batch.Items)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetCellStyle(resultSheet, "A1", resultEndCell, leftStyleID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infoSheet := "批次信息"
|
||||
if _, err = f.NewSheet(infoSheet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoHeaders := []string{"字段", "值"}
|
||||
if err = f.SetSheetRow(infoSheet, "A1", &infoHeaders); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoRows := appRenewBatchResultInfoRows(batch, files, batchNo, durationMonths, singleCardAmount, totalAmount, beginAt, endAt, now, userId, resultOssId)
|
||||
for index, item := range infoRows {
|
||||
row := []interface{}{item[0], item[1]}
|
||||
cell, err := excelize.CoordinatesToCellName(1, index+2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetSheetRow(infoSheet, cell, &row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
infoEndCell, err := excelize.CoordinatesToCellName(2, len(infoRows)+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.SetCellStyle(infoSheet, "A1", infoEndCell, leftStyleID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = f.SetColWidth(resultSheet, "A", "A", 14)
|
||||
_ = f.SetColWidth(resultSheet, "B", "B", 24)
|
||||
_ = f.SetColWidth(resultSheet, "C", "C", 12)
|
||||
_ = f.SetColWidth(resultSheet, "D", "D", 40)
|
||||
_ = f.SetColWidth(infoSheet, "A", "B", 24)
|
||||
f.SetActiveSheet(0)
|
||||
return appExcelBytes(f)
|
||||
}
|
||||
|
||||
func appApplyHeaderColumnsTextStyle(f *excelize.File, sheetName string) error {
|
||||
if f == nil {
|
||||
return gerror.New("Excel文件不能为空")
|
||||
}
|
||||
sheetName = strings.TrimSpace(sheetName)
|
||||
if sheetName == "" {
|
||||
sheetName = f.GetSheetName(f.GetActiveSheetIndex())
|
||||
}
|
||||
if sheetName == "" {
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return gerror.New("Excel文件没有工作表")
|
||||
}
|
||||
sheetName = sheets[0]
|
||||
}
|
||||
rows, err := f.GetRows(sheetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 || len(rows[0]) == 0 {
|
||||
return gerror.New("Excel文件表头为空")
|
||||
}
|
||||
styleID, err := f.NewStyle(&excelize.Style{NumFmt: 49})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for index, header := range rows[0] {
|
||||
if strings.TrimSpace(header) == "" {
|
||||
continue
|
||||
}
|
||||
colName, err := excelize.ColumnNumberToName(index + 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetColStyle(sheetName, colName, styleID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appRenewBatchResultInfoRows(batch *appRenewBatchContext, files *appRenewBatchLogFiles, batchNo string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, now *gtime.Time, userId int64, resultOssId int64) [][2]string {
|
||||
importFileName := ""
|
||||
importOssId := ""
|
||||
if files != nil {
|
||||
importFileName = strings.TrimSpace(files.ImportFileName)
|
||||
if files.ImportOssId > 0 {
|
||||
importOssId = fmt.Sprintf("%d", files.ImportOssId)
|
||||
}
|
||||
}
|
||||
if importFileName == "" {
|
||||
importFileName = appRenewBatchImportFileNameByBatchNo(batchNo)
|
||||
}
|
||||
resultFileOssId := ""
|
||||
if resultOssId > 0 {
|
||||
resultFileOssId = fmt.Sprintf("%d", resultOssId)
|
||||
}
|
||||
return [][2]string{
|
||||
{"批次号", strings.TrimSpace(batchNo)},
|
||||
{"批次类型", "卡续费"},
|
||||
{"处理结果", "成功"},
|
||||
{"总行数", fmt.Sprintf("%d", len(batch.Items))},
|
||||
{"校验通过行数", fmt.Sprintf("%d", len(batch.Items))},
|
||||
{"失败行数", "0"},
|
||||
{"实际写入行数", fmt.Sprintf("%d", len(batch.Items))},
|
||||
{"原始文件名", importFileName},
|
||||
{"原始文件OSS ID", importOssId},
|
||||
{"结果文件OSS ID", resultFileOssId},
|
||||
{"操作人", fmt.Sprintf("%d", userId)},
|
||||
{"开始时间", appRenewFormatTime(now)},
|
||||
{"结束时间", appRenewFormatTime(now)},
|
||||
{"备注", "导入成功"},
|
||||
}
|
||||
}
|
||||
|
||||
func appFillRenewResultOssId(ctx context.Context, tx gdb.TX, ossId int64, filePath string, userId int64, now *gtime.Time) error {
|
||||
if ossId <= 0 || strings.TrimSpace(filePath) == "" {
|
||||
return gerror.New("续费结果文件OSS信息不完整")
|
||||
}
|
||||
f, err := excelize.OpenFile(filePath)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "打开续费结果文件失败")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
rows, err := f.GetRows("批次信息")
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取续费结果批次信息失败")
|
||||
}
|
||||
found := false
|
||||
for index, row := range rows {
|
||||
if len(row) == 0 || strings.TrimSpace(row[0]) != "结果文件OSS ID" {
|
||||
continue
|
||||
}
|
||||
cell, err := excelize.CoordinatesToCellName(2, index+1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetCellStr("批次信息", cell, fmt.Sprintf("%d", ossId)); err != nil {
|
||||
return err
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
return gerror.New("续费结果文件缺少结果文件OSS ID字段")
|
||||
}
|
||||
if err = f.SaveAs(filePath); err != nil {
|
||||
return gerror.Wrap(err, "回写续费结果文件OSS ID失败")
|
||||
}
|
||||
return appRefreshRenewOssFileInfo(ctx, tx, ossId, filePath, userId, now)
|
||||
}
|
||||
|
||||
func appRefreshRenewOssFileInfo(ctx context.Context, tx gdb.TX, ossId int64, filePath string, userId int64, now *gtime.Time) error {
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取续费结果文件信息失败")
|
||||
}
|
||||
md5, _ := gmd5.EncryptFile(filePath)
|
||||
crc16 := hash.Crc16ChecksumFile(filePath)
|
||||
sum16 := hash.Sum16ChecksumFile(filePath)
|
||||
_, err = dao.SysOss.Ctx(ctx).TX(tx).
|
||||
Where(dao.SysOss.Columns().OssId, ossId).
|
||||
Data(do.SysOss{
|
||||
Md5: md5,
|
||||
FileSize: fileInfo.Size(),
|
||||
FileCrc16: crc16,
|
||||
FileSum: sum16,
|
||||
UpdatedAt: now,
|
||||
UpdatedBy: userId,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新续费结果文件信息失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appLoadRenewOssUpload(ctx context.Context, tx gdb.TX, ossId int64, fallback model.UploadResponse) (model.UploadResponse, error) {
|
||||
if ossId <= 0 {
|
||||
return fallback, nil
|
||||
}
|
||||
var oss *entity.SysOss
|
||||
err := dao.SysOss.Ctx(ctx).TX(tx).
|
||||
Where(dao.SysOss.Columns().OssId, ossId).
|
||||
Limit(1).
|
||||
Scan(&oss)
|
||||
if err != nil {
|
||||
return fallback, gerror.Wrap(err, "读取续费结果文件OSS记录失败")
|
||||
}
|
||||
if oss == nil || oss.OssId == 0 {
|
||||
return fallback, nil
|
||||
}
|
||||
fallback.Name = oss.FileName
|
||||
fallback.OriginalName = oss.OriginalName
|
||||
fallback.Ext = oss.FileSuffix
|
||||
fallback.Path = oss.Path
|
||||
fallback.FullPath = oss.Url
|
||||
fallback.Size = int64(oss.FileSize)
|
||||
fallback.Md5 = oss.Md5
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
func appRenewBatchImportFileName(ctx context.Context, tx gdb.TX, ossId int64, sourceName string) string {
|
||||
if ossId <= 0 {
|
||||
sourceName = strings.TrimSpace(sourceName)
|
||||
if sourceName != "" {
|
||||
return sourceName
|
||||
}
|
||||
return "input-prepare"
|
||||
}
|
||||
var oss *entity.SysOss
|
||||
if err := dao.SysOss.Ctx(ctx).TX(tx).
|
||||
Fields(dao.SysOss.Columns().FileName).
|
||||
Where(dao.SysOss.Columns().OssId, ossId).
|
||||
Limit(1).
|
||||
Scan(&oss); err != nil {
|
||||
return strings.TrimSpace(sourceName)
|
||||
}
|
||||
if oss != nil && strings.TrimSpace(oss.FileName) != "" {
|
||||
return strings.TrimSpace(oss.FileName)
|
||||
}
|
||||
return strings.TrimSpace(sourceName)
|
||||
}
|
||||
|
||||
func appRenewBatchImportFileNameByBatchNo(batchNo string) string {
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
if batchNo == "" {
|
||||
return "input-prepare"
|
||||
}
|
||||
return batchNo + "-import.xlsx"
|
||||
}
|
||||
|
||||
func appRenewBatchRenewDate(now *gtime.Time) string {
|
||||
if now == nil {
|
||||
return ""
|
||||
}
|
||||
return now.Format("Y-m-d") + " 00:00:00"
|
||||
}
|
||||
|
||||
func appExcelBytes(f *excelize.File) ([]byte, error) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if err := f.Write(buffer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func TestAppBuildRenewBatchImportExcelMatchesRenewTemplate(t *testing.T) {
|
||||
data, err := appBuildRenewBatchImportExcel(appRenewResultTestBatch(), "REN20260709164715A8F22BD5", 3, "测试备注", gtime.NewFromStr("2026-07-09 16:47:15"))
|
||||
if err != nil {
|
||||
t.Fatalf("build import excel: %v", err)
|
||||
}
|
||||
|
||||
f, err := excelize.OpenReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatalf("open import excel: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
headers, err := f.GetRows("Sheet1")
|
||||
if err != nil {
|
||||
t.Fatalf("read import rows: %v", err)
|
||||
}
|
||||
wantHeaders := []string{"ICCID", "MSISDN", "IMSI", "续费年限", "是否走线下"}
|
||||
if len(headers) < 2 || !reflect.DeepEqual(headers[0], wantHeaders) {
|
||||
t.Fatalf("headers mismatch: %#v", headers)
|
||||
}
|
||||
wantRow := []string{"898604F4152391181413", "1441234567890", "460041234567890", "3", "TRUE"}
|
||||
if !reflect.DeepEqual(headers[1], wantRow) {
|
||||
t.Fatalf("row mismatch: %#v", headers[1])
|
||||
}
|
||||
|
||||
for _, cell := range []string{"A1", "B1", "C1", "D1", "E1", "A2", "B2", "C2", "D2", "E2"} {
|
||||
styleID, err := f.GetCellStyle("Sheet1", cell)
|
||||
if err != nil {
|
||||
t.Fatalf("get style %s: %v", cell, err)
|
||||
}
|
||||
style, err := f.GetStyle(styleID)
|
||||
if err != nil {
|
||||
t.Fatalf("get style detail %s: %v", cell, err)
|
||||
}
|
||||
if style.NumFmt != 49 {
|
||||
t.Fatalf("%s numFmt = %d, want 49", cell, style.NumFmt)
|
||||
}
|
||||
}
|
||||
for col, want := range map[string]float64{
|
||||
"A": 24,
|
||||
"B": 14,
|
||||
"C": 18,
|
||||
"D": 18,
|
||||
"E": 18,
|
||||
} {
|
||||
got, err := f.GetColWidth("Sheet1", col)
|
||||
if err != nil {
|
||||
t.Fatalf("get width %s: %v", col, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("width %s = %v, want %v", col, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppBuildRenewBatchResultExcelMatchesBatchResultFormat(t *testing.T) {
|
||||
data, err := appBuildRenewBatchResultExcel(appRenewResultTestBatch(), &appRenewBatchLogFiles{
|
||||
ImportOssId: 309,
|
||||
ImportFileName: "REN20260709164715A8F22BD5-import.xlsx",
|
||||
}, "REN20260709164715A8F22BD5", "SO202607091647150000875NYX", 3, 30, 30, nil, nil, nil, gtime.NewFromStr("2026-07-09 16:47:15"), 87, 310)
|
||||
if err != nil {
|
||||
t.Fatalf("build result excel: %v", err)
|
||||
}
|
||||
|
||||
f, err := excelize.OpenReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatalf("open result excel: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if sheets := f.GetSheetList(); !reflect.DeepEqual(sheets, []string{"导入结果", "批次信息"}) {
|
||||
t.Fatalf("sheets mismatch: %#v", sheets)
|
||||
}
|
||||
resultRows, err := f.GetRows("导入结果")
|
||||
if err != nil {
|
||||
t.Fatalf("read result rows: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(resultRows[0], []string{"源文件行号", "item_id", "处理状态", "异常原因"}) {
|
||||
t.Fatalf("result headers mismatch: %#v", resultRows[0])
|
||||
}
|
||||
if !reflect.DeepEqual(resultRows[1][:3], []string{"2", "898604F4152391181413", "成功"}) {
|
||||
t.Fatalf("result row mismatch: %#v", resultRows[1])
|
||||
}
|
||||
|
||||
infoRows, err := f.GetRows("批次信息")
|
||||
if err != nil {
|
||||
t.Fatalf("read info rows: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(infoRows[0], []string{"字段", "值"}) {
|
||||
t.Fatalf("info headers mismatch: %#v", infoRows[0])
|
||||
}
|
||||
if !reflect.DeepEqual(infoRows[1], []string{"批次号", "REN20260709164715A8F22BD5"}) {
|
||||
t.Fatalf("info row mismatch: %#v", infoRows[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRenewBatchResultInfoRowsUsesBatchImportFileName(t *testing.T) {
|
||||
rows := appRenewBatchResultInfoRows(
|
||||
appRenewResultTestBatch(),
|
||||
&appRenewBatchLogFiles{},
|
||||
"REN202607091748552P7KDBXN",
|
||||
3,
|
||||
10,
|
||||
10,
|
||||
nil,
|
||||
nil,
|
||||
gtime.NewFromStr("2026-07-09 17:48:56"),
|
||||
87,
|
||||
340,
|
||||
)
|
||||
info := map[string]string{}
|
||||
for _, row := range rows {
|
||||
info[row[0]] = row[1]
|
||||
}
|
||||
if info["原始文件名"] != "REN202607091748552P7KDBXN-import.xlsx" {
|
||||
t.Fatalf("source file name = %q, want batch import file name", info["原始文件名"])
|
||||
}
|
||||
if info["原始文件OSS ID"] != "" {
|
||||
t.Fatalf("source file oss id = %q, want empty", info["原始文件OSS ID"])
|
||||
}
|
||||
if info["结果文件OSS ID"] != "340" {
|
||||
t.Fatalf("result file oss id = %q, want 340", info["结果文件OSS ID"])
|
||||
}
|
||||
}
|
||||
|
||||
func appRenewResultTestBatch() *appRenewBatchContext {
|
||||
return &appRenewBatchContext{
|
||||
CurrentPackage: &entity.TSimFlowPackageInfo{
|
||||
PackageCode: "OLDPKG",
|
||||
PackageName: "原套餐",
|
||||
},
|
||||
Package: &entity.TSimFlowPackageInfo{
|
||||
Id: 2001,
|
||||
PackageCode: "NEWPKG",
|
||||
PackageName: "续费套餐",
|
||||
},
|
||||
Price: &appRenewPriceSnapshot{
|
||||
UnitPrice: 10,
|
||||
PriceSource: "system",
|
||||
},
|
||||
Party: &appRenewOrderParty{
|
||||
CustomerAgentId: 175,
|
||||
CreatedDept: 175,
|
||||
},
|
||||
Durations: []int{1, 2, 3},
|
||||
OwnerType: appCardOwnerAgent,
|
||||
RenewablePackageCodes: []string{"NEWPKG"},
|
||||
Items: []*appRenewPackageContext{
|
||||
{
|
||||
ExpectedCurrentPackageCode: "OLDPKG",
|
||||
Card: &appCardQueryRow{
|
||||
TSimCardInfo: entity.TSimCardInfo{
|
||||
Id: 1001,
|
||||
Iccid: "898604F4152391181413",
|
||||
Msisdn: "1441234567890",
|
||||
Imsi: "460041234567890",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,147 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func TestAppLifecycleShouldPublishWorkerIncludesRestart(t *testing.T) {
|
||||
for _, code := range []string{"stop", "start", "restart"} {
|
||||
operation, err := appCardLifecycleOperationByCode(code)
|
||||
if err != nil {
|
||||
t.Fatalf("operation %s: %v", code, err)
|
||||
}
|
||||
if !appLifecycleShouldPublishWorker(operation) {
|
||||
t.Fatalf("%s should publish worker batch", code)
|
||||
}
|
||||
}
|
||||
|
||||
if appLifecycleShouldPublishWorker(appCardLifecycleOperation{BatchType: -1}) {
|
||||
t.Fatal("unsupported batch type should not publish worker batch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppLifecycleBatchInfoMatchesWorkerRestartProtocol(t *testing.T) {
|
||||
operation, err := appCardLifecycleOperationByCode("restart")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
info := appLifecycleBatchInfo(operation, []*appCardLifecycleItem{testLifecycleItem()}, "RST20260901094936", "manual", gtime.NewFromStr("2026-09-01 09:49:36"))
|
||||
data := info.Map()
|
||||
|
||||
if info.Get("operation").String() != "restart" {
|
||||
t.Fatalf("operation = %q", info.Get("operation").String())
|
||||
}
|
||||
if info.Get("stopOperType").String() != consts.SimCardStatusOperTypeStop {
|
||||
t.Fatalf("stopOperType = %q", info.Get("stopOperType").String())
|
||||
}
|
||||
if info.Get("resumeOperType").String() != consts.SimCardStatusOperTypeStart {
|
||||
t.Fatalf("resumeOperType = %q", info.Get("resumeOperType").String())
|
||||
}
|
||||
if info.Get("resumeDelaySeconds").Int() != appLifecycleRestartDelaySeconds {
|
||||
t.Fatalf("resumeDelaySeconds = %d", info.Get("resumeDelaySeconds").Int())
|
||||
}
|
||||
if info.Get("restart.phase").String() != appLifecycleRestartPhaseCreated {
|
||||
t.Fatalf("restart.phase = %q", info.Get("restart.phase").String())
|
||||
}
|
||||
if _, ok := data["operType"]; ok {
|
||||
t.Fatal("restart batch should not contain single-stage operType")
|
||||
}
|
||||
if _, ok := data["onelinkMock"]; ok {
|
||||
t.Fatal("worker batch info should not contain onelinkMock")
|
||||
}
|
||||
if _, ok := data["localMock"]; ok {
|
||||
t.Fatal("worker batch info should not contain localMock")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppLifecycleBatchInfoUsesOperTypeForStopAndStart(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
want string
|
||||
}{
|
||||
{code: "stop", want: consts.SimCardStatusOperTypeStop},
|
||||
{code: "start", want: consts.SimCardStatusOperTypeStart},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
operation, err := appCardLifecycleOperationByCode(tt.code)
|
||||
if err != nil {
|
||||
t.Fatalf("operation %s: %v", tt.code, err)
|
||||
}
|
||||
info := appLifecycleBatchInfo(operation, []*appCardLifecycleItem{testLifecycleItem()}, "BATCH", "", gtime.Now())
|
||||
if got := info.Get("operType").String(); got != tt.want {
|
||||
t.Fatalf("%s operType = %q, want %q", tt.code, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppLifecycleBatchInfoMatchesWorkerDataServiceProtocol(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
batchType int
|
||||
operType int
|
||||
}{
|
||||
{code: "dataStart", batchType: consts.BatchTypeDataStart, operType: consts.SimCardDataOperTypeStart},
|
||||
{code: "dataStop", batchType: consts.BatchTypeDataStop, operType: consts.SimCardDataOperTypeStop},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
operation, err := appCardLifecycleOperationByCode(tt.code)
|
||||
if err != nil {
|
||||
t.Fatalf("operation %s: %v", tt.code, err)
|
||||
}
|
||||
if operation.BatchType != tt.batchType {
|
||||
t.Fatalf("%s batch type = %d, want %d", tt.code, operation.BatchType, tt.batchType)
|
||||
}
|
||||
if !appLifecycleShouldPublishWorker(operation) {
|
||||
t.Fatalf("%s should publish worker batch", tt.code)
|
||||
}
|
||||
info := appLifecycleBatchInfo(operation, []*appCardLifecycleItem{testLifecycleItem()}, "BATCH", "", gtime.Now())
|
||||
if got := info.Get("operType").Int(); got != tt.operType {
|
||||
t.Fatalf("%s operType = %d, want %d", tt.code, got, tt.operType)
|
||||
}
|
||||
if got := info.Get("apnName").String(); got != consts.SimCardDataApnName {
|
||||
t.Fatalf("%s apnName = %q", tt.code, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppLifecyclePendingDetailSnapshotDoesNotMarkWorkerBatchAsMock(t *testing.T) {
|
||||
operation, err := appCardLifecycleOperationByCode("restart")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
snapshot := appLifecyclePendingDetailSnapshot(operation, testLifecycleItem(), "RST20260901094936")
|
||||
data := snapshot.Map()
|
||||
|
||||
if _, ok := data["onelinkMock"]; ok {
|
||||
t.Fatal("worker detail snapshot should not contain onelinkMock")
|
||||
}
|
||||
if _, ok := data["localMock"]; ok {
|
||||
t.Fatal("worker detail snapshot should not contain localMock")
|
||||
}
|
||||
}
|
||||
|
||||
func testLifecycleItem() *appCardLifecycleItem {
|
||||
card := &appCardQueryRow{
|
||||
TSimCardInfo: entity.TSimCardInfo{
|
||||
Id: 1,
|
||||
Iccid: "89860400000000000001",
|
||||
Msisdn: "1064900000001",
|
||||
Imsi: "460040000000001",
|
||||
ChanKey: "onelink",
|
||||
BossCardStatus: consts.V3ApiSimBossCardStatusActivated,
|
||||
},
|
||||
}
|
||||
return &appCardLifecycleItem{
|
||||
Card: card,
|
||||
RowNo: 2,
|
||||
StatusBefore: card.BossCardStatus,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func (s *sAppCard) attachAppCardRealtimeSession(ctx context.Context, row *appCardQueryRow, card *model.AppCardModel) *model.AppCardModel {
|
||||
if row == nil || card == nil {
|
||||
return card
|
||||
}
|
||||
if strings.TrimSpace(row.Msisdn) == "" && strings.TrimSpace(row.Iccid) == "" && strings.TrimSpace(row.Imsi) == "" {
|
||||
return card
|
||||
}
|
||||
session, source, err := queryAppCardRealtimeSession(ctx, row)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "app card session query failed, iccid:%s, msisdn:%s, err:%+v", row.Iccid, row.Msisdn, err)
|
||||
return card
|
||||
}
|
||||
if source == "onelink_mock" {
|
||||
g.Log().Debugf(ctx, "app card session loaded from onelink mock, iccid:%s", row.Iccid)
|
||||
}
|
||||
if session == nil {
|
||||
return card
|
||||
}
|
||||
return applyAppCardRealtimeSession(card, session)
|
||||
}
|
||||
|
||||
func applyAppCardRealtimeSession(card *model.AppCardModel, session *appOneLinkMockSimSessionItem) *model.AppCardModel {
|
||||
if card == nil || session == nil {
|
||||
return card
|
||||
}
|
||||
card.OnlineStatus = strings.TrimSpace(session.Status)
|
||||
card.ApnId = strings.TrimSpace(session.ApnId)
|
||||
card.Rat = strings.TrimSpace(session.Rat)
|
||||
card.Ipv4 = strings.TrimSpace(session.Ipv4)
|
||||
card.Ipv6 = strings.TrimSpace(session.Ipv6)
|
||||
card.Ipv6Prefix = strings.TrimSpace(session.Ipv6Prefix)
|
||||
card.SessionCreateTime = strings.TrimSpace(session.CreateTime)
|
||||
return card
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/appidentity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchimport"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const appCardBatchNoAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
|
||||
func (s *sAppCard) isTerminalUser(ctx context.Context, user *entity.SysUser) bool {
|
||||
return appIsTerminalUser(ctx, user)
|
||||
}
|
||||
|
||||
func (s *sAppCard) ownerType(ctx context.Context, user *entity.SysUser) string {
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
return appCardOwnerTerminal
|
||||
}
|
||||
return appCardOwnerAgent
|
||||
}
|
||||
|
||||
func (s *sAppCard) canOperateCard(ctx context.Context, user *entity.SysUser, card *appCardQueryRow) bool {
|
||||
if user == nil || card == nil {
|
||||
return false
|
||||
}
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
return card.TerminalUserId == user.UserId
|
||||
}
|
||||
return user.DeptId > 0
|
||||
}
|
||||
|
||||
func (s *sAppCard) ensureOperateCard(ctx context.Context, user *entity.SysUser, card *appCardQueryRow, actionName string) error {
|
||||
if s.canOperateCard(ctx, user, card) {
|
||||
return nil
|
||||
}
|
||||
cardLabel := ""
|
||||
if card != nil {
|
||||
cardLabel = appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei)
|
||||
}
|
||||
actionName = strings.TrimSpace(actionName)
|
||||
if actionName == "" {
|
||||
actionName = "办理业务"
|
||||
}
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
return gerror.Newf("卡%s不在当前终端用户可操作范围内", cardLabel)
|
||||
}
|
||||
return gerror.Newf("卡%s不在当前企业可操作范围内", cardLabel)
|
||||
}
|
||||
|
||||
func (s *sAppCard) canOperatePackageRow(ctx context.Context, user *entity.SysUser, row *appPackageQueryRow) bool {
|
||||
if user == nil || row == nil {
|
||||
return false
|
||||
}
|
||||
if s.isTerminalUser(ctx, user) {
|
||||
return true
|
||||
}
|
||||
return user.DeptId > 0
|
||||
}
|
||||
|
||||
func appIsTerminalUser(ctx context.Context, user *entity.SysUser) bool {
|
||||
isTerminal, err := appidentity.IsTerminalUser(ctx, user)
|
||||
return err == nil && isTerminal
|
||||
}
|
||||
|
||||
// appEnterpriseDeptAndSubIds 返回企业账号可查看的机构范围:当前机构及所有下级机构。
|
||||
// 这里按 sys_dept.parent_id 逐层递归,不依赖 ancestors 字符串,避免祖级字段不规范导致漏数据。
|
||||
func appEnterpriseDeptAndSubIds(ctx context.Context, tenantId string, rootDeptId int64) ([]int64, error) {
|
||||
if rootDeptId <= 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
tenantId = strings.TrimSpace(tenantId)
|
||||
if tenantId == "" {
|
||||
tenantId = consts.DefaultSystemTenantCode
|
||||
}
|
||||
|
||||
seen := map[int64]struct{}{
|
||||
rootDeptId: {},
|
||||
}
|
||||
result := []int64{rootDeptId}
|
||||
level := []int64{rootDeptId}
|
||||
columns := dao.SysDept.Columns()
|
||||
|
||||
for len(level) > 0 {
|
||||
var rows []*entity.SysDept
|
||||
err := dao.SysDept.Ctx(ctx).
|
||||
Fields(columns.DeptId).
|
||||
Where(columns.TenantId, tenantId).
|
||||
WhereIn(columns.ParentId, level).
|
||||
WhereNull(columns.DeletedAt).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业下级机构失败")
|
||||
}
|
||||
|
||||
nextLevel := make([]int64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil || row.DeptId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[row.DeptId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[row.DeptId] = struct{}{}
|
||||
result = append(result, row.DeptId)
|
||||
nextLevel = append(nextLevel, row.DeptId)
|
||||
}
|
||||
level = nextLevel
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// appEnterpriseDisplayDept 按当前企业视角返回卡片归属展示机构。
|
||||
// 真实卡归属可能在下下级,前端只允许看到本级或直属下级,避免泄露更深层级组织信息。
|
||||
func appEnterpriseDisplayDept(ctx context.Context, tenantId string, rootDeptId int64, actualDeptId int64) (*entity.SysDept, error) {
|
||||
if rootDeptId <= 0 || actualDeptId <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if actualDeptId == rootDeptId {
|
||||
return appLoadDeptForDisplay(ctx, tenantId, rootDeptId)
|
||||
}
|
||||
|
||||
currentDeptId := actualDeptId
|
||||
seen := make(map[int64]struct{})
|
||||
for currentDeptId > 0 {
|
||||
if _, ok := seen[currentDeptId]; ok {
|
||||
return nil, nil
|
||||
}
|
||||
seen[currentDeptId] = struct{}{}
|
||||
dept, err := appLoadDeptForDisplay(ctx, tenantId, currentDeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if dept.ParentId == rootDeptId {
|
||||
return dept, nil
|
||||
}
|
||||
if dept.ParentId <= 0 || dept.ParentId == dept.DeptId {
|
||||
return nil, nil
|
||||
}
|
||||
currentDeptId = dept.ParentId
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func appLoadDeptForDisplay(ctx context.Context, tenantId string, deptId int64) (*entity.SysDept, error) {
|
||||
tenantId = strings.TrimSpace(tenantId)
|
||||
if tenantId == "" {
|
||||
tenantId = consts.DefaultSystemTenantCode
|
||||
}
|
||||
var dept *entity.SysDept
|
||||
if err := dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, tenantId).
|
||||
Where(dao.SysDept.Columns().DeptId, deptId).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
Limit(1).
|
||||
Scan(&dept); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询机构展示信息失败")
|
||||
}
|
||||
return dept, nil
|
||||
}
|
||||
|
||||
func appCurrentUser(ctx context.Context) (*entity.SysUser, error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId == 0 {
|
||||
return nil, gerror.New("登录已过期")
|
||||
}
|
||||
var user *entity.SysUser
|
||||
err := dao.SysUser.Ctx(ctx).
|
||||
Where(dao.SysUser.Columns().UserId, userId).
|
||||
Scan(&user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, gerror.New("用户不存在")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func appContextWithUser(ctx context.Context, user *entity.SysUser) context.Context {
|
||||
if user == nil || user.UserId <= 0 {
|
||||
return ctx
|
||||
}
|
||||
if current := contexts.GetUser(ctx); current != nil && current.ID > 0 && strings.TrimSpace(current.TenantId) != "" {
|
||||
return ctx
|
||||
}
|
||||
return contexts.Set(ctx, &model.Context{
|
||||
User: &model.Identity{
|
||||
BaseClaims: model.BaseClaims{
|
||||
ID: user.UserId,
|
||||
Username: user.UserName,
|
||||
NickName: user.NickName,
|
||||
TenantId: user.TenantId,
|
||||
DeptId: user.DeptId,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func appCardValueCondition() string {
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
return "(" + cardTable + "." + dao.TSimCardInfo.Columns().Iccid + " = ? OR " +
|
||||
cardTable + "." + dao.TSimCardInfo.Columns().Msisdn + " = ? OR " +
|
||||
cardTable + "." + dao.TSimCardInfo.Columns().Imsi + " = ? OR " +
|
||||
cardTable + "." + dao.TSimCardInfo.Columns().Imei + " = ?)"
|
||||
}
|
||||
|
||||
func appCardValueArgs(cardValue string) []interface{} {
|
||||
value := strings.TrimSpace(cardValue)
|
||||
return []interface{}{value, value, value, value}
|
||||
}
|
||||
|
||||
func appNextBatchNo(ctx context.Context, prefix string) (string, error) {
|
||||
return batchimport.NextBatchNoContext(ctx, prefix)
|
||||
}
|
||||
|
||||
func appCardBatchNo(prefix string, userId int64, now *gtime.Time) string {
|
||||
if now == nil {
|
||||
now = gtime.Now()
|
||||
}
|
||||
prefix = strings.TrimSpace(strings.ToUpper(prefix))
|
||||
random := make([]byte, 8)
|
||||
randomBytes := make([]byte, len(random))
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
fallback := strings.ToUpper(utility.RandomString(len(random)))
|
||||
for i := range random {
|
||||
random[i] = appCardBatchNoAlphabet[int(fallback[i])%len(appCardBatchNoAlphabet)]
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s", prefix, now.Format("YmdHis"), string(random))
|
||||
}
|
||||
for i, b := range randomBytes {
|
||||
random[i] = appCardBatchNoAlphabet[int(b)%len(appCardBatchNoAlphabet)]
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s", prefix, now.Format("YmdHis"), string(random))
|
||||
}
|
||||
|
||||
func appOrderNo(prefix string, userId int64, now *gtime.Time) string {
|
||||
return appCardBatchNo(prefix, userId, now)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func TestAppOrderNoMatchesBatchNoFormat(t *testing.T) {
|
||||
now := gtime.NewFromStr("2026-07-09 16:47:15")
|
||||
got := appOrderNo("SO", 875, now)
|
||||
want := regexp.MustCompile(`^SO20260709164715[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$`)
|
||||
if !want.MatchString(got) {
|
||||
t.Fatalf("order no = %q, want SO + yyyyMMddHHmmss + 8 chars", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
const (
|
||||
appCustomerTypeAll = "all"
|
||||
appCustomerTypeEnterprise = "enterprise"
|
||||
appCustomerTypeTerminal = "terminal"
|
||||
)
|
||||
|
||||
type appCustomerCardStat struct {
|
||||
DeptId int64 `orm:"dept_id"`
|
||||
TerminalId int64 `orm:"terminal_id"`
|
||||
CardCount int `orm:"card_count"`
|
||||
NormalCardCount int `orm:"normal_card_count"`
|
||||
}
|
||||
|
||||
type appTerminalCustomerIdRow struct {
|
||||
TerminalId int64 `orm:"terminal_id"`
|
||||
}
|
||||
|
||||
func (s *sAppUser) CustomerList(ctx context.Context, param *model.AppCustomerListParam) (items []*model.AppCustomerModel, total int, err error) {
|
||||
if param == nil {
|
||||
param = &model.AppCustomerListParam{}
|
||||
}
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err = appEnsureEnterpriseCustomerScope(ctx, user); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if param.Page <= 0 {
|
||||
param.Page = 1
|
||||
}
|
||||
if param.PageSize <= 0 {
|
||||
param.PageSize = 20
|
||||
}
|
||||
if param.PageSize > 200 {
|
||||
param.PageSize = 200
|
||||
}
|
||||
|
||||
customerType := appNormalizeCustomerType(param.CustomerType)
|
||||
if customerType == appCustomerTypeTerminal {
|
||||
return nil, 0, gerror.New("暂不支持查看终端客户")
|
||||
}
|
||||
|
||||
list, err := s.enterpriseCustomerList(ctx, user, param)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total = len(list)
|
||||
start := (param.Page - 1) * param.PageSize
|
||||
if start >= total {
|
||||
return []*model.AppCustomerModel{}, total, nil
|
||||
}
|
||||
end := start + param.PageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
return list[start:end], total, nil
|
||||
}
|
||||
|
||||
func (s *sAppUser) CustomerDetail(ctx context.Context, param *model.AppCustomerDetailParam) (*model.AppCustomerDetailModel, error) {
|
||||
if param == nil {
|
||||
param = &model.AppCustomerDetailParam{}
|
||||
}
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = appEnsureEnterpriseCustomerScope(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customerType := appNormalizeCustomerType(param.CustomerType)
|
||||
switch customerType {
|
||||
case appCustomerTypeEnterprise:
|
||||
return s.enterpriseCustomerDetail(ctx, user, param.CustomerId)
|
||||
case appCustomerTypeTerminal:
|
||||
return nil, gerror.New("暂不支持查看终端客户")
|
||||
default:
|
||||
return nil, gerror.New("客户类型不正确")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sAppUser) enterpriseCustomerList(ctx context.Context, user *entity.SysUser, param *model.AppCustomerListParam) ([]*model.AppCustomerModel, error) {
|
||||
depts, err := appEnterpriseDirectCustomerDepts(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
depts = appFilterEnterpriseDepts(depts, strings.TrimSpace(param.Keyword), strings.TrimSpace(param.Status))
|
||||
deptIds := appDeptIds(depts)
|
||||
stats, err := appCardStatsByDept(ctx, user.TenantId, deptIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profiles, err := appEnterpriseProfileMap(ctx, user.TenantId, deptIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := make([]*model.AppCustomerModel, 0, len(depts))
|
||||
for _, dept := range depts {
|
||||
profile := profiles[dept.DeptId]
|
||||
stat := stats[dept.DeptId]
|
||||
item := appEnterpriseCustomerModel(dept, profile, 0, stat)
|
||||
list = append(list, item)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *sAppUser) enterpriseCustomerDetail(ctx context.Context, user *entity.SysUser, deptId int64) (*model.AppCustomerDetailModel, error) {
|
||||
dept, err := appLoadCustomerDept(ctx, user.TenantId, deptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
return nil, gerror.New("企业客户不存在")
|
||||
}
|
||||
visible, err := appEnterpriseCustomerDeptVisible(ctx, user, dept.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !visible {
|
||||
return nil, gerror.New("企业客户不在当前企业可查看范围内")
|
||||
}
|
||||
profile, err := appEnterpriseProfileByDeptId(ctx, user.TenantId, dept.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats, err := appCardStatsByDept(ctx, user.TenantId, []int64{dept.DeptId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := appEnterpriseCustomerModel(dept, profile, 0, stats[dept.DeptId])
|
||||
return &model.AppCustomerDetailModel{
|
||||
AppCustomerModel: *item,
|
||||
EnterpriseProfile: appEnterpriseProfileModel(dept, profile),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppUser) terminalCustomerList(ctx context.Context, user *entity.SysUser, param *model.AppCustomerListParam) ([]*model.AppCustomerModel, error) {
|
||||
visibleDeptIds, err := appVisibleCustomerDeptIds(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
terminalIds, err := appTerminalIdsByVisibleCards(ctx, user.TenantId, visibleDeptIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(terminalIds) == 0 {
|
||||
return []*model.AppCustomerModel{}, nil
|
||||
}
|
||||
terminals, err := appTerminalMap(ctx, user.TenantId, terminalIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats, err := appCardStatsByTerminal(ctx, user.TenantId, visibleDeptIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(param.Keyword)
|
||||
status := strings.TrimSpace(param.Status)
|
||||
list := make([]*model.AppCustomerModel, 0, len(terminals))
|
||||
for _, terminalId := range terminalIds {
|
||||
terminal := terminals[terminalId]
|
||||
if terminal == nil || terminal.TerminalId == 0 {
|
||||
continue
|
||||
}
|
||||
if status != "" && strings.TrimSpace(terminal.Status) != status {
|
||||
continue
|
||||
}
|
||||
if keyword != "" && !appTerminalMatchesKeyword(terminal, keyword) {
|
||||
continue
|
||||
}
|
||||
item := appTerminalCustomerModel(terminal, stats[terminal.TerminalId])
|
||||
list = append(list, item)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *sAppUser) terminalCustomerDetail(ctx context.Context, user *entity.SysUser, terminalId int64) (*model.AppCustomerDetailModel, error) {
|
||||
visibleDeptIds, err := appVisibleCustomerDeptIds(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
visible, err := appTerminalHasVisibleCard(ctx, user.TenantId, terminalId, visibleDeptIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !visible {
|
||||
return nil, gerror.New("终端客户不在当前企业可查看范围内")
|
||||
}
|
||||
terminals, err := appTerminalMap(ctx, user.TenantId, []int64{terminalId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
terminal := terminals[terminalId]
|
||||
if terminal == nil || terminal.TerminalId == 0 {
|
||||
return nil, gerror.New("终端客户不存在")
|
||||
}
|
||||
stats, err := appCardStatsByTerminal(ctx, user.TenantId, visibleDeptIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := appTerminalCustomerModel(terminal, stats[terminal.TerminalId])
|
||||
return &model.AppCustomerDetailModel{
|
||||
AppCustomerModel: *item,
|
||||
TerminalProfile: appTerminalProfileModel(terminal),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appEnsureEnterpriseCustomerScope(ctx context.Context, user *entity.SysUser) error {
|
||||
if user == nil {
|
||||
return gerror.New("登录已过期")
|
||||
}
|
||||
if appIsTerminalUser(ctx, user) {
|
||||
return gerror.New("终端用户不能查看客户管理")
|
||||
}
|
||||
if user.DeptId <= 0 {
|
||||
return gerror.New("当前账号未绑定企业,不能查看客户管理")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appNormalizeCustomerType(customerType string) string {
|
||||
switch strings.TrimSpace(customerType) {
|
||||
case appCustomerTypeEnterprise:
|
||||
return appCustomerTypeEnterprise
|
||||
case appCustomerTypeTerminal:
|
||||
return appCustomerTypeTerminal
|
||||
default:
|
||||
return appCustomerTypeAll
|
||||
}
|
||||
}
|
||||
|
||||
func appDirectChildDepts(ctx context.Context, user *entity.SysUser) ([]*entity.SysDept, error) {
|
||||
var rows []*entity.SysDept
|
||||
err := dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, user.TenantId).
|
||||
Where(dao.SysDept.Columns().ParentId, user.DeptId).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
OrderAsc(dao.SysDept.Columns().OrderNum).
|
||||
OrderAsc(dao.SysDept.Columns().DeptId).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询直属下级企业客户失败")
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func appEnterpriseDirectCustomerDepts(ctx context.Context, user *entity.SysUser) ([]*entity.SysDept, error) {
|
||||
rows, err := appDirectChildDepts(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
excludeIds, err := appEnterpriseCustomerExcludedDeptIds(ctx, user.TenantId, user.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*entity.SysDept, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil || row.DeptId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, excluded := excludeIds[row.DeptId]; excluded {
|
||||
continue
|
||||
}
|
||||
result = append(result, row)
|
||||
}
|
||||
return appFilterDeptsByEnterpriseDataScope(ctx, user, result)
|
||||
}
|
||||
|
||||
func appEnterpriseSubCustomerDepts(ctx context.Context, user *entity.SysUser) ([]*entity.SysDept, error) {
|
||||
if user == nil || user.DeptId <= 0 {
|
||||
return []*entity.SysDept{}, nil
|
||||
}
|
||||
visibleIds, err := appEnterpriseDeptAndSubIds(ctx, user.TenantId, user.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
excludeIds, err := appEnterpriseCustomerExcludedDeptIds(ctx, user.TenantId, user.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deptIds := make([]int64, 0, len(visibleIds))
|
||||
for _, deptId := range visibleIds {
|
||||
if deptId <= 0 || deptId == user.DeptId {
|
||||
continue
|
||||
}
|
||||
if _, excluded := excludeIds[deptId]; excluded {
|
||||
continue
|
||||
}
|
||||
deptIds = append(deptIds, deptId)
|
||||
}
|
||||
if len(deptIds) == 0 {
|
||||
return []*entity.SysDept{}, nil
|
||||
}
|
||||
|
||||
var rows []*entity.SysDept
|
||||
err = dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, user.TenantId).
|
||||
WhereIn(dao.SysDept.Columns().DeptId, deptIds).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
OrderAsc(dao.SysDept.Columns().ParentId).
|
||||
OrderAsc(dao.SysDept.Columns().OrderNum).
|
||||
OrderAsc(dao.SysDept.Columns().DeptId).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询下级企业客户失败")
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func appEnterpriseCustomerDeptVisible(ctx context.Context, user *entity.SysUser, deptId int64) (bool, error) {
|
||||
if user == nil || user.DeptId <= 0 || deptId <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
if deptId == user.DeptId {
|
||||
return true, nil
|
||||
}
|
||||
excludeIds, err := appEnterpriseCustomerExcludedDeptIds(ctx, user.TenantId, user.DeptId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if _, excluded := excludeIds[deptId]; excluded {
|
||||
return false, nil
|
||||
}
|
||||
dept, err := appLoadCustomerDept(ctx, user.TenantId, deptId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if dept == nil || dept.DeptId <= 0 || dept.ParentId != user.DeptId {
|
||||
return false, nil
|
||||
}
|
||||
return appEnterpriseDeptVisibleByDataScope(ctx, user, dept.DeptId)
|
||||
}
|
||||
|
||||
func appEnterpriseCustomerExcludedDeptIds(ctx context.Context, tenantId string, currentDeptId int64) (map[int64]struct{}, error) {
|
||||
excludeIds := make(map[int64]struct{})
|
||||
registerDeptId := appRegisterDeptOrDefault(ctx, tenantId)
|
||||
if registerDeptId <= 0 {
|
||||
return excludeIds, nil
|
||||
}
|
||||
registerDeptIds, err := appEnterpriseDeptAndSubIds(ctx, tenantId, registerDeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, deptId := range registerDeptIds {
|
||||
if deptId == currentDeptId {
|
||||
return excludeIds, nil
|
||||
}
|
||||
}
|
||||
for _, deptId := range registerDeptIds {
|
||||
if deptId > 0 {
|
||||
excludeIds[deptId] = struct{}{}
|
||||
}
|
||||
}
|
||||
return excludeIds, nil
|
||||
}
|
||||
|
||||
func appVisibleCustomerDeptIds(ctx context.Context, user *entity.SysUser) ([]int64, error) {
|
||||
children, err := appEnterpriseSubCustomerDepts(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]int64, 0, len(children)+1)
|
||||
ids = append(ids, user.DeptId)
|
||||
for _, child := range children {
|
||||
if child != nil && child.DeptId > 0 {
|
||||
ids = append(ids, child.DeptId)
|
||||
}
|
||||
}
|
||||
depts := make([]*entity.SysDept, 0, len(ids))
|
||||
for _, deptId := range ids {
|
||||
depts = append(depts, &entity.SysDept{DeptId: deptId})
|
||||
}
|
||||
filtered, err := appFilterDeptsByEnterpriseDataScope(ctx, user, depts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appDeptIds(filtered), nil
|
||||
}
|
||||
|
||||
func appFilterEnterpriseDepts(depts []*entity.SysDept, keyword string, status string) []*entity.SysDept {
|
||||
result := make([]*entity.SysDept, 0, len(depts))
|
||||
for _, dept := range depts {
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
continue
|
||||
}
|
||||
if status != "" && strings.TrimSpace(dept.Status) != status {
|
||||
continue
|
||||
}
|
||||
if keyword != "" && !appDeptMatchesKeyword(dept, keyword) {
|
||||
continue
|
||||
}
|
||||
result = append(result, dept)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func appDeptMatchesKeyword(dept *entity.SysDept, keyword string) bool {
|
||||
if dept == nil {
|
||||
return false
|
||||
}
|
||||
fields := []string{
|
||||
dept.DeptName,
|
||||
dept.DeptKey,
|
||||
dept.Phone,
|
||||
dept.Email,
|
||||
dept.CreditCode,
|
||||
strconv.FormatInt(dept.DeptId, 10),
|
||||
}
|
||||
for _, field := range fields {
|
||||
if strings.Contains(strings.TrimSpace(field), keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appTerminalMatchesKeyword(terminal *entity.TTerminal, keyword string) bool {
|
||||
if terminal == nil {
|
||||
return false
|
||||
}
|
||||
fields := []string{
|
||||
terminal.TerminalName,
|
||||
terminal.Phone,
|
||||
terminal.Email,
|
||||
terminal.FullName,
|
||||
terminal.CreditCode,
|
||||
strconv.FormatInt(terminal.TerminalId, 10),
|
||||
}
|
||||
for _, field := range fields {
|
||||
if strings.Contains(strings.TrimSpace(field), keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appDeptIds(depts []*entity.SysDept) []int64 {
|
||||
ids := make([]int64, 0, len(depts))
|
||||
for _, dept := range depts {
|
||||
if dept != nil && dept.DeptId > 0 {
|
||||
ids = append(ids, dept.DeptId)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func appCardStatsByDept(ctx context.Context, tenantId string, deptIds []int64) (map[int64]*appCustomerCardStat, error) {
|
||||
result := make(map[int64]*appCustomerCardStat)
|
||||
if len(deptIds) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
var rows []*appCustomerCardStat
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
normalCountExpr := "SUM(CASE WHEN " + cardTable + "." + dao.TSimCardInfo.Columns().BossCardStatus + " = " + strconv.Itoa(consts.V3ApiSimBossCardStatusActivated) +
|
||||
" AND " + cardTable + "." + dao.TSimCardInfo.Columns().PackageStatus + " <> " + strconv.Itoa(consts.PackageStatusExpired) + " THEN 1 ELSE 0 END) AS normal_card_count"
|
||||
err := dao.TSimCardInfo.Ctx(ctx).
|
||||
Fields(cardTable+"."+dao.TSimCardInfo.Columns().DeptId+" AS dept_id").
|
||||
Fields("COUNT(1) AS card_count").
|
||||
Fields(normalCountExpr).
|
||||
Where(cardTable+"."+dao.TSimCardInfo.Columns().TenantId, tenantId).
|
||||
WhereIn(cardTable+"."+dao.TSimCardInfo.Columns().DeptId, deptIds).
|
||||
Group(cardTable + "." + dao.TSimCardInfo.Columns().DeptId).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "统计企业客户卡数量失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row != nil {
|
||||
result[row.DeptId] = row
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func appCardStatsByTerminal(ctx context.Context, tenantId string, visibleDeptIds []int64) (map[int64]*appCustomerCardStat, error) {
|
||||
result := make(map[int64]*appCustomerCardStat)
|
||||
if len(visibleDeptIds) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
bindTable := dao.TSimTerminal.Table()
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
var rows []*appCustomerCardStat
|
||||
normalCountExpr := "SUM(CASE WHEN " + cardTable + "." + dao.TSimCardInfo.Columns().BossCardStatus + " = " + strconv.Itoa(consts.V3ApiSimBossCardStatusActivated) +
|
||||
" AND " + cardTable + "." + dao.TSimCardInfo.Columns().PackageStatus + " <> " + strconv.Itoa(consts.PackageStatusExpired) + " THEN 1 ELSE 0 END) AS normal_card_count"
|
||||
err := dao.TSimTerminal.Ctx(ctx).
|
||||
Fields(bindTable+"."+dao.TSimTerminal.Columns().TerminalId+" AS terminal_id").
|
||||
Fields("COUNT(DISTINCT "+bindTable+"."+dao.TSimTerminal.Columns().Iccid+") AS card_count").
|
||||
Fields(normalCountExpr).
|
||||
InnerJoin(cardTable, cardTable+"."+dao.TSimCardInfo.Columns().Iccid+"="+bindTable+"."+dao.TSimTerminal.Columns().Iccid+" AND "+cardTable+"."+dao.TSimCardInfo.Columns().TenantId+"="+bindTable+"."+dao.TSimTerminal.Columns().TenantId).
|
||||
Where(bindTable+"."+dao.TSimTerminal.Columns().TenantId, tenantId).
|
||||
WhereIn(cardTable+"."+dao.TSimCardInfo.Columns().DeptId, visibleDeptIds).
|
||||
Group(bindTable + "." + dao.TSimTerminal.Columns().TerminalId).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "统计终端客户卡数量失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row != nil {
|
||||
result[row.TerminalId] = row
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func appAgentBalanceMap(ctx context.Context, tenantId string, deptIds []int64) (map[int64]float64, error) {
|
||||
result := make(map[int64]float64)
|
||||
if len(deptIds) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
var rows []*entity.TAgentBalance
|
||||
err := dao.TAgentBalance.Ctx(ctx).
|
||||
Where(dao.TAgentBalance.Columns().TenantId, tenantId).
|
||||
WhereIn(dao.TAgentBalance.Columns().DeptId, deptIds).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业客户余额失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row != nil {
|
||||
result[row.DeptId] = appRoundAmount(row.Balance)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func appEnterpriseProfileMap(ctx context.Context, tenantId string, deptIds []int64) (map[int64]*entity.TAgentEnterprise, error) {
|
||||
result := make(map[int64]*entity.TAgentEnterprise)
|
||||
if len(deptIds) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
var rows []*entity.TAgentEnterprise
|
||||
err := dao.TAgentEnterprise.Ctx(ctx).
|
||||
Where(dao.TAgentEnterprise.Columns().TenantId, tenantId).
|
||||
WhereIn(dao.TAgentEnterprise.Columns().DeptId, deptIds).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业客户实名资料失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row != nil {
|
||||
result[row.DeptId] = row
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func appEnterpriseProfileByDeptId(ctx context.Context, tenantId string, deptId int64) (*entity.TAgentEnterprise, error) {
|
||||
var profile *entity.TAgentEnterprise
|
||||
err := dao.TAgentEnterprise.Ctx(ctx).
|
||||
Where(dao.TAgentEnterprise.Columns().TenantId, tenantId).
|
||||
Where(dao.TAgentEnterprise.Columns().DeptId, deptId).
|
||||
Limit(1).
|
||||
Scan(&profile)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业客户实名资料失败")
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func appTerminalIdsByVisibleCards(ctx context.Context, tenantId string, visibleDeptIds []int64) ([]int64, error) {
|
||||
if len(visibleDeptIds) == 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
bindTable := dao.TSimTerminal.Table()
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
var rows []*appTerminalCustomerIdRow
|
||||
err := dao.TSimTerminal.Ctx(ctx).
|
||||
Fields(bindTable+"."+dao.TSimTerminal.Columns().TerminalId+" AS terminal_id").
|
||||
InnerJoin(cardTable, cardTable+"."+dao.TSimCardInfo.Columns().Iccid+"="+bindTable+"."+dao.TSimTerminal.Columns().Iccid+" AND "+cardTable+"."+dao.TSimCardInfo.Columns().TenantId+"="+bindTable+"."+dao.TSimTerminal.Columns().TenantId).
|
||||
Where(bindTable+"."+dao.TSimTerminal.Columns().TenantId, tenantId).
|
||||
WhereIn(cardTable+"."+dao.TSimCardInfo.Columns().DeptId, visibleDeptIds).
|
||||
Group(bindTable + "." + dao.TSimTerminal.Columns().TerminalId).
|
||||
OrderAsc(bindTable + "." + dao.TSimTerminal.Columns().TerminalId).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询终端客户范围失败")
|
||||
}
|
||||
ids := make([]int64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row != nil && row.TerminalId > 0 {
|
||||
ids = append(ids, row.TerminalId)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func appTerminalMap(ctx context.Context, tenantId string, terminalIds []int64) (map[int64]*entity.TTerminal, error) {
|
||||
result := make(map[int64]*entity.TTerminal)
|
||||
if len(terminalIds) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
var rows []*entity.TTerminal
|
||||
err := dao.TTerminal.Ctx(ctx).
|
||||
Where(dao.TTerminal.Columns().TenantId, tenantId).
|
||||
WhereIn(dao.TTerminal.Columns().TerminalId, terminalIds).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询终端客户资料失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row != nil {
|
||||
result[row.TerminalId] = row
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func appTerminalHasVisibleCard(ctx context.Context, tenantId string, terminalId int64, visibleDeptIds []int64) (bool, error) {
|
||||
if terminalId <= 0 || len(visibleDeptIds) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
bindTable := dao.TSimTerminal.Table()
|
||||
cardTable := dao.TSimCardInfo.Table()
|
||||
count, err := dao.TSimTerminal.Ctx(ctx).
|
||||
InnerJoin(cardTable, cardTable+"."+dao.TSimCardInfo.Columns().Iccid+"="+bindTable+"."+dao.TSimTerminal.Columns().Iccid+" AND "+cardTable+"."+dao.TSimCardInfo.Columns().TenantId+"="+bindTable+"."+dao.TSimTerminal.Columns().TenantId).
|
||||
Where(bindTable+"."+dao.TSimTerminal.Columns().TenantId, tenantId).
|
||||
Where(bindTable+"."+dao.TSimTerminal.Columns().TerminalId, terminalId).
|
||||
WhereIn(cardTable+"."+dao.TSimCardInfo.Columns().DeptId, visibleDeptIds).
|
||||
Count()
|
||||
if err != nil {
|
||||
return false, gerror.Wrap(err, "校验终端客户范围失败")
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func appLoadCustomerDept(ctx context.Context, tenantId string, deptId int64) (*entity.SysDept, error) {
|
||||
var dept *entity.SysDept
|
||||
err := dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, tenantId).
|
||||
Where(dao.SysDept.Columns().DeptId, deptId).
|
||||
Limit(1).
|
||||
Scan(&dept)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业客户失败")
|
||||
}
|
||||
return dept, nil
|
||||
}
|
||||
|
||||
func appEnterpriseCustomerModel(dept *entity.SysDept, profile *entity.TAgentEnterprise, balance float64, stat *appCustomerCardStat) *model.AppCustomerModel {
|
||||
item := &model.AppCustomerModel{
|
||||
CustomerType: appCustomerTypeEnterprise,
|
||||
CustomerTypeName: "企业客户",
|
||||
CustomerId: dept.DeptId,
|
||||
DeptId: dept.DeptId,
|
||||
ParentDeptId: dept.ParentId,
|
||||
CustomerCode: strings.TrimSpace(dept.DeptKey),
|
||||
CustomerName: strings.TrimSpace(dept.DeptName),
|
||||
DeptCategory: strings.TrimSpace(dept.DeptCategory),
|
||||
Phone: appMaskPhone(dept.Phone),
|
||||
Email: appMaskEmail(dept.Email),
|
||||
Status: strings.TrimSpace(dept.Status),
|
||||
StatusText: appDeptStatusText(dept.Status),
|
||||
Balance: balance,
|
||||
Address: appMaskAddress(dept.Address),
|
||||
CreatedAt: appProfileDate(dept.CreatedAt),
|
||||
}
|
||||
if profile != nil {
|
||||
item.FullName = strings.TrimSpace(profile.FullName)
|
||||
item.RealNameStatus = appRealNameStatus(profile.FullName, profile.LegalPersonName, profile.LegalPersonIdCard)
|
||||
} else {
|
||||
item.RealNameStatus = "实名资料待完善"
|
||||
}
|
||||
if stat != nil {
|
||||
item.CardCount = stat.CardCount
|
||||
item.NormalCardCount = stat.NormalCardCount
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func appTerminalCustomerModel(terminal *entity.TTerminal, stat *appCustomerCardStat) *model.AppCustomerModel {
|
||||
item := &model.AppCustomerModel{
|
||||
CustomerType: appCustomerTypeTerminal,
|
||||
CustomerTypeName: "终端客户",
|
||||
CustomerId: terminal.TerminalId,
|
||||
TerminalId: terminal.TerminalId,
|
||||
CustomerName: strings.TrimSpace(terminal.TerminalName),
|
||||
Phone: appMaskPhone(terminal.Phone),
|
||||
Email: appMaskEmail(terminal.Email),
|
||||
Status: strings.TrimSpace(terminal.Status),
|
||||
StatusText: appTerminalStatusText(terminal.Status),
|
||||
RealNameStatus: appRealNameStatus(terminal.FullName, terminal.LegalPersonName, terminal.LegalPersonIdCard),
|
||||
Balance: appRoundAmount(terminal.Balance),
|
||||
Address: appMaskAddress(terminal.Address),
|
||||
FullName: strings.TrimSpace(terminal.FullName),
|
||||
CreatedAt: appProfileDate(terminal.CreatedAt),
|
||||
}
|
||||
if item.CustomerName == "" {
|
||||
item.CustomerName = item.FullName
|
||||
}
|
||||
if item.CustomerName == "" {
|
||||
item.CustomerName = "终端客户" + strconv.FormatInt(terminal.TerminalId, 10)
|
||||
}
|
||||
if stat != nil {
|
||||
item.CardCount = stat.CardCount
|
||||
item.NormalCardCount = stat.NormalCardCount
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func appEnterpriseProfileModel(dept *entity.SysDept, enterprise *entity.TAgentEnterprise) *model.AppEnterpriseProfileModel {
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
return nil
|
||||
}
|
||||
profile := &model.AppEnterpriseProfileModel{
|
||||
DeptId: dept.DeptId,
|
||||
DeptName: strings.TrimSpace(dept.DeptName),
|
||||
DeptKey: strings.TrimSpace(dept.DeptKey),
|
||||
Phone: appMaskPhone(dept.Phone),
|
||||
Email: appMaskEmail(dept.Email),
|
||||
StatusText: appDeptStatusText(dept.Status),
|
||||
CreditCode: appMaskCreditCode(dept.CreditCode),
|
||||
AreaCode: strings.TrimSpace(dept.AreaCode),
|
||||
Address: appMaskAddress(dept.Address),
|
||||
FullName: strings.TrimSpace(dept.DeptName),
|
||||
RealNameStatus: "实名资料待完善",
|
||||
}
|
||||
if enterprise != nil && enterprise.Id > 0 {
|
||||
profile.FullName = appFirstNotEmpty(enterprise.FullName, dept.DeptName)
|
||||
profile.Industry = strings.TrimSpace(enterprise.Industry)
|
||||
profile.CompanyType = strings.TrimSpace(enterprise.CompanyType)
|
||||
profile.EstablishTime = appProfileDate(enterprise.EstablishTime)
|
||||
profile.RealNameStatus = appRealNameStatus(enterprise.FullName, enterprise.LegalPersonName, enterprise.LegalPersonIdCard)
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
func appDeptStatusText(status string) string {
|
||||
switch strings.TrimSpace(status) {
|
||||
case consts.SysDeptStatusNormal:
|
||||
return "正常"
|
||||
case consts.SysDeptStatusDisable:
|
||||
return "停用"
|
||||
case "":
|
||||
return "未设置"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/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/util/gconv"
|
||||
)
|
||||
|
||||
type appEnterpriseDataScope struct {
|
||||
Configured bool
|
||||
All bool
|
||||
Restricted bool
|
||||
DeptIds []int64
|
||||
UserIds []int64
|
||||
}
|
||||
|
||||
func appApplyEnterpriseDataScope(ctx context.Context, user *entity.SysUser, mod *gdb.Model, deptField string, userField string, legacy func(*gdb.Model) *gdb.Model) *gdb.Model {
|
||||
scope, err := appResolveEnterpriseDataScope(ctx, user)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "appApplyEnterpriseDataScope resolve error: %v", err)
|
||||
return mod.Where("1 = 0")
|
||||
}
|
||||
if !scope.Configured {
|
||||
if legacy != nil {
|
||||
return legacy(mod)
|
||||
}
|
||||
return mod
|
||||
}
|
||||
if scope.All {
|
||||
return mod
|
||||
}
|
||||
|
||||
hasDeptScope := deptField != "" && len(scope.DeptIds) > 0
|
||||
hasUserScope := userField != "" && len(scope.UserIds) > 0
|
||||
if hasDeptScope && hasUserScope {
|
||||
return mod.Where("("+deptField+" IN (?) OR "+userField+" IN (?))", scope.DeptIds, scope.UserIds)
|
||||
}
|
||||
if hasDeptScope {
|
||||
return mod.WhereIn(deptField, scope.DeptIds)
|
||||
}
|
||||
if hasUserScope {
|
||||
return mod.WhereIn(userField, scope.UserIds)
|
||||
}
|
||||
if scope.Restricted {
|
||||
return mod.Where("1 = 0")
|
||||
}
|
||||
return mod.Where("1 = 0")
|
||||
}
|
||||
|
||||
func appResolveEnterpriseDataScope(ctx context.Context, user *entity.SysUser) (*appEnterpriseDataScope, error) {
|
||||
scope := &appEnterpriseDataScope{}
|
||||
if user == nil || user.UserId <= 0 {
|
||||
scope.Restricted = true
|
||||
return scope, nil
|
||||
}
|
||||
configured, err := appAccessConfigured(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope.Configured = configured
|
||||
if !configured {
|
||||
return scope, nil
|
||||
}
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
scope.All = true
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
currentRoleId, currentRoleDataScope := appCurrentRoleDataScope(ctx)
|
||||
if currentRoleId > 0 && currentRoleDataScope != "" {
|
||||
return appBuildEnterpriseDataScope(ctx, user, currentRoleId, currentRoleDataScope, true)
|
||||
}
|
||||
|
||||
roles := contexts.GetRoles(ctx)
|
||||
if len(roles) == 0 {
|
||||
roles, err = appLoadUserRoleScopes(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(roles) == 0 {
|
||||
scope.Restricted = true
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
scope.Restricted = true
|
||||
for _, role := range roles {
|
||||
part, err := appBuildEnterpriseDataScope(ctx, user, role.RoleId, role.DataScope, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if part.All {
|
||||
return part, nil
|
||||
}
|
||||
scope.DeptIds = append(scope.DeptIds, part.DeptIds...)
|
||||
scope.UserIds = append(scope.UserIds, part.UserIds...)
|
||||
}
|
||||
scope.DeptIds = appUniqueInt64(scope.DeptIds)
|
||||
scope.UserIds = appUniqueInt64(scope.UserIds)
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
func appCurrentRoleDataScope(ctx context.Context) (int64, string) {
|
||||
roleId := int64(0)
|
||||
dataScope := ""
|
||||
if value := contexts.GetDataValue(ctx, "currentRoleId"); value != nil {
|
||||
roleId = gconv.Int64(value)
|
||||
}
|
||||
if value := contexts.GetDataValue(ctx, "currentRoleDataScope"); value != nil {
|
||||
dataScope = strings.TrimSpace(gconv.String(value))
|
||||
}
|
||||
return roleId, dataScope
|
||||
}
|
||||
|
||||
func appBuildEnterpriseDataScope(ctx context.Context, user *entity.SysUser, roleId int64, dataScope string, configured bool) (*appEnterpriseDataScope, error) {
|
||||
scope := &appEnterpriseDataScope{
|
||||
Configured: configured,
|
||||
}
|
||||
dataScope = strings.TrimSpace(dataScope)
|
||||
if dataScope == "" {
|
||||
dataScope = consts.SysRoleDataScopeAll
|
||||
}
|
||||
switch dataScope {
|
||||
case consts.SysRoleDataScopeAll:
|
||||
scope.All = true
|
||||
case consts.SysRoleDataScopeCustom:
|
||||
scope.Restricted = true
|
||||
deptIds, err := appCustomRoleDeptIds(ctx, user.TenantId, roleId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope.DeptIds = deptIds
|
||||
case consts.SysRoleDataScopeDept:
|
||||
scope.Restricted = true
|
||||
if user.DeptId > 0 {
|
||||
scope.DeptIds = []int64{user.DeptId}
|
||||
}
|
||||
case consts.SysRoleDataScopeDeptAndBelow:
|
||||
scope.Restricted = true
|
||||
deptIds, err := appEnterpriseDeptAndSubIds(ctx, user.TenantId, user.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope.DeptIds = deptIds
|
||||
case consts.SysRoleDataScopePersonal:
|
||||
scope.Restricted = true
|
||||
scope.UserIds = []int64{user.UserId}
|
||||
case consts.SysRoleDataScopeDeptAndBelowOrPersonal:
|
||||
scope.Restricted = true
|
||||
deptIds, err := appEnterpriseDeptAndSubIds(ctx, user.TenantId, user.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope.DeptIds = deptIds
|
||||
scope.UserIds = []int64{user.UserId}
|
||||
case consts.SysRoleDataScopeOrgAndNextLevel:
|
||||
scope.Restricted = true
|
||||
deptIds, err := appOrgAndNextLevelDeptIds(ctx, user.TenantId, user.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope.DeptIds = deptIds
|
||||
case consts.SysRoleDataScopeOrgNextLevel:
|
||||
scope.Restricted = true
|
||||
deptIds, err := appOrgNextLevelDeptIds(ctx, user.TenantId, user.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope.DeptIds = deptIds
|
||||
default:
|
||||
scope.Restricted = true
|
||||
}
|
||||
scope.DeptIds = appUniqueInt64(scope.DeptIds)
|
||||
scope.UserIds = appUniqueInt64(scope.UserIds)
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
func appLoadUserRoleScopes(ctx context.Context, user *entity.SysUser) ([]model.Role, error) {
|
||||
if user == nil || user.UserId <= 0 {
|
||||
return []model.Role{}, nil
|
||||
}
|
||||
var userRoles []*entity.SysUserRole
|
||||
if err := dao.SysUserRole.Ctx(ctx).
|
||||
Where(dao.SysUserRole.Columns().UserId, user.UserId).
|
||||
Scan(&userRoles); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roleIds := make([]int64, 0, len(userRoles))
|
||||
for _, userRole := range userRoles {
|
||||
if userRole != nil && userRole.RoleId > 0 {
|
||||
roleIds = append(roleIds, userRole.RoleId)
|
||||
}
|
||||
}
|
||||
roleIds = appUniqueInt64(roleIds)
|
||||
if len(roleIds) == 0 {
|
||||
return []model.Role{}, nil
|
||||
}
|
||||
|
||||
var rows []*entity.SysRole
|
||||
err := dao.SysRole.Ctx(ctx).
|
||||
Fields(dao.SysRole.Columns().RoleId, dao.SysRole.Columns().DataScope).
|
||||
Where(dao.SysRole.Columns().TenantId, user.TenantId).
|
||||
WhereIn(dao.SysRole.Columns().RoleId, roleIds).
|
||||
Where(dao.SysRole.Columns().Status, consts.SysStatusNormal).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles := make([]model.Role, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil || row.RoleId <= 0 {
|
||||
continue
|
||||
}
|
||||
roles = append(roles, model.Role{
|
||||
RoleId: row.RoleId,
|
||||
DataScope: row.DataScope,
|
||||
})
|
||||
}
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func appCustomRoleDeptIds(ctx context.Context, tenantId string, roleId int64) ([]int64, error) {
|
||||
if roleId <= 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
var rows []*entity.SysRoleDept
|
||||
err := dao.SysRoleDept.Ctx(ctx).
|
||||
Where("tenant_id", tenantId).
|
||||
Where(dao.SysRoleDept.Columns().RoleId, roleId).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deptIds := make([]int64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row != nil && row.DeptId > 0 {
|
||||
deptIds = append(deptIds, row.DeptId)
|
||||
}
|
||||
}
|
||||
return appUniqueInt64(deptIds), nil
|
||||
}
|
||||
|
||||
func appLegacyEnterpriseDeptAndSubScope(ctx context.Context, user *entity.SysUser, mod *gdb.Model, deptField string) *gdb.Model {
|
||||
if user == nil || user.DeptId <= 0 || deptField == "" {
|
||||
return mod.Where("1 = 0")
|
||||
}
|
||||
deptIds, err := appEnterpriseDeptAndSubIds(ctx, user.TenantId, user.DeptId)
|
||||
if err != nil || len(deptIds) == 0 {
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "appLegacyEnterpriseDeptAndSubScope error: %v", err)
|
||||
}
|
||||
return mod.Where("1 = 0")
|
||||
}
|
||||
return mod.WhereIn(deptField, deptIds)
|
||||
}
|
||||
|
||||
func appFilterDeptsByEnterpriseDataScope(ctx context.Context, user *entity.SysUser, depts []*entity.SysDept) ([]*entity.SysDept, error) {
|
||||
scope, err := appResolveEnterpriseDataScope(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !scope.Configured || scope.All {
|
||||
return depts, nil
|
||||
}
|
||||
if len(scope.DeptIds) == 0 {
|
||||
return []*entity.SysDept{}, nil
|
||||
}
|
||||
allowed := make(map[int64]struct{}, len(scope.DeptIds))
|
||||
for _, deptId := range scope.DeptIds {
|
||||
allowed[deptId] = struct{}{}
|
||||
}
|
||||
result := make([]*entity.SysDept, 0, len(depts))
|
||||
for _, dept := range depts {
|
||||
if dept == nil || dept.DeptId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := allowed[dept.DeptId]; ok {
|
||||
result = append(result, dept)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func appEnterpriseDeptVisibleByDataScope(ctx context.Context, user *entity.SysUser, deptId int64) (bool, error) {
|
||||
if deptId <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
scope, err := appResolveEnterpriseDataScope(ctx, user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !scope.Configured || scope.All {
|
||||
return true, nil
|
||||
}
|
||||
for _, item := range scope.DeptIds {
|
||||
if item == deptId {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func appParentCompanyDeptId(ctx context.Context, tenantId string, deptId int64) (int64, error) {
|
||||
if deptId <= 0 {
|
||||
return deptId, nil
|
||||
}
|
||||
var dept *entity.SysDept
|
||||
if err := dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, tenantId).
|
||||
Where(dao.SysDept.Columns().DeptId, deptId).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
Limit(1).
|
||||
Scan(&dept); err != nil {
|
||||
return 0, gerror.Wrap(err, "查询上级组织失败")
|
||||
}
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
return deptId, nil
|
||||
}
|
||||
if dept.ParentId == 0 || dept.DeptType == 1 {
|
||||
return dept.DeptId, nil
|
||||
}
|
||||
return appParentCompanyDeptId(ctx, tenantId, dept.ParentId)
|
||||
}
|
||||
|
||||
func appDeptDirectSubIds(ctx context.Context, tenantId string, deptId int64) ([]int64, error) {
|
||||
if deptId <= 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
var rows []*entity.SysDept
|
||||
err := dao.SysDept.Ctx(ctx).
|
||||
Fields(dao.SysDept.Columns().DeptId).
|
||||
Where(dao.SysDept.Columns().TenantId, tenantId).
|
||||
Where(dao.SysDept.Columns().ParentId, deptId).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询下级组织失败")
|
||||
}
|
||||
deptIds := make([]int64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row != nil && row.DeptId > 0 {
|
||||
deptIds = append(deptIds, row.DeptId)
|
||||
}
|
||||
}
|
||||
return appUniqueInt64(deptIds), nil
|
||||
}
|
||||
|
||||
func appOrgAndNextLevelDeptIds(ctx context.Context, tenantId string, deptId int64) ([]int64, error) {
|
||||
orgDeptId, err := appParentCompanyDeptId(ctx, tenantId, deptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := []int64{}
|
||||
if orgDeptId > 0 {
|
||||
ids = append(ids, orgDeptId)
|
||||
}
|
||||
children, err := appDeptDirectSubIds(ctx, tenantId, orgDeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, children...)
|
||||
return appUniqueInt64(ids), nil
|
||||
}
|
||||
|
||||
func appOrgNextLevelDeptIds(ctx context.Context, tenantId string, deptId int64) ([]int64, error) {
|
||||
orgDeptId, err := appParentCompanyDeptId(ctx, tenantId, deptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appDeptDirectSubIds(ctx, tenantId, orgDeptId)
|
||||
}
|
||||
|
||||
func appUniqueInt64(values []int64) []int64 {
|
||||
result := make([]int64, 0, len(values))
|
||||
seen := make(map[int64]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
if value <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,228 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
)
|
||||
|
||||
func TestQueryDiagnosisFlowByCardRejectsUnsupportedPeriod(t *testing.T) {
|
||||
_, err := NewAppDiagnosis().queryDiagnosisFlowByCard(nil, nil, "12M")
|
||||
if err == nil || !strings.Contains(err.Error(), "3M") {
|
||||
t.Fatalf("unsupported period error = %v, want query type hint", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppCachedDiagnosisRegionModuleTreatsCachedDataAsSuccess(t *testing.T) {
|
||||
raw, err := json.Marshal(map[string]string{
|
||||
"regionLimitStatus": "1",
|
||||
"orderStatus": "0",
|
||||
"limitStatus": "1",
|
||||
"limitType": "province",
|
||||
"limitArea": "山东",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal region data: %v", err)
|
||||
}
|
||||
item := &appUpstreamSimQueryItem{
|
||||
Code: 1001,
|
||||
Message: "上游一小时内限制重复查询",
|
||||
ResData: raw,
|
||||
}
|
||||
|
||||
status, message := appCachedDiagnosisRegionModule(item)
|
||||
if status != "cached" {
|
||||
t.Fatalf("status = %q, want cached", status)
|
||||
}
|
||||
if !strings.Contains(message, "区域限制结果来自缓存") {
|
||||
t.Fatalf("message = %q, want cache hint", message)
|
||||
}
|
||||
|
||||
res := &model.AppDiagnosisInfoModel{Info: &model.AppDiagnosisInfoItem{}}
|
||||
appDiagnosisApplyRegionLimit(res, item)
|
||||
if res.Info.RegionLimitStatus == nil || *res.Info.RegionLimitStatus != "1" {
|
||||
t.Fatalf("RegionLimitStatus = %#v, want 1", res.Info.RegionLimitStatus)
|
||||
}
|
||||
if res.Info.RegionLimitArea == nil || *res.Info.RegionLimitArea != "山东" {
|
||||
t.Fatalf("RegionLimitArea = %#v, want 山东", res.Info.RegionLimitArea)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppCachedDiagnosisRegionModuleFailsEmptyCacheData(t *testing.T) {
|
||||
status, _ := appCachedDiagnosisRegionModule(&appUpstreamSimQueryItem{Code: 0})
|
||||
if status != "error" {
|
||||
t.Fatalf("status = %q, want error", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppDiagnosisApplyRateLimitIncludesApnDetails(t *testing.T) {
|
||||
inner, err := json.Marshal(map[string]any{
|
||||
"rateLimitStatus": "0",
|
||||
"rateLimitReason": "套餐达量",
|
||||
"rateLimitDetail": "APN触发限速",
|
||||
"apnDetails": []map[string]any{
|
||||
{
|
||||
"apnName": "cmiot",
|
||||
"apnStatus": "0",
|
||||
"modifyTime": "2026-08-28 10:00:00",
|
||||
"overUseDetails": "流量超额",
|
||||
"apnServiceUsageState": "超额",
|
||||
"pccDetails": []map[string]any{
|
||||
{
|
||||
"pccServiceCode": "PCC001",
|
||||
"pccName": "白名单",
|
||||
"pccPolicyStatus": "1",
|
||||
"pccServiceUsageState": "正常",
|
||||
"modifyTime": "2026-08-28 10:01:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal rate limit data: %v", err)
|
||||
}
|
||||
raw, err := json.Marshal(map[string]any{"resData": json.RawMessage(inner)})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal wrapped rate limit data: %v", err)
|
||||
}
|
||||
|
||||
res := &model.AppDiagnosisInfoModel{Info: &model.AppDiagnosisInfoItem{}}
|
||||
appDiagnosisApplyRateLimit(res, &appUpstreamSimQueryItem{Code: 0, ResData: raw})
|
||||
if res.Info.RateLimitStatus == nil || *res.Info.RateLimitStatus != "0" {
|
||||
t.Fatalf("RateLimitStatus = %#v, want 0", res.Info.RateLimitStatus)
|
||||
}
|
||||
if len(res.Info.RateLimitApnDetails) != 1 {
|
||||
t.Fatalf("RateLimitApnDetails length = %d, want 1", len(res.Info.RateLimitApnDetails))
|
||||
}
|
||||
apn := res.Info.RateLimitApnDetails[0]
|
||||
if apn.ApnName != "cmiot" || len(apn.PccDetails) != 1 || apn.PccDetails[0].PccServiceCode != "PCC001" {
|
||||
t.Fatalf("unexpected APN detail: %#v", apn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeOpenDiagnosisInfoMasksIdentifiersAndRemovesNetworkData(t *testing.T) {
|
||||
res := &model.AppDiagnosisInfoModel{
|
||||
Info: &model.AppDiagnosisInfoItem{
|
||||
Iccid: appDiagnosisStringPtr("8986001234567890123"),
|
||||
Imsi: appDiagnosisStringPtr("460001234567890"),
|
||||
Msisdn: appDiagnosisStringPtr("13800008000"),
|
||||
RateLimitApnDetails: []model.AppDiagnosisRateLimitApnItem{
|
||||
{ApnName: "cmiot"},
|
||||
},
|
||||
},
|
||||
Session: &model.AppDiagnosisSessionItem{
|
||||
ApnId: appDiagnosisStringPtr("internet"),
|
||||
Imei: appDiagnosisStringPtr("860123456789012"),
|
||||
Ipv4: appDiagnosisStringPtr("10.0.0.1"),
|
||||
Ipv6: appDiagnosisStringPtr("2001:db8::1"),
|
||||
},
|
||||
Card: &model.AppCardModel{
|
||||
Iccid: "8986001234567890123",
|
||||
Msisdn: "13800008000",
|
||||
Imsi: "460001234567890",
|
||||
Imei: "860123456789012",
|
||||
Ipv4: "10.0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
sanitizeOpenDiagnosisInfo(res)
|
||||
if got := *res.Info.Iccid; got != "8986****0123" {
|
||||
t.Fatalf("masked ICCID = %q", got)
|
||||
}
|
||||
if got := *res.Info.Msisdn; got != "1380****8000" {
|
||||
t.Fatalf("masked MSISDN = %q", got)
|
||||
}
|
||||
if res.Info.Imsi != nil || len(res.Info.RateLimitApnDetails) != 0 || res.Session.ApnId != nil || res.Session.Imei != nil || res.Session.Ipv4 != nil {
|
||||
t.Fatal("sensitive diagnosis fields were not removed")
|
||||
}
|
||||
if res.Card.Imsi != "" || res.Card.Imei != "" || res.Card.Ipv4 != "" {
|
||||
t.Fatal("sensitive card fields were not removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDiagnosisInfoKeepsSessionImeiBeforeOpenSanitization(t *testing.T) {
|
||||
res := &model.AppDiagnosisInfoModel{
|
||||
Session: &model.AppDiagnosisSessionItem{Imei: appDiagnosisStringPtr("860123456789012")},
|
||||
}
|
||||
if res.Session.Imei == nil || *res.Session.Imei != "860123456789012" {
|
||||
t.Fatal("authenticated diagnosis should keep session IMEI")
|
||||
}
|
||||
sanitizeOpenDiagnosisInfo(res)
|
||||
if res.Session.Imei != nil {
|
||||
t.Fatal("open diagnosis should remove session IMEI")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppDiagnosisStatusMappingsMatchConsoleContract(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
mapper func(string) *string
|
||||
}{
|
||||
{name: "imei separated", input: "0", want: "0", mapper: appDiagnosisImeiStatus},
|
||||
{name: "imei bound", input: "1", want: "1", mapper: appDiagnosisImeiStatus},
|
||||
{name: "imei unknown", input: "2", want: "2", mapper: appDiagnosisImeiStatus},
|
||||
{name: "online", input: "0", want: "1", mapper: appDiagnosisOnlineStatus},
|
||||
{name: "offline", input: "-1", want: "0", mapper: appDiagnosisOnlineStatus},
|
||||
{name: "unknown", input: "-2", want: "2", mapper: appDiagnosisOnlineStatus},
|
||||
{name: "human limited", input: "1", want: "0", mapper: appDiagnosisHumanLimitStatus},
|
||||
{name: "human normal", input: "0", want: "1", mapper: appDiagnosisHumanLimitStatus},
|
||||
{name: "human unknown", input: "2", want: "2", mapper: appDiagnosisHumanLimitStatus},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := test.mapper(test.input)
|
||||
if got == nil || *got != test.want {
|
||||
t.Fatalf("mapping %q = %#v, want %q", test.input, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppDiagnosisBindStatusSupportsTelecomCardBindingType(t *testing.T) {
|
||||
tests := []struct {
|
||||
bindStatus string
|
||||
cardBindingType string
|
||||
want string
|
||||
}{
|
||||
{bindStatus: "0", cardBindingType: "1", want: "0"},
|
||||
{bindStatus: "1", cardBindingType: "2", want: "1"},
|
||||
{bindStatus: "2", cardBindingType: "1", want: "2"},
|
||||
{cardBindingType: "1", want: "1"},
|
||||
{cardBindingType: "5", want: "1"},
|
||||
{cardBindingType: "-1", want: "2"},
|
||||
{cardBindingType: "unknown", want: "2"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := appDiagnosisBindStatus(test.bindStatus, test.cardBindingType); got != test.want {
|
||||
t.Fatalf("bindStatus=%q cardBindingType=%q => %q, want %q", test.bindStatus, test.cardBindingType, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppDiagnosisSessionStatusNormalizesUpstreamValues(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"0": "0",
|
||||
"01": "0",
|
||||
"ONLINE": "0",
|
||||
"-1": "-1",
|
||||
"00": "-1",
|
||||
"OFFLINE": "-1",
|
||||
"-2": "-2",
|
||||
"02": "-2",
|
||||
"2": "-2",
|
||||
"": "-2",
|
||||
}
|
||||
|
||||
for input, want := range tests {
|
||||
if got := appDiagnosisSessionStatus(input); got != want {
|
||||
t.Fatalf("session status %q => %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
_ "github.com/taosdata/driver-go/v3/taosRestful"
|
||||
_ "github.com/taosdata/driver-go/v3/taosWS"
|
||||
)
|
||||
|
||||
const (
|
||||
appDiagnosisDailyStable = "t_sim_card_usage_daily"
|
||||
appDiagnosisMonthlyStable = "t_sim_card_usage_monthly"
|
||||
)
|
||||
|
||||
var appDiagnosisIdentifierPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
type appDiagnosisTrafficConfig struct {
|
||||
Enabled bool
|
||||
DriverName string
|
||||
Dsn string
|
||||
DbName string
|
||||
DailyDbName string
|
||||
MonthlyDbName string
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
}
|
||||
|
||||
// dbNameFor 返回指定超级表所在 database。
|
||||
//
|
||||
// 日流量与月流量分库存储(两库 duration 不同,而 duration 只能建库时确定);
|
||||
// 未单独配置 dailyDbName/monthlyDbName 时回退到 DbName,与改造前行为一致。
|
||||
func (c appDiagnosisTrafficConfig) dbNameFor(stable string) string {
|
||||
if stable == appDiagnosisDailyStable && c.DailyDbName != "" {
|
||||
return c.DailyDbName
|
||||
}
|
||||
if stable == appDiagnosisMonthlyStable && c.MonthlyDbName != "" {
|
||||
return c.MonthlyDbName
|
||||
}
|
||||
return c.DbName
|
||||
}
|
||||
|
||||
type appDiagnosisTrafficRecord struct {
|
||||
Ts time.Time
|
||||
UsageMb float64
|
||||
PackageCode string
|
||||
Iccid string
|
||||
}
|
||||
|
||||
func (s *sAppDiagnosis) queryDiagnosisDailyFlow(ctx context.Context, iccid string, days int) (*model.AppDiagnosisFlowModel, error) {
|
||||
if days <= 0 {
|
||||
days = 7
|
||||
}
|
||||
now := time.Now()
|
||||
startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -(days - 1))
|
||||
endTime := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
|
||||
|
||||
records, err := appDiagnosisQueryTrafficRows(ctx, iccid, appDiagnosisDailyStable, startTime, endTime)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询SIM卡使用流量失败,请稍后重试")
|
||||
}
|
||||
usageMap := make(map[string]float64, len(records))
|
||||
for _, item := range records {
|
||||
usageMap[item.Ts.Format("2006-01-02")] = item.UsageMb
|
||||
}
|
||||
|
||||
res := &model.AppDiagnosisFlowModel{
|
||||
Times: make([]string, 0, days),
|
||||
Flow: make([]float64, 0, days),
|
||||
Unit: "MB",
|
||||
Granularity: "day",
|
||||
HasData: len(records) > 0,
|
||||
}
|
||||
for i := 0; i < days; i++ {
|
||||
current := startTime.AddDate(0, 0, i)
|
||||
res.Times = append(res.Times, current.Format("01-02"))
|
||||
res.Flow = append(res.Flow, usageMap[current.Format("2006-01-02")])
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *sAppDiagnosis) queryDiagnosisMonthlyFlow(ctx context.Context, iccid string, months int) (*model.AppDiagnosisFlowModel, error) {
|
||||
if months <= 0 {
|
||||
months = 6
|
||||
}
|
||||
now := time.Now()
|
||||
currentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
startMonth := currentMonth.AddDate(0, -(months - 1), 0)
|
||||
endTime := time.Date(now.Year(), now.Month()+1, 0, 23, 59, 59, 0, now.Location())
|
||||
|
||||
records, err := appDiagnosisQueryTrafficRows(ctx, iccid, appDiagnosisMonthlyStable, startMonth, endTime)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询SIM卡使用流量失败,请稍后重试")
|
||||
}
|
||||
usageMap := make(map[string]float64, len(records))
|
||||
for _, item := range records {
|
||||
usageMap[item.Ts.Format("2006-01")] = item.UsageMb
|
||||
}
|
||||
|
||||
res := &model.AppDiagnosisFlowModel{
|
||||
Times: make([]string, 0, months),
|
||||
Flow: make([]float64, 0, months),
|
||||
Unit: "MB",
|
||||
Granularity: "month",
|
||||
HasData: len(records) > 0,
|
||||
}
|
||||
for i := 0; i < months; i++ {
|
||||
current := startMonth.AddDate(0, i, 0)
|
||||
key := current.Format("2006-01")
|
||||
res.Times = append(res.Times, key)
|
||||
res.Flow = append(res.Flow, usageMap[key])
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func appDiagnosisQueryTrafficRows(ctx context.Context, iccid string, stable string, startTime, endTime time.Time) ([]appDiagnosisTrafficRecord, error) {
|
||||
iccid = strings.TrimSpace(iccid)
|
||||
if iccid == "" {
|
||||
return nil, gerror.New("traffic usage query iccid is empty")
|
||||
}
|
||||
cfg := appDiagnosisGetTrafficConfig(ctx)
|
||||
if !cfg.Enabled || strings.TrimSpace(cfg.Dsn) == "" {
|
||||
g.Log().Debug(ctx, "app diagnosis traffic query skipped, tdengine disabled")
|
||||
return []appDiagnosisTrafficRecord{}, nil
|
||||
}
|
||||
dbName := cfg.dbNameFor(stable)
|
||||
if err := appDiagnosisValidateTableConfig(dbName, stable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
driverName := strings.TrimSpace(cfg.DriverName)
|
||||
if driverName == "" {
|
||||
driverName = "taosWS"
|
||||
}
|
||||
db, err := sql.Open(driverName, cfg.Dsn)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "tdengine open failed")
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
db.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
|
||||
table := appDiagnosisQualify(dbName, appDiagnosisChildTrafficTableName(stable, iccid))
|
||||
rows, err := db.QueryContext(ctx, fmt.Sprintf(
|
||||
"SELECT ts, usage_mb, package_code, iccid FROM %s%s ORDER BY ts ASC",
|
||||
table,
|
||||
appDiagnosisWhereRangeSQL(startTime, endTime),
|
||||
))
|
||||
if err != nil {
|
||||
if appDiagnosisIsTableNotExist(err) {
|
||||
return []appDiagnosisTrafficRecord{}, nil
|
||||
}
|
||||
return nil, gerror.Wrap(err, "tdengine query traffic usage failed")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]appDiagnosisTrafficRecord, 0)
|
||||
for rows.Next() {
|
||||
var item appDiagnosisTrafficRecord
|
||||
if err = rows.Scan(&item.Ts, &item.UsageMb, &item.PackageCode, &item.Iccid); err != nil {
|
||||
return nil, gerror.Wrap(err, "tdengine scan traffic usage failed")
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, gerror.Wrap(err, "tdengine rows traffic usage failed")
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func appDiagnosisGetTrafficConfig(ctx context.Context) appDiagnosisTrafficConfig {
|
||||
// dbName 只是兑底键名,不决定实际访问哪个库;默认取日库,避免兑底到不存在的库名。
|
||||
dbName := strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.dbName", "iotlcm_daily").String())
|
||||
return appDiagnosisTrafficConfig{
|
||||
Enabled: strings.EqualFold(g.Cfg().MustGet(ctx, "tsdb.database", "TdEngine").String(), "TdEngine"),
|
||||
DriverName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.type", "taosWS").String()),
|
||||
Dsn: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.dsn", "").String()),
|
||||
DbName: dbName,
|
||||
DailyDbName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.dailyDbName", dbName).String()),
|
||||
MonthlyDbName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.monthlyDbName", dbName).String()),
|
||||
MaxOpenConns: g.Cfg().MustGet(ctx, "tsdb.tdengine.maxOpenConns", 100).Int(),
|
||||
MaxIdleConns: g.Cfg().MustGet(ctx, "tsdb.tdengine.maxIdleConns", 10).Int(),
|
||||
}
|
||||
}
|
||||
|
||||
func appDiagnosisValidateTableConfig(database string, stable string) error {
|
||||
if !appDiagnosisIdentifierPattern.MatchString(database) {
|
||||
return gerror.Newf("invalid tdengine database name: %s", database)
|
||||
}
|
||||
if !appDiagnosisIdentifierPattern.MatchString(stable) {
|
||||
return gerror.Newf("invalid tdengine stable name: %s", stable)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appDiagnosisQualify(database, table string) string {
|
||||
return fmt.Sprintf("%s.%s", database, table)
|
||||
}
|
||||
|
||||
func appDiagnosisChildTrafficTableName(stable string, iccid string) string {
|
||||
prefix := "sim_card_usage_m_"
|
||||
if stable == appDiagnosisDailyStable {
|
||||
prefix = "sim_card_usage_d_"
|
||||
}
|
||||
return appDiagnosisChildTableNameWithPrefix(prefix, iccid)
|
||||
}
|
||||
|
||||
func appDiagnosisChildTableNameWithPrefix(prefix string, value string) string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString(prefix)
|
||||
for _, r := range strings.ToLower(value) {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
|
||||
builder.WriteRune(r)
|
||||
}
|
||||
}
|
||||
name := builder.String()
|
||||
if name == prefix || len(name) > 190 {
|
||||
return fmt.Sprintf("%s%x", prefix, appDiagnosisHashString(value))
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func appDiagnosisHashString(value string) uint32 {
|
||||
hash := fnv.New32a()
|
||||
_, _ = hash.Write([]byte(value))
|
||||
return hash.Sum32()
|
||||
}
|
||||
|
||||
func appDiagnosisWhereRangeSQL(startTime, endTime time.Time) string {
|
||||
clauses := make([]string, 0, 2)
|
||||
if !startTime.IsZero() {
|
||||
clauses = append(clauses, "ts >= "+appDiagnosisSQLTime(startTime))
|
||||
}
|
||||
if !endTime.IsZero() {
|
||||
clauses = append(clauses, "ts <= "+appDiagnosisSQLTime(endTime))
|
||||
}
|
||||
if len(clauses) == 0 {
|
||||
return ""
|
||||
}
|
||||
return " WHERE " + strings.Join(clauses, " AND ")
|
||||
}
|
||||
|
||||
func appDiagnosisSQLTime(value time.Time) string {
|
||||
return appDiagnosisSQLString(value.Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
func appDiagnosisSQLString(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
|
||||
}
|
||||
|
||||
func appDiagnosisIsTableNotExist(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
text := strings.ToLower(err.Error())
|
||||
return strings.Contains(text, "table does not exist") ||
|
||||
strings.Contains(text, "table not exist") ||
|
||||
strings.Contains(text, "does not exist")
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sAppMessage struct{}
|
||||
|
||||
func NewAppMessage() *sAppMessage {
|
||||
return &sAppMessage{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAppMessage(NewAppMessage())
|
||||
}
|
||||
|
||||
func (s *sAppMessage) List(ctx context.Context, param *model.AppMessageListParam) (items []*model.AppMessageModel, total int, err error) {
|
||||
if param == nil {
|
||||
param = &model.AppMessageListParam{}
|
||||
}
|
||||
if param.Page <= 0 {
|
||||
param.Page = 1
|
||||
}
|
||||
if param.PageSize <= 0 {
|
||||
param.PageSize = 20
|
||||
}
|
||||
if param.PageSize > 100 {
|
||||
param.PageSize = 100
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
mod := s.applyFilters(s.ownerMessageModel(ctx, user), param)
|
||||
total, err = mod.Count()
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "查询消息数量失败")
|
||||
}
|
||||
if total == 0 {
|
||||
return []*model.AppMessageModel{}, 0, nil
|
||||
}
|
||||
|
||||
if err = mod.
|
||||
Fields(appMessageFields()).
|
||||
Page(param.Page, param.PageSize).
|
||||
OrderDesc("COALESCE(ml." + dao.TMessageLog.Columns().SendTime + ", um." + dao.TUserMessage.Columns().CreatedAt + ")").
|
||||
OrderDesc("um." + dao.TUserMessage.Columns().MessageId).
|
||||
Scan(&items); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "查询消息列表失败")
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
fillAppMessageText(item)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *sAppMessage) Detail(ctx context.Context, param *model.AppMessageDetailParam) (*model.AppMessageModel, error) {
|
||||
if param == nil || param.MessageId == 0 {
|
||||
return nil, gerror.New("消息ID不能为空")
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var item *model.AppMessageModel
|
||||
if err = s.ownerMessageModel(ctx, user).
|
||||
Fields(appMessageFields()).
|
||||
Where("um."+dao.TUserMessage.Columns().MessageId, param.MessageId).
|
||||
Limit(1).
|
||||
Scan(&item); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询消息详情失败")
|
||||
}
|
||||
if item == nil || item.MessageId == 0 {
|
||||
return nil, gerror.New("消息不存在或无权查看")
|
||||
}
|
||||
|
||||
fillAppMessageText(item)
|
||||
if item.IsRead == 0 {
|
||||
if err = s.Read(ctx, &model.AppMessageReadParam{Ids: []uint64{item.MessageId}}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.IsRead = 1
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *sAppMessage) Read(ctx context.Context, param *model.AppMessageReadParam) error {
|
||||
if param == nil || len(param.Ids) == 0 {
|
||||
return gerror.New("消息ID不能为空")
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = dao.TUserMessage.Ctx(ctx).
|
||||
Where(dao.TUserMessage.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TUserMessage.Columns().UserId, user.UserId).
|
||||
WhereIn(dao.TUserMessage.Columns().MessageId, param.Ids).
|
||||
Where(dao.TUserMessage.Columns().IsRead, 0).
|
||||
Data(do.TUserMessage{
|
||||
IsRead: 1,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "标记消息已读失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppMessage) ownerMessageModel(ctx context.Context, user *entity.SysUser) *gdb.Model {
|
||||
return dao.TUserMessage.Ctx(ctx).As("um").
|
||||
LeftJoin(dao.TMessageLog.Table()+" ml", "ml."+dao.TMessageLog.Columns().MessageLogId+" = um."+dao.TUserMessage.Columns().MessageLogId).
|
||||
Where("um."+dao.TUserMessage.Columns().TenantId, user.TenantId).
|
||||
Where("um."+dao.TUserMessage.Columns().UserId, user.UserId).
|
||||
Where("(ml."+dao.TMessageLog.Columns().MessageLogId+" IS NULL OR ml."+dao.TMessageLog.Columns().IsRecall+" = ?)", 0)
|
||||
}
|
||||
|
||||
func (s *sAppMessage) applyFilters(mod *gdb.Model, param *model.AppMessageListParam) *gdb.Model {
|
||||
if param.IsRead != nil {
|
||||
mod = mod.Where("um."+dao.TUserMessage.Columns().IsRead, *param.IsRead)
|
||||
}
|
||||
if param.MessageType != nil && *param.MessageType > 0 {
|
||||
mod = mod.Where("ml."+dao.TMessageLog.Columns().MessageType, *param.MessageType)
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(param.Keyword)
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
mod = mod.Where(
|
||||
"(ml."+dao.TMessageLog.Columns().MessageTitle+" LIKE ? OR ml."+dao.TMessageLog.Columns().MessageContent+" LIKE ? OR ml."+dao.TMessageLog.Columns().MessageLink+" LIKE ?)",
|
||||
like, like, like,
|
||||
)
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
func appMessageFields() string {
|
||||
return strings.Join([]string{
|
||||
"um." + dao.TUserMessage.Columns().MessageId + " AS message_id",
|
||||
"um." + dao.TUserMessage.Columns().MessageId + " AS id",
|
||||
"um." + dao.TUserMessage.Columns().MessageLogId + " AS message_log_id",
|
||||
"COALESCE(ml." + dao.TMessageLog.Columns().MessageType + ", 1) AS message_type",
|
||||
"COALESCE(ml." + dao.TMessageLog.Columns().MessageTitle + ", '') AS title",
|
||||
"COALESCE(ml." + dao.TMessageLog.Columns().MessageContent + ", '') AS content",
|
||||
"COALESCE(ml." + dao.TMessageLog.Columns().MessageLink + ", '') AS link",
|
||||
"um." + dao.TUserMessage.Columns().IsRead + " AS is_read",
|
||||
"um." + dao.TUserMessage.Columns().CreatedAt + " AS created_at",
|
||||
"um." + dao.TUserMessage.Columns().UpdatedAt + " AS updated_at",
|
||||
"COALESCE(ml." + dao.TMessageLog.Columns().SendTime + ", um." + dao.TUserMessage.Columns().CreatedAt + ") AS send_time",
|
||||
}, ", ")
|
||||
}
|
||||
|
||||
func fillAppMessageText(item *model.AppMessageModel) {
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
item.Id = item.MessageId
|
||||
if strings.TrimSpace(item.Title) == "" {
|
||||
item.Title = "未命名消息"
|
||||
}
|
||||
item.MessageTypeText = appMessageTypeText(item.MessageType)
|
||||
}
|
||||
|
||||
func appMessageTypeText(messageType int) string {
|
||||
switch messageType {
|
||||
case 1:
|
||||
return "系统通知"
|
||||
case 2:
|
||||
return "平台公告"
|
||||
case 3:
|
||||
return "订单通知"
|
||||
case 4:
|
||||
return "账户通知"
|
||||
default:
|
||||
return "消息"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type appOneLinkMockConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
AppID string `json:"appid"`
|
||||
Password string `json:"password"`
|
||||
Timeout string `json:"timeout"`
|
||||
}
|
||||
|
||||
type appOneLinkMockClient struct {
|
||||
cfg *appOneLinkMockConfig
|
||||
httpClient *http.Client
|
||||
token string
|
||||
}
|
||||
|
||||
type appOneLinkMockTokenResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
Token string `json:"token"`
|
||||
TTL string `json:"ttl"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type appOneLinkMockChangeStatusResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
Msisdn string `json:"msisdn"`
|
||||
OrderNum string `json:"orderNum"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type appOneLinkMockBasicInfoResponse struct {
|
||||
Status string `json:"status"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Data []struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
ResData json.RawMessage `json:"resData"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type appOneLinkMockSimSessionResponse struct {
|
||||
Status string `json:"status"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Data []struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
ResData appOneLinkMockSimSessionItem `json:"resData"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type appOneLinkMockSimSessionItem struct {
|
||||
ApnId string `json:"apnId"`
|
||||
Rat string `json:"rat"`
|
||||
Imei string `json:"imei"`
|
||||
Ipv4 string `json:"ipv4"`
|
||||
Ipv6 string `json:"ipv6"`
|
||||
Ipv6Prefix string `json:"ipv6Prefix"`
|
||||
Status string `json:"status"`
|
||||
CreateTime string `json:"createTime"`
|
||||
}
|
||||
|
||||
type appOneLinkMockStatusOutcome = appSimStatusChangeOutcome
|
||||
|
||||
func newAppOneLinkMockClient(ctx context.Context) (*appOneLinkMockClient, error) {
|
||||
cfg, err := appLoadOneLinkMockConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := 10 * time.Second
|
||||
if cfg.Timeout != "" {
|
||||
if parsed, err := time.ParseDuration(cfg.Timeout); err == nil && parsed > 0 {
|
||||
timeout = parsed
|
||||
}
|
||||
}
|
||||
return &appOneLinkMockClient{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{Timeout: timeout},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appLoadOneLinkMockConfig(ctx context.Context) (*appOneLinkMockConfig, error) {
|
||||
cfg := &appOneLinkMockConfig{
|
||||
BaseURL: "https://iotlcm.example.com/onelink-mock/v5",
|
||||
Timeout: "10s",
|
||||
}
|
||||
if err := g.Cfg().MustGet(ctx, "upstream.onelinkMock").Scan(cfg); err != nil {
|
||||
return nil, gerror.Wrap(err, "读取OneLink挡板配置失败")
|
||||
}
|
||||
cfg.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
|
||||
cfg.AppID = strings.TrimSpace(cfg.AppID)
|
||||
cfg.Password = strings.TrimSpace(cfg.Password)
|
||||
cfg.Timeout = strings.TrimSpace(cfg.Timeout)
|
||||
if !cfg.Enabled {
|
||||
return nil, gerror.New("OneLink挡板未启用")
|
||||
}
|
||||
if cfg.BaseURL == "" {
|
||||
return nil, gerror.New("OneLink挡板地址未配置")
|
||||
}
|
||||
if cfg.AppID == "" || cfg.Password == "" {
|
||||
return nil, gerror.New("OneLink挡板appid或password未配置")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c *appOneLinkMockClient) ChangeSimStatus(ctx context.Context, operation appCardLifecycleOperation, row *appCardLifecycleExecutionRow) (appOneLinkMockStatusOutcome, error) {
|
||||
if c == nil || c.cfg == nil {
|
||||
return appOneLinkMockStatusOutcome{}, gerror.New("OneLink挡板客户端未初始化")
|
||||
}
|
||||
if row == nil {
|
||||
return appOneLinkMockStatusOutcome{}, gerror.New("卡片操作行不能为空")
|
||||
}
|
||||
msisdn := strings.TrimSpace(row.Msisdn)
|
||||
if msisdn == "" && row.Card != nil {
|
||||
msisdn = strings.TrimSpace(row.Card.Msisdn)
|
||||
}
|
||||
if msisdn == "" {
|
||||
return appOneLinkMockStatusOutcome{}, gerror.New("卡片缺少MSISDN,不能请求OneLink挡板")
|
||||
}
|
||||
operType, err := appOneLinkMockOperType(operation)
|
||||
if err != nil {
|
||||
return appOneLinkMockStatusOutcome{}, err
|
||||
}
|
||||
token, err := c.ensureToken(ctx)
|
||||
if err != nil {
|
||||
return appOneLinkMockStatusOutcome{}, err
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("appid", c.cfg.AppID)
|
||||
params.Set("transid", appOneLinkMockTransID(c.cfg.AppID))
|
||||
params.Set("token", token)
|
||||
params.Set("msisdn", msisdn)
|
||||
params.Set("operType", operType)
|
||||
|
||||
body, err := c.get(ctx, c.cfg.BaseURL+"/ec/change/sim-status?"+params.Encode())
|
||||
if err != nil {
|
||||
return appOneLinkMockStatusOutcome{}, err
|
||||
}
|
||||
var response appOneLinkMockChangeStatusResponse
|
||||
if err = json.Unmarshal(body, &response); err != nil {
|
||||
return appOneLinkMockStatusOutcome{}, gerror.Wrap(err, "解析OneLink挡板状态变更响应失败")
|
||||
}
|
||||
if strings.TrimSpace(response.Status) != "0" {
|
||||
message := strings.TrimSpace(response.Message)
|
||||
if message == "" {
|
||||
message = "OneLink挡板状态变更失败"
|
||||
}
|
||||
return appOneLinkMockStatusOutcome{Code: -1, Message: message}, nil
|
||||
}
|
||||
orderNum := ""
|
||||
if len(response.Result) > 0 {
|
||||
orderNum = strings.TrimSpace(response.Result[0].OrderNum)
|
||||
}
|
||||
return appOneLinkMockStatusOutcome{
|
||||
Success: true,
|
||||
Code: 0,
|
||||
Message: appOneLinkMockSuccessMessage(response.Message),
|
||||
OrderNum: orderNum,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *appOneLinkMockClient) QuerySimBasicInfo(ctx context.Context, input appSimBasicInfoInput) (*appSimBasicInfoOutput, error) {
|
||||
if c == nil || c.cfg == nil {
|
||||
return nil, gerror.New("OneLink鎸℃澘瀹㈡埛绔湭鍒濆鍖?")
|
||||
}
|
||||
token, err := c.ensureToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("appid", c.cfg.AppID)
|
||||
params.Set("transid", appOneLinkMockTransID(c.cfg.AppID))
|
||||
params.Set("token", token)
|
||||
appOneLinkMockAddBasicInfoQueryParam(params, input)
|
||||
if params.Get("msisdn") == "" && params.Get("iccid") == "" && params.Get("imsi") == "" && params.Get("imei") == "" {
|
||||
return nil, gerror.New("鍗$墖缂哄皯鏌ヨ鏍囪瘑锛屼笉鑳借姹侽neLink鎸℃澘")
|
||||
}
|
||||
|
||||
body, err := c.get(ctx, c.cfg.BaseURL+"/ec/query/sim-basic-info?"+params.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response appOneLinkMockBasicInfoResponse
|
||||
if err = json.Unmarshal(body, &response); err != nil {
|
||||
return nil, gerror.Wrap(err, "瑙f瀽OneLink鎸℃澘鍩烘湰淇℃伅鍝嶅簲澶辫触")
|
||||
}
|
||||
if status := strings.TrimSpace(response.Status); status != "" && status != "0" {
|
||||
message := strings.TrimSpace(response.Message)
|
||||
if message == "" {
|
||||
message = "OneLink鎸℃澘鍩烘湰淇℃伅鏌ヨ澶辫触"
|
||||
}
|
||||
return nil, gerror.New(message)
|
||||
}
|
||||
if response.Code != 0 {
|
||||
message := strings.TrimSpace(response.Message)
|
||||
if message == "" {
|
||||
message = "OneLink鎸℃澘鍩烘湰淇℃伅鏌ヨ澶辫触"
|
||||
}
|
||||
return nil, gerror.New(message)
|
||||
}
|
||||
|
||||
if output := appOneLinkMockParseBasicInfoOutput(response.Result, input); output != nil {
|
||||
return output, nil
|
||||
}
|
||||
for _, item := range response.Data {
|
||||
if item.Code != 0 {
|
||||
continue
|
||||
}
|
||||
if output := appOneLinkMockParseBasicInfoOutput(item.ResData, input); output != nil {
|
||||
return output, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *appOneLinkMockClient) QuerySimSession(ctx context.Context, card *appCardQueryRow) (*appOneLinkMockSimSessionItem, error) {
|
||||
if c == nil || c.cfg == nil {
|
||||
return nil, gerror.New("OneLink挡板客户端未初始化")
|
||||
}
|
||||
if card == nil {
|
||||
return nil, gerror.New("卡片信息为空,不能查询会话")
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Set("appid", c.cfg.AppID)
|
||||
params.Set("transid", appOneLinkMockTransID(c.cfg.AppID))
|
||||
if token, err := c.ensureToken(ctx); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
params.Set("token", token)
|
||||
}
|
||||
if value := strings.TrimSpace(card.Msisdn); value != "" {
|
||||
params.Set("msisdn", value)
|
||||
}
|
||||
if value := strings.TrimSpace(card.Iccid); value != "" {
|
||||
params.Set("iccid", value)
|
||||
}
|
||||
if value := strings.TrimSpace(card.Imsi); value != "" {
|
||||
params.Set("imsi", value)
|
||||
}
|
||||
if params.Get("msisdn") == "" && params.Get("iccid") == "" && params.Get("imsi") == "" {
|
||||
return nil, gerror.New("卡片缺少查询会话所需标识")
|
||||
}
|
||||
|
||||
body, err := c.get(ctx, c.cfg.BaseURL+"/ec/query/sim-session?"+params.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response appOneLinkMockSimSessionResponse
|
||||
if err = json.Unmarshal(body, &response); err != nil {
|
||||
return nil, gerror.Wrap(err, "解析OneLink挡板会话响应失败")
|
||||
}
|
||||
if strings.TrimSpace(response.Status) != "" && strings.TrimSpace(response.Status) != "0" {
|
||||
message := strings.TrimSpace(response.Message)
|
||||
if message == "" {
|
||||
message = "OneLink挡板会话查询失败"
|
||||
}
|
||||
return nil, gerror.New(message)
|
||||
}
|
||||
if response.Code != 0 {
|
||||
message := strings.TrimSpace(response.Message)
|
||||
if message == "" {
|
||||
message = "OneLink挡板会话查询失败"
|
||||
}
|
||||
return nil, gerror.New(message)
|
||||
}
|
||||
for _, item := range appOneLinkMockParseSessionResult(response.Result) {
|
||||
if !item.isZero() {
|
||||
return &item, nil
|
||||
}
|
||||
}
|
||||
for _, item := range response.Data {
|
||||
if item.Code == 0 && !item.ResData.isZero() {
|
||||
session := item.ResData
|
||||
return &session, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func appOneLinkMockAddBasicInfoQueryParam(params url.Values, input appSimBasicInfoInput) {
|
||||
if params == nil {
|
||||
return
|
||||
}
|
||||
if value := strings.TrimSpace(input.Msisdn); value != "" {
|
||||
params.Set("msisdn", value)
|
||||
}
|
||||
if value := strings.TrimSpace(input.Iccid); value != "" {
|
||||
params.Set("iccid", value)
|
||||
}
|
||||
if value := strings.TrimSpace(input.Imsi); value != "" {
|
||||
params.Set("imsi", value)
|
||||
}
|
||||
if value := strings.TrimSpace(input.Imei); value != "" {
|
||||
params.Set("imei", value)
|
||||
}
|
||||
if params.Get("msisdn") == "" && params.Get("iccid") == "" && params.Get("imsi") == "" && params.Get("imei") == "" {
|
||||
if value := strings.TrimSpace(input.CardValue); value != "" {
|
||||
params.Set("msisdn", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appOneLinkMockParseBasicInfoOutput(raw json.RawMessage, input appSimBasicInfoInput) *appSimBasicInfoOutput {
|
||||
payload := appOneLinkMockFirstPayload(raw)
|
||||
if len(payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
output := &appSimBasicInfoOutput{
|
||||
Provider: appUpstreamProviderMock,
|
||||
Iccid: appFirstNotEmpty(appOneLinkMockMapString(payload, "iccid", "Iccid"), strings.TrimSpace(input.Iccid)),
|
||||
Msisdn: appFirstNotEmpty(appOneLinkMockMapString(payload, "msisdn", "Msisdn"), strings.TrimSpace(input.Msisdn), strings.TrimSpace(input.CardValue)),
|
||||
Imsi: appFirstNotEmpty(appOneLinkMockMapString(payload, "imsi", "Imsi"), strings.TrimSpace(input.Imsi)),
|
||||
Imei: appFirstNotEmpty(appOneLinkMockMapString(payload, "imei", "Imei"), strings.TrimSpace(input.Imei)),
|
||||
OpenDate: appOneLinkMockMapString(payload, "openDate", "open_date", "OpenDate"),
|
||||
ActiveDate: appOneLinkMockMapString(payload, "activeDate", "active_date", "ActiveDate"),
|
||||
Remark: appOneLinkMockMapString(payload, "remark", "Remark"),
|
||||
LastChangeDate: appOneLinkMockMapString(payload, "lastChangeDate", "last_change_date", "LastChangeDate"),
|
||||
}
|
||||
statusText := appOneLinkMockMapString(payload, "cardStatus", "card_status", "status", "Status")
|
||||
if statusText != "" {
|
||||
if status, err := strconv.Atoi(statusText); err == nil {
|
||||
output.CardStatus = status
|
||||
}
|
||||
if output.CardStatus == 0 {
|
||||
switch strings.ToLower(statusText) {
|
||||
case "active", "activated", "normal", "ok", "success":
|
||||
output.CardStatus = consts.V3ApiSimBossCardStatusActivated
|
||||
case "stop", "stopped", "disable", "disabled":
|
||||
output.CardStatus = consts.V3ApiSimBossCardStatusStopped
|
||||
}
|
||||
}
|
||||
}
|
||||
if output.Iccid == "" && output.Msisdn == "" && output.Imsi == "" && output.Imei == "" && output.OpenDate == "" && output.ActiveDate == "" && output.Remark == "" && output.LastChangeDate == "" && output.CardStatus == 0 {
|
||||
return nil
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func appOneLinkMockFirstPayload(raw json.RawMessage) map[string]any {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(raw, &payload); err == nil && len(payload) > 0 {
|
||||
if nested, ok := payload["resData"]; ok && nested != nil {
|
||||
if nestedRaw, err := json.Marshal(nested); err == nil {
|
||||
if nestedPayload := appOneLinkMockFirstPayload(nestedRaw); len(nestedPayload) > 0 {
|
||||
return nestedPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
if nested, ok := payload["data"]; ok && nested != nil {
|
||||
if nestedRaw, err := json.Marshal(nested); err == nil {
|
||||
if nestedPayload := appOneLinkMockFirstPayload(nestedRaw); len(nestedPayload) > 0 {
|
||||
return nestedPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
var payloadList []map[string]any
|
||||
if err := json.Unmarshal(raw, &payloadList); err == nil {
|
||||
for _, item := range payloadList {
|
||||
if len(item) > 0 {
|
||||
if nested, ok := item["resData"]; ok && nested != nil {
|
||||
if nestedRaw, err := json.Marshal(nested); err == nil {
|
||||
if nestedPayload := appOneLinkMockFirstPayload(nestedRaw); len(nestedPayload) > 0 {
|
||||
return nestedPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
var payloadWrapper struct {
|
||||
ResData json.RawMessage `json:"resData"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &payloadWrapper); err == nil && len(payloadWrapper.ResData) > 0 {
|
||||
return appOneLinkMockFirstPayload(payloadWrapper.ResData)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appOneLinkMockMapString(data map[string]any, keys ...string) string {
|
||||
if len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, key := range keys {
|
||||
if value, ok := data[key]; ok && value != nil {
|
||||
text := strings.TrimSpace(fmt.Sprint(value))
|
||||
if text != "" && text != "<nil>" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *appOneLinkMockClient) ensureToken(ctx context.Context) (string, error) {
|
||||
if strings.TrimSpace(c.token) != "" {
|
||||
return c.token, nil
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Set("appid", c.cfg.AppID)
|
||||
params.Set("password", c.cfg.Password)
|
||||
params.Set("transid", appOneLinkMockTransID(c.cfg.AppID))
|
||||
|
||||
body, err := c.get(ctx, c.cfg.BaseURL+"/ec/get/token?"+params.Encode())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var response appOneLinkMockTokenResponse
|
||||
if err = json.Unmarshal(body, &response); err != nil {
|
||||
return "", gerror.Wrap(err, "解析OneLink挡板token响应失败")
|
||||
}
|
||||
if strings.TrimSpace(response.Status) != "0" || len(response.Result) == 0 || strings.TrimSpace(response.Result[0].Token) == "" {
|
||||
message := strings.TrimSpace(response.Message)
|
||||
if message == "" {
|
||||
message = "获取OneLink挡板token失败"
|
||||
}
|
||||
return "", gerror.New(message)
|
||||
}
|
||||
c.token = strings.TrimSpace(response.Result[0].Token)
|
||||
return c.token, nil
|
||||
}
|
||||
|
||||
func (c *appOneLinkMockClient) get(ctx context.Context, requestURL string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "请求OneLink挡板失败")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取OneLink挡板响应失败")
|
||||
}
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return nil, gerror.Newf("OneLink挡板HTTP状态异常:%d", resp.StatusCode)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func appOneLinkMockParseSessionResult(raw json.RawMessage) []appOneLinkMockSimSessionItem {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil
|
||||
}
|
||||
var directList []appOneLinkMockSimSessionItem
|
||||
if err := json.Unmarshal(raw, &directList); err == nil && len(directList) > 0 {
|
||||
return directList
|
||||
}
|
||||
var direct appOneLinkMockSimSessionItem
|
||||
if err := json.Unmarshal(raw, &direct); err == nil && !direct.isZero() {
|
||||
return []appOneLinkMockSimSessionItem{direct}
|
||||
}
|
||||
var wrappedList []struct {
|
||||
Code int `json:"code"`
|
||||
ResData appOneLinkMockSimSessionItem `json:"resData"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &wrappedList); err == nil && len(wrappedList) > 0 {
|
||||
items := make([]appOneLinkMockSimSessionItem, 0, len(wrappedList))
|
||||
for _, wrapped := range wrappedList {
|
||||
if wrapped.Code == 0 && !wrapped.ResData.isZero() {
|
||||
items = append(items, wrapped.ResData)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
var wrapped struct {
|
||||
Code int `json:"code"`
|
||||
ResData appOneLinkMockSimSessionItem `json:"resData"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &wrapped); err == nil && wrapped.Code == 0 && !wrapped.ResData.isZero() {
|
||||
return []appOneLinkMockSimSessionItem{wrapped.ResData}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (item appOneLinkMockSimSessionItem) isZero() bool {
|
||||
return strings.TrimSpace(item.ApnId) == "" &&
|
||||
strings.TrimSpace(item.Rat) == "" &&
|
||||
strings.TrimSpace(item.Imei) == "" &&
|
||||
strings.TrimSpace(item.Ipv4) == "" &&
|
||||
strings.TrimSpace(item.Ipv6) == "" &&
|
||||
strings.TrimSpace(item.Ipv6Prefix) == "" &&
|
||||
strings.TrimSpace(item.Status) == "" &&
|
||||
strings.TrimSpace(item.CreateTime) == ""
|
||||
}
|
||||
|
||||
func appOneLinkMockOperType(operation appCardLifecycleOperation) (string, error) {
|
||||
switch operation.Code {
|
||||
case "stop":
|
||||
return consts.SimCardStatusOperTypeStop, nil
|
||||
case "start":
|
||||
return consts.SimCardStatusOperTypeStart, nil
|
||||
default:
|
||||
return "", gerror.New("OneLink挡板暂不支持该卡片操作")
|
||||
}
|
||||
}
|
||||
|
||||
func appOneLinkMockTransID(appID string) string {
|
||||
random := time.Now().UnixNano() % 100000000
|
||||
return fmt.Sprintf("%s%s%08d", strings.TrimSpace(appID), gtime.Now().Format("YmdHis"), random)
|
||||
}
|
||||
|
||||
func appOneLinkMockSuccessMessage(message string) string {
|
||||
message = strings.TrimSpace(message)
|
||||
if message == "" {
|
||||
return "OneLink挡板处理成功"
|
||||
}
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const (
|
||||
appUpstreamProviderMock = "onelink_mock"
|
||||
)
|
||||
|
||||
type appSimUpstreamProvider interface {
|
||||
QuerySimBasicInfo(ctx context.Context, input appSimBasicInfoInput) (*appSimBasicInfoOutput, error)
|
||||
ChangeSimStatus(ctx context.Context, operation appCardLifecycleOperation, row *appCardLifecycleExecutionRow) (appSimStatusChangeOutcome, error)
|
||||
}
|
||||
|
||||
type appSimBasicInfoInput struct {
|
||||
CardValue string
|
||||
Iccid string
|
||||
Msisdn string
|
||||
Imsi string
|
||||
Imei string
|
||||
}
|
||||
|
||||
type appSimBasicInfoOutput struct {
|
||||
Provider string
|
||||
Iccid string
|
||||
Msisdn string
|
||||
Imsi string
|
||||
Imei string
|
||||
OpenDate string
|
||||
ActiveDate string
|
||||
Remark string
|
||||
CardStatus int
|
||||
LastChangeDate string
|
||||
}
|
||||
|
||||
type appSimStatusChangeOutcome struct {
|
||||
Success bool
|
||||
Code int
|
||||
Message string
|
||||
OrderNum string
|
||||
}
|
||||
|
||||
func newAppSimUpstreamProvider(ctx context.Context) (appSimUpstreamProvider, error) {
|
||||
provider := strings.TrimSpace(g.Cfg().MustGet(ctx, "upstream.provider", appUpstreamProviderMock).String())
|
||||
if provider == "" {
|
||||
provider = appUpstreamProviderMock
|
||||
}
|
||||
switch provider {
|
||||
case appUpstreamProviderMock:
|
||||
return newAppOneLinkMockClient(ctx)
|
||||
default:
|
||||
return nil, gerror.Newf("不支持的上游服务提供方:%s", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func appSimBasicInfoInputFromCardValue(cardValue string) appSimBasicInfoInput {
|
||||
value := strings.TrimSpace(cardValue)
|
||||
input := appSimBasicInfoInput{CardValue: value}
|
||||
switch {
|
||||
case len(value) == 20:
|
||||
input.Iccid = value
|
||||
case len(value) == 15:
|
||||
input.Imsi = value
|
||||
default:
|
||||
input.Msisdn = value
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func appErrUpstreamUnavailable(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return gerror.Wrap(err, "上游挡板暂不可用")
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type sAppOrder struct{}
|
||||
|
||||
func NewAppOrder() *sAppOrder {
|
||||
return &sAppOrder{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAppOrder(NewAppOrder())
|
||||
}
|
||||
|
||||
func (s *sAppOrder) List(ctx context.Context, param *model.AppOrderListParam) (items []*model.AppOrderModel, total int, err error) {
|
||||
if param == nil {
|
||||
param = &model.AppOrderListParam{}
|
||||
}
|
||||
if param.Page <= 0 {
|
||||
param.Page = 1
|
||||
}
|
||||
if param.PageSize <= 0 {
|
||||
param.PageSize = 10
|
||||
}
|
||||
if param.PageSize > 100 {
|
||||
param.PageSize = 100
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
mod := s.ownerOrderModel(ctx, user)
|
||||
mod = s.applyOrderFilters(mod, param)
|
||||
|
||||
total, err = mod.Count()
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "查询订单数量失败")
|
||||
}
|
||||
if total == 0 {
|
||||
return []*model.AppOrderModel{}, 0, nil
|
||||
}
|
||||
|
||||
var rows []*entity.TOrder
|
||||
if err = mod.
|
||||
Page(param.Page, param.PageSize).
|
||||
OrderDesc(dao.TOrder.Columns().CreatedAt).
|
||||
OrderDesc(dao.TOrder.Columns().Id).
|
||||
Scan(&rows); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "查询订单列表失败")
|
||||
}
|
||||
|
||||
orderSns := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row != nil && strings.TrimSpace(row.OrderSn) != "" {
|
||||
orderSns = append(orderSns, row.OrderSn)
|
||||
}
|
||||
}
|
||||
itemMap, err := s.orderItemMap(ctx, user.TenantId, orderSns)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
items = make([]*model.AppOrderModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, s.mapOrder(ctx, row, itemMap[row.OrderSn], false))
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *sAppOrder) Detail(ctx context.Context, param *model.AppOrderDetailParam) (*model.AppOrderModel, error) {
|
||||
if param == nil {
|
||||
param = &model.AppOrderDetailParam{}
|
||||
}
|
||||
orderSn := strings.TrimSpace(param.OrderSn)
|
||||
if orderSn == "" {
|
||||
return nil, gerror.New("订单号不能为空")
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var order *entity.TOrder
|
||||
if err = s.ownerOrderModel(ctx, user).
|
||||
Where(dao.TOrder.Columns().OrderSn, orderSn).
|
||||
Limit(1).
|
||||
Scan(&order); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询订单详情失败")
|
||||
}
|
||||
if order == nil || order.Id == 0 {
|
||||
return nil, gerror.New("订单不存在或无权查看")
|
||||
}
|
||||
|
||||
itemMap, err := s.orderItemMap(ctx, user.TenantId, []string{order.OrderSn})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.mapOrder(ctx, order, itemMap[order.OrderSn], true), nil
|
||||
}
|
||||
|
||||
func (s *sAppOrder) ownerOrderModel(ctx context.Context, user *entity.SysUser) *gdb.Model {
|
||||
orderTable := dao.TOrder.Table()
|
||||
mod := dao.TOrder.Ctx(ctx).
|
||||
Where(orderTable+"."+dao.TOrder.Columns().TenantId, user.TenantId)
|
||||
if appIsTerminalUser(ctx, user) {
|
||||
return mod.Where(orderTable+"."+dao.TOrder.Columns().CreatedBy, user.UserId)
|
||||
}
|
||||
return appApplyEnterpriseDataScope(
|
||||
ctx,
|
||||
user,
|
||||
mod,
|
||||
orderTable+"."+dao.TOrder.Columns().CreatedDept,
|
||||
orderTable+"."+dao.TOrder.Columns().CreatedBy,
|
||||
func(legacyMod *gdb.Model) *gdb.Model {
|
||||
return legacyMod.Where(orderTable+"."+dao.TOrder.Columns().CreatedBy, user.UserId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sAppOrder) applyOrderFilters(mod *gdb.Model, param *model.AppOrderListParam) *gdb.Model {
|
||||
if param.OrderType > 0 {
|
||||
mod = mod.Where(dao.TOrder.Columns().OrderType, param.OrderType)
|
||||
}
|
||||
if param.PayStatus != nil && *param.PayStatus >= 0 {
|
||||
mod = mod.Where(dao.TOrder.Columns().PayStatus, *param.PayStatus)
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(param.Keyword)
|
||||
if keyword == "" {
|
||||
keyword = strings.TrimSpace(param.PageInfo.Keyword)
|
||||
}
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
orderTable := dao.TOrder.Table()
|
||||
itemTable := dao.TOrderItem.Table()
|
||||
mod = mod.Where(
|
||||
"("+orderTable+"."+dao.TOrder.Columns().OrderSn+" LIKE ? OR "+
|
||||
orderTable+"."+dao.TOrder.Columns().PayOrderSn+" LIKE ? OR "+
|
||||
orderTable+"."+dao.TOrder.Columns().Remark+" LIKE ? OR EXISTS (SELECT 1 FROM "+itemTable+" oi WHERE oi."+dao.TOrderItem.Columns().OrderSn+" = "+orderTable+"."+dao.TOrder.Columns().OrderSn+" AND oi."+dao.TOrderItem.Columns().ProductName+" LIKE ?))",
|
||||
like, like, like, like,
|
||||
)
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
func (s *sAppOrder) orderItemMap(ctx context.Context, tenantId string, orderSns []string) (map[string][]*entity.TOrderItem, error) {
|
||||
result := make(map[string][]*entity.TOrderItem)
|
||||
if len(orderSns) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var rows []*entity.TOrderItem
|
||||
if err := dao.TOrderItem.Ctx(ctx).
|
||||
Where(dao.TOrderItem.Columns().TenantId, tenantId).
|
||||
WhereIn(dao.TOrderItem.Columns().OrderSn, orderSns).
|
||||
OrderAsc(dao.TOrderItem.Columns().ItemId).
|
||||
Scan(&rows); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询订单项失败")
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
result[row.OrderSn] = append(result[row.OrderSn], row)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *sAppOrder) mapOrder(ctx context.Context, row *entity.TOrder, items []*entity.TOrderItem, includeItems bool) *model.AppOrderModel {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
mappedItems := make([]*model.AppOrderItemModel, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
mappedItems = append(mappedItems, &model.AppOrderItemModel{
|
||||
ItemId: item.ItemId,
|
||||
ProductName: item.ProductName,
|
||||
ProductSn: item.ProductSn,
|
||||
Price: appRoundAmount(item.Price),
|
||||
PriceText: appFormatMoney(item.Price),
|
||||
Quantity: item.Quantity,
|
||||
OriginPrice: appRoundAmount(item.OriginPrice),
|
||||
OriginPriceText: appFormatMoney(item.OriginPrice),
|
||||
ProductType: item.ProductType,
|
||||
ServiceBeginTime: item.ServiceBeginTime,
|
||||
ServiceEndTime: item.ServiceEndTime,
|
||||
})
|
||||
}
|
||||
|
||||
itemSummary := "订单商品"
|
||||
if len(mappedItems) > 0 && strings.TrimSpace(mappedItems[0].ProductName) != "" {
|
||||
itemSummary = strings.TrimSpace(mappedItems[0].ProductName)
|
||||
if len(mappedItems) > 1 {
|
||||
itemSummary += " 等"
|
||||
}
|
||||
}
|
||||
orderTypeText := appOrderTypeText(row.OrderType)
|
||||
switch appOrderBusinessFromItems(items) {
|
||||
case "miniapp_card_change_batch":
|
||||
orderTypeText = "套餐变更"
|
||||
case "miniapp_card_renew_batch":
|
||||
orderTypeText = "套餐续费"
|
||||
}
|
||||
paymentExpireAt, paymentRemainingSeconds, paymentExpired := appBalanceRechargePaymentTiming(ctx, row)
|
||||
|
||||
res := &model.AppOrderModel{
|
||||
OrderId: row.Id,
|
||||
OrderSn: row.OrderSn,
|
||||
OrderType: row.OrderType,
|
||||
OrderTypeText: orderTypeText,
|
||||
OrderStatus: row.OrderStatus,
|
||||
OrderStatusText: appOrderStatusText(row.OrderStatus),
|
||||
PayType: row.PayType,
|
||||
PayTypeText: appOrderPayTypeText(row.PayType),
|
||||
PayStatus: row.PayStatus,
|
||||
PayStatusText: appOrderPayStatusText(row.PayStatus),
|
||||
PayOrderSn: row.PayOrderSn,
|
||||
ReturnStatus: row.ReturnStatus,
|
||||
ReturnStatusText: appOrderReturnStatusText(row.ReturnStatus),
|
||||
PaymentExpireAt: paymentExpireAt,
|
||||
PaymentRemainingSeconds: paymentRemainingSeconds,
|
||||
PaymentExpired: paymentExpired,
|
||||
TotalAmount: appRoundAmount(row.TotalAmount),
|
||||
TotalAmountText: appFormatMoney(row.TotalAmount),
|
||||
PaidAmount: appRoundAmount(row.PaidAmount),
|
||||
PaidAmountText: appFormatMoney(row.PaidAmount),
|
||||
OnlinePaidAmount: appRoundAmount(row.OnlinePaidAmount),
|
||||
OnlinePaidAmountText: appFormatMoney(row.OnlinePaidAmount),
|
||||
Balance: appRoundAmount(row.Balance),
|
||||
BalanceText: appFormatMoney(row.Balance),
|
||||
UnpaidAmount: appRoundAmount(row.UnpaidAmount),
|
||||
UnpaidAmountText: appFormatMoney(row.UnpaidAmount),
|
||||
UnrefundAmount: appRoundAmount(row.UnrefundAmount),
|
||||
UnrefundAmountText: appFormatMoney(row.UnrefundAmount),
|
||||
BuyerNote: row.BuyerNote,
|
||||
Remark: row.Remark,
|
||||
CreatedAt: row.CreatedAt,
|
||||
PayTime: row.PayTime,
|
||||
ItemSummary: itemSummary,
|
||||
ItemCount: len(mappedItems),
|
||||
}
|
||||
if includeItems {
|
||||
res.Items = mappedItems
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func appOrderBusinessFromItems(items []*entity.TOrderItem) string {
|
||||
for _, item := range items {
|
||||
if item == nil || strings.TrimSpace(item.ExtraSkuData) == "" {
|
||||
continue
|
||||
}
|
||||
var extra struct {
|
||||
Business string `json:"business"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(item.ExtraSkuData), &extra); err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(extra.Business) != "" {
|
||||
return strings.TrimSpace(extra.Business)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func appOrderTypeText(orderType int) string {
|
||||
switch orderType {
|
||||
case consts.OrderTypeCardSubscribe:
|
||||
return "套餐续费"
|
||||
case consts.OrderTypeBalanceRecharge:
|
||||
return "余额充值"
|
||||
default:
|
||||
return "其他订单"
|
||||
}
|
||||
}
|
||||
|
||||
func appOrderStatusText(status int) string {
|
||||
switch status {
|
||||
case consts.OrderStatusPendingPay:
|
||||
return "待支付"
|
||||
case consts.OrderStatusCompleted:
|
||||
return "已完成"
|
||||
default:
|
||||
return "处理中"
|
||||
}
|
||||
}
|
||||
|
||||
func appOrderPayTypeText(payType int) string {
|
||||
switch payType {
|
||||
case consts.PayTypeBalance:
|
||||
return "余额"
|
||||
case consts.PayTypeOnline:
|
||||
return "线上支付"
|
||||
default:
|
||||
return "未支付"
|
||||
}
|
||||
}
|
||||
|
||||
func appOrderPayStatusText(payStatus int) string {
|
||||
switch payStatus {
|
||||
case consts.PayStatusUnpaid:
|
||||
return "待支付"
|
||||
case consts.PayStatusPaid:
|
||||
return "已支付"
|
||||
default:
|
||||
return "处理中"
|
||||
}
|
||||
}
|
||||
|
||||
func appOrderReturnStatusText(status int) string {
|
||||
switch status {
|
||||
case consts.OrderReturnStatusPending:
|
||||
return "退款申请中"
|
||||
case consts.OrderReturnStatusRunning:
|
||||
return "退款处理中"
|
||||
case consts.OrderReturnStatusPartial:
|
||||
return "退款异常"
|
||||
case consts.OrderReturnStatusSuccess:
|
||||
return "已退款"
|
||||
default:
|
||||
return "未退款"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,906 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/h5"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||||
)
|
||||
|
||||
const (
|
||||
appWechatPayMethodH5 = "wechat_h5"
|
||||
appWechatPayMethodMini = "wechat_mini"
|
||||
appWechatNotifyPath = "/api/v1/payment/wechat/notify"
|
||||
appWechatRefundPath = "/api/v1/payment/wechat/refund-notify"
|
||||
)
|
||||
|
||||
type appWechatConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
EnvFile string `json:"envFile"`
|
||||
ApiBase string `json:"apiBase"`
|
||||
MchId string `json:"mchId"`
|
||||
MchSerialNo string `json:"mchSerialNo"`
|
||||
ApiV3Key string `json:"apiV3Key"`
|
||||
PrivateKeyPath string `json:"privateKeyPath"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
NotifyUrl string `json:"notifyUrl"`
|
||||
NotifyBaseUrl string `json:"notifyBaseUrl"`
|
||||
RefundNotifyUrl string `json:"refundNotifyUrl"`
|
||||
TimeoutMinutes int `json:"timeoutMinutes"`
|
||||
PlatformPublicKeyId string `json:"platformPublicKeyId"`
|
||||
PlatformPublicKeyPath string `json:"platformPublicKeyPath"`
|
||||
PlatformPublicKey string `json:"platformPublicKey"`
|
||||
H5 appWechatH5Config `json:"h5"`
|
||||
Mini appWechatMiniConfig `json:"mini"`
|
||||
}
|
||||
|
||||
type appWechatH5Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AppId string `json:"appId"`
|
||||
AppName string `json:"appName"`
|
||||
AppUrl string `json:"appUrl"`
|
||||
PayerClientIp string `json:"payerClientIp"`
|
||||
}
|
||||
|
||||
type appWechatMiniConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AppId string `json:"appId"`
|
||||
AppSecret string `json:"appSecret"`
|
||||
}
|
||||
|
||||
type appWechatPayRequest struct {
|
||||
PayUrl string
|
||||
PayMethod string
|
||||
PayParams map[string]string
|
||||
}
|
||||
|
||||
func appWechatPayUrl(payRequest *appWechatPayRequest) string {
|
||||
if payRequest == nil {
|
||||
return "物联网卡管理平台"
|
||||
}
|
||||
return payRequest.PayUrl
|
||||
}
|
||||
|
||||
func appWechatPayMethod(payRequest *appWechatPayRequest) string {
|
||||
if payRequest == nil {
|
||||
return ""
|
||||
}
|
||||
return payRequest.PayMethod
|
||||
}
|
||||
|
||||
func appWechatPayParams(payRequest *appWechatPayRequest) map[string]string {
|
||||
if payRequest == nil || len(payRequest.PayParams) == 0 {
|
||||
return nil
|
||||
}
|
||||
copied := make(map[string]string, len(payRequest.PayParams))
|
||||
for key, value := range payRequest.PayParams {
|
||||
copied[key] = value
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
func appLoadWechatConfig(ctx context.Context) (*appWechatConfig, error) {
|
||||
cfg := &appWechatConfig{}
|
||||
if err := g.Cfg().MustGet(ctx, "payment.wechat").Scan(cfg); err != nil {
|
||||
return nil, gerror.Wrap(err, "读取微信支付配置失败")
|
||||
}
|
||||
|
||||
envDir, err := appLoadWechatEnvFile(ctx, cfg.EnvFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appApplyWechatEnvOverrides(cfg, envDir)
|
||||
appNormalizeWechatConfig(cfg)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func appLoadWechatEnvFile(ctx context.Context, configured string) (string, error) {
|
||||
envFile := strings.TrimSpace(os.Getenv("WECHAT_PAY_ENV_FILE"))
|
||||
if envFile == "" {
|
||||
envFile = strings.TrimSpace(configured)
|
||||
}
|
||||
if envFile == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(filepath.Clean(envFile))
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "解析微信支付环境配置路径失败")
|
||||
}
|
||||
data, err := os.ReadFile(absPath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
g.Log().Warningf(ctx, "微信支付环境配置文件不存在,将继续读取系统环境变量: %s", absPath)
|
||||
return filepath.Dir(absPath), nil
|
||||
}
|
||||
if err != nil {
|
||||
return filepath.Dir(absPath), gerror.Wrap(err, "读取微信支付环境配置失败")
|
||||
}
|
||||
|
||||
values := appParseDotenv(string(data))
|
||||
for key, value := range values {
|
||||
|
||||
if _, exists := os.LookupEnv(key); exists {
|
||||
continue
|
||||
}
|
||||
if err = os.Setenv(key, value); err != nil {
|
||||
return filepath.Dir(absPath), gerror.Wrapf(err, "写入微信支付环境变量失败: %s", key)
|
||||
}
|
||||
}
|
||||
g.Log().Debugf(ctx, "微信支付环境配置已加载: %s", absPath)
|
||||
return filepath.Dir(absPath), nil
|
||||
}
|
||||
|
||||
func appParseDotenv(content string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
content = strings.TrimPrefix(content, "\ufeff")
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
line = strings.TrimSpace(strings.TrimRight(line, "\r"))
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
index := strings.Index(line, "=")
|
||||
if index <= 0 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(line[:index])
|
||||
value := strings.TrimSpace(line[index+1:])
|
||||
value = strings.Trim(value, `"'`)
|
||||
if key != "" {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func appApplyWechatEnvOverrides(cfg *appWechatConfig, envDir string) {
|
||||
cfg.Enabled = appEnvBool("WECHAT_PAY_ENABLED", cfg.Enabled)
|
||||
cfg.H5.Enabled = appEnvBool("WECHAT_PAY_H5_ENABLED", cfg.H5.Enabled)
|
||||
cfg.Mini.Enabled = appEnvBool("WECHAT_PAY_MINI_ENABLED", cfg.Mini.Enabled)
|
||||
|
||||
cfg.MchId = appEnvValue("WECHAT_PAY_MCH_ID", cfg.MchId)
|
||||
cfg.MchSerialNo = appEnvValue("WECHAT_PAY_MCH_SERIAL_NO", cfg.MchSerialNo)
|
||||
cfg.ApiV3Key = appEnvValue("WECHAT_PAY_API_V3_KEY", cfg.ApiV3Key)
|
||||
cfg.PrivateKey = appEnvValue("WECHAT_PAY_MCH_PRIVATE_KEY", cfg.PrivateKey)
|
||||
cfg.PrivateKeyPath = appEnvPath("WECHAT_PAY_MCH_PRIVATE_KEY_PATH", cfg.PrivateKeyPath, envDir)
|
||||
cfg.PlatformPublicKey = appEnvValue("WECHAT_PAY_PUBLIC_KEY", cfg.PlatformPublicKey)
|
||||
cfg.PlatformPublicKeyId = appEnvValue("WECHAT_PAY_PUBLIC_KEY_ID", cfg.PlatformPublicKeyId)
|
||||
cfg.PlatformPublicKeyPath = appEnvPath("WECHAT_PAY_PUBLIC_KEY_PATH", cfg.PlatformPublicKeyPath, envDir)
|
||||
cfg.NotifyUrl = appEnvValue("WECHAT_PAY_NOTIFY_URL", cfg.NotifyUrl)
|
||||
cfg.NotifyBaseUrl = appEnvValue("WECHAT_PAY_NOTIFY_BASE_URL", cfg.NotifyBaseUrl)
|
||||
cfg.RefundNotifyUrl = appEnvValue("WECHAT_PAY_REFUND_NOTIFY_URL", cfg.RefundNotifyUrl)
|
||||
|
||||
if timeout := strings.TrimSpace(os.Getenv("WECHAT_PAY_TIMEOUT_MINUTES")); timeout != "" {
|
||||
if value, err := strconv.Atoi(timeout); err == nil {
|
||||
cfg.TimeoutMinutes = value
|
||||
}
|
||||
}
|
||||
|
||||
// AppID 优先使用本项目配置,避免模板 .env 中的旧 AppID 覆盖当前小程序配置。
|
||||
if value := appEnvFirst("APP_WECHAT_H5_APP_ID", "WECHAT_PAY_H5_APP_ID"); value != "" {
|
||||
cfg.H5.AppId = value
|
||||
} else if cfg.H5.AppId == "" {
|
||||
cfg.H5.AppId = appEnvFirst("WECHAT_PAY_SERVICE_APP_ID", "WECHAT_PAY_APP_ID")
|
||||
}
|
||||
cfg.H5.AppName = appEnvValue("WECHAT_PAY_H5_APP_NAME", cfg.H5.AppName)
|
||||
cfg.H5.AppUrl = appEnvValue("WECHAT_PAY_H5_APP_URL", cfg.H5.AppUrl)
|
||||
cfg.H5.PayerClientIp = appEnvValue("WECHAT_PAY_H5_PAYER_CLIENT_IP", cfg.H5.PayerClientIp)
|
||||
|
||||
if value := appEnvFirst("APP_WECHAT_MINI_APP_ID", "WECHAT_PAY_MINI_APP_ID"); value != "" {
|
||||
cfg.Mini.AppId = value
|
||||
} else if cfg.Mini.AppId == "" {
|
||||
cfg.Mini.AppId = strings.TrimSpace(os.Getenv("WECHAT_PAY_MINI_PROGRAM_APP_ID"))
|
||||
}
|
||||
if value := appEnvFirst("APP_WECHAT_MINI_APP_SECRET", "WECHAT_MINI_APP_SECRET", "WECHAT_PAY_MINI_APP_SECRET", "WECHAT_PAY_MINI_PROGRAM_APP_SECRET"); value != "" {
|
||||
cfg.Mini.AppSecret = value
|
||||
}
|
||||
}
|
||||
|
||||
func appNormalizeWechatConfig(cfg *appWechatConfig) {
|
||||
cfg.EnvFile = strings.TrimSpace(cfg.EnvFile)
|
||||
cfg.ApiBase = strings.TrimRight(strings.TrimSpace(cfg.ApiBase), "/")
|
||||
cfg.MchId = strings.TrimSpace(cfg.MchId)
|
||||
cfg.MchSerialNo = strings.TrimSpace(cfg.MchSerialNo)
|
||||
cfg.ApiV3Key = strings.TrimSpace(cfg.ApiV3Key)
|
||||
cfg.PrivateKeyPath = strings.TrimSpace(cfg.PrivateKeyPath)
|
||||
cfg.PrivateKey = strings.TrimSpace(cfg.PrivateKey)
|
||||
cfg.NotifyUrl = strings.TrimSpace(cfg.NotifyUrl)
|
||||
cfg.NotifyBaseUrl = strings.TrimSpace(cfg.NotifyBaseUrl)
|
||||
cfg.RefundNotifyUrl = strings.TrimSpace(cfg.RefundNotifyUrl)
|
||||
cfg.PlatformPublicKeyId = strings.TrimSpace(cfg.PlatformPublicKeyId)
|
||||
cfg.PlatformPublicKeyPath = strings.TrimSpace(cfg.PlatformPublicKeyPath)
|
||||
cfg.PlatformPublicKey = strings.TrimSpace(cfg.PlatformPublicKey)
|
||||
cfg.H5.AppId = strings.TrimSpace(cfg.H5.AppId)
|
||||
cfg.H5.AppName = strings.TrimSpace(cfg.H5.AppName)
|
||||
cfg.H5.AppUrl = strings.TrimSpace(cfg.H5.AppUrl)
|
||||
cfg.H5.PayerClientIp = strings.TrimSpace(cfg.H5.PayerClientIp)
|
||||
cfg.Mini.AppId = strings.TrimSpace(cfg.Mini.AppId)
|
||||
cfg.Mini.AppSecret = strings.TrimSpace(cfg.Mini.AppSecret)
|
||||
|
||||
if cfg.ApiBase == "" {
|
||||
cfg.ApiBase = "https://api.mch.weixin.qq.com"
|
||||
}
|
||||
if cfg.TimeoutMinutes <= 0 {
|
||||
cfg.TimeoutMinutes = 5
|
||||
}
|
||||
if cfg.NotifyBaseUrl != "" {
|
||||
cfg.NotifyUrl = appWechatBuildNotifyUrl(cfg.NotifyBaseUrl)
|
||||
if cfg.RefundNotifyUrl == "" {
|
||||
cfg.RefundNotifyUrl = appWechatBuildUrl(cfg.NotifyBaseUrl, appWechatRefundPath)
|
||||
}
|
||||
} else {
|
||||
cfg.NotifyUrl = appWechatNormalizeNotifyUrl(cfg.NotifyUrl)
|
||||
}
|
||||
if cfg.RefundNotifyUrl != "" {
|
||||
cfg.RefundNotifyUrl = appWechatNormalizePathUrl(cfg.RefundNotifyUrl, appWechatRefundPath)
|
||||
}
|
||||
}
|
||||
|
||||
func appEnvValue(key string, current string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func appEnvFirst(keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func appEnvBool(key string, current bool) bool {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return current
|
||||
}
|
||||
switch strings.ToLower(value) {
|
||||
case "1", "true", "yes", "y", "on", "enabled":
|
||||
return true
|
||||
case "0", "false", "no", "n", "off", "disabled":
|
||||
return false
|
||||
default:
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
func appEnvPath(key string, current string, envDir string) string {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return current
|
||||
}
|
||||
if filepath.IsAbs(value) || envDir == "" {
|
||||
return value
|
||||
}
|
||||
return filepath.Join(envDir, value)
|
||||
}
|
||||
|
||||
func appWechatBuildNotifyUrl(base string) string {
|
||||
return appWechatBuildUrl(base, appWechatNotifyPath)
|
||||
}
|
||||
|
||||
func appWechatBuildUrl(base string, path string) string {
|
||||
base = strings.TrimSpace(base)
|
||||
if base == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := url.Parse(base)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return strings.TrimRight(base, "/") + path
|
||||
}
|
||||
parsed.Path = path
|
||||
parsed.RawPath = ""
|
||||
parsed.RawQuery = ""
|
||||
parsed.Fragment = ""
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func appWechatNormalizeNotifyUrl(raw string) string {
|
||||
return appWechatNormalizePathUrl(raw, appWechatNotifyPath)
|
||||
}
|
||||
|
||||
func appWechatNormalizePathUrl(raw string, path string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return raw
|
||||
}
|
||||
if strings.TrimSpace(parsed.Path) == "" || strings.TrimRight(parsed.Path, "/") == strings.TrimRight(path, "/") {
|
||||
parsed.Path = path
|
||||
parsed.RawPath = ""
|
||||
parsed.RawQuery = ""
|
||||
parsed.Fragment = ""
|
||||
return parsed.String()
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func appWechatEnabled(ctx context.Context) bool {
|
||||
cfg, err := appLoadWechatConfig(ctx)
|
||||
return err == nil && cfg.Enabled
|
||||
}
|
||||
|
||||
func appWechatPaymentTimeoutDuration(ctx context.Context) time.Duration {
|
||||
minutes := g.Cfg().MustGet(ctx, "payment.wechat.timeoutMinutes", 5).Int()
|
||||
if timeout := strings.TrimSpace(os.Getenv("WECHAT_PAY_TIMEOUT_MINUTES")); timeout != "" {
|
||||
if value, err := strconv.Atoi(timeout); err == nil {
|
||||
minutes = value
|
||||
}
|
||||
}
|
||||
if minutes <= 0 {
|
||||
minutes = 5
|
||||
}
|
||||
return time.Duration(minutes) * time.Minute
|
||||
}
|
||||
|
||||
func appBalanceRechargePaymentTiming(ctx context.Context, order *entity.TOrder) (*gtime.Time, int64, bool) {
|
||||
if order == nil || order.OrderType != consts.OrderTypeBalanceRecharge || order.PayStatus != consts.PayStatusUnpaid {
|
||||
return nil, 0, false
|
||||
}
|
||||
if order.CreatedAt == nil || order.CreatedAt.IsZero() {
|
||||
return nil, 0, false
|
||||
}
|
||||
|
||||
expireAt := order.CreatedAt.Add(appWechatPaymentTimeoutDuration(ctx))
|
||||
remaining := expireAt.Sub(gtime.Now())
|
||||
if remaining <= 0 {
|
||||
return expireAt, 0, true
|
||||
}
|
||||
return expireAt, int64(math.Ceil(remaining.Seconds())), false
|
||||
}
|
||||
|
||||
func appWechatPrepayExpireTime(ctx context.Context, cfg *appWechatConfig, order *entity.TOrder) time.Time {
|
||||
timeout := appWechatPaymentTimeoutDuration(ctx)
|
||||
if cfg != nil && cfg.TimeoutMinutes > 0 {
|
||||
timeout = time.Duration(cfg.TimeoutMinutes) * time.Minute
|
||||
}
|
||||
if order != nil && order.CreatedAt != nil && !order.CreatedAt.IsZero() {
|
||||
return time.Unix(order.CreatedAt.Add(timeout).Timestamp(), 0)
|
||||
}
|
||||
return time.Now().Add(timeout)
|
||||
}
|
||||
|
||||
func appMockPaymentEnabled(ctx context.Context) bool {
|
||||
return g.Cfg().MustGet(ctx, "payment.mock.enabled", false).Bool()
|
||||
}
|
||||
|
||||
func appBuildWechatPayRequest(ctx context.Context, order *entity.TOrder, payScene string, user *entity.SysUser) (*appWechatPayRequest, error) {
|
||||
if order == nil || strings.TrimSpace(order.OrderSn) == "" {
|
||||
return nil, gerror.New("微信支付下单订单信息不完整")
|
||||
}
|
||||
cfg, err := appLoadWechatConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if err = appValidateWechatCommonConfig(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, expired := appBalanceRechargePaymentTiming(ctx, order); expired {
|
||||
return nil, gerror.New("支付订单已过期,请重新提交充值")
|
||||
}
|
||||
|
||||
switch appNormalizeWechatPayScene(payScene) {
|
||||
case "mini":
|
||||
return appBuildWechatMiniPayRequest(ctx, cfg, order, user)
|
||||
default:
|
||||
return appBuildWechatH5PayRequest(ctx, cfg, order)
|
||||
}
|
||||
}
|
||||
|
||||
func appValidateWechatCommonConfig(cfg *appWechatConfig) error {
|
||||
if cfg.MchId == "" {
|
||||
return gerror.New("微信支付商户号未配置")
|
||||
}
|
||||
if cfg.MchSerialNo == "" {
|
||||
return gerror.New("微信支付商户证书序列号未配置")
|
||||
}
|
||||
if cfg.ApiV3Key == "" {
|
||||
return gerror.New("微信支付 API v3 密钥未配置")
|
||||
}
|
||||
if len([]byte(cfg.ApiV3Key)) != 32 {
|
||||
return gerror.New("微信支付 API v3 密钥长度必须为 32 字节")
|
||||
}
|
||||
if cfg.NotifyUrl == "" {
|
||||
return gerror.New("微信支付回调地址未配置")
|
||||
}
|
||||
if cfg.PrivateKey == "" && cfg.PrivateKeyPath == "" {
|
||||
return gerror.New("微信支付商户私钥未配置")
|
||||
}
|
||||
if cfg.PlatformPublicKey == "" && cfg.PlatformPublicKeyPath == "" {
|
||||
return gerror.New("微信支付平台公钥未配置")
|
||||
}
|
||||
if cfg.PlatformPublicKeyId == "" {
|
||||
return gerror.New("微信支付平台公钥 ID 未配置")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appBuildWechatH5PayRequest(ctx context.Context, cfg *appWechatConfig, order *entity.TOrder) (*appWechatPayRequest, error) {
|
||||
if !cfg.H5.Enabled {
|
||||
return nil, gerror.New("微信 H5 支付未启用")
|
||||
}
|
||||
appId := cfg.H5.AppId
|
||||
if appId == "" {
|
||||
appId = cfg.Mini.AppId
|
||||
}
|
||||
if appId == "" {
|
||||
return nil, gerror.New("微信 H5 支付 AppID 未配置")
|
||||
}
|
||||
clientIp := appWechatClientIp(ctx, cfg)
|
||||
if clientIp == "" {
|
||||
return nil, gerror.New("微信 H5 支付终端 IP 未获取到")
|
||||
}
|
||||
|
||||
expireAt := appWechatPrepayExpireTime(ctx, cfg, order)
|
||||
prepayReq := h5.PrepayRequest{
|
||||
Appid: core.String(appId),
|
||||
Mchid: core.String(cfg.MchId),
|
||||
Description: core.String(appWechatOrderDescription(order)),
|
||||
OutTradeNo: core.String(order.OrderSn),
|
||||
TimeExpire: &expireAt,
|
||||
NotifyUrl: core.String(cfg.NotifyUrl),
|
||||
Amount: &h5.Amount{
|
||||
Total: core.Int64(appWechatAmountFen(order.TotalAmount)),
|
||||
Currency: core.String("CNY"),
|
||||
},
|
||||
SceneInfo: &h5.SceneInfo{
|
||||
PayerClientIp: core.String(clientIp),
|
||||
H5Info: &h5.H5Info{
|
||||
Type: core.String("Wap"),
|
||||
AppName: appStringPtr(appWechatH5AppName(cfg)),
|
||||
AppUrl: appStringPtr(cfg.H5.AppUrl),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
client, err := appWechatClient(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsp, _, err := (&h5.H5ApiService{Client: client}).Prepay(ctx, prepayReq)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "微信 H5 支付预下单失败")
|
||||
}
|
||||
if rsp == nil || rsp.H5Url == nil || strings.TrimSpace(*rsp.H5Url) == "" {
|
||||
return nil, gerror.New("微信 H5 支付未返回 h5_url")
|
||||
}
|
||||
return &appWechatPayRequest{
|
||||
PayUrl: appWechatH5PayUrl(strings.TrimSpace(*rsp.H5Url), cfg, order),
|
||||
PayMethod: appWechatPayMethodH5,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appWechatH5PayUrl(raw string, cfg *appWechatConfig, order *entity.TOrder) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || cfg == nil || order == nil || strings.TrimSpace(order.OrderSn) == "" {
|
||||
return raw
|
||||
}
|
||||
returnUrl := appWechatH5ReturnUrl(cfg, order.OrderSn)
|
||||
if returnUrl == "" {
|
||||
return raw
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return raw
|
||||
}
|
||||
query := parsed.Query()
|
||||
if strings.TrimSpace(query.Get("redirect_url")) == "" {
|
||||
query.Set("redirect_url", returnUrl)
|
||||
parsed.RawQuery = query.Encode()
|
||||
}
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func appWechatH5ReturnUrl(cfg *appWechatConfig, orderSn string) string {
|
||||
base := strings.TrimRight(strings.TrimSpace(cfg.H5.AppUrl), "/")
|
||||
if base == "" {
|
||||
base = strings.TrimRight(strings.TrimSpace(cfg.NotifyBaseUrl), "/")
|
||||
}
|
||||
if base == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s/#/pages/business/views/balanceRecharge/index?orderSn=%s", base, url.QueryEscape(strings.TrimSpace(orderSn)))
|
||||
}
|
||||
|
||||
func appBuildWechatMiniPayRequest(ctx context.Context, cfg *appWechatConfig, order *entity.TOrder, user *entity.SysUser) (*appWechatPayRequest, error) {
|
||||
if !cfg.Mini.Enabled {
|
||||
return nil, gerror.New("微信小程序支付未启用")
|
||||
}
|
||||
if cfg.Mini.AppId == "" {
|
||||
return nil, gerror.New("微信小程序支付 AppID 未配置")
|
||||
}
|
||||
openId, err := appCurrentWechatOpenId(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expireAt := appWechatPrepayExpireTime(ctx, cfg, order)
|
||||
prepayReq := jsapi.PrepayRequest{
|
||||
Appid: core.String(cfg.Mini.AppId),
|
||||
Mchid: core.String(cfg.MchId),
|
||||
Description: core.String(appWechatOrderDescription(order)),
|
||||
OutTradeNo: core.String(order.OrderSn),
|
||||
TimeExpire: &expireAt,
|
||||
NotifyUrl: core.String(cfg.NotifyUrl),
|
||||
Amount: &jsapi.Amount{
|
||||
Total: core.Int64(appWechatAmountFen(order.TotalAmount)),
|
||||
Currency: core.String("CNY"),
|
||||
},
|
||||
Payer: &jsapi.Payer{
|
||||
Openid: core.String(openId),
|
||||
},
|
||||
}
|
||||
|
||||
client, err := appWechatClient(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsp, _, err := (&jsapi.JsapiApiService{Client: client}).PrepayWithRequestPayment(ctx, prepayReq)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "微信小程序支付预下单失败")
|
||||
}
|
||||
if rsp == nil || rsp.Package == nil || strings.TrimSpace(*rsp.Package) == "" {
|
||||
return nil, gerror.New("微信小程序支付未返回调起支付参数")
|
||||
}
|
||||
return &appWechatPayRequest{
|
||||
PayMethod: appWechatPayMethodMini,
|
||||
PayParams: map[string]string{
|
||||
"appId": appStringValue(rsp.Appid),
|
||||
"timeStamp": appStringValue(rsp.TimeStamp),
|
||||
"nonceStr": appStringValue(rsp.NonceStr),
|
||||
"package": appStringValue(rsp.Package),
|
||||
"signType": appStringValue(rsp.SignType),
|
||||
"paySign": appStringValue(rsp.PaySign),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appWechatClient(ctx context.Context, cfg *appWechatConfig) (*core.Client, error) {
|
||||
privateKey, err := appWechatPrivateKey(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verifier, err := appWechatVerifier(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := core.NewClient(ctx,
|
||||
option.WithMerchantCredential(cfg.MchId, cfg.MchSerialNo, privateKey),
|
||||
option.WithVerifier(verifier),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "初始化微信支付 SDK 客户端失败")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func appWechatVerifier(cfg *appWechatConfig) (auth.Verifier, error) {
|
||||
publicKey, err := appWechatPlatformPublicKey(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return verifiers.NewSHA256WithRSAPubkeyVerifier(cfg.PlatformPublicKeyId, *publicKey), nil
|
||||
}
|
||||
|
||||
func appCurrentWechatOpenId(ctx context.Context, user *entity.SysUser) (string, error) {
|
||||
if user == nil || user.UserId <= 0 {
|
||||
return "", gerror.New("当前用户信息不完整,无法发起微信小程序支付")
|
||||
}
|
||||
var social *entity.SysSocial
|
||||
if err := dao.SysSocial.Ctx(ctx).
|
||||
Where(dao.SysSocial.Columns().TenantId, user.TenantId).
|
||||
Where(dao.SysSocial.Columns().UserId, user.UserId).
|
||||
Where(dao.SysSocial.Columns().Source, consts.SourceWechatMp).
|
||||
OrderDesc(dao.SysSocial.Columns().Id).
|
||||
Limit(1).
|
||||
Scan(&social); err != nil {
|
||||
return "", gerror.Wrap(err, "查询微信 openid 失败")
|
||||
}
|
||||
if social == nil || strings.TrimSpace(social.OpenId) == "" {
|
||||
return "", gerror.New("当前账号未绑定微信 openid,无法发起小程序支付")
|
||||
}
|
||||
return strings.TrimSpace(social.OpenId), nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) BalanceRechargeWechatNotify(ctx context.Context) error {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
if r == nil || r.Request == nil {
|
||||
return gerror.New("微信支付通知请求不存在")
|
||||
}
|
||||
cfg, err := appLoadWechatConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
return gerror.New("微信支付未启用")
|
||||
}
|
||||
if err = appValidateWechatCommonConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
verifier, err := appWechatVerifier(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := notify.NewNotifyHandler(cfg.ApiV3Key, verifier)
|
||||
transaction := new(payments.Transaction)
|
||||
notifyReq, err := handler.ParseNotifyRequest(ctx, r.Request, transaction)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "解析微信支付通知失败")
|
||||
}
|
||||
|
||||
tradeState := appStringValue(transaction.TradeState)
|
||||
outTradeNo := appStringValue(transaction.OutTradeNo)
|
||||
transactionId := appStringValue(transaction.TransactionId)
|
||||
eventType := ""
|
||||
if notifyReq != nil {
|
||||
eventType = notifyReq.EventType
|
||||
}
|
||||
if notifyReq == nil || eventType != "TRANSACTION.SUCCESS" || tradeState != "SUCCESS" {
|
||||
g.Log().Infof(ctx, "微信支付余额充值通知忽略非成功状态: order=%s event=%s status=%s", outTradeNo, eventType, tradeState)
|
||||
return nil
|
||||
}
|
||||
if outTradeNo == "" {
|
||||
return gerror.New("微信支付通知订单号为空")
|
||||
}
|
||||
g.Log().Infof(ctx, "微信支付余额充值通知开始处理: order=%s event=%s status=%s tradeNo=%s", outTradeNo, eventType, tradeState, transactionId)
|
||||
|
||||
paidAt := appParseWechatPayTime(appStringValue(transaction.SuccessTime))
|
||||
if paidAt == nil {
|
||||
paidAt = gtime.Now()
|
||||
}
|
||||
amountFen := appWechatTransactionAmountFen(transaction)
|
||||
if amountFen <= 0 {
|
||||
return gerror.New("微信支付通知金额不正确")
|
||||
}
|
||||
_, err = s.completeAppBalanceRechargePayment(
|
||||
ctx,
|
||||
outTradeNo,
|
||||
nil,
|
||||
transactionId,
|
||||
"wechat",
|
||||
consts.BalanceTransactionChannelWechat,
|
||||
float64(amountFen)/100,
|
||||
paidAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.Log().Infof(ctx, "微信支付余额充值通知处理成功: order=%s tradeNo=%s amountFen=%d", outTradeNo, transactionId, amountFen)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sAppCard) BalanceRechargeWechatRefundNotify(ctx context.Context) error {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
if r == nil || r.Request == nil {
|
||||
return gerror.New("微信退款通知请求不存在")
|
||||
}
|
||||
cfg, err := appLoadWechatConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
return gerror.New("微信支付未启用")
|
||||
}
|
||||
if err = appValidateWechatCommonConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
verifier, err := appWechatVerifier(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := notify.NewNotifyHandler(cfg.ApiV3Key, verifier)
|
||||
refund := new(refunddomestic.Refund)
|
||||
notifyReq, err := handler.ParseNotifyRequest(ctx, r.Request, refund)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "解析微信退款通知失败")
|
||||
}
|
||||
|
||||
eventType := ""
|
||||
summary := ""
|
||||
if notifyReq != nil {
|
||||
eventType = notifyReq.EventType
|
||||
summary = notifyReq.Summary
|
||||
}
|
||||
refundStatus := ""
|
||||
refundStatus = appWechatRefundStatus(refund)
|
||||
refundAmountFen := appWechatRefundAmountFen(refund)
|
||||
|
||||
g.Log().Infof(
|
||||
ctx,
|
||||
"微信退款通知已验签: event=%s status=%s order=%s refundNo=%s refundId=%s amountFen=%d summary=%s",
|
||||
eventType,
|
||||
refundStatus,
|
||||
appStringValue(refund.OutTradeNo),
|
||||
appStringValue(refund.OutRefundNo),
|
||||
appStringValue(refund.RefundId),
|
||||
refundAmountFen,
|
||||
summary,
|
||||
)
|
||||
return s.applyAppBalanceRechargeRefundNotify(ctx, refund)
|
||||
}
|
||||
|
||||
func appWechatTransactionAmountFen(transaction *payments.Transaction) int64 {
|
||||
if transaction == nil || transaction.Amount == nil {
|
||||
return 0
|
||||
}
|
||||
if transaction.Amount.PayerTotal != nil && *transaction.Amount.PayerTotal > 0 {
|
||||
return *transaction.Amount.PayerTotal
|
||||
}
|
||||
if transaction.Amount.Total != nil && *transaction.Amount.Total > 0 {
|
||||
return *transaction.Amount.Total
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func appWechatPrivateKey(cfg *appWechatConfig) (*rsa.PrivateKey, error) {
|
||||
raw, err := appWechatKeyContent(cfg.PrivateKey, cfg.PrivateKeyPath)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取微信支付商户私钥失败")
|
||||
}
|
||||
der, err := appWechatDecodeKey(raw)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "解析微信支付商户私钥失败")
|
||||
}
|
||||
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||
if privateKey, ok := key.(*rsa.PrivateKey); ok {
|
||||
return privateKey, nil
|
||||
}
|
||||
}
|
||||
if privateKey, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return privateKey, nil
|
||||
}
|
||||
return nil, gerror.New("微信支付商户私钥不是有效 RSA 私钥")
|
||||
}
|
||||
|
||||
func appWechatPlatformPublicKey(cfg *appWechatConfig) (*rsa.PublicKey, error) {
|
||||
raw, err := appWechatKeyContent(cfg.PlatformPublicKey, cfg.PlatformPublicKeyPath)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取微信支付平台公钥失败")
|
||||
}
|
||||
der, err := appWechatDecodeKey(raw)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "解析微信支付平台公钥失败")
|
||||
}
|
||||
if key, err := x509.ParsePKIXPublicKey(der); err == nil {
|
||||
if publicKey, ok := key.(*rsa.PublicKey); ok {
|
||||
return publicKey, nil
|
||||
}
|
||||
}
|
||||
if publicKey, err := x509.ParsePKCS1PublicKey(der); err == nil {
|
||||
return publicKey, nil
|
||||
}
|
||||
return nil, gerror.New("微信支付平台公钥不是有效 RSA 公钥")
|
||||
}
|
||||
|
||||
func appWechatKeyContent(raw string, path string) (string, error) {
|
||||
if strings.TrimSpace(raw) != "" {
|
||||
return strings.TrimSpace(raw), nil
|
||||
}
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return "", gerror.New("密钥路径未配置")
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func appWechatDecodeKey(raw string) ([]byte, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil, gerror.New("密钥内容为空")
|
||||
}
|
||||
if block, _ := pem.Decode([]byte(raw)); block != nil {
|
||||
return block.Bytes, nil
|
||||
}
|
||||
compact := strings.Join(strings.Fields(raw), "")
|
||||
der, err := base64.StdEncoding.DecodeString(compact)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return der, nil
|
||||
}
|
||||
|
||||
func appNormalizeWechatPayScene(payScene string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(payScene)) {
|
||||
case "mini", "mp", "mp-weixin", "wechat_mini", "miniprogram":
|
||||
return "mini"
|
||||
default:
|
||||
return "h5"
|
||||
}
|
||||
}
|
||||
|
||||
func appWechatAmountFen(amount float64) int64 {
|
||||
return int64(math.Round(appRoundAmount(amount) * 100))
|
||||
}
|
||||
|
||||
func appWechatClientIp(ctx context.Context, cfg *appWechatConfig) string {
|
||||
clientIp := strings.TrimSpace(cfg.H5.PayerClientIp)
|
||||
if clientIp == "" {
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
clientIp = strings.TrimSpace(r.GetClientIp())
|
||||
}
|
||||
}
|
||||
if parsed := net.ParseIP(clientIp); parsed != nil {
|
||||
return parsed.String()
|
||||
}
|
||||
return clientIp
|
||||
}
|
||||
|
||||
func appWechatOrderDescription(order *entity.TOrder) string {
|
||||
amount := appFormatMoney(order.TotalAmount)
|
||||
return fmt.Sprintf("余额充值 %s", amount)
|
||||
}
|
||||
|
||||
func appWechatH5AppName(cfg *appWechatConfig) string {
|
||||
if cfg.H5.AppName != "" {
|
||||
return cfg.H5.AppName
|
||||
}
|
||||
return "物联网卡管理平台"
|
||||
}
|
||||
|
||||
func appParseWechatPayTime(value string) *gtime.Time {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return gtime.NewFromTime(parsed)
|
||||
}
|
||||
|
||||
func appStringPtr(value string) *string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return core.String(value)
|
||||
}
|
||||
|
||||
func appStringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(*value)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type appRegisterSettings struct {
|
||||
DeptId int64
|
||||
RoleId int64
|
||||
}
|
||||
|
||||
func appUserDeptOrDefault(user *entity.SysUser) int64 {
|
||||
if user != nil && user.DeptId > 0 {
|
||||
return user.DeptId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func appRegisterDeptOrDefault(ctx context.Context, tenantId string) int64 {
|
||||
deptId, err := appLoadRegisterDept(ctx, tenantId)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return deptId
|
||||
}
|
||||
|
||||
func appLoadRegisterSettings(ctx context.Context, tenantId string) (*appRegisterSettings, error) {
|
||||
if tenantId == "" {
|
||||
tenantId = consts.DefaultSystemTenantCode
|
||||
}
|
||||
|
||||
deptId, err := appLoadRegisterDept(ctx, tenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = appValidateRegisterDept(ctx, tenantId, deptId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleValue, err := appTenantConfigValue(ctx, tenantId, consts.ConfigKeyAppRegisterRoleId)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取小程序默认角色失败")
|
||||
}
|
||||
roleId, err := appPositiveConfigInt64(roleValue, "小程序默认角色ID")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = appValidateRegisterRole(ctx, tenantId, roleId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &appRegisterSettings{
|
||||
DeptId: deptId,
|
||||
RoleId: roleId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appLoadRegisterDept(ctx context.Context, tenantId string) (int64, error) {
|
||||
if tenantId == "" {
|
||||
tenantId = consts.DefaultSystemTenantCode
|
||||
}
|
||||
|
||||
deptValue, err := appTenantConfigValue(ctx, tenantId, consts.ConfigKeyAppRegisterDeptId)
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "读取小程序默认部门失败")
|
||||
}
|
||||
return appPositiveConfigInt64(deptValue, "小程序默认部门ID")
|
||||
}
|
||||
|
||||
func appTenantConfigValue(ctx context.Context, tenantId string, key string) (string, error) {
|
||||
var config *entity.SysConfig
|
||||
if err := dao.SysConfig.Ctx(ctx).
|
||||
Where(dao.SysConfig.Columns().TenantId, tenantId).
|
||||
Where(dao.SysConfig.Columns().ConfigKey, key).
|
||||
OrderDesc(dao.SysConfig.Columns().ConfigId).
|
||||
Limit(1).
|
||||
Scan(&config); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if config == nil || config.ConfigId == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return strings.TrimSpace(config.ConfigValue), nil
|
||||
}
|
||||
|
||||
func appPositiveConfigInt64(value string, label string) (int64, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, gerror.Newf("%s未配置", label)
|
||||
}
|
||||
id, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, gerror.Newf("%s配置不正确", label)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func appValidateRegisterDept(ctx context.Context, tenantId string, deptId int64) error {
|
||||
var dept *entity.SysDept
|
||||
if err := dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, tenantId).
|
||||
Where(dao.SysDept.Columns().DeptId, deptId).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
Limit(1).
|
||||
Scan(&dept); err != nil {
|
||||
return gerror.Wrap(err, "校验小程序默认部门失败")
|
||||
}
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
return gerror.New("小程序默认部门不存在")
|
||||
}
|
||||
if strings.TrimSpace(dept.Status) != consts.SysDeptStatusNormal {
|
||||
return gerror.New("小程序默认部门已停用")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appValidateRegisterRole(ctx context.Context, tenantId string, roleId int64) error {
|
||||
var role *entity.SysRole
|
||||
if err := dao.SysRole.Ctx(ctx).
|
||||
Where(dao.SysRole.Columns().TenantId, tenantId).
|
||||
Where(dao.SysRole.Columns().RoleId, roleId).
|
||||
WhereNull(dao.SysRole.Columns().DeletedAt).
|
||||
Limit(1).
|
||||
Scan(&role); err != nil {
|
||||
return gerror.Wrap(err, "校验小程序默认角色失败")
|
||||
}
|
||||
if role == nil || role.RoleId == 0 {
|
||||
return gerror.New("小程序默认角色不存在")
|
||||
}
|
||||
if strings.TrimSpace(role.Status) != consts.SysDeptStatusNormal {
|
||||
return gerror.New("小程序默认角色已停用")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
"github.com/gogf/gf/v2/net/gsvc"
|
||||
)
|
||||
|
||||
type appUpstreamDefaultConfig struct {
|
||||
ClientName string `json:"clientName"`
|
||||
Timeout string `json:"timeout"`
|
||||
}
|
||||
|
||||
type appUpstreamSimKey struct {
|
||||
ChanKey string `json:"chanKey,omitempty"`
|
||||
ICCID string `json:"iccid,omitempty"`
|
||||
IMSI string `json:"imsi,omitempty"`
|
||||
MSISDN string `json:"msisdn,omitempty"`
|
||||
}
|
||||
|
||||
type appUpstreamSimQueryInput struct {
|
||||
SimKeys []*appUpstreamSimKey `json:"simKeys"`
|
||||
ReqData any `json:"reqData"`
|
||||
}
|
||||
|
||||
type appUpstreamSimQueryItem struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
SimKey *appUpstreamSimKey `json:"simKey"`
|
||||
ResData json.RawMessage `json:"resData"`
|
||||
}
|
||||
|
||||
type appUpstreamSimQueryResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []appUpstreamSimQueryItem `json:"data"`
|
||||
}
|
||||
|
||||
func queryAppCardRealtimeSession(ctx context.Context, row *appCardQueryRow) (*appOneLinkMockSimSessionItem, string, error) {
|
||||
simKey, err := appUpstreamSimKeyFromRow(row)
|
||||
if err != nil {
|
||||
return nil, "upstream", err
|
||||
}
|
||||
return queryAppCardRealtimeSessionWithKey(ctx, row, simKey)
|
||||
}
|
||||
|
||||
func queryAppCardRealtimeSessionWithKey(ctx context.Context, row *appCardQueryRow, simKey *appUpstreamSimKey) (*appOneLinkMockSimSessionItem, string, error) {
|
||||
session, err := queryAppCardRealtimeSessionByUpstreamWithKey(ctx, simKey)
|
||||
if err == nil {
|
||||
return session, "upstream", nil
|
||||
}
|
||||
if !appUpstreamMockFallbackEnabled(ctx) {
|
||||
return nil, "upstream", err
|
||||
}
|
||||
g.Log().Debugf(ctx, "app card session upstream query failed, fallback to onelink mock, iccid:%s, err:%+v", appCardQueryLogIccid(row), err)
|
||||
|
||||
session, mockErr := queryAppCardRealtimeSessionByMock(ctx, row)
|
||||
if mockErr != nil {
|
||||
return nil, "onelink_mock", gerror.Wrapf(mockErr, "查询OneLink挡板会话失败,上游网关错误:%v", err)
|
||||
}
|
||||
return session, "onelink_mock", nil
|
||||
}
|
||||
|
||||
func queryAppCardRealtimeSessionByUpstream(ctx context.Context, row *appCardQueryRow) (*appOneLinkMockSimSessionItem, error) {
|
||||
simKey, err := appUpstreamSimKeyFromRow(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return queryAppCardRealtimeSessionByUpstreamWithKey(ctx, simKey)
|
||||
}
|
||||
|
||||
func queryAppCardRealtimeSessionByUpstreamWithKey(ctx context.Context, simKey *appUpstreamSimKey) (*appOneLinkMockSimSessionItem, error) {
|
||||
items, err := queryAppUpstreamSimWithKey(ctx, simKey, "/sim/query/sim-session", map[string]any{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range items {
|
||||
if item.Code != 0 || len(item.ResData) == 0 {
|
||||
continue
|
||||
}
|
||||
session := appOneLinkMockSimSessionItem{}
|
||||
if err = json.Unmarshal(item.ResData, &session); err != nil {
|
||||
return nil, gerror.Wrap(err, "解析上游会话响应失败")
|
||||
}
|
||||
if !session.isZero() {
|
||||
return &session, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func queryAppCardRealtimeSessionByMock(ctx context.Context, row *appCardQueryRow) (*appOneLinkMockSimSessionItem, error) {
|
||||
client, err := newAppOneLinkMockClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.QuerySimSession(ctx, row)
|
||||
}
|
||||
|
||||
func appUpstreamMockFallbackEnabled(ctx context.Context) bool {
|
||||
return g.Cfg().MustGet(ctx, "upstream.onelinkMock.enabled", false).Bool()
|
||||
}
|
||||
|
||||
func appCardQueryLogIccid(row *appCardQueryRow) string {
|
||||
if row == nil {
|
||||
return ""
|
||||
}
|
||||
return row.Iccid
|
||||
}
|
||||
|
||||
func queryAppUpstreamSim(ctx context.Context, row *appCardQueryRow, path string, reqData any) ([]appUpstreamSimQueryItem, error) {
|
||||
simKey, err := appUpstreamSimKeyFromRow(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return queryAppUpstreamSimWithKey(ctx, simKey, path, reqData)
|
||||
}
|
||||
|
||||
func queryAppUpstreamSimWithKey(ctx context.Context, simKey *appUpstreamSimKey, path string, reqData any) ([]appUpstreamSimQueryItem, error) {
|
||||
if simKey == nil {
|
||||
return nil, gerror.New("卡片信息为空,不能查询上游")
|
||||
}
|
||||
cleanKey := &appUpstreamSimKey{
|
||||
ChanKey: strings.TrimSpace(simKey.ChanKey),
|
||||
ICCID: strings.TrimSpace(simKey.ICCID),
|
||||
IMSI: strings.TrimSpace(simKey.IMSI),
|
||||
MSISDN: strings.TrimSpace(simKey.MSISDN),
|
||||
}
|
||||
if cleanKey.ICCID == "" && cleanKey.IMSI == "" && cleanKey.MSISDN == "" {
|
||||
return nil, gerror.New("卡片缺少上游查询标识")
|
||||
}
|
||||
cfg := appUpstreamDefaultConfig{}
|
||||
if err := g.Cfg().MustGet(ctx, "upstream.default").Scan(&cfg); err != nil {
|
||||
return nil, gerror.Wrap(err, "读取上游代理配置失败")
|
||||
}
|
||||
cfg.ClientName = strings.TrimSpace(cfg.ClientName)
|
||||
cfg.Timeout = strings.TrimSpace(cfg.Timeout)
|
||||
if cfg.ClientName == "" {
|
||||
return nil, gerror.New("上游代理地址未配置")
|
||||
}
|
||||
|
||||
timeout := 10 * time.Second
|
||||
if cfg.Timeout != "" {
|
||||
if parsed, err := time.ParseDuration(cfg.Timeout); err == nil && parsed > 0 {
|
||||
timeout = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if reqData == nil {
|
||||
reqData = map[string]any{}
|
||||
}
|
||||
input := appUpstreamSimQueryInput{
|
||||
SimKeys: []*appUpstreamSimKey{cleanKey},
|
||||
ReqData: reqData,
|
||||
}
|
||||
body, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "组装上游查询参数失败")
|
||||
}
|
||||
|
||||
requestURL := appUpstreamURL(cfg.ClientName, path)
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
appSetUpstreamClientTimeout(client, timeout)
|
||||
client.SetHeader("Content-Type", "application/json")
|
||||
resp, err := client.Post(ctx, requestURL, string(body))
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "请求上游信息失败")
|
||||
}
|
||||
defer resp.Close()
|
||||
respBody := resp.ReadAll()
|
||||
|
||||
output := appUpstreamSimQueryResponse{}
|
||||
if err = json.Unmarshal(respBody, &output); err != nil {
|
||||
return nil, gerror.Wrap(err, "解析上游响应失败")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
message := strings.TrimSpace(output.Message)
|
||||
if message == "" {
|
||||
message = "上游查询失败"
|
||||
}
|
||||
return nil, gerror.New(message)
|
||||
}
|
||||
return output.Data, nil
|
||||
}
|
||||
|
||||
func appSetUpstreamClientTimeout(client *gclient.Client, timeout time.Duration) {
|
||||
client.SetTimeout(timeout)
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
transport.ResponseHeaderTimeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
func appUpstreamSimKeyFromRow(row *appCardQueryRow) (*appUpstreamSimKey, error) {
|
||||
if row == nil {
|
||||
return nil, gerror.New("卡片信息为空,不能查询上游")
|
||||
}
|
||||
simKey := &appUpstreamSimKey{
|
||||
ChanKey: strings.TrimSpace(row.ChanKey),
|
||||
ICCID: strings.TrimSpace(row.Iccid),
|
||||
IMSI: strings.TrimSpace(row.Imsi),
|
||||
MSISDN: strings.TrimSpace(row.Msisdn),
|
||||
}
|
||||
if simKey.ICCID == "" && simKey.IMSI == "" && simKey.MSISDN == "" {
|
||||
return nil, gerror.New("卡片缺少上游查询标识")
|
||||
}
|
||||
return simKey, nil
|
||||
}
|
||||
|
||||
func appUpstreamURL(clientName string, path string) string {
|
||||
clientName = strings.TrimRight(strings.TrimSpace(clientName), "/")
|
||||
path = "/" + strings.TrimLeft(path, "/")
|
||||
if strings.HasPrefix(clientName, "http://") || strings.HasPrefix(clientName, "https://") {
|
||||
return clientName + path
|
||||
}
|
||||
return fmt.Sprintf("http://%s%s", clientName, path)
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sAppUser struct{}
|
||||
|
||||
func NewAppUser() *sAppUser {
|
||||
return &sAppUser{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAppUser(NewAppUser())
|
||||
}
|
||||
|
||||
func (s *sAppUser) Info(ctx context.Context) (res *model.AppUserInfoModel, err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId == 0 {
|
||||
return nil, gerror.New("登录已过期")
|
||||
}
|
||||
return getAppUserInfoById(ctx, userId)
|
||||
}
|
||||
|
||||
func (s *sAppUser) UpdateInfo(ctx context.Context, param *model.AppUpdateUserInfoParam) (res *model.AppUserInfoModel, err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId == 0 {
|
||||
return nil, gerror.New("登录已过期")
|
||||
}
|
||||
if param == nil {
|
||||
return nil, gerror.New("请求参数不能为空")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
err = dao.SysUser.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
var user *entity.SysUser
|
||||
if err := dao.SysUser.Ctx(ctx).TX(tx).
|
||||
Where(dao.SysUser.Columns().UserId, userId).
|
||||
Scan(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return gerror.New("用户不存在")
|
||||
}
|
||||
if !appIsTerminalUser(ctx, user) {
|
||||
return gerror.New("代理商账号仅支持查看资料")
|
||||
}
|
||||
|
||||
phone := strings.TrimSpace(param.Phone)
|
||||
if phone == "" {
|
||||
phone = strings.TrimSpace(user.Phonenumber)
|
||||
}
|
||||
if phone == "" {
|
||||
return gerror.New("联系电话不能为空")
|
||||
}
|
||||
if phone != "" && phone != strings.TrimSpace(user.Phonenumber) {
|
||||
var exists *entity.SysUser
|
||||
if err := dao.SysUser.Ctx(ctx).TX(tx).
|
||||
Where(dao.SysUser.Columns().TenantId, user.TenantId).
|
||||
Where(dao.SysUser.Columns().Phonenumber, phone).
|
||||
WhereNot(dao.SysUser.Columns().UserId, userId).
|
||||
WhereNull(dao.SysUser.Columns().DeletedAt).
|
||||
Limit(1).
|
||||
Scan(&exists); err != nil {
|
||||
return err
|
||||
}
|
||||
if exists != nil && exists.UserId > 0 {
|
||||
return gerror.New("手机号已被其他账号使用")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := dao.SysUser.Ctx(ctx).TX(tx).
|
||||
Where(dao.SysUser.Columns().UserId, userId).
|
||||
Data(do.SysUser{
|
||||
NickName: param.NickName,
|
||||
Avatar: param.Avatar,
|
||||
Email: strings.TrimSpace(param.Email),
|
||||
Phonenumber: phone,
|
||||
Sex: param.Sex,
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
OmitNil().
|
||||
Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.NickName = strings.TrimSpace(param.NickName)
|
||||
user.Email = strings.TrimSpace(param.Email)
|
||||
user.Phonenumber = phone
|
||||
return syncTerminalUserProfile(ctx, tx, user, param, now)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getAppUserInfoById(ctx, userId)
|
||||
}
|
||||
|
||||
func (s *sAppUser) BindWechat(ctx context.Context, param *model.AppWechatBindParam) (res *model.AppWechatBindModel, err error) {
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = service.AppAuth().BindWechatOpenIdByLoginCode(ctx, user, param.WxCode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userInfo, err := getAppUserInfoById(ctx, user.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.AppWechatBindModel{
|
||||
Success: true,
|
||||
UserInfo: userInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func syncTerminalUserProfile(ctx context.Context, tx gdb.TX, user *entity.SysUser, param *model.AppUpdateUserInfoParam, now *gtime.Time) error {
|
||||
nickName := strings.TrimSpace(param.NickName)
|
||||
terminalName := strings.TrimSpace(appFirstNotEmpty(param.TerminalName, nickName, user.NickName, user.UserName))
|
||||
phone := strings.TrimSpace(appFirstNotEmpty(param.Phone, user.Phonenumber))
|
||||
email := strings.TrimSpace(appFirstNotEmpty(param.Email, user.Email))
|
||||
establishTime, err := appParseProfileDate(param.EstablishTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var terminal *entity.TTerminal
|
||||
if err = dao.TTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TTerminal.Columns().TerminalId, user.UserId).
|
||||
Scan(&terminal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if terminal == nil {
|
||||
if _, err := dao.TTerminal.Ctx(ctx).TX(tx).Data(do.TTerminal{
|
||||
TerminalId: user.UserId,
|
||||
TerminalName: terminalName,
|
||||
Phone: phone,
|
||||
Email: email,
|
||||
Status: consts.SysUserStatusNormal,
|
||||
CreditCode: strings.TrimSpace(param.CreditCode),
|
||||
AreaCode: strings.TrimSpace(param.AreaCode),
|
||||
Address: strings.TrimSpace(param.Address),
|
||||
FullName: strings.TrimSpace(appFirstNotEmpty(param.FullName, terminalName)),
|
||||
Industry: strings.TrimSpace(param.Industry),
|
||||
LegalPersonName: strings.TrimSpace(appFirstNotEmpty(param.LegalPersonName, terminalName)),
|
||||
LegalPersonIdCard: strings.TrimSpace(appFirstNotEmpty(param.LegalPersonIdCard, "000000000000000000")),
|
||||
CompanyType: strings.TrimSpace(param.CompanyType),
|
||||
EstablishTime: establishTime,
|
||||
BankName: strings.TrimSpace(param.BankName),
|
||||
BankAccount: strings.TrimSpace(param.BankAccount),
|
||||
Balance: 0,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: appUserDeptOrDefault(user),
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := dao.TTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TTerminal.Columns().TerminalId, user.UserId).
|
||||
Data(do.TTerminal{
|
||||
TerminalName: terminalName,
|
||||
Phone: phone,
|
||||
Email: email,
|
||||
CreditCode: strings.TrimSpace(param.CreditCode),
|
||||
AreaCode: strings.TrimSpace(param.AreaCode),
|
||||
Address: strings.TrimSpace(param.Address),
|
||||
FullName: strings.TrimSpace(param.FullName),
|
||||
Industry: strings.TrimSpace(param.Industry),
|
||||
LegalPersonName: strings.TrimSpace(param.LegalPersonName),
|
||||
LegalPersonIdCard: strings.TrimSpace(param.LegalPersonIdCard),
|
||||
CompanyType: strings.TrimSpace(param.CompanyType),
|
||||
EstablishTime: establishTime,
|
||||
BankName: strings.TrimSpace(param.BankName),
|
||||
BankAccount: strings.TrimSpace(param.BankAccount),
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
OmitNil().
|
||||
Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := dao.TSimTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TSimTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TSimTerminal.Columns().TerminalId, user.UserId).
|
||||
Data(do.TSimTerminal{
|
||||
TerminalName: terminalName,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
OmitNil().
|
||||
Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := dao.TPackageTerminal.Ctx(ctx).TX(tx).
|
||||
Where(dao.TPackageTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TPackageTerminal.Columns().TerminalId, user.UserId).
|
||||
Data(do.TPackageTerminal{
|
||||
TerminalName: terminalName,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
OmitNil().
|
||||
Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appParseProfileDate(value string) (*gtime.Time, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.Parse("2006-01-02", value)
|
||||
if err != nil {
|
||||
return nil, gerror.New("成立时间格式应为YYYY-MM-DD")
|
||||
}
|
||||
return gtime.NewFromTime(parsed), nil
|
||||
}
|
||||
|
||||
func getAppUserInfoById(ctx context.Context, userId int64) (res *model.AppUserInfoModel, err error) {
|
||||
var user *entity.SysUser
|
||||
err = dao.SysUser.Ctx(ctx).
|
||||
Where(dao.SysUser.Columns().UserId, userId).
|
||||
Scan(&user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, gerror.New("用户不存在")
|
||||
}
|
||||
|
||||
isTerminal := appIsTerminalUser(ctx, user)
|
||||
canEnterpriseBusiness := !isTerminal && user.DeptId > 0
|
||||
balance := float64(0)
|
||||
role := "企业用户"
|
||||
phoneText := appMaskPhone(user.Phonenumber)
|
||||
var enterpriseProfile *model.AppEnterpriseProfileModel
|
||||
var terminalProfile *model.AppTerminalProfileModel
|
||||
if isTerminal {
|
||||
role = "终端用户"
|
||||
phoneText = strings.TrimSpace(user.Phonenumber)
|
||||
var terminal *entity.TTerminal
|
||||
if err = dao.TTerminal.Ctx(ctx).
|
||||
Where(dao.TTerminal.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TTerminal.Columns().TerminalId, user.UserId).
|
||||
Limit(1).
|
||||
Scan(&terminal); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if terminal != nil {
|
||||
balance = terminal.Balance
|
||||
terminalProfile = appTerminalProfileModel(terminal)
|
||||
}
|
||||
} else if user.DeptId > 0 {
|
||||
var agentBalance *entity.TAgentBalance
|
||||
if err = dao.TAgentBalance.Ctx(ctx).
|
||||
Where(dao.TAgentBalance.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TAgentBalance.Columns().DeptId, user.DeptId).
|
||||
Scan(&agentBalance); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agentBalance != nil {
|
||||
balance = agentBalance.Balance
|
||||
}
|
||||
enterpriseProfile, err = appEnterpriseProfileByDept(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
roleName, roleKey, roleNames := appLoadBoundRoleNames(ctx, user.UserId)
|
||||
|
||||
wechatBound, wechatBindText, err := appUserWechatBindingStatus(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.AppUserInfoModel{
|
||||
UserId: user.UserId,
|
||||
TenantId: user.TenantId,
|
||||
DeptId: user.DeptId,
|
||||
Username: user.UserName,
|
||||
NickName: user.NickName,
|
||||
Phone: phoneText,
|
||||
Avatar: user.Avatar,
|
||||
Sex: user.Sex,
|
||||
UserType: user.UserType,
|
||||
IsTerminal: isTerminal,
|
||||
CanEnterpriseBusiness: canEnterpriseBusiness,
|
||||
WechatBound: wechatBound,
|
||||
WechatBindText: wechatBindText,
|
||||
Balance: balance,
|
||||
Role: role,
|
||||
RoleName: roleName,
|
||||
RoleKey: roleKey,
|
||||
RoleNames: roleNames,
|
||||
EnterpriseProfile: enterpriseProfile,
|
||||
TerminalProfile: terminalProfile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// appLoadBoundRoleNames 读取账户在 sys_user_role 中绑定的角色,并关联 sys_role 返回真实角色名称。
|
||||
// 这样既保留原有账号类型 role,又能让前端按真实的角色表名称展示账户身份标识。
|
||||
func appLoadBoundRoleNames(ctx context.Context, userId int64) (roleName string, roleKey string, roleNames []string) {
|
||||
roleNames = make([]string, 0)
|
||||
var userRoles []*entity.SysUserRole
|
||||
if err := dao.SysUserRole.Ctx(ctx).
|
||||
Where(dao.SysUserRole.Columns().UserId, userId).
|
||||
Scan(&userRoles); err != nil || len(userRoles) == 0 {
|
||||
return "", "", roleNames
|
||||
}
|
||||
roleIds := make([]int64, 0, len(userRoles))
|
||||
for _, ur := range userRoles {
|
||||
if ur != nil && ur.RoleId > 0 {
|
||||
roleIds = append(roleIds, ur.RoleId)
|
||||
}
|
||||
}
|
||||
if len(roleIds) == 0 {
|
||||
return "", "", roleNames
|
||||
}
|
||||
var roles []*entity.SysRole
|
||||
if err := dao.SysRole.Ctx(ctx).
|
||||
WhereIn(dao.SysRole.Columns().RoleId, roleIds).
|
||||
Scan(&roles); err != nil || len(roles) == 0 {
|
||||
return "", "", roleNames
|
||||
}
|
||||
for _, r := range roles {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
if name := strings.TrimSpace(r.RoleName); name != "" {
|
||||
roleNames = append(roleNames, name)
|
||||
}
|
||||
}
|
||||
if roles[0] != nil {
|
||||
roleName = strings.TrimSpace(roles[0].RoleName)
|
||||
roleKey = strings.TrimSpace(roles[0].RoleKey)
|
||||
}
|
||||
return roleName, roleKey, roleNames
|
||||
}
|
||||
|
||||
func appUserWechatBindingStatus(ctx context.Context, user *entity.SysUser) (bool, string, error) {
|
||||
if user == nil || user.UserId <= 0 {
|
||||
return false, "未绑定", nil
|
||||
}
|
||||
var social *entity.SysSocial
|
||||
err := dao.SysSocial.Ctx(ctx).
|
||||
Fields(dao.SysSocial.Columns().Id).
|
||||
Where(dao.SysSocial.Columns().TenantId, user.TenantId).
|
||||
Where(dao.SysSocial.Columns().UserId, user.UserId).
|
||||
Where(dao.SysSocial.Columns().Source, consts.SourceWechatMp).
|
||||
Where(dao.SysSocial.Columns().OpenId+" IS NOT NULL").
|
||||
Where(dao.SysSocial.Columns().OpenId+" <> ?", "").
|
||||
WhereNull(dao.SysSocial.Columns().DeletedAt).
|
||||
OrderDesc(dao.SysSocial.Columns().Id).
|
||||
Limit(1).
|
||||
Scan(&social)
|
||||
if err != nil {
|
||||
return false, "", gerror.Wrap(err, "查询微信绑定状态失败")
|
||||
}
|
||||
if social == nil || social.Id == 0 {
|
||||
return false, "未绑定", nil
|
||||
}
|
||||
return true, "已绑定", nil
|
||||
}
|
||||
|
||||
func appEnterpriseProfileByDept(ctx context.Context, user *entity.SysUser) (*model.AppEnterpriseProfileModel, error) {
|
||||
if user == nil || user.DeptId <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var dept *entity.SysDept
|
||||
if err := dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, user.TenantId).
|
||||
Where(dao.SysDept.Columns().DeptId, user.DeptId).
|
||||
Limit(1).
|
||||
Scan(&dept); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业基础信息失败")
|
||||
}
|
||||
if dept == nil || dept.DeptId == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var enterprise *entity.TAgentEnterprise
|
||||
if err := dao.TAgentEnterprise.Ctx(ctx).
|
||||
Where(dao.TAgentEnterprise.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TAgentEnterprise.Columns().DeptId, user.DeptId).
|
||||
Limit(1).
|
||||
Scan(&enterprise); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询企业客户档案失败")
|
||||
}
|
||||
|
||||
profile := &model.AppEnterpriseProfileModel{
|
||||
DeptId: dept.DeptId,
|
||||
DeptName: strings.TrimSpace(dept.DeptName),
|
||||
DeptKey: strings.TrimSpace(dept.DeptKey),
|
||||
Phone: appMaskPhone(dept.Phone),
|
||||
Email: appMaskEmail(dept.Email),
|
||||
StatusText: appDeptStatusText(dept.Status),
|
||||
CreditCode: appMaskCreditCode(dept.CreditCode),
|
||||
AreaCode: strings.TrimSpace(dept.AreaCode),
|
||||
Address: appMaskAddress(dept.Address),
|
||||
FullName: strings.TrimSpace(dept.DeptName),
|
||||
RealNameStatus: "实名资料待完善",
|
||||
}
|
||||
if enterprise != nil && enterprise.Id > 0 {
|
||||
profile.FullName = appFirstNotEmpty(enterprise.FullName, dept.DeptName)
|
||||
profile.Industry = strings.TrimSpace(enterprise.Industry)
|
||||
profile.LegalPersonName = appMaskName(enterprise.LegalPersonName)
|
||||
profile.LegalPersonIdCard = appMaskIdCard(enterprise.LegalPersonIdCard)
|
||||
profile.CompanyType = strings.TrimSpace(enterprise.CompanyType)
|
||||
profile.EstablishTime = appProfileDate(enterprise.EstablishTime)
|
||||
profile.BankName = strings.TrimSpace(enterprise.BankName)
|
||||
profile.BankAccount = appMaskBankAccount(enterprise.BankAccount)
|
||||
profile.RealNameStatus = appRealNameStatus(enterprise.FullName, enterprise.LegalPersonName, enterprise.LegalPersonIdCard)
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func appTerminalProfileModel(terminal *entity.TTerminal) *model.AppTerminalProfileModel {
|
||||
if terminal == nil || terminal.TerminalId == 0 {
|
||||
return nil
|
||||
}
|
||||
return &model.AppTerminalProfileModel{
|
||||
TerminalId: terminal.TerminalId,
|
||||
TerminalName: strings.TrimSpace(terminal.TerminalName),
|
||||
Phone: strings.TrimSpace(terminal.Phone),
|
||||
Email: strings.TrimSpace(terminal.Email),
|
||||
Status: strings.TrimSpace(terminal.Status),
|
||||
StatusText: appTerminalStatusText(terminal.Status),
|
||||
CreditCode: strings.TrimSpace(terminal.CreditCode),
|
||||
AreaCode: strings.TrimSpace(terminal.AreaCode),
|
||||
Address: strings.TrimSpace(terminal.Address),
|
||||
FullName: strings.TrimSpace(terminal.FullName),
|
||||
Industry: strings.TrimSpace(terminal.Industry),
|
||||
LegalPersonName: strings.TrimSpace(terminal.LegalPersonName),
|
||||
LegalPersonIdCard: strings.TrimSpace(terminal.LegalPersonIdCard),
|
||||
CompanyType: strings.TrimSpace(terminal.CompanyType),
|
||||
EstablishTime: appProfileDate(terminal.EstablishTime),
|
||||
BankName: strings.TrimSpace(terminal.BankName),
|
||||
BankAccount: strings.TrimSpace(terminal.BankAccount),
|
||||
Balance: appRoundAmount(terminal.Balance),
|
||||
RealNameStatus: appRealNameStatus(terminal.FullName, terminal.LegalPersonName, terminal.LegalPersonIdCard),
|
||||
}
|
||||
}
|
||||
|
||||
func appProfileDate(value *gtime.Time) string {
|
||||
if value == nil || value.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return value.Format("Y-m-d")
|
||||
}
|
||||
|
||||
func appTerminalStatusText(status string) string {
|
||||
switch strings.TrimSpace(status) {
|
||||
case consts.SysUserStatusNormal:
|
||||
return "正常"
|
||||
case "1":
|
||||
return "停用"
|
||||
case "":
|
||||
return "未设置"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
func appRealNameStatus(fullName string, legalPersonName string, legalPersonIdCard string) string {
|
||||
if strings.TrimSpace(fullName) == "" && strings.TrimSpace(legalPersonName) == "" {
|
||||
return "资料待完善"
|
||||
}
|
||||
idCard := strings.TrimSpace(legalPersonIdCard)
|
||||
if idCard == "" || idCard == "000000000000000000" {
|
||||
return "实名资料待完善"
|
||||
}
|
||||
return "已录入实名资料"
|
||||
}
|
||||
|
||||
func appMaskIdCard(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || value == "000000000000000000" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) <= 8 {
|
||||
return value
|
||||
}
|
||||
return string(runes[:4]) + strings.Repeat("*", len(runes)-8) + string(runes[len(runes)-4:])
|
||||
}
|
||||
|
||||
func appMaskBankAccount(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) <= 8 {
|
||||
return value
|
||||
}
|
||||
return string(runes[:4]) + strings.Repeat("*", len(runes)-8) + string(runes[len(runes)-4:])
|
||||
}
|
||||
|
||||
func appMaskPhone(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
runes := []rune(value)
|
||||
if len(runes) < 7 {
|
||||
return value
|
||||
}
|
||||
return string(runes[:3]) + "****" + string(runes[len(runes)-4:])
|
||||
}
|
||||
|
||||
func appMaskEmail(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.SplitN(value, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return appMaskMiddle(value, 1, 1)
|
||||
}
|
||||
name := []rune(parts[0])
|
||||
if len(name) <= 1 {
|
||||
return "*" + "@" + parts[1]
|
||||
}
|
||||
return string(name[:1]) + strings.Repeat("*", len(name)-1) + "@" + parts[1]
|
||||
}
|
||||
|
||||
func appMaskCreditCode(value string) string {
|
||||
return appMaskMiddle(value, 4, 4)
|
||||
}
|
||||
|
||||
func appMaskAddress(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) <= 6 {
|
||||
return strings.Repeat("*", len(runes))
|
||||
}
|
||||
return string(runes[:6]) + strings.Repeat("*", len(runes)-6)
|
||||
}
|
||||
|
||||
func appMaskName(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) <= 1 {
|
||||
return "*"
|
||||
}
|
||||
return string(runes[:1]) + strings.Repeat("*", len(runes)-1)
|
||||
}
|
||||
|
||||
func appMaskMiddle(value string, prefix int, suffix int) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) <= prefix+suffix {
|
||||
if len(runes) <= 2 {
|
||||
return strings.Repeat("*", len(runes))
|
||||
}
|
||||
return string(runes[:1]) + strings.Repeat("*", len(runes)-2) + string(runes[len(runes)-1:])
|
||||
}
|
||||
return string(runes[:prefix]) + strings.Repeat("*", len(runes)-prefix-suffix) + string(runes[len(runes)-suffix:])
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"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 (
|
||||
appWarningItemTypeCard = 1
|
||||
|
||||
appWarningUnitMb = 1
|
||||
|
||||
appWarningTypeCardTraffic = 1
|
||||
|
||||
appWarningMessageMethod = "3"
|
||||
|
||||
// 微信订阅消息模板 ID 默认值:物联卡预警提醒
|
||||
appWarningWechatTemplateIdDefault = "DMDP2PKktdtG1ES_3Ic1JcR0LGELQmWE6LunYyerHCY"
|
||||
)
|
||||
|
||||
type sAppWarning struct{}
|
||||
|
||||
type appWarningConfigRow = entity.TWarnConfig
|
||||
|
||||
func NewAppWarning() *sAppWarning {
|
||||
return &sAppWarning{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAppWarning(NewAppWarning())
|
||||
}
|
||||
|
||||
func (s *sAppWarning) ConfigList(ctx context.Context, param *model.AppWarningConfigListParam) (res *model.AppWarningConfigListModel, err error) {
|
||||
if param == nil {
|
||||
return nil, gerror.New("预警配置查询参数不能为空")
|
||||
}
|
||||
param.CardValue = strings.TrimSpace(param.CardValue)
|
||||
user, cardRow, cardModel, err := s.resolveWarningCard(ctx, param.CardValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rows []*appWarningConfigRow
|
||||
err = s.configModel(ctx).
|
||||
Where(dao.TWarnConfig.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TWarnConfig.Columns().ItemType, appWarningItemTypeCard).
|
||||
Where(dao.TWarnConfig.Columns().ItemId, cardRow.Id).
|
||||
Where(dao.TWarnConfig.Columns().WarnUnit, appWarningUnitMb).
|
||||
OrderAsc(dao.TWarnConfig.Columns().WarnUnit).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询预警配置失败,请稍后重试")
|
||||
}
|
||||
|
||||
wechatBound, wechatBindText, err := appUserWechatBindingStatus(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.AppWarningConfigListModel{
|
||||
Card: cardModel,
|
||||
Items: s.configRowsToModels(rows),
|
||||
TemplateId: appWarningWechatTemplateId(ctx),
|
||||
TemplateReady: appWarningTemplateReady(ctx),
|
||||
WechatBound: wechatBound,
|
||||
WechatBindText: wechatBindText,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppWarning) ConfigMeta(ctx context.Context) (res *model.AppWarningConfigMetaModel, err error) {
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wechatBound, wechatBindText, err := appUserWechatBindingStatus(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.AppWarningConfigMetaModel{
|
||||
TemplateId: appWarningWechatTemplateId(ctx),
|
||||
TemplateReady: appWarningTemplateReady(ctx),
|
||||
WechatBound: wechatBound,
|
||||
WechatBindText: wechatBindText,
|
||||
AppNotifyEnabled: appWarningNotifyEnabledValue(nil),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppWarning) ConfigSave(ctx context.Context, param *model.AppWarningConfigSaveParam) (res *model.AppWarningConfigSaveModel, err error) {
|
||||
if err = normalizeAppWarningSaveParam(param); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, cardRow, _, err := s.resolveWarningCard(ctx, param.CardValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
saveParam, err := s.prepareWarningSaveParam(ctx, nil, user, cardRow, param)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := gtime.Now()
|
||||
batchNo, err := appNextBatchNo(ctx, consts.BatchNoPrefixWarning)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := appWarningBuildSaveRow(ctx, user, cardRow, saveParam, batchNo, now)
|
||||
|
||||
_, err = dao.TWarnConfig.Ctx(ctx).
|
||||
Data(data).
|
||||
OnConflict(
|
||||
dao.TWarnConfig.Columns().TenantId,
|
||||
dao.TWarnConfig.Columns().ItemType,
|
||||
dao.TWarnConfig.Columns().ItemId,
|
||||
dao.TWarnConfig.Columns().WarnUnit,
|
||||
).
|
||||
OnDuplicateEx(appWarningSaveDuplicateExcludeFields()).
|
||||
Save()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "保存预警配置失败,请稍后重试")
|
||||
}
|
||||
|
||||
config, err := s.findConfigByItem(ctx, user.TenantId, cardRow.Id, param.WarnUnit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wechatBound, wechatBindText, err := appUserWechatBindingStatus(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil || config.Id == 0 {
|
||||
return nil, gerror.New("保存预警配置后未查询到配置")
|
||||
}
|
||||
return &model.AppWarningConfigSaveModel{
|
||||
Id: config.Id,
|
||||
Config: s.configRowToModel(config),
|
||||
TemplateId: appWarningWechatTemplateId(ctx),
|
||||
TemplateReady: appWarningTemplateReady(ctx),
|
||||
WechatBound: wechatBound,
|
||||
WechatBindText: wechatBindText,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppWarning) ConfigBatchSave(ctx context.Context, param *model.AppWarningConfigBatchSaveParam) (res *model.AppWarningConfigBatchSaveModel, err error) {
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cardValues, configs, err := normalizeAppWarningBatchSaveParam(param)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cardLogic := NewAppCard()
|
||||
type validCard struct {
|
||||
Value string
|
||||
Row *appCardQueryRow
|
||||
}
|
||||
validCards := make([]validCard, 0, len(cardValues))
|
||||
failItems := make([]*model.AppWarningConfigBatchFailItem, 0)
|
||||
seenCardIds := make(map[int64]struct{}, len(cardValues))
|
||||
for _, cardValue := range cardValues {
|
||||
cardRow, err := cardLogic.findVisibleCard(ctx, user, cardValue)
|
||||
if err != nil {
|
||||
failItems = append(failItems, &model.AppWarningConfigBatchFailItem{CardValue: cardValue, Reason: "查询卡片失败"})
|
||||
continue
|
||||
}
|
||||
if cardRow == nil || cardRow.Id == 0 {
|
||||
failItems = append(failItems, &model.AppWarningConfigBatchFailItem{CardValue: cardValue, Reason: "卡片不存在或无权配置"})
|
||||
continue
|
||||
}
|
||||
if _, exists := seenCardIds[cardRow.Id]; exists {
|
||||
continue
|
||||
}
|
||||
if err = cardLogic.ensureOperateCard(ctx, user, cardRow, "配置预警"); err != nil {
|
||||
failItems = append(failItems, &model.AppWarningConfigBatchFailItem{CardValue: cardValue, Reason: err.Error()})
|
||||
continue
|
||||
}
|
||||
seenCardIds[cardRow.Id] = struct{}{}
|
||||
validCards = append(validCards, validCard{Value: cardValue, Row: cardRow})
|
||||
}
|
||||
if len(validCards) == 0 {
|
||||
return nil, gerror.New("没有可配置预警的卡片")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
batchNo, err := appNextBatchNo(ctx, consts.BatchNoPrefixWarning)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = dao.TWarnConfig.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
for _, card := range validCards {
|
||||
for _, config := range configs {
|
||||
saveParam, err := s.prepareWarningSaveParam(ctx, tx, user, card.Row, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := appWarningBuildSaveRow(ctx, user, card.Row, saveParam, batchNo, now)
|
||||
if _, err = dao.TWarnConfig.Ctx(ctx).TX(tx).
|
||||
Data(data).
|
||||
OnConflict(
|
||||
dao.TWarnConfig.Columns().TenantId,
|
||||
dao.TWarnConfig.Columns().ItemType,
|
||||
dao.TWarnConfig.Columns().ItemId,
|
||||
dao.TWarnConfig.Columns().WarnUnit,
|
||||
).
|
||||
OnDuplicateEx(appWarningSaveDuplicateExcludeFields()).
|
||||
Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "批量保存预警配置失败,请稍后重试")
|
||||
}
|
||||
|
||||
wechatBound, wechatBindText, err := appUserWechatBindingStatus(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.AppWarningConfigBatchSaveModel{
|
||||
BatchNo: batchNo,
|
||||
TotalCount: len(cardValues),
|
||||
SuccessCount: len(validCards),
|
||||
FailCount: len(failItems),
|
||||
FailItems: failItems,
|
||||
TemplateId: appWarningWechatTemplateId(ctx),
|
||||
TemplateReady: appWarningTemplateReady(ctx),
|
||||
WechatBound: wechatBound,
|
||||
WechatBindText: wechatBindText,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppWarning) ConfigNotify(ctx context.Context, param *model.AppWarningConfigNotifyParam) (res *model.AppWarningConfigNotifyModel, err error) {
|
||||
if err = normalizeAppWarningNotifyParam(param); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, cardRow, _, err := s.resolveWarningCard(ctx, param.CardValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := s.findConfigByItem(ctx, user.TenantId, cardRow.Id, param.WarnUnit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil || config.Id == 0 {
|
||||
return nil, gerror.New("请先保存预警阈值后再设置小程序提醒")
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
_, err = dao.TWarnConfig.Ctx(ctx).
|
||||
Where(dao.TWarnConfig.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TWarnConfig.Columns().ItemType, appWarningItemTypeCard).
|
||||
Where(dao.TWarnConfig.Columns().ItemId, cardRow.Id).
|
||||
Where(dao.TWarnConfig.Columns().WarnUnit, param.WarnUnit).
|
||||
Data(do.TWarnConfig{
|
||||
AppNotifyEnabled: appWarningNotifyEnabledValue(param.AppNotifyEnabled),
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "设置小程序预警通知失败,请稍后重试")
|
||||
}
|
||||
|
||||
config, err = s.findConfigByItem(ctx, user.TenantId, cardRow.Id, param.WarnUnit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wechatBound, wechatBindText, err := appUserWechatBindingStatus(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.AppWarningConfigNotifyModel{
|
||||
AppNotifyEnabled: appWarningNotifyEnabledValue(param.AppNotifyEnabled),
|
||||
Config: s.configRowToModel(config),
|
||||
TemplateId: appWarningWechatTemplateId(ctx),
|
||||
TemplateReady: appWarningTemplateReady(ctx),
|
||||
WechatBound: wechatBound,
|
||||
WechatBindText: wechatBindText,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppWarning) ConfigBatchNotify(ctx context.Context, param *model.AppWarningConfigBatchNotifyParam) (res *model.AppWarningConfigBatchNotifyModel, err error) {
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cardValues, err := normalizeAppWarningBatchNotifyParam(param)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cardLogic := NewAppCard()
|
||||
type validCard struct {
|
||||
Value string
|
||||
Row *appCardQueryRow
|
||||
}
|
||||
validCards := make([]validCard, 0, len(cardValues))
|
||||
failItems := make([]*model.AppWarningConfigBatchFailItem, 0)
|
||||
seenCardIds := make(map[int64]struct{}, len(cardValues))
|
||||
for _, cardValue := range cardValues {
|
||||
cardRow, err := cardLogic.findVisibleCard(ctx, user, cardValue)
|
||||
if err != nil {
|
||||
failItems = append(failItems, &model.AppWarningConfigBatchFailItem{CardValue: cardValue, Reason: "查询卡片失败"})
|
||||
continue
|
||||
}
|
||||
if cardRow == nil || cardRow.Id == 0 {
|
||||
failItems = append(failItems, &model.AppWarningConfigBatchFailItem{CardValue: cardValue, Reason: "卡片不存在或无权配置"})
|
||||
continue
|
||||
}
|
||||
if _, exists := seenCardIds[cardRow.Id]; exists {
|
||||
continue
|
||||
}
|
||||
if err = cardLogic.ensureOperateCard(ctx, user, cardRow, "配置预警"); err != nil {
|
||||
failItems = append(failItems, &model.AppWarningConfigBatchFailItem{CardValue: cardValue, Reason: err.Error()})
|
||||
continue
|
||||
}
|
||||
config, err := s.findConfigByItem(ctx, user.TenantId, cardRow.Id, param.WarnUnit)
|
||||
if err != nil {
|
||||
failItems = append(failItems, &model.AppWarningConfigBatchFailItem{CardValue: cardValue, Reason: "查询预警配置失败"})
|
||||
continue
|
||||
}
|
||||
if config == nil || config.Id == 0 {
|
||||
failItems = append(failItems, &model.AppWarningConfigBatchFailItem{CardValue: cardValue, Reason: "请先保存预警配置"})
|
||||
continue
|
||||
}
|
||||
seenCardIds[cardRow.Id] = struct{}{}
|
||||
validCards = append(validCards, validCard{Value: cardValue, Row: cardRow})
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
err = dao.TWarnConfig.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
for _, card := range validCards {
|
||||
if _, err = dao.TWarnConfig.Ctx(ctx).TX(tx).
|
||||
Where(dao.TWarnConfig.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TWarnConfig.Columns().ItemType, appWarningItemTypeCard).
|
||||
Where(dao.TWarnConfig.Columns().ItemId, card.Row.Id).
|
||||
Where(dao.TWarnConfig.Columns().WarnUnit, param.WarnUnit).
|
||||
Data(do.TWarnConfig{
|
||||
AppNotifyEnabled: appWarningNotifyEnabledValue(param.AppNotifyEnabled),
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "批量设置小程序预警通知失败,请稍后重试")
|
||||
}
|
||||
|
||||
wechatBound, wechatBindText, err := appUserWechatBindingStatus(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.AppWarningConfigBatchNotifyModel{
|
||||
AppNotifyEnabled: appWarningNotifyEnabledValue(param.AppNotifyEnabled),
|
||||
TotalCount: len(cardValues),
|
||||
SuccessCount: len(validCards),
|
||||
FailCount: len(failItems),
|
||||
FailItems: failItems,
|
||||
TemplateId: appWarningWechatTemplateId(ctx),
|
||||
TemplateReady: appWarningTemplateReady(ctx),
|
||||
WechatBound: wechatBound,
|
||||
WechatBindText: wechatBindText,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sAppWarning) resolveWarningCard(ctx context.Context, cardValue string) (*entity.SysUser, *appCardQueryRow, *model.AppCardModel, error) {
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
cardLogic := NewAppCard()
|
||||
cardRow, err := cardLogic.findVisibleCard(ctx, user, cardValue)
|
||||
if err != nil {
|
||||
return nil, nil, nil, gerror.Wrap(err, "查询卡片失败,请稍后重试")
|
||||
}
|
||||
if cardRow == nil || cardRow.Id == 0 {
|
||||
return nil, nil, nil, gerror.New("卡片不存在或不在当前账号可配置范围内")
|
||||
}
|
||||
if err = cardLogic.ensureOperateCard(ctx, user, cardRow, "配置预警"); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return user, cardRow, cardLogic.cardRowToModel(ctx, cardRow, user, cardLogic.ownerType(ctx, user), true), nil
|
||||
}
|
||||
|
||||
func (s *sAppWarning) configModel(ctx context.Context) *gdb.Model {
|
||||
return dao.TWarnConfig.Ctx(ctx)
|
||||
}
|
||||
|
||||
func (s *sAppWarning) findConfigByItem(ctx context.Context, tenantId string, itemId int64, warnUnit int) (*appWarningConfigRow, error) {
|
||||
return s.findConfigByItemWithModel(
|
||||
dao.TWarnConfig.Ctx(ctx),
|
||||
tenantId,
|
||||
itemId,
|
||||
warnUnit,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sAppWarning) findConfigByItemWithTx(ctx context.Context, tx gdb.TX, tenantId string, itemId int64, warnUnit int) (*appWarningConfigRow, error) {
|
||||
return s.findConfigByItemWithModel(
|
||||
dao.TWarnConfig.Ctx(ctx).TX(tx),
|
||||
tenantId,
|
||||
itemId,
|
||||
warnUnit,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sAppWarning) findConfigByItemWithModel(mod *gdb.Model, tenantId string, itemId int64, warnUnit int) (*appWarningConfigRow, error) {
|
||||
var row *appWarningConfigRow
|
||||
err := mod.
|
||||
Where(dao.TWarnConfig.Columns().TenantId, tenantId).
|
||||
Where(dao.TWarnConfig.Columns().ItemType, appWarningItemTypeCard).
|
||||
Where(dao.TWarnConfig.Columns().ItemId, itemId).
|
||||
Where(dao.TWarnConfig.Columns().WarnUnit, warnUnit).
|
||||
Limit(1).
|
||||
Scan(&row)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询预警配置失败,请稍后重试")
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
func (s *sAppWarning) prepareWarningSaveParam(
|
||||
ctx context.Context,
|
||||
tx gdb.TX,
|
||||
user *entity.SysUser,
|
||||
cardRow *appCardQueryRow,
|
||||
param *model.AppWarningConfigSaveParam,
|
||||
) (*model.AppWarningConfigSaveParam, error) {
|
||||
if param == nil {
|
||||
return nil, gerror.New("预警配置参数不能为空")
|
||||
}
|
||||
|
||||
saveParam := *param
|
||||
var (
|
||||
config *appWarningConfigRow
|
||||
err error
|
||||
)
|
||||
if tx != nil {
|
||||
config, err = s.findConfigByItemWithTx(ctx, tx, user.TenantId, cardRow.Id, param.WarnUnit)
|
||||
} else {
|
||||
config, err = s.findConfigByItem(ctx, user.TenantId, cardRow.Id, param.WarnUnit)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config != nil {
|
||||
if saveParam.IsEnabled == nil {
|
||||
value := config.IsEnabled
|
||||
saveParam.IsEnabled = &value
|
||||
}
|
||||
if saveParam.AppNotifyEnabled == nil {
|
||||
value := config.AppNotifyEnabled
|
||||
saveParam.AppNotifyEnabled = &value
|
||||
}
|
||||
}
|
||||
return &saveParam, nil
|
||||
}
|
||||
|
||||
func (s *sAppWarning) configRowsToModels(rows []*appWarningConfigRow) []*model.AppWarningConfigModel {
|
||||
items := make([]*model.AppWarningConfigModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, s.configRowToModel(row))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *sAppWarning) configRowToModel(row *appWarningConfigRow) *model.AppWarningConfigModel {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
warnType := appWarningTypeByUnit(row.WarnUnit)
|
||||
return &model.AppWarningConfigModel{
|
||||
Id: row.Id,
|
||||
ItemType: row.ItemType,
|
||||
ItemId: row.ItemId,
|
||||
ItemCode: row.ItemCode,
|
||||
WarnType: warnType,
|
||||
WarnUnit: row.WarnUnit,
|
||||
WarnTitle: appWarningTitle(row.WarnUnit),
|
||||
UnitText: appWarningUnitText(row.WarnUnit),
|
||||
FirstWarn: row.FirstWarn,
|
||||
SecondWarn: row.SecondWarn,
|
||||
ThirdWarn: row.ThirdWarn,
|
||||
AutoAction: row.AutoAction,
|
||||
IsEnabled: row.IsEnabled,
|
||||
AppNotifyEnabled: row.AppNotifyEnabled,
|
||||
WxTemplateId: row.WxTemplateId,
|
||||
Remark: row.Remark,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAppWarningSaveParam(param *model.AppWarningConfigSaveParam) error {
|
||||
if param == nil {
|
||||
return gerror.New("预警配置参数不能为空")
|
||||
}
|
||||
param.CardValue = strings.TrimSpace(param.CardValue)
|
||||
param.Remark = strings.TrimSpace(param.Remark)
|
||||
if param.IsEnabled != nil && *param.IsEnabled != 0 && *param.IsEnabled != 1 {
|
||||
return gerror.New("启停状态无效")
|
||||
}
|
||||
if param.AppNotifyEnabled != nil && *param.AppNotifyEnabled != 0 && *param.AppNotifyEnabled != 1 {
|
||||
return gerror.New("小程序预警通知状态无效")
|
||||
}
|
||||
|
||||
switch param.WarnUnit {
|
||||
case appWarningUnitMb:
|
||||
return validateAppWarningTrafficMb(param.FirstWarn, param.SecondWarn, param.ThirdWarn)
|
||||
default:
|
||||
return gerror.New("小程序仅支持按MB配置流量预警")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAppWarningBatchSaveParam(param *model.AppWarningConfigBatchSaveParam) ([]string, []*model.AppWarningConfigSaveParam, error) {
|
||||
if param == nil {
|
||||
return nil, nil, gerror.New("批量预警配置参数不能为空")
|
||||
}
|
||||
cardValues, err := normalizeAppWarningCardValues(param.CardValues)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(cardValues) > 100 {
|
||||
return nil, nil, gerror.New("单次批量预警配置最多支持100张卡")
|
||||
}
|
||||
|
||||
configs := make([]*model.AppWarningConfigSaveParam, 0, len(param.Configs))
|
||||
seenUnits := make(map[int]struct{}, len(param.Configs))
|
||||
for _, config := range param.Configs {
|
||||
if config == nil {
|
||||
continue
|
||||
}
|
||||
copied := model.AppWarningConfigSaveParam{
|
||||
WarnUnit: config.WarnUnit,
|
||||
FirstWarn: config.FirstWarn,
|
||||
SecondWarn: config.SecondWarn,
|
||||
ThirdWarn: config.ThirdWarn,
|
||||
IsEnabled: config.IsEnabled,
|
||||
AppNotifyEnabled: config.AppNotifyEnabled,
|
||||
Remark: config.Remark,
|
||||
}
|
||||
if _, exists := seenUnits[copied.WarnUnit]; exists {
|
||||
return nil, nil, gerror.New("同一预警类型不能重复配置")
|
||||
}
|
||||
if err := normalizeAppWarningSaveParam(&copied); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
seenUnits[copied.WarnUnit] = struct{}{}
|
||||
configs = append(configs, &copied)
|
||||
}
|
||||
if len(configs) == 0 {
|
||||
return nil, nil, gerror.New("请至少填写一项预警配置")
|
||||
}
|
||||
return cardValues, configs, nil
|
||||
}
|
||||
|
||||
func normalizeAppWarningNotifyParam(param *model.AppWarningConfigNotifyParam) error {
|
||||
if param == nil {
|
||||
return gerror.New("小程序预警通知参数不能为空")
|
||||
}
|
||||
param.CardValue = strings.TrimSpace(param.CardValue)
|
||||
if param.WarnUnit == 0 {
|
||||
param.WarnUnit = appWarningUnitMb
|
||||
}
|
||||
if param.AppNotifyEnabled == nil {
|
||||
return gerror.New("请选择是否开启小程序预警通知")
|
||||
}
|
||||
if *param.AppNotifyEnabled != 0 && *param.AppNotifyEnabled != 1 {
|
||||
return gerror.New("小程序预警通知状态无效")
|
||||
}
|
||||
if param.WarnUnit != appWarningUnitMb {
|
||||
return gerror.New("小程序仅支持配置MB流量预警通知")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeAppWarningBatchNotifyParam(param *model.AppWarningConfigBatchNotifyParam) ([]string, error) {
|
||||
if param == nil {
|
||||
return nil, gerror.New("批量小程序预警通知参数不能为空")
|
||||
}
|
||||
if param.WarnUnit == 0 {
|
||||
param.WarnUnit = appWarningUnitMb
|
||||
}
|
||||
if param.AppNotifyEnabled == nil {
|
||||
return nil, gerror.New("请选择是否开启小程序预警通知")
|
||||
}
|
||||
if *param.AppNotifyEnabled != 0 && *param.AppNotifyEnabled != 1 {
|
||||
return nil, gerror.New("小程序预警通知状态无效")
|
||||
}
|
||||
if param.WarnUnit != appWarningUnitMb {
|
||||
return nil, gerror.New("小程序仅支持配置MB流量预警通知")
|
||||
}
|
||||
cardValues, err := normalizeAppWarningCardValues(param.CardValues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cardValues) > 100 {
|
||||
return nil, gerror.New("单次批量预警通知最多支持100张卡")
|
||||
}
|
||||
return cardValues, nil
|
||||
}
|
||||
|
||||
func normalizeAppWarningCardValues(values []string) ([]string, error) {
|
||||
cardValues := make([]string, 0, len(values))
|
||||
seenCards := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seenCards[value]; exists {
|
||||
continue
|
||||
}
|
||||
seenCards[value] = struct{}{}
|
||||
cardValues = append(cardValues, value)
|
||||
}
|
||||
if len(cardValues) == 0 {
|
||||
return nil, gerror.New("请选择需要配置预警的卡片")
|
||||
}
|
||||
return cardValues, nil
|
||||
}
|
||||
|
||||
func appWarningBuildSaveRow(ctx context.Context, user *entity.SysUser, cardRow *appCardQueryRow, param *model.AppWarningConfigSaveParam, batchNo string, now *gtime.Time) do.TWarnConfig {
|
||||
return do.TWarnConfig{
|
||||
TenantId: user.TenantId,
|
||||
BatchNo: batchNo,
|
||||
ItemType: appWarningItemTypeCard,
|
||||
ItemId: cardRow.Id,
|
||||
ItemCode: appWarningCardCode(cardRow),
|
||||
WarnUnit: param.WarnUnit,
|
||||
FirstWarn: param.FirstWarn,
|
||||
SecondWarn: param.SecondWarn,
|
||||
ThirdWarn: param.ThirdWarn,
|
||||
WarnMethods: appWarningMessageMethod,
|
||||
WarnPhones: "",
|
||||
WarnEmails: "",
|
||||
AutoAction: 0,
|
||||
IsEnabled: appWarningEnabledValue(param.IsEnabled),
|
||||
AppNotifyEnabled: appWarningNotifyEnabledValue(param.AppNotifyEnabled),
|
||||
WxTemplateId: appWarningWechatTemplateId(ctx),
|
||||
Remark: strings.TrimSpace(param.Remark),
|
||||
CreatedDept: appUserDeptOrDefault(user),
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
func appWarningSaveDuplicateExcludeFields() string {
|
||||
columns := dao.TWarnConfig.Columns()
|
||||
return strings.Join([]string{
|
||||
columns.Id,
|
||||
columns.TenantId,
|
||||
columns.ItemType,
|
||||
columns.ItemId,
|
||||
columns.WarnUnit,
|
||||
columns.CreatedDept,
|
||||
columns.CreatedBy,
|
||||
columns.CreatedAt,
|
||||
}, ",")
|
||||
}
|
||||
|
||||
func validateAppWarningTrafficMb(first, second, third float64) error {
|
||||
if first <= 0 {
|
||||
return gerror.New("第一档阈值必须大于0")
|
||||
}
|
||||
if second < 0 || third < 0 {
|
||||
return gerror.New("预警阈值不能小于0")
|
||||
}
|
||||
if second > 0 && second <= first {
|
||||
return gerror.New("第二档阈值必须大于第一档")
|
||||
}
|
||||
if third > 0 {
|
||||
if second <= 0 {
|
||||
return gerror.New("配置第三档时必须先配置第二档")
|
||||
}
|
||||
if third <= second {
|
||||
return gerror.New("第三档阈值必须大于第二档")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appWarningEnabledValue(value *int) int {
|
||||
if value == nil {
|
||||
return 1
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func appWarningNotifyEnabledValue(value *int) int {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func appWarningTypeByUnit(warnUnit int) int {
|
||||
switch warnUnit {
|
||||
case appWarningUnitMb:
|
||||
return appWarningTypeCardTraffic
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func appWarningTitle(warnUnit int) string {
|
||||
switch warnUnit {
|
||||
case appWarningUnitMb:
|
||||
return "流量使用预警"
|
||||
default:
|
||||
return "预警配置"
|
||||
}
|
||||
}
|
||||
|
||||
func appWarningUnitText(warnUnit int) string {
|
||||
switch warnUnit {
|
||||
case appWarningUnitMb:
|
||||
return "MB"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func appWarningCardCode(card *appCardQueryRow) string {
|
||||
if card == nil {
|
||||
return ""
|
||||
}
|
||||
return appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei)
|
||||
}
|
||||
|
||||
func appWarningWechatTemplateId(ctx context.Context) string {
|
||||
return strings.TrimSpace(g.Cfg().MustGet(ctx, "appWarning.wechatSubscribeTemplateId", appWarningWechatTemplateIdDefault).String())
|
||||
}
|
||||
|
||||
func appWarningTemplateReady(ctx context.Context) bool {
|
||||
value := appWarningWechatTemplateId(ctx)
|
||||
return value != "" && !strings.Contains(strings.ToUpper(value), "TODO") && !strings.Contains(strings.ToUpper(value), "REPLACE")
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
)
|
||||
|
||||
func TestValidateAppWarningTrafficMbAllowsLargeThreshold(t *testing.T) {
|
||||
if err := validateAppWarningTrafficMb(1024, 2048, 4096); err != nil {
|
||||
t.Fatalf("MB warning threshold should allow values greater than 100: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAppWarningSaveParamOnlyAllowsMb(t *testing.T) {
|
||||
for _, warnUnit := range []int{2, 3, 4} {
|
||||
err := normalizeAppWarningSaveParam(&model.AppWarningConfigSaveParam{
|
||||
WarnUnit: warnUnit,
|
||||
FirstWarn: 80,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("warnUnit %d should be rejected for mini-program warning config", warnUnit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppWarningUnitTextForMb(t *testing.T) {
|
||||
if got := appWarningUnitText(appWarningUnitMb); got != "MB" {
|
||||
t.Fatalf("MB warning unit text = %q, want MB", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppWarningNotifyEnabledValueDefaultsToDisabled(t *testing.T) {
|
||||
if got := appWarningNotifyEnabledValue(nil); got != 0 {
|
||||
t.Fatalf("nil app notify enabled = %d, want 0", got)
|
||||
}
|
||||
enabled := 1
|
||||
if got := appWarningNotifyEnabledValue(&enabled); got != 1 {
|
||||
t.Fatalf("enabled app notify value = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAppWarningNotifyParam(t *testing.T) {
|
||||
enabled := 0
|
||||
param := &model.AppWarningConfigNotifyParam{
|
||||
CardValue: " 8986040000000000000 ",
|
||||
AppNotifyEnabled: &enabled,
|
||||
}
|
||||
if err := normalizeAppWarningNotifyParam(param); err != nil {
|
||||
t.Fatalf("valid notify param should pass: %v", err)
|
||||
}
|
||||
if param.CardValue != "8986040000000000000" {
|
||||
t.Fatalf("card value was not trimmed: %q", param.CardValue)
|
||||
}
|
||||
if param.WarnUnit != appWarningUnitMb {
|
||||
t.Fatalf("empty warn unit = %d, want MB", param.WarnUnit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAppWarningNotifyParamRejectsInvalidStatus(t *testing.T) {
|
||||
invalid := 2
|
||||
err := normalizeAppWarningNotifyParam(&model.AppWarningConfigNotifyParam{
|
||||
CardValue: "8986040000000000000",
|
||||
WarnUnit: appWarningUnitMb,
|
||||
AppNotifyEnabled: &invalid,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("invalid app notify status should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAppWarningNotifyParamRejectsNonMbUnit(t *testing.T) {
|
||||
enabled := 1
|
||||
err := normalizeAppWarningNotifyParam(&model.AppWarningConfigNotifyParam{
|
||||
CardValue: "8986040000000000000",
|
||||
WarnUnit: 2,
|
||||
AppNotifyEnabled: &enabled,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("non-MB warning notify unit should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAppWarningBatchNotifyParamRejectsEmptyCards(t *testing.T) {
|
||||
enabled := 1
|
||||
_, err := normalizeAppWarningBatchNotifyParam(&model.AppWarningConfigBatchNotifyParam{
|
||||
CardValues: []string{" ", ""},
|
||||
AppNotifyEnabled: &enabled,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("empty batch notify cards should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigNotifyRejectsNilParamBeforeCardLookup(t *testing.T) {
|
||||
defer func() {
|
||||
if value := recover(); value != nil {
|
||||
t.Fatalf("ConfigNotify should not panic for nil param: %v", value)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err := NewAppWarning().ConfigNotify(context.Background(), nil)
|
||||
if err == nil {
|
||||
t.Fatal("nil notify param should be rejected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
appWorkOrderCreatorTerminal = 1
|
||||
appWorkOrderCreatorEnterprise = 2
|
||||
|
||||
appWorkOrderStatusAssigned = 1
|
||||
appWorkOrderStatusProcessing = 2
|
||||
appWorkOrderStatusResolved = 3
|
||||
appWorkOrderStatusClosed = 4
|
||||
appWorkOrderStatusCanceled = 5
|
||||
|
||||
appWorkOrderOperationCreate = 0
|
||||
)
|
||||
|
||||
type sAppWorkOrder struct{}
|
||||
|
||||
func NewAppWorkOrder() *sAppWorkOrder {
|
||||
return &sAppWorkOrder{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterAppWorkOrder(NewAppWorkOrder())
|
||||
}
|
||||
|
||||
func (s *sAppWorkOrder) Upload(ctx context.Context) (output *model.SysOssUploadModel, err error) {
|
||||
if err = appEnsureFeatureAccess(ctx, "workorder:create"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return service.SysOss().Upload(ctx, &model.SysOssUploadParam{FileType: "file"})
|
||||
}
|
||||
|
||||
func (s *sAppWorkOrder) List(ctx context.Context, param *model.AppWorkOrderListParam) (items []*model.AppWorkOrderModel, total int, err error) {
|
||||
if err = appEnsureFeatureAccess(ctx, "workorder:list"); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if param == nil {
|
||||
param = &model.AppWorkOrderListParam{}
|
||||
}
|
||||
if param.Page <= 0 {
|
||||
param.Page = 1
|
||||
}
|
||||
if param.PageSize <= 0 {
|
||||
param.PageSize = 20
|
||||
}
|
||||
if param.PageSize > 100 {
|
||||
param.PageSize = 100
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
mod := s.ownerModel(ctx, user)
|
||||
if param.CurrentStatus > 0 {
|
||||
mod = mod.Where(dao.TWoMain.Columns().CurrentStatus, param.CurrentStatus)
|
||||
}
|
||||
if param.WoType > 0 {
|
||||
mod = mod.Where(dao.TWoMain.Columns().WoType, param.WoType)
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(param.Keyword)
|
||||
if keyword == "" {
|
||||
keyword = strings.TrimSpace(param.PageInfo.Keyword)
|
||||
}
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
columns := dao.TWoMain.Columns()
|
||||
mod = mod.Where(
|
||||
"("+columns.WoNo+" LIKE ? OR "+columns.Title+" LIKE ? OR "+columns.Content+" LIKE ?)",
|
||||
like, like, like,
|
||||
)
|
||||
}
|
||||
|
||||
total, err = mod.Count()
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "查询工单数量失败")
|
||||
}
|
||||
if total == 0 {
|
||||
return []*model.AppWorkOrderModel{}, 0, nil
|
||||
}
|
||||
|
||||
var rows []*entity.TWoMain
|
||||
if err = mod.
|
||||
OrderDesc(dao.TWoMain.Columns().CreatedAt).
|
||||
OrderDesc(dao.TWoMain.Columns().Id).
|
||||
Page(param.Page, param.PageSize).
|
||||
Scan(&rows); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "查询工单列表失败")
|
||||
}
|
||||
|
||||
items = make([]*model.AppWorkOrderModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row != nil {
|
||||
items = append(items, appWorkOrderToModel(row))
|
||||
}
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *sAppWorkOrder) Detail(ctx context.Context, param *model.AppWorkOrderDetailParam) (res *model.AppWorkOrderModel, err error) {
|
||||
if err = appEnsureFeatureAccess(ctx, "workorder:detail"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if param == nil || strings.TrimSpace(param.WoNo) == "" {
|
||||
return nil, gerror.New("工单编号不能为空")
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var row *entity.TWoMain
|
||||
if err = s.ownerModel(ctx, user).
|
||||
Where(dao.TWoMain.Columns().WoNo, strings.TrimSpace(param.WoNo)).
|
||||
Limit(1).
|
||||
Scan(&row); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询工单详情失败")
|
||||
}
|
||||
if row == nil || row.Id == 0 {
|
||||
return nil, gerror.New("工单不存在或无权查看")
|
||||
}
|
||||
|
||||
res = appWorkOrderToModel(row)
|
||||
var communications []*entity.TWoCommunication
|
||||
if err = dao.TWoCommunication.Ctx(ctx).
|
||||
Where(dao.TWoCommunication.Columns().TenantId, user.TenantId).
|
||||
Where(dao.TWoCommunication.Columns().WoNo, row.WoNo).
|
||||
OrderAsc(dao.TWoCommunication.Columns().Id).
|
||||
Scan(&communications); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询工单沟通记录失败")
|
||||
}
|
||||
res.Communications = appWorkOrderCommunicationsToModels(communications)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *sAppWorkOrder) Create(ctx context.Context, param *model.AppWorkOrderCreateParam) (res *model.AppWorkOrderModel, err error) {
|
||||
if err = appEnsureFeatureAccess(ctx, "workorder:create"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if param == nil {
|
||||
return nil, gerror.New("工单参数不能为空")
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(param.Title)
|
||||
content := strings.TrimSpace(param.Content)
|
||||
if title == "" {
|
||||
return nil, gerror.New("工单标题不能为空")
|
||||
}
|
||||
if content == "" {
|
||||
return nil, gerror.New("问题描述不能为空")
|
||||
}
|
||||
if param.WoType < 0 || param.WoType > 5 {
|
||||
return nil, gerror.New("工单类型不正确")
|
||||
}
|
||||
|
||||
user, err := appCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
woNo := appCardBatchNo("WO", user.UserId, now)
|
||||
createdDept := user.DeptId
|
||||
if createdDept <= 0 {
|
||||
createdDept = consts.AppTerminalDeptId
|
||||
}
|
||||
creatorType := appWorkOrderCreatorEnterprise
|
||||
if appIsTerminalUser(ctx, user) {
|
||||
creatorType = appWorkOrderCreatorTerminal
|
||||
}
|
||||
creatorName := strings.TrimSpace(appFirstNotEmpty(user.NickName, user.UserName))
|
||||
handlerDeptId, handlerUserId, err := appWorkOrderHandler(ctx, user, createdDept)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = dao.TWoMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := dao.TWoMain.Ctx(ctx).TX(tx).Data(do.TWoMain{
|
||||
WoNo: woNo,
|
||||
WoType: param.WoType,
|
||||
RelatedId: param.RelatedId,
|
||||
Title: title,
|
||||
Content: content,
|
||||
Attachment: firstAttachment(param.AttachmentUrls),
|
||||
CreatorType: creatorType,
|
||||
CreatorDeptId: createdDept,
|
||||
CreatorUserId: user.UserId,
|
||||
CreatorName: creatorName,
|
||||
CreatorPhone: strings.TrimSpace(user.Phonenumber),
|
||||
AssignDeptId: handlerDeptId,
|
||||
AssignUserId: handlerUserId,
|
||||
CurrentStatus: appWorkOrderStatusProcessing,
|
||||
HandlerDeptId: handlerDeptId,
|
||||
HandlerUserId: handlerUserId,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "创建工单失败")
|
||||
}
|
||||
|
||||
communicationData := do.TWoCommunication{
|
||||
WoNo: woNo,
|
||||
CommType: 1,
|
||||
SenderDeptId: createdDept,
|
||||
SenderUserId: user.UserId,
|
||||
SenderName: creatorName,
|
||||
Content: content,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if len(param.AttachmentUrls) > 0 {
|
||||
communicationData.AttachmentUrls = gjson.New(param.AttachmentUrls)
|
||||
}
|
||||
if _, err = dao.TWoCommunication.Ctx(ctx).TX(tx).Data(communicationData).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "创建工单沟通记录失败")
|
||||
}
|
||||
|
||||
if _, err = dao.TWoRecord.Ctx(ctx).TX(tx).Data(do.TWoRecord{
|
||||
WoNo: woNo,
|
||||
OperationType: appWorkOrderOperationCreate,
|
||||
OperationDeptId: createdDept,
|
||||
OperationUserId: user.UserId,
|
||||
BeforeStatus: 0,
|
||||
AfterStatus: appWorkOrderStatusProcessing,
|
||||
TenantId: user.TenantId,
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: user.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: user.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "创建工单操作记录失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.Detail(ctx, &model.AppWorkOrderDetailParam{WoNo: woNo})
|
||||
}
|
||||
|
||||
func (s *sAppWorkOrder) ownerModel(ctx context.Context, user *entity.SysUser) *gdb.Model {
|
||||
columns := dao.TWoMain.Columns()
|
||||
mod := dao.TWoMain.Ctx(ctx).
|
||||
Where(columns.TenantId, user.TenantId)
|
||||
if appIsTerminalUser(ctx, user) {
|
||||
return mod.Where(columns.CreatorUserId, user.UserId)
|
||||
}
|
||||
return appApplyEnterpriseDataScope(
|
||||
ctx,
|
||||
user,
|
||||
mod,
|
||||
columns.CreatorDeptId,
|
||||
columns.CreatorUserId,
|
||||
func(legacyMod *gdb.Model) *gdb.Model {
|
||||
return legacyMod.Where(
|
||||
"("+columns.CreatorDeptId+" = ? OR "+columns.CreatorUserId+" = ?)",
|
||||
user.DeptId,
|
||||
user.UserId,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func appWorkOrderHandler(ctx context.Context, user *entity.SysUser, createdDept int64) (deptId int64, userId int64, err error) {
|
||||
if user == nil {
|
||||
return createdDept, 0, nil
|
||||
}
|
||||
|
||||
var topDept *entity.SysDept
|
||||
if err = dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, user.TenantId).
|
||||
Where(dao.SysDept.Columns().ParentId, 0).
|
||||
OrderAsc(dao.SysDept.Columns().OrderNum).
|
||||
OrderAsc(dao.SysDept.Columns().DeptId).
|
||||
Limit(1).
|
||||
Scan(&topDept); err != nil {
|
||||
return 0, 0, gerror.Wrap(err, "查询顶级工单处理部门失败")
|
||||
}
|
||||
if topDept != nil && topDept.DeptId > 0 {
|
||||
return topDept.DeptId, topDept.Leader, nil
|
||||
}
|
||||
|
||||
deptId = createdDept
|
||||
if deptId <= 0 {
|
||||
deptId = appUserDeptOrDefault(user)
|
||||
}
|
||||
var dept *entity.SysDept
|
||||
if err = dao.SysDept.Ctx(ctx).
|
||||
Where(dao.SysDept.Columns().TenantId, user.TenantId).
|
||||
Where(dao.SysDept.Columns().DeptId, deptId).
|
||||
Limit(1).
|
||||
Scan(&dept); err != nil {
|
||||
return 0, 0, gerror.Wrap(err, "查询工单处理部门失败")
|
||||
}
|
||||
if dept != nil && dept.Leader > 0 {
|
||||
userId = dept.Leader
|
||||
}
|
||||
return deptId, userId, nil
|
||||
}
|
||||
|
||||
func appWorkOrderToModel(row *entity.TWoMain) *model.AppWorkOrderModel {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
return &model.AppWorkOrderModel{
|
||||
Id: row.Id,
|
||||
WoNo: row.WoNo,
|
||||
WoType: row.WoType,
|
||||
WoTypeText: appWorkOrderTypeText(row.WoType),
|
||||
RelatedId: row.RelatedId,
|
||||
Title: row.Title,
|
||||
Content: row.Content,
|
||||
AttachmentUrls: decodeAttachmentUrls(row.Attachment),
|
||||
CreatorType: row.CreatorType,
|
||||
CreatorName: row.CreatorName,
|
||||
CreatorPhone: row.CreatorPhone,
|
||||
CurrentStatus: row.CurrentStatus,
|
||||
CurrentStatusText: appWorkOrderStatusText(row.CurrentStatus),
|
||||
StatusType: appWorkOrderStatusType(row.CurrentStatus),
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func appWorkOrderCommunicationsToModels(rows []*entity.TWoCommunication) []*model.AppWorkOrderCommModel {
|
||||
items := make([]*model.AppWorkOrderCommModel, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, &model.AppWorkOrderCommModel{
|
||||
Id: row.Id,
|
||||
WoNo: row.WoNo,
|
||||
CommType: row.CommType,
|
||||
CommTypeText: appWorkOrderCommTypeText(row.CommType),
|
||||
SenderName: row.SenderName,
|
||||
Content: row.Content,
|
||||
AttachmentUrls: row.AttachmentUrls,
|
||||
CreatedAt: row.CreatedAt,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func firstAttachment(urls []string) string {
|
||||
if len(urls) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(urls[0])
|
||||
}
|
||||
|
||||
func decodeAttachmentUrls(value string) []string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
var urls []string
|
||||
if err := gjson.DecodeTo(value, &urls); err == nil && len(urls) > 0 {
|
||||
return urls
|
||||
}
|
||||
return []string{value}
|
||||
}
|
||||
func appWorkOrderTypeText(value int) string {
|
||||
switch value {
|
||||
case 0:
|
||||
return "卡片"
|
||||
case 1:
|
||||
return "模组"
|
||||
case 2:
|
||||
return "设备"
|
||||
case 3:
|
||||
return "订单"
|
||||
case 4:
|
||||
return "余额"
|
||||
case 5:
|
||||
return "提现"
|
||||
default:
|
||||
return "其他"
|
||||
}
|
||||
}
|
||||
|
||||
func appWorkOrderStatusText(value int) string {
|
||||
switch value {
|
||||
case appWorkOrderStatusAssigned:
|
||||
return "已分派"
|
||||
case appWorkOrderStatusProcessing:
|
||||
return "处理中"
|
||||
case appWorkOrderStatusResolved:
|
||||
return "已解决"
|
||||
case appWorkOrderStatusClosed:
|
||||
return "已完结"
|
||||
case appWorkOrderStatusCanceled:
|
||||
return "已取消"
|
||||
default:
|
||||
return "待处理"
|
||||
}
|
||||
}
|
||||
|
||||
func appWorkOrderStatusType(value int) string {
|
||||
switch value {
|
||||
case appWorkOrderStatusResolved, appWorkOrderStatusClosed:
|
||||
return "success"
|
||||
case appWorkOrderStatusCanceled:
|
||||
return "danger"
|
||||
case appWorkOrderStatusProcessing:
|
||||
return "primary"
|
||||
default:
|
||||
return "warning"
|
||||
}
|
||||
}
|
||||
|
||||
func appWorkOrderCommTypeText(value int) string {
|
||||
if value == 1 {
|
||||
return "反馈"
|
||||
}
|
||||
return "回复"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/rabbitmq"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const appWorkerBatchExecuteQueueDefault = "iotlcm.batch.execute"
|
||||
|
||||
type appWorkerBatchMessage struct {
|
||||
BatchNo string `json:"batchNo"`
|
||||
BatchType int `json:"batchType"`
|
||||
}
|
||||
|
||||
func appWorkerBatchAsyncEnabled(ctx context.Context) bool {
|
||||
return g.Cfg().MustGet(ctx, "workerBatchAsync.enabled", false).Bool()
|
||||
}
|
||||
|
||||
func (s *sAppCard) publishWorkerBatchExecute(ctx context.Context, batchNo string, batchType int) error {
|
||||
if strings.TrimSpace(batchNo) == "" {
|
||||
return gerror.New("批次号不能为空")
|
||||
}
|
||||
if !appWorkerBatchAsyncEnabled(ctx) {
|
||||
return nil
|
||||
}
|
||||
queueName := strings.TrimSpace(g.Cfg().MustGet(ctx, "workerBatchAsync.queueName", appWorkerBatchExecuteQueueDefault).String())
|
||||
if queueName == "" {
|
||||
queueName = appWorkerBatchExecuteQueueDefault
|
||||
}
|
||||
|
||||
err := rabbitmq.Publish(ctx, queueName, appWorkerBatchMessage{
|
||||
BatchNo: strings.TrimSpace(batchNo),
|
||||
BatchType: batchType,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "投递 worker 批次失败, queue:%s, batchNo:%s, batchType:%d, err:%+v", queueName, batchNo, batchType, err)
|
||||
if g.Cfg().MustGet(ctx, "workerBatchAsync.failOnPublish", true).Bool() {
|
||||
return gerror.Wrap(err, "批次已创建但投递 worker 失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
g.Log().Infof(ctx, "投递 worker 批次成功, queue:%s, batchNo:%s, batchType:%d", queueName, batchNo, batchType)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/genin"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/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)
|
||||
data := do.TestDemo{}
|
||||
gconv.Struct(in, &data)
|
||||
data.Id = nil
|
||||
if _, err = s.Model(ctx).TX(tx).
|
||||
Fields(genin.TestDemoUpdateFields{}).
|
||||
WherePri(in.Id).Data(data).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)
|
||||
data := do.TestDemo{}
|
||||
gconv.Struct(in, &data)
|
||||
data.Id = nil
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx).
|
||||
Fields(genin.TestDemoInsertFields{}).
|
||||
Data(data).OmitEmptyData().Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增测试单表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除测试单表
|
||||
func (s *sGenTestDemo) Delete(ctx context.Context, in *genin.TestDemoDeleteParam) (err error) {
|
||||
|
||||
data := do.TestDemo{
|
||||
DeletedBy: contexts.GetUserId(ctx),
|
||||
DeletedAt: gtime.Now(),
|
||||
}
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(data).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,206 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2026 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/genin"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/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"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/tree"
|
||||
)
|
||||
|
||||
type sGenTestTree struct{}
|
||||
|
||||
func NewGenTestTree() *sGenTestTree {
|
||||
return &sGenTestTree{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterGenTestTree(NewGenTestTree())
|
||||
}
|
||||
|
||||
// Model 测试树表ORM模型
|
||||
func (s *sGenTestTree) 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.TestTree.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取测试树表列表
|
||||
func (s *sGenTestTree) List(ctx context.Context, in *genin.TestTreeListParam) (list []*genin.TestTreeListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 字段过滤
|
||||
mod = mod.Fields(genin.TestTreeListModel{})
|
||||
|
||||
// 查询主键
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.TestTree.Columns().Id, in.Id)
|
||||
}
|
||||
|
||||
// 查询父id
|
||||
if in.ParentId > 0 {
|
||||
mod = mod.Where(dao.TestTree.Columns().ParentId, in.ParentId)
|
||||
}
|
||||
|
||||
// 查询创建时间
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.TestTree.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
// 树形列表判断是否需要分页
|
||||
if in.Page > 0 && in.PageSize > 0 {
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
}
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.TestTree.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 *sGenTestTree) Export(ctx context.Context, in *genin.TestTreeListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(genin.TestTreeExportModel{})
|
||||
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.TestTreeExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增测试树表
|
||||
func (s *sGenTestTree) Edit(ctx context.Context, in *genin.TestTreeEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
in.UpdatedAt = gtime.Now()
|
||||
updatedBy := contexts.GetUserId(ctx)
|
||||
in.UpdatedBy = &updatedBy
|
||||
data := do.TestTree{}
|
||||
gconv.Struct(in, &data)
|
||||
data.Id = nil
|
||||
if _, err = s.Model(ctx).TX(tx).
|
||||
Fields(genin.TestTreeUpdateFields{}).
|
||||
WherePri(in.Id).Data(data).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改测试树表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
in.TenantId = &tenantId
|
||||
createdDept := contexts.GetDeptId(ctx)
|
||||
in.CreatedDept = &createdDept
|
||||
in.CreatedAt = gtime.Now()
|
||||
createdBy := contexts.GetUserId(ctx)
|
||||
in.CreatedBy = &createdBy
|
||||
data := do.TestTree{}
|
||||
gconv.Struct(in, &data)
|
||||
data.Id = nil
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx).
|
||||
Fields(genin.TestTreeInsertFields{}).
|
||||
Data(data).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增测试树表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除测试树表
|
||||
func (s *sGenTestTree) Delete(ctx context.Context, in *genin.TestTreeDeleteParam) (err error) {
|
||||
count, err := dao.TestTree.Ctx(ctx).Where(dao.TestTree.Columns().ParentId, in.Id).Count()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, "查询测试树表下级失败,请稍后重试!")
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("请先删除该测试树表下的所有下级!")
|
||||
}
|
||||
|
||||
data := do.TestTree{
|
||||
DeletedBy: contexts.GetUserId(ctx),
|
||||
DeletedAt: gtime.Now(),
|
||||
}
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(data).Unscoped().Update(); err != nil {
|
||||
err = gerror.Wrap(err, "删除测试树表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取测试树表指定信息
|
||||
func (s *sGenTestTree) View(ctx context.Context, in *genin.TestTreeViewParam) (res *genin.TestTreeViewModel, err error) {
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取测试树表信息,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TreeOption 获取测试树表关系树选项
|
||||
func (s *sGenTestTree) TreeOption(ctx context.Context) (nodes []tree.Node, err error) {
|
||||
var models []*genin.TestTreeTreeOption
|
||||
if err = s.Model(ctx).Fields(genin.TestTreeTreeOption{}).OrderAsc(dao.TestTree.Columns().ParentId).OrderDesc(dao.TestTree.Columns().Id).Scan(&models); err != nil {
|
||||
err = gerror.Wrap(err, "获取测试树表关系树选项失败!")
|
||||
return
|
||||
}
|
||||
nodes = make([]tree.Node, len(models))
|
||||
for i, v := range models {
|
||||
nodes[i] = v
|
||||
}
|
||||
return tree.ListToTree(0, nodes), nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// package gen_codes
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @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 <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package gen_codes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen"
|
||||
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/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,136 @@
|
||||
package info
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"runtime"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
)
|
||||
|
||||
type sInfo struct {
|
||||
Info model.InfoModel
|
||||
}
|
||||
|
||||
func NewInfo() *sInfo {
|
||||
return &sInfo{
|
||||
Info: model.InfoModel{
|
||||
Data: make(map[string]interface{}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterInfo(NewInfo())
|
||||
}
|
||||
func toLogLevelStr(level int) string {
|
||||
if level == glog.LEVEL_NONE {
|
||||
return "NONE"
|
||||
}
|
||||
if level == glog.LEVEL_ALL {
|
||||
return "ALL"
|
||||
}
|
||||
if level == glog.LEVEL_DEV {
|
||||
return "DEV"
|
||||
}
|
||||
if level == glog.LEVEL_PROD {
|
||||
return "PROD"
|
||||
}
|
||||
str := ""
|
||||
if level&glog.LEVEL_DEBU != 0 {
|
||||
str += "DEBUG "
|
||||
}
|
||||
if level&glog.LEVEL_INFO != 0 {
|
||||
str += "INFO "
|
||||
}
|
||||
if level&glog.LEVEL_NOTI != 0 {
|
||||
str += "NOTI "
|
||||
}
|
||||
if level&glog.LEVEL_WARN != 0 {
|
||||
str += "WARN "
|
||||
}
|
||||
if level&glog.LEVEL_ERRO != 0 {
|
||||
str += "ERRO "
|
||||
}
|
||||
if level&glog.LEVEL_CRIT != 0 {
|
||||
str += "CRIT "
|
||||
}
|
||||
if level&glog.LEVEL_PANI != 0 {
|
||||
str += "PANI "
|
||||
}
|
||||
if level&glog.LEVEL_FATA != 0 {
|
||||
str += "FATA "
|
||||
}
|
||||
return str
|
||||
}
|
||||
func (s *sInfo) GetInfo() *model.InfoModel {
|
||||
ctx := context.Background()
|
||||
logLevel := g.Cfg().MustGet(ctx, "logger.level", "").String()
|
||||
if logLevel != "" {
|
||||
g.Log().SetLevelStr(logLevel)
|
||||
}
|
||||
s.AddInfoData("log_level_conf", logLevel)
|
||||
s.AddInfoData("log_level_real", toLogLevelStr(g.Log().GetLevel()))
|
||||
s.AddInfoData("Goroutines", runtime.NumGoroutine())
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
s.AddInfoData("MemAllocKB", m.Alloc/1024)
|
||||
s.AddInfoData("MemTotalAllocKB", m.TotalAlloc/1024)
|
||||
s.AddInfoData("MemSysKB", m.Sys/1024)
|
||||
s.AddInfoData("MemHeapAllocKB", m.HeapAlloc/1024)
|
||||
s.AddInfoData("MemHeapSysKB", m.HeapSys/1024)
|
||||
s.AddInfoData("MemHeapIdleKB", m.HeapIdle/1024)
|
||||
s.AddInfoData("MemHeapInuseKB", m.HeapInuse/1024)
|
||||
s.AddInfoData("MemHeapReleasedKB", m.HeapReleased/1024)
|
||||
s.AddInfoData("MemHeapObjects", m.HeapObjects)
|
||||
s.AddInfoData("MemStackInuseKB", m.StackInuse/1024)
|
||||
s.AddInfoData("MemStackSysKB", m.StackSys/1024)
|
||||
s.AddInfoData("MemNumGC", m.NumGC)
|
||||
return &s.Info
|
||||
}
|
||||
|
||||
func (s *sInfo) SetInfoName(Type, Name, Title, Description string) {
|
||||
if s.Info.Name == "" {
|
||||
s.Info.Type = Type
|
||||
s.Info.Name = Name
|
||||
s.Info.Title = Title
|
||||
s.Info.Description = Description
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sInfo) SetInfoBuild(buildVersion, buildTime, commitID string) {
|
||||
s.Info.Version = buildVersion
|
||||
s.Info.Build = buildTime
|
||||
s.Info.Commit = commitID
|
||||
}
|
||||
|
||||
func (s *sInfo) SetInfoData(data map[string]interface{}) {
|
||||
s.Info.Data = data
|
||||
}
|
||||
|
||||
func (s *sInfo) GetInfoData() map[string]interface{} {
|
||||
return s.Info.Data
|
||||
}
|
||||
|
||||
func (s *sInfo) AddInfoData(key string, value interface{}) {
|
||||
if s.Info.Data == nil {
|
||||
s.Info.Data = make(map[string]interface{})
|
||||
}
|
||||
s.Info.Data[key] = value
|
||||
}
|
||||
|
||||
func (s *sInfo) DelInfoData(key string) {
|
||||
delete(s.Info.Data, key)
|
||||
}
|
||||
|
||||
func (s *sInfo) GetInfoDataByKey(key string) interface{} {
|
||||
if s.Info.Data == nil {
|
||||
return nil
|
||||
}
|
||||
if _, ok := s.Info.Data[key]; !ok {
|
||||
return nil
|
||||
}
|
||||
return s.Info.Data[key]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// ==========================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// ==========================================================================
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-app/internal/logic/app"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-app/internal/logic/gen"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-app/internal/logic/gen_codes"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-app/internal/logic/info"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-app/internal/logic/memorydb"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-app/internal/logic/middleware"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-app/internal/logic/monitor"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-app/internal/logic/system"
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
package memorydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/cmd/inithttp"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var db gdb.DB
|
||||
|
||||
type memDB interface {
|
||||
Init(ctx context.Context) error
|
||||
EventHandler(ctx context.Context) error
|
||||
TableName(ctx context.Context) string
|
||||
CreateTable(ctx context.Context) error
|
||||
LoadData(ctx context.Context, pk int64) error
|
||||
DeleteData(ctx context.Context, ids []int64) error
|
||||
}
|
||||
|
||||
var memDBMap = make(map[string]memDB)
|
||||
|
||||
func RegisterMemDB(name string, memDB memDB) {
|
||||
memDBMap[name] = memDB
|
||||
}
|
||||
|
||||
type sMemoryDB struct {
|
||||
memDBMap map[string]interface{}
|
||||
}
|
||||
|
||||
func NewMemoryDB() *sMemoryDB {
|
||||
return &sMemoryDB{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterMemoryDB(NewMemoryDB())
|
||||
inithttp.RegisterHttpInitFunc("memorydb", func(ctx context.Context) error {
|
||||
return service.MemoryDB().Init(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sMemoryDB) Init(ctx context.Context) error {
|
||||
link := g.Cfg().MustGet(ctx, "database.memorydb.link", "").String()
|
||||
debug := g.Cfg().MustGet(ctx, "database.memorydb.debug", "false").Bool()
|
||||
if link == "" {
|
||||
g.Log().Warning(ctx, "sMemoryDB.Init link is empty")
|
||||
return nil
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemoryDB.Init link", link)
|
||||
var err error
|
||||
db, err = gdb.New(gdb.ConfigNode{
|
||||
Link: link,
|
||||
MaxIdleConnCount: 10,
|
||||
MaxOpenConnCount: 1,
|
||||
MaxConnLifeTime: 24 * 3650 * time.Hour,
|
||||
Debug: debug,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemoryDB.Init get db err: %v", err)
|
||||
return err
|
||||
}
|
||||
if db == nil {
|
||||
g.Log().Errorf(ctx, "sMemoryDB.Init not found memorydb")
|
||||
return errors.New("sMemoryDB.Init get db err")
|
||||
}
|
||||
for _, memDB := range memDBMap {
|
||||
err = memDB.Init(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemoryDB.Init end.", db.GetConfig())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemoryDB) EventHandler(ctx context.Context, chg *model.BroadcastDbChgCache) error {
|
||||
memDB, ok := memDBMap[chg.TableName]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if chg.Type == "add" || chg.Type == "edit" {
|
||||
for _, pk := range chg.PK {
|
||||
memDB.LoadData(ctx, pk)
|
||||
}
|
||||
} else if chg.Type == "del" {
|
||||
memDB.DeleteData(ctx, chg.PK)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemoryDB) DB(ctx context.Context) gdb.DB {
|
||||
link := g.Cfg().MustGet(ctx, "database.memorydb.link", "sqlite::@file(:memory:)").String()
|
||||
if link == "" {
|
||||
return g.DB()
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func (s *sMemoryDB) Ctx(ctx context.Context) gdb.DB {
|
||||
return s.DB(ctx).Ctx(ctx)
|
||||
}
|
||||
|
||||
func entityListToDo[E any, D any](source []*E) ([]*D, error) {
|
||||
target := make([]*D, 0, len(source))
|
||||
for _, item := range source {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
value := new(D)
|
||||
if err := gconv.Struct(item, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target = append(target, value)
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package memorydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sMemDBSysConfig struct {
|
||||
}
|
||||
|
||||
var instanceMemDBSysConfig *sMemDBSysConfig
|
||||
|
||||
func NewMemDBSysConfig() *sMemDBSysConfig {
|
||||
if instanceMemDBSysConfig != nil {
|
||||
return instanceMemDBSysConfig
|
||||
}
|
||||
instanceMemDBSysConfig = &sMemDBSysConfig{}
|
||||
initLoadMemoryDB := g.Cfg().MustGet(context.TODO(), "server.initLoadMemoryDB").Bool()
|
||||
if initLoadMemoryDB {
|
||||
instanceMemDBSysConfig.Init(context.TODO())
|
||||
}
|
||||
return instanceMemDBSysConfig
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewMemDBSysConfig()
|
||||
service.RegisterMemDBSysConfig(s)
|
||||
RegisterMemDB(s.TableName(context.TODO()), s)
|
||||
}
|
||||
|
||||
func (s *sMemDBSysConfig) Init(ctx context.Context) error {
|
||||
err := s.CreateTable(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.EventHandler(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysConfig) EventHandler(ctx context.Context) error {
|
||||
eventList := []string{
|
||||
consts.EventKeyDBSysConfigCreate,
|
||||
consts.EventKeyDBSysConfigUpdate,
|
||||
consts.EventKeyDBSysConfigDelete,
|
||||
}
|
||||
event.EventsInstance().Register(eventList, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
g.Log().Errorf(ctx, "memorydb/sys_config.go event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList, ok := args[0].([]int64)
|
||||
if !ok {
|
||||
pk, ok := args[0].(int64)
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList = []int64{pk}
|
||||
}
|
||||
if len(pkList) == 0 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
if eventKey == consts.EventKeyDBSysConfigDelete {
|
||||
s.DeleteData(ctx, pkList)
|
||||
} else {
|
||||
for _, pk := range pkList {
|
||||
s.LoadData(ctx, pk)
|
||||
}
|
||||
}
|
||||
|
||||
dbType := "edit"
|
||||
if eventKey == consts.EventKeyDBSysConfigCreate {
|
||||
dbType = "add"
|
||||
} else if eventKey == consts.EventKeyDBSysConfigDelete {
|
||||
dbType = "del"
|
||||
} else {
|
||||
dbType = "edit"
|
||||
}
|
||||
queue.Push(ctx, consts.BroadcastDbChg, &model.BroadcastDbChgCache{
|
||||
TableName: s.TableName(ctx),
|
||||
PK: pkList,
|
||||
Type: dbType,
|
||||
Data: nil,
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysConfig) TableName(ctx context.Context) string {
|
||||
return dao.SysConfig.Table()
|
||||
}
|
||||
|
||||
func (s *sMemDBSysConfig) CreateTable(ctx context.Context) error {
|
||||
_, err := db.Exec(ctx, "DROP TABLE IF EXISTS "+s.TableName(ctx))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.CreateTable delete table err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
total, err := db.Model("sqlite_master").Where("type", "table").Where("name", dao.SysConfig.Table()).Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.CreateTable count err: %v", err)
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
g.Log().Debug(ctx, "sMemDBSysConfig.List create table ", s.TableName(ctx))
|
||||
res, err := db.Exec(ctx, `
|
||||
CREATE TABLE "sys_config" (
|
||||
"config_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"tenant_id" TEXT DEFAULT '000000' CHECK(length("tenant_id") <= 20),
|
||||
"config_name" TEXT DEFAULT '' CHECK(length("config_name") <= 100),
|
||||
"config_key" TEXT DEFAULT '' CHECK(length("config_key") <= 100),
|
||||
"config_value" TEXT DEFAULT '' CHECK(length("config_value") <= 1024),
|
||||
"config_type" TEXT DEFAULT 'N',
|
||||
"created_dept" INTEGER,
|
||||
"created_by" INTEGER,
|
||||
"created_at" TEXT,
|
||||
"updated_by" INTEGER,
|
||||
"updated_at" TEXT,
|
||||
"remark" TEXT CHECK(length("remark") <= 500)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.CreateTable create table err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemDBSysConfig.CreateTable create table success", res)
|
||||
// 加载数据
|
||||
s.LoadData(ctx, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysConfig) LoadData(ctx context.Context, pk int64) error {
|
||||
count := 0
|
||||
if pk == 0 {
|
||||
maxConfigId := int64(0)
|
||||
for {
|
||||
page := 1
|
||||
pageSize := 500
|
||||
dataList := make([]*entity.SysConfig, 0)
|
||||
dao.SysConfig.Ctx(ctx).Page(page, pageSize).WhereGT(dao.SysConfig.Columns().ConfigId, maxConfigId).
|
||||
OrderAsc(dao.SysConfig.Columns().ConfigId).Scan(&dataList)
|
||||
if len(dataList) == 0 {
|
||||
break
|
||||
}
|
||||
count += len(dataList)
|
||||
doList, err := entityListToDo[entity.SysConfig, do.SysConfig](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysConfig.Table()).Data(doList).OnConflict(dao.SysConfig.Columns().ConfigId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.Load data insert err: %v", err)
|
||||
return err
|
||||
}
|
||||
maxConfigId = dataList[len(dataList)-1].ConfigId
|
||||
if len(dataList) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataList := make([]*entity.SysConfig, 0)
|
||||
dao.SysConfig.Ctx(ctx).Where(dao.SysConfig.Columns().ConfigId, pk).Scan(&dataList)
|
||||
doList, err := entityListToDo[entity.SysConfig, do.SysConfig](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysConfig.Table()).Data(doList).OnConflict(dao.SysConfig.Columns().ConfigId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.Load data insert err: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Debugf(ctx, "sMemDBSysConfig.LoadData load data success, total: %d, pk: %d", count, pk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysConfig) DeleteData(ctx context.Context, ids []int64) error {
|
||||
_, err := db.Model(dao.SysConfig.Table()).WhereIn(dao.SysConfig.Columns().ConfigId, ids).Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysConfig.DeleteData err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysConfig.DeleteData delete data success, total: %d, ids: %+v", len(ids), ids)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package memorydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sMemDBSysDept struct {
|
||||
}
|
||||
|
||||
var instanceMemDBSysDept *sMemDBSysDept
|
||||
|
||||
func NewMemDBSysDept() *sMemDBSysDept {
|
||||
if instanceMemDBSysDept != nil {
|
||||
return instanceMemDBSysDept
|
||||
}
|
||||
instanceMemDBSysDept = &sMemDBSysDept{}
|
||||
initLoadMemoryDB := g.Cfg().MustGet(context.TODO(), "server.initLoadMemoryDB").Bool()
|
||||
if initLoadMemoryDB {
|
||||
instanceMemDBSysDept.Init(context.TODO())
|
||||
}
|
||||
return instanceMemDBSysDept
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewMemDBSysDept()
|
||||
service.RegisterMemDBSysDept(s)
|
||||
RegisterMemDB(s.TableName(context.TODO()), s)
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDept) Init(ctx context.Context) error {
|
||||
err := s.CreateTable(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.EventHandler(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDept) EventHandler(ctx context.Context) error {
|
||||
eventList := []string{
|
||||
consts.EventKeyDBSysDeptCreate,
|
||||
consts.EventKeyDBSysDeptUpdate,
|
||||
consts.EventKeyDBSysDeptDelete,
|
||||
}
|
||||
event.EventsInstance().Register(eventList, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList, ok := args[0].([]int64)
|
||||
if !ok {
|
||||
pk, ok := args[0].(int64)
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList = []int64{pk}
|
||||
}
|
||||
if len(pkList) == 0 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
if eventKey == consts.EventKeyDBSysDeptDelete {
|
||||
s.DeleteData(ctx, pkList)
|
||||
} else {
|
||||
for _, pk := range pkList {
|
||||
s.LoadData(ctx, pk)
|
||||
}
|
||||
}
|
||||
|
||||
dbType := "edit"
|
||||
if eventKey == consts.EventKeyDBSysDeptCreate {
|
||||
dbType = "add"
|
||||
} else if eventKey == consts.EventKeyDBSysDeptDelete {
|
||||
dbType = "del"
|
||||
} else {
|
||||
dbType = "edit"
|
||||
}
|
||||
queue.Push(ctx, consts.BroadcastDbChg, &model.BroadcastDbChgCache{
|
||||
TableName: s.TableName(ctx),
|
||||
PK: pkList,
|
||||
Type: dbType,
|
||||
Data: nil,
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDept) TableName(ctx context.Context) string {
|
||||
return dao.SysDept.Table()
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDept) CreateTable(ctx context.Context) error {
|
||||
_, err := db.Exec(ctx, "DROP TABLE IF EXISTS "+s.TableName(ctx))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.CreateTable delete table err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
total, err := db.Model("sqlite_master").Where("type", "table").Where("name", dao.SysDept.Table()).Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.CreateTable count err: %v", err)
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
g.Log().Debug(ctx, "sMemDBSysDept.List create table ", s.TableName(ctx))
|
||||
res, err := db.Exec(ctx, `
|
||||
CREATE TABLE "sys_dept" (
|
||||
"dept_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"tenant_id" TEXT DEFAULT '000000' CHECK(length("tenant_id") <= 20),
|
||||
"parent_id" INTEGER DEFAULT 0,
|
||||
"ancestors" TEXT DEFAULT '' CHECK(length("ancestors") <= 500),
|
||||
"dept_name" TEXT DEFAULT '' CHECK(length("dept_name") <= 30),
|
||||
"dept_type" INTEGER DEFAULT 0,
|
||||
"dept_category" TEXT CHECK(length("dept_category") <= 100),
|
||||
"order_num" INTEGER DEFAULT 0,
|
||||
"leader" INTEGER,
|
||||
"phone" TEXT CHECK(length("phone") <= 11),
|
||||
"email" TEXT CHECK(length("email") <= 50),
|
||||
"status" TEXT DEFAULT '0' ,
|
||||
"created_dept" INTEGER,
|
||||
"created_by" INTEGER NOT NULL,
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_by" INTEGER,
|
||||
"updated_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted_by" INTEGER,
|
||||
"deleted_at" DATETIME
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.CreateTable create table err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemDBSysDept.CreateTable create table success", res)
|
||||
// 加载数据
|
||||
s.LoadData(ctx, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDept) LoadData(ctx context.Context, pk int64) error {
|
||||
count := 0
|
||||
if pk == 0 {
|
||||
maxDeptId := int64(0)
|
||||
for {
|
||||
page := 1
|
||||
pageSize := 100
|
||||
dataList := make([]*entity.SysDept, 0)
|
||||
dao.SysDept.Ctx(ctx).Page(page, pageSize).WhereGT(dao.SysDept.Columns().DeptId, maxDeptId).
|
||||
OrderAsc(dao.SysDept.Columns().DeptId).Scan(&dataList)
|
||||
if len(dataList) == 0 {
|
||||
break
|
||||
}
|
||||
count += len(dataList)
|
||||
doList, err := entityListToDo[entity.SysDept, do.SysDept](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysDept.Table()).Data(doList).OnConflict(dao.SysDept.Columns().DeptId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.Load data insert err: %v", err)
|
||||
return err
|
||||
}
|
||||
maxDeptId = dataList[len(dataList)-1].DeptId
|
||||
if len(dataList) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataList := make([]*entity.SysDept, 0)
|
||||
dao.SysDept.Ctx(ctx).Where(dao.SysDept.Columns().DeptId, pk).Scan(&dataList)
|
||||
doList, err := entityListToDo[entity.SysDept, do.SysDept](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysDept.Table()).Data(doList).OnConflict(dao.SysDept.Columns().DeptId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.Load data insert err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Debugf(ctx, "sMemDBSysDept.LoadData load data success, total: %d, pk: %d", count, pk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDept) DeleteData(ctx context.Context, ids []int64) error {
|
||||
_, err := db.Model(dao.SysDept.Table()).WhereIn(dao.SysDept.Columns().DeptId, ids).Unscoped().Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDept.DeleteData err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysDept.DeleteData delete data success, total: %d, ids: %+v", len(ids), ids)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package memorydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sMemDBSysDictData struct {
|
||||
}
|
||||
|
||||
var instanceMemDBSysDictData *sMemDBSysDictData
|
||||
|
||||
func NewMemDBSysDictData() *sMemDBSysDictData {
|
||||
if instanceMemDBSysDictData != nil {
|
||||
return instanceMemDBSysDictData
|
||||
}
|
||||
instanceMemDBSysDictData = &sMemDBSysDictData{}
|
||||
initLoadMemoryDB := g.Cfg().MustGet(context.TODO(), "server.initLoadMemoryDB").Bool()
|
||||
if initLoadMemoryDB {
|
||||
instanceMemDBSysDictData.Init(context.TODO())
|
||||
}
|
||||
return instanceMemDBSysDictData
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewMemDBSysDictData()
|
||||
service.RegisterMemDBSysDictData(s)
|
||||
RegisterMemDB(s.TableName(context.TODO()), s)
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictData) Init(ctx context.Context) error {
|
||||
err := s.CreateTable(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.EventHandler(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictData) EventHandler(ctx context.Context) error {
|
||||
eventList := []string{
|
||||
consts.EventKeyDBSysDictDataCreate,
|
||||
consts.EventKeyDBSysDictDataUpdate,
|
||||
consts.EventKeyDBSysDictDataDelete,
|
||||
}
|
||||
event.EventsInstance().Register(eventList, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList, ok := args[0].([]int64)
|
||||
if !ok {
|
||||
pk, ok := args[0].(int64)
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList = []int64{pk}
|
||||
}
|
||||
if len(pkList) == 0 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
if eventKey == consts.EventKeyDBSysDictDataDelete {
|
||||
s.DeleteData(ctx, pkList)
|
||||
} else {
|
||||
for _, pk := range pkList {
|
||||
s.LoadData(ctx, pk)
|
||||
}
|
||||
}
|
||||
dbType := "edit"
|
||||
if eventKey == consts.EventKeyDBSysDictDataCreate {
|
||||
dbType = "add"
|
||||
} else if eventKey == consts.EventKeyDBSysDictDataDelete {
|
||||
dbType = "del"
|
||||
} else {
|
||||
dbType = "edit"
|
||||
}
|
||||
queue.Push(ctx, consts.BroadcastDbChg, &model.BroadcastDbChgCache{
|
||||
TableName: s.TableName(ctx),
|
||||
PK: pkList,
|
||||
Type: dbType,
|
||||
Data: nil,
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictData) TableName(ctx context.Context) string {
|
||||
return dao.SysDictData.Table()
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictData) CreateTable(ctx context.Context) error {
|
||||
_, err := db.Exec(ctx, "DROP TABLE IF EXISTS "+s.TableName(ctx))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.CreateTable delete table err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
total, err := db.Model("sqlite_master").Where("type", "table").Where("name", dao.SysDictData.Table()).Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.CreateTable count err: %v", err)
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
g.Log().Debug(ctx, "sMemDBSysDictData.List create table ", s.TableName(ctx))
|
||||
res, err := db.Exec(ctx, `
|
||||
CREATE TABLE "sys_dict_data" (
|
||||
"dict_code" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"tenant_id" TEXT DEFAULT '000000' CHECK(length("tenant_id") <= 20),
|
||||
"dict_sort" INTEGER DEFAULT 0,
|
||||
"dict_label" TEXT DEFAULT '' CHECK(length("dict_label") <= 100),
|
||||
"dict_value" TEXT DEFAULT '' CHECK(length("dict_value") <= 100),
|
||||
"dict_type" TEXT DEFAULT '' CHECK(length("dict_type") <= 100),
|
||||
"css_class" TEXT CHECK(length("css_class") <= 100),
|
||||
"list_class" TEXT CHECK(length("list_class") <= 100),
|
||||
"is_default" TEXT DEFAULT 'N' ,
|
||||
"created_dept" INTEGER,
|
||||
"created_by" INTEGER,
|
||||
"created_at" TEXT,
|
||||
"updated_by" INTEGER,
|
||||
"updated_at" TEXT,
|
||||
"remark" TEXT CHECK(length("remark") <= 500)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.CreateTable create table err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemDBSysDictData.CreateTable create table success", res)
|
||||
// 加载数据
|
||||
s.LoadData(ctx, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictData) LoadData(ctx context.Context, pk int64) error {
|
||||
count := 0
|
||||
if pk == 0 {
|
||||
maxDictCode := int64(0)
|
||||
for {
|
||||
page := 1
|
||||
pageSize := 500
|
||||
dataList := make([]*entity.SysDictData, 0)
|
||||
dao.SysDictData.Ctx(ctx).Page(page, pageSize).WhereGT(dao.SysDictData.Columns().DictCode, maxDictCode).
|
||||
OrderAsc(dao.SysDictData.Columns().DictCode).Scan(&dataList)
|
||||
if len(dataList) == 0 {
|
||||
break
|
||||
}
|
||||
count += len(dataList)
|
||||
doList, err := entityListToDo[entity.SysDictData, do.SysDictData](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysDictData.Table()).Data(doList).OnConflict(dao.SysDictData.Columns().DictCode).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.Load data insert err: %v", err)
|
||||
return err
|
||||
}
|
||||
maxDictCode = dataList[len(dataList)-1].DictCode
|
||||
if len(dataList) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataList := make([]*entity.SysDictData, 0)
|
||||
dao.SysDictData.Ctx(ctx).Where(dao.SysDictData.Columns().DictCode, pk).Scan(&dataList)
|
||||
doList, err := entityListToDo[entity.SysDictData, do.SysDictData](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysDictData.Table()).Data(doList).OnConflict(dao.SysDictData.Columns().DictCode).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.Load data insert err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Debugf(ctx, "sMemDBSysDictData.LoadData load data success, total: %d, pk: %d", count, pk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictData) DeleteData(ctx context.Context, ids []int64) error {
|
||||
_, err := db.Model(dao.SysDictData.Table()).WhereIn(dao.SysDictData.Columns().DictCode, ids).Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictData.DeleteData err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysDictData.DeleteData delete data success, total: %d, ids: %+v", len(ids), ids)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package memorydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sMemDBSysDictType struct {
|
||||
}
|
||||
|
||||
var instanceMemDBSysDictType *sMemDBSysDictType
|
||||
|
||||
func NewMemDBSysDictType() *sMemDBSysDictType {
|
||||
if instanceMemDBSysDictType != nil {
|
||||
return instanceMemDBSysDictType
|
||||
}
|
||||
instanceMemDBSysDictType = &sMemDBSysDictType{}
|
||||
initLoadMemoryDB := g.Cfg().MustGet(context.TODO(), "server.initLoadMemoryDB").Bool()
|
||||
if initLoadMemoryDB {
|
||||
instanceMemDBSysDictType.Init(context.TODO())
|
||||
}
|
||||
return instanceMemDBSysDictType
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewMemDBSysDictType()
|
||||
service.RegisterMemDBSysDictType(s)
|
||||
RegisterMemDB(s.TableName(context.TODO()), s)
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictType) Init(ctx context.Context) error {
|
||||
err := s.CreateTable(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.EventHandler(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictType) EventHandler(ctx context.Context) error {
|
||||
eventList := []string{
|
||||
consts.EventKeyDBSysDictTypeCreate,
|
||||
consts.EventKeyDBSysDictTypeUpdate,
|
||||
consts.EventKeyDBSysDictTypeDelete,
|
||||
}
|
||||
event.EventsInstance().Register(eventList, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList, ok := args[0].([]int64)
|
||||
if !ok {
|
||||
pk, ok := args[0].(int64)
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList = []int64{pk}
|
||||
}
|
||||
if len(pkList) == 0 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
if eventKey == consts.EventKeyDBSysDictTypeDelete {
|
||||
s.DeleteData(ctx, pkList)
|
||||
} else {
|
||||
for _, pk := range pkList {
|
||||
s.LoadData(ctx, pk)
|
||||
}
|
||||
}
|
||||
dbType := "edit"
|
||||
if eventKey == consts.EventKeyDBSysDictTypeCreate {
|
||||
dbType = "add"
|
||||
} else if eventKey == consts.EventKeyDBSysDictTypeDelete {
|
||||
dbType = "del"
|
||||
} else {
|
||||
dbType = "edit"
|
||||
}
|
||||
queue.Push(ctx, consts.BroadcastDbChg, &model.BroadcastDbChgCache{
|
||||
TableName: s.TableName(ctx),
|
||||
PK: pkList,
|
||||
Type: dbType,
|
||||
Data: nil,
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictType) TableName(ctx context.Context) string {
|
||||
return dao.SysDictType.Table()
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictType) CreateTable(ctx context.Context) error {
|
||||
_, err := db.Exec(ctx, "DROP TABLE IF EXISTS "+s.TableName(ctx))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.CreateTable delete table err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
total, err := db.Model("sqlite_master").Where("type", "table").Where("name", dao.SysDictType.Table()).Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.CreateTable count err: %v", err)
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
g.Log().Debug(ctx, "sMemDBSysDictType.List create table ", s.TableName(ctx))
|
||||
res, err := db.Exec(ctx, `
|
||||
CREATE TABLE "sys_dict_type" (
|
||||
"dict_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"tenant_id" TEXT DEFAULT '000000' CHECK(length("tenant_id") <= 20),
|
||||
"dict_name" TEXT DEFAULT '' CHECK(length("dict_name") <= 100),
|
||||
"dict_type" TEXT DEFAULT '' CHECK(length("dict_type") <= 100),
|
||||
"is_sys" TEXT DEFAULT '1' ,
|
||||
"created_dept" INTEGER,
|
||||
"created_by" INTEGER,
|
||||
"created_at" TEXT,
|
||||
"updated_by" INTEGER,
|
||||
"updated_at" TEXT,
|
||||
"remark" TEXT CHECK(length("remark") <= 500)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.CreateTable create table err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemDBSysDictType.CreateTable create table success", res)
|
||||
// 加载数据
|
||||
s.LoadData(ctx, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictType) LoadData(ctx context.Context, pk int64) error {
|
||||
count := 0
|
||||
if pk == 0 {
|
||||
maxDictId := int64(0)
|
||||
for {
|
||||
page := 1
|
||||
pageSize := 500
|
||||
dataList := make([]*entity.SysDictType, 0)
|
||||
dao.SysDictType.Ctx(ctx).Page(page, pageSize).WhereGT(dao.SysDictType.Columns().DictId, maxDictId).
|
||||
OrderAsc(dao.SysDictType.Columns().DictId).Scan(&dataList)
|
||||
if len(dataList) == 0 {
|
||||
break
|
||||
}
|
||||
count += len(dataList)
|
||||
doList, err := entityListToDo[entity.SysDictType, do.SysDictType](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysDictType.Table()).Data(doList).OnConflict(dao.SysDictType.Columns().DictId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.Load data insert err: %v", err)
|
||||
return err
|
||||
}
|
||||
maxDictId = dataList[len(dataList)-1].DictId
|
||||
if len(dataList) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataList := make([]*entity.SysDictType, 0)
|
||||
dao.SysDictType.Ctx(ctx).Where(dao.SysDictType.Columns().DictId, pk).Scan(&dataList)
|
||||
doList, err := entityListToDo[entity.SysDictType, do.SysDictType](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysDictType.Table()).Data(doList).OnConflict(dao.SysDictType.Columns().DictId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.Load data insert err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Debugf(ctx, "sMemDBSysDictType.LoadData load data success, total: %d, pk: %d", count, pk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysDictType) DeleteData(ctx context.Context, ids []int64) error {
|
||||
_, err := db.Model(dao.SysDictType.Table()).WhereIn(dao.SysDictType.Columns().DictId, ids).Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysDictType.DeleteData err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysDictType.DeleteData delete data success, total: %d, ids: %+v", len(ids), ids)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package memorydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sMemDBSysMenu struct {
|
||||
}
|
||||
|
||||
var instanceMemDBSysMenu *sMemDBSysMenu
|
||||
|
||||
func NewMemDBSysMenu() *sMemDBSysMenu {
|
||||
if instanceMemDBSysMenu != nil {
|
||||
return instanceMemDBSysMenu
|
||||
}
|
||||
instanceMemDBSysMenu = &sMemDBSysMenu{}
|
||||
initLoadMemoryDB := g.Cfg().MustGet(context.TODO(), "server.initLoadMemoryDB").Bool()
|
||||
if initLoadMemoryDB {
|
||||
instanceMemDBSysMenu.Init(context.TODO())
|
||||
}
|
||||
return instanceMemDBSysMenu
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewMemDBSysMenu()
|
||||
service.RegisterMemDBSysMenu(s)
|
||||
RegisterMemDB(s.TableName(context.TODO()), s)
|
||||
}
|
||||
|
||||
func (s *sMemDBSysMenu) Init(ctx context.Context) error {
|
||||
err := s.CreateTable(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.EventHandler(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysMenu) EventHandler(ctx context.Context) error {
|
||||
eventList := []string{
|
||||
consts.EventKeyDBSysMenuCreate,
|
||||
consts.EventKeyDBSysMenuUpdate,
|
||||
consts.EventKeyDBSysMenuDelete,
|
||||
}
|
||||
event.EventsInstance().Register(eventList, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList, ok := args[0].([]int64)
|
||||
if !ok {
|
||||
pk, ok := args[0].(int64)
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList = []int64{pk}
|
||||
}
|
||||
if len(pkList) == 0 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
if eventKey == consts.EventKeyDBSysMenuDelete {
|
||||
s.DeleteData(ctx, pkList)
|
||||
} else {
|
||||
for _, pk := range pkList {
|
||||
s.LoadData(ctx, pk)
|
||||
}
|
||||
}
|
||||
|
||||
roleIds := make([]int64, 0)
|
||||
if err := dao.SysRoleMenu.Ctx(ctx).
|
||||
Fields(dao.SysRoleMenu.Columns().RoleId).
|
||||
WhereIn(dao.SysRoleMenu.Columns().MenuId, pkList).
|
||||
Group(dao.SysRoleMenu.Columns().RoleId).
|
||||
Scan(&roleIds); err == nil && len(roleIds) > 0 {
|
||||
userIds := make([]int64, 0)
|
||||
if err := dao.SysUserRole.Ctx(ctx).
|
||||
Fields(dao.SysUserRole.Columns().UserId).
|
||||
WhereIn(dao.SysUserRole.Columns().RoleId, roleIds).
|
||||
Group(dao.SysUserRole.Columns().UserId).
|
||||
Scan(&userIds); err == nil {
|
||||
for _, userId := range userIds {
|
||||
_ = mcache.RemoveUserAccessCodeList(ctx, userId)
|
||||
_ = mcache.RemoveUserRoleDataAccessCodeList(ctx, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbType := "edit"
|
||||
if eventKey == consts.EventKeyDBSysMenuCreate {
|
||||
dbType = "add"
|
||||
} else if eventKey == consts.EventKeyDBSysMenuDelete {
|
||||
dbType = "del"
|
||||
} else {
|
||||
dbType = "edit"
|
||||
}
|
||||
queue.Push(ctx, consts.BroadcastDbChg, &model.BroadcastDbChgCache{
|
||||
TableName: s.TableName(ctx),
|
||||
PK: pkList,
|
||||
Type: dbType,
|
||||
Data: nil,
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysMenu) TableName(ctx context.Context) string {
|
||||
return dao.SysMenu.Table()
|
||||
}
|
||||
|
||||
func (s *sMemDBSysMenu) CreateTable(ctx context.Context) error {
|
||||
_, err := db.Exec(ctx, "DROP TABLE IF EXISTS "+s.TableName(ctx))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.CreateTable delete table err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
total, err := db.Model("sqlite_master").Where("type", "table").Where("name", dao.SysMenu.Table()).Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.CreateTable count err: %v", err)
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
g.Log().Debug(ctx, "sMemDBSysMenu.List create table ", s.TableName(ctx))
|
||||
res, err := db.Exec(ctx, `
|
||||
CREATE TABLE "sys_menu" (
|
||||
"menu_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"menu_name" TEXT NOT NULL CHECK(length("menu_name") <= 50),
|
||||
"parent_id" INTEGER DEFAULT 0,
|
||||
"level" INTEGER DEFAULT 1,
|
||||
"tree" TEXT CHECK(length("tree") <= 255),
|
||||
"order_num" INTEGER DEFAULT 0,
|
||||
"path" TEXT DEFAULT '' CHECK(length("path") <= 200),
|
||||
"component" TEXT CHECK(length("component") <= 255),
|
||||
"query_param" TEXT CHECK(length("query_param") <= 255),
|
||||
"is_frame" INTEGER DEFAULT 1 ,
|
||||
"is_cache" INTEGER DEFAULT 0 ,
|
||||
"menu_type" TEXT DEFAULT '' ,
|
||||
"visible" TEXT DEFAULT '0',
|
||||
"status" TEXT DEFAULT '0' ,
|
||||
"perms" TEXT CHECK(length("perms") <= 100),
|
||||
"icon" TEXT DEFAULT '#' CHECK(length("icon") <= 100),
|
||||
"created_dept" INTEGER,
|
||||
"created_by" INTEGER,
|
||||
"created_at" TEXT,
|
||||
"updated_by" INTEGER,
|
||||
"updated_at" TEXT,
|
||||
"remark" TEXT DEFAULT '' CHECK(length("remark") <= 500)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.CreateTable create table err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemDBSysMenu.CreateTable create table success", res)
|
||||
s.LoadData(ctx, 0)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysMenu) LoadData(ctx context.Context, pk int64) error {
|
||||
count := 0
|
||||
if pk == 0 {
|
||||
maxMenuId := int64(0)
|
||||
for {
|
||||
page := 1
|
||||
pageSize := 500
|
||||
dataList := make([]*entity.SysMenu, 0)
|
||||
dao.SysMenu.Ctx(ctx).Page(page, pageSize).WhereGT(dao.SysMenu.Columns().MenuId, maxMenuId).
|
||||
OrderAsc(dao.SysMenu.Columns().MenuId).Scan(&dataList)
|
||||
if len(dataList) == 0 {
|
||||
break
|
||||
}
|
||||
count += len(dataList)
|
||||
doList, err := entityListToDo[entity.SysMenu, do.SysMenu](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysMenu.Table()).Data(doList).OnConflict(dao.SysMenu.Columns().MenuId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.Load data insert err: %v", err)
|
||||
return err
|
||||
}
|
||||
maxMenuId = dataList[len(dataList)-1].MenuId
|
||||
if len(dataList) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataList := make([]*entity.SysMenu, 0)
|
||||
dao.SysMenu.Ctx(ctx).Where(dao.SysMenu.Columns().MenuId, pk).Scan(&dataList)
|
||||
doList, err := entityListToDo[entity.SysMenu, do.SysMenu](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysMenu.Table()).Data(doList).OnConflict(dao.SysMenu.Columns().MenuId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.Load data insert err: %v", err)
|
||||
}
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysMenu.LoadData load data success, total: %d, pk: %d", count, pk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysMenu) DeleteData(ctx context.Context, ids []int64) error {
|
||||
_, err := db.Model(dao.SysMenu.Table()).WhereIn(dao.SysMenu.Columns().MenuId, ids).Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysMenu.DeleteData err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysMenu.DeleteData delete data success, total: %d, ids: %+v", len(ids), ids)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package memorydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sMemDBSysPost struct {
|
||||
}
|
||||
|
||||
var instanceMemDBSysPost *sMemDBSysPost
|
||||
|
||||
func NewMemDBSysPost() *sMemDBSysPost {
|
||||
if instanceMemDBSysPost != nil {
|
||||
return instanceMemDBSysPost
|
||||
}
|
||||
instanceMemDBSysPost = &sMemDBSysPost{}
|
||||
initLoadMemoryDB := g.Cfg().MustGet(context.TODO(), "server.initLoadMemoryDB").Bool()
|
||||
if initLoadMemoryDB {
|
||||
instanceMemDBSysPost.Init(context.TODO())
|
||||
}
|
||||
return instanceMemDBSysPost
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewMemDBSysPost()
|
||||
service.RegisterMemDBSysPost(s)
|
||||
RegisterMemDB(s.TableName(context.TODO()), s)
|
||||
}
|
||||
|
||||
func (s *sMemDBSysPost) Init(ctx context.Context) error {
|
||||
err := s.CreateTable(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.EventHandler(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysPost) EventHandler(ctx context.Context) error {
|
||||
eventList := []string{
|
||||
consts.EventKeyDBSysPostCreate,
|
||||
consts.EventKeyDBSysPostUpdate,
|
||||
consts.EventKeyDBSysPostDelete,
|
||||
}
|
||||
event.EventsInstance().Register(eventList, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList, ok := args[0].([]int64)
|
||||
if !ok {
|
||||
pk, ok := args[0].(int64)
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList = []int64{pk}
|
||||
}
|
||||
if len(pkList) == 0 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
if eventKey == consts.EventKeyDBSysPostDelete {
|
||||
s.DeleteData(ctx, pkList)
|
||||
} else {
|
||||
for _, pk := range pkList {
|
||||
s.LoadData(ctx, pk)
|
||||
}
|
||||
}
|
||||
dbType := "edit"
|
||||
if eventKey == consts.EventKeyDBSysPostCreate {
|
||||
dbType = "add"
|
||||
} else if eventKey == consts.EventKeyDBSysPostDelete {
|
||||
dbType = "del"
|
||||
} else {
|
||||
dbType = "edit"
|
||||
}
|
||||
queue.Push(ctx, consts.BroadcastDbChg, &model.BroadcastDbChgCache{
|
||||
TableName: s.TableName(ctx),
|
||||
PK: pkList,
|
||||
Type: dbType,
|
||||
Data: nil,
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysPost) TableName(ctx context.Context) string {
|
||||
return dao.SysPost.Table()
|
||||
}
|
||||
|
||||
func (s *sMemDBSysPost) CreateTable(ctx context.Context) error {
|
||||
_, err := db.Exec(ctx, "DROP TABLE IF EXISTS "+s.TableName(ctx))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.CreateTable delete table err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
total, err := db.Model("sqlite_master").Where("type", "table").Where("name", dao.SysPost.Table()).Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.CreateTable count err: %v", err)
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
g.Log().Debug(ctx, "sMemDBSysPost.List create table ", s.TableName(ctx))
|
||||
res, err := db.Exec(ctx, `
|
||||
CREATE TABLE "sys_post" (
|
||||
"post_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"tenant_id" TEXT DEFAULT '000000' CHECK(length("tenant_id") <= 20),
|
||||
"dept_id" INTEGER NOT NULL,
|
||||
"post_code" TEXT NOT NULL CHECK(length("post_code") <= 64),
|
||||
"post_category" TEXT CHECK(length("post_category") <= 100),
|
||||
"post_name" TEXT NOT NULL CHECK(length("post_name") <= 50),
|
||||
"post_sort" INTEGER NOT NULL,
|
||||
"status" TEXT NOT NULL ,
|
||||
"created_dept" INTEGER,
|
||||
"created_by" INTEGER NOT NULL,
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_by" INTEGER,
|
||||
"updated_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted_by" INTEGER,
|
||||
"deleted_at" DATETIME,
|
||||
"remark" TEXT CHECK(length("remark") <= 500)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.CreateTable create table err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemDBSysPost.CreateTable create table success", res)
|
||||
// 加载数据
|
||||
s.LoadData(ctx, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysPost) LoadData(ctx context.Context, pk int64) error {
|
||||
count := 0
|
||||
if pk == 0 {
|
||||
maxPostId := int64(0)
|
||||
for {
|
||||
page := 1
|
||||
pageSize := 500
|
||||
dataList := make([]*entity.SysPost, 0)
|
||||
dao.SysPost.Ctx(ctx).Page(page, pageSize).WhereGT(dao.SysPost.Columns().PostId, maxPostId).
|
||||
OrderAsc(dao.SysPost.Columns().PostId).Scan(&dataList)
|
||||
if len(dataList) == 0 {
|
||||
break
|
||||
}
|
||||
count += len(dataList)
|
||||
doList, err := entityListToDo[entity.SysPost, do.SysPost](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysPost.Table()).Data(doList).OnConflict(dao.SysPost.Columns().PostId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.Load data insert err: %v", err)
|
||||
return err
|
||||
}
|
||||
maxPostId = dataList[len(dataList)-1].PostId
|
||||
if len(dataList) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataList := make([]*entity.SysPost, 0)
|
||||
dao.SysPost.Ctx(ctx).Where(dao.SysPost.Columns().PostId, pk).Scan(&dataList)
|
||||
doList, err := entityListToDo[entity.SysPost, do.SysPost](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysPost.Table()).Data(doList).OnConflict(dao.SysPost.Columns().PostId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.Load data insert err: %v", err)
|
||||
}
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysPost.LoadData load data success, total: %d, pk: %d", count, pk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysPost) DeleteData(ctx context.Context, ids []int64) error {
|
||||
_, err := db.Model(dao.SysPost.Table()).WhereIn(dao.SysPost.Columns().PostId, ids).Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysPost.DeleteData err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysPost.DeleteData delete data success, total: %d, ids: %+v", len(ids), ids)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package memorydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sMemDBSysRole struct {
|
||||
}
|
||||
|
||||
var instanceMemDBSysRole *sMemDBSysRole
|
||||
|
||||
func NewMemDBSysRole() *sMemDBSysRole {
|
||||
if instanceMemDBSysRole != nil {
|
||||
return instanceMemDBSysRole
|
||||
}
|
||||
instanceMemDBSysRole = &sMemDBSysRole{}
|
||||
initLoadMemoryDB := g.Cfg().MustGet(context.TODO(), "server.initLoadMemoryDB").Bool()
|
||||
if initLoadMemoryDB {
|
||||
instanceMemDBSysRole.Init(context.TODO())
|
||||
}
|
||||
return instanceMemDBSysRole
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewMemDBSysRole()
|
||||
service.RegisterMemDBSysRole(s)
|
||||
RegisterMemDB(s.TableName(context.TODO()), s)
|
||||
}
|
||||
|
||||
func (s *sMemDBSysRole) Init(ctx context.Context) error {
|
||||
err := s.CreateTable(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.EventHandler(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysRole) EventHandler(ctx context.Context) error {
|
||||
eventList := []string{
|
||||
consts.EventKeyDBSysRoleCreate,
|
||||
consts.EventKeyDBSysRoleUpdate,
|
||||
consts.EventKeyDBSysRoleDelete,
|
||||
}
|
||||
event.EventsInstance().Register(eventList, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList, ok := args[0].([]int64)
|
||||
if !ok {
|
||||
pk, ok := args[0].(int64)
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList = []int64{pk}
|
||||
}
|
||||
if len(pkList) == 0 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
if eventKey == consts.EventKeyDBSysRoleDelete {
|
||||
s.DeleteData(ctx, pkList)
|
||||
} else {
|
||||
for _, pk := range pkList {
|
||||
s.LoadData(ctx, pk)
|
||||
}
|
||||
}
|
||||
|
||||
userIds := make([]int64, 0)
|
||||
if err := dao.SysUserRole.Ctx(ctx).
|
||||
Fields(dao.SysUserRole.Columns().UserId).
|
||||
WhereIn(dao.SysUserRole.Columns().RoleId, pkList).
|
||||
Group(dao.SysUserRole.Columns().UserId).
|
||||
Scan(&userIds); err == nil {
|
||||
for _, userId := range userIds {
|
||||
_ = mcache.RemoveUserAccessCodeList(ctx, userId)
|
||||
_ = mcache.RemoveUserRoleDataAccessCodeList(ctx, userId)
|
||||
}
|
||||
}
|
||||
|
||||
dbType := "edit"
|
||||
if eventKey == consts.EventKeyDBSysRoleCreate {
|
||||
dbType = "add"
|
||||
} else if eventKey == consts.EventKeyDBSysRoleDelete {
|
||||
dbType = "del"
|
||||
} else {
|
||||
dbType = "edit"
|
||||
}
|
||||
queue.Push(ctx, consts.BroadcastDbChg, &model.BroadcastDbChgCache{
|
||||
TableName: s.TableName(ctx),
|
||||
PK: pkList,
|
||||
Type: dbType,
|
||||
Data: nil,
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysRole) TableName(ctx context.Context) string {
|
||||
return dao.SysRole.Table()
|
||||
}
|
||||
|
||||
func (s *sMemDBSysRole) CreateTable(ctx context.Context) error {
|
||||
_, err := db.Exec(ctx, "DROP TABLE IF EXISTS "+s.TableName(ctx))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.CreateTable delete table err: %v", err)
|
||||
return err
|
||||
}
|
||||
total, err := db.Model("sqlite_master").Where("type", "table").Where("name", dao.SysRole.Table()).Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.CreateTable count err: %v", err)
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
g.Log().Debug(ctx, "sMemDBSysRole.List create table ", s.TableName(ctx))
|
||||
res, err := db.Exec(ctx, `
|
||||
CREATE TABLE "sys_role" (
|
||||
"role_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"tenant_id" TEXT DEFAULT '000000' CHECK(length("tenant_id") <= 20),
|
||||
"dept_id" INTEGER DEFAULT 0,
|
||||
"role_name" TEXT NOT NULL CHECK(length("role_name") <= 30),
|
||||
"role_key" TEXT NOT NULL CHECK(length("role_key") <= 100),
|
||||
"role_sort" INTEGER NOT NULL,
|
||||
"data_scope" TEXT DEFAULT '1',
|
||||
"menu_check_strictly" INTEGER DEFAULT 1,
|
||||
"dept_check_strictly" INTEGER DEFAULT 1,
|
||||
"status" TEXT NOT NULL ,
|
||||
"created_dept" INTEGER,
|
||||
"created_by" INTEGER NOT NULL,
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_by" INTEGER,
|
||||
"updated_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted_by" INTEGER,
|
||||
"deleted_at" DATETIME,
|
||||
"remark" TEXT CHECK(length("remark") <= 500)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.CreateTable create table err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemDBSysRole.CreateTable create table success", res)
|
||||
s.LoadData(ctx, 0)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysRole) LoadData(ctx context.Context, pk int64) error {
|
||||
count := 0
|
||||
if pk == 0 {
|
||||
maxRoleId := int64(0)
|
||||
for {
|
||||
page := 1
|
||||
pageSize := 500
|
||||
dataList := make([]*entity.SysRole, 0)
|
||||
dao.SysRole.Ctx(ctx).Page(page, pageSize).WhereGT(dao.SysRole.Columns().RoleId, maxRoleId).
|
||||
OrderAsc(dao.SysRole.Columns().RoleId).Scan(&dataList)
|
||||
if len(dataList) == 0 {
|
||||
break
|
||||
}
|
||||
count += len(dataList)
|
||||
doList, err := entityListToDo[entity.SysRole, do.SysRole](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysRole.Table()).Data(doList).OnConflict(dao.SysRole.Columns().RoleId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.Load data insert err: %v", err)
|
||||
return err
|
||||
}
|
||||
maxRoleId = dataList[len(dataList)-1].RoleId
|
||||
if len(dataList) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataList := make([]*entity.SysRole, 0)
|
||||
dao.SysRole.Ctx(ctx).Where(dao.SysRole.Columns().RoleId, pk).Scan(&dataList)
|
||||
doList, err := entityListToDo[entity.SysRole, do.SysRole](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysRole.Table()).Data(doList).OnConflict(dao.SysRole.Columns().RoleId).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.Load data insert err: %v", err)
|
||||
}
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysRole.LoadData load data success, total: %d, pk: %d", count, pk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysRole) DeleteData(ctx context.Context, ids []int64) error {
|
||||
g.Log().Debugf(ctx, "sMemDBSysRole.DeleteData called with ids: %+v, len: %d", ids, len(ids))
|
||||
|
||||
if len(ids) == 0 {
|
||||
g.Log().Warningf(ctx, "sMemDBSysRole.DeleteData called with empty ids")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用 Unscoped() 来确保删除操作被执行
|
||||
_, err := db.Model(dao.SysRole.Table()).WhereIn(dao.SysRole.Columns().RoleId, ids).Unscoped().Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysRole.DeleteData err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysRole.DeleteData delete data success, total: %d, ids: %+v", len(ids), ids)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package memorydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sMemDBSysTenant struct {
|
||||
}
|
||||
|
||||
var instanceMemDBSysTenant *sMemDBSysTenant
|
||||
|
||||
func NewMemDBSysTenant() *sMemDBSysTenant {
|
||||
if instanceMemDBSysTenant != nil {
|
||||
return instanceMemDBSysTenant
|
||||
}
|
||||
instanceMemDBSysTenant = &sMemDBSysTenant{}
|
||||
initLoadMemoryDB := g.Cfg().MustGet(context.TODO(), "server.initLoadMemoryDB").Bool()
|
||||
if initLoadMemoryDB {
|
||||
instanceMemDBSysTenant.Init(context.TODO())
|
||||
}
|
||||
return instanceMemDBSysTenant
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewMemDBSysTenant()
|
||||
service.RegisterMemDBSysTenant(s)
|
||||
RegisterMemDB(s.TableName(context.TODO()), s)
|
||||
}
|
||||
|
||||
func (s *sMemDBSysTenant) Init(ctx context.Context) error {
|
||||
err := s.CreateTable(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.EventHandler(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysTenant) EventHandler(ctx context.Context) error {
|
||||
eventList := []string{
|
||||
consts.EventKeyDBSysTenantCreate,
|
||||
consts.EventKeyDBSysTenantUpdate,
|
||||
consts.EventKeyDBSysTenantDelete,
|
||||
}
|
||||
event.EventsInstance().Register(eventList, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList, ok := args[0].([]int64)
|
||||
if !ok {
|
||||
pk, ok := args[0].(int64)
|
||||
if !ok {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
pkList = []int64{pk}
|
||||
}
|
||||
if len(pkList) == 0 {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.EventHandler event args error. eventKey: %s, args: %+v", eventKey, args)
|
||||
return
|
||||
}
|
||||
if eventKey == consts.EventKeyDBSysTenantDelete {
|
||||
s.DeleteData(ctx, pkList)
|
||||
} else {
|
||||
for _, pk := range pkList {
|
||||
s.LoadData(ctx, pk)
|
||||
}
|
||||
}
|
||||
dbType := "edit"
|
||||
if eventKey == consts.EventKeyDBSysTenantCreate {
|
||||
dbType = "add"
|
||||
} else if eventKey == consts.EventKeyDBSysTenantDelete {
|
||||
dbType = "del"
|
||||
} else {
|
||||
dbType = "edit"
|
||||
}
|
||||
queue.Push(ctx, consts.BroadcastDbChg, &model.BroadcastDbChgCache{
|
||||
TableName: s.TableName(ctx),
|
||||
PK: pkList,
|
||||
Type: dbType,
|
||||
Data: nil,
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysTenant) DB(ctx context.Context) gdb.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
func (s *sMemDBSysTenant) TableName(ctx context.Context) string {
|
||||
return dao.SysTenant.Table()
|
||||
}
|
||||
|
||||
func (s *sMemDBSysTenant) CreateTable(ctx context.Context) error {
|
||||
_, err := db.Exec(ctx, "DROP TABLE IF EXISTS "+s.TableName(ctx))
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.CreateTable delete table err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
total, err := db.Model("sqlite_master").Where("type", "table").Where("name", dao.SysTenant.Table()).Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.CreateTable count err: %v", err)
|
||||
return err
|
||||
}
|
||||
if total == 0 {
|
||||
g.Log().Debug(ctx, "sMemDBSysTenant.List create table ", s.TableName(ctx))
|
||||
res, err := db.Exec(ctx, `
|
||||
CREATE TABLE "sys_tenant" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"tenant_id" TEXT NOT NULL CHECK(length("tenant_id") <= 20),
|
||||
"contact_user_name" TEXT CHECK(length("contact_user_name") <= 20),
|
||||
"contact_phone" TEXT CHECK(length("contact_phone") <= 20),
|
||||
"company_name" TEXT CHECK(length("company_name") <= 50),
|
||||
"license_number" TEXT CHECK(length("license_number") <= 30),
|
||||
"address" TEXT CHECK(length("address") <= 200),
|
||||
"intro" TEXT CHECK(length("intro") <= 200),
|
||||
"domain" TEXT CHECK(length("domain") <= 200),
|
||||
"remark" TEXT CHECK(length("remark") <= 200),
|
||||
"package_id" INTEGER,
|
||||
"expire_time" TEXT,
|
||||
"account_count" INTEGER DEFAULT -1,
|
||||
"status" TEXT DEFAULT '0',
|
||||
"created_dept" INTEGER,
|
||||
"created_by" INTEGER NOT NULL,
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_by" INTEGER,
|
||||
"updated_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted_by" INTEGER,
|
||||
"deleted_at" DATETIME,
|
||||
CONSTRAINT "sys_tenant_tenant_id_unique" UNIQUE ("tenant_id")
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.CreateTable create table err: %v", err)
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, "sMemDBSysTenant.CreateTable create table success", res)
|
||||
// 加载数据
|
||||
s.LoadData(ctx, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysTenant) LoadData(ctx context.Context, pk int64) error {
|
||||
count := 0
|
||||
if pk == 0 {
|
||||
maxId := int64(0)
|
||||
for {
|
||||
page := 1
|
||||
pageSize := 500
|
||||
dataList := make([]*entity.SysTenant, 0)
|
||||
dao.SysTenant.Ctx(ctx).Page(page, pageSize).WhereGT(dao.SysTenant.Columns().Id, maxId).
|
||||
OrderAsc(dao.SysTenant.Columns().Id).Scan(&dataList)
|
||||
if len(dataList) == 0 {
|
||||
break
|
||||
}
|
||||
count += len(dataList)
|
||||
doList, err := entityListToDo[entity.SysTenant, do.SysTenant](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysTenant.Table()).Data(doList).OnConflict(dao.SysTenant.Columns().Id).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.Load data insert err: %v, dataList: %+v", err, dataList)
|
||||
return err
|
||||
}
|
||||
maxId = dataList[len(dataList)-1].Id
|
||||
if len(dataList) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataList := make([]*entity.SysTenant, 0)
|
||||
dao.SysTenant.Ctx(ctx).Where(dao.SysTenant.Columns().Id, pk).Scan(&dataList)
|
||||
doList, err := entityListToDo[entity.SysTenant, do.SysTenant](dataList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.Load convert data err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Model(dao.SysTenant.Table()).Data(doList).OnConflict(dao.SysTenant.Columns().Id).Save()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.Load data insert err: %v", err)
|
||||
}
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysTenant.LoadData load data success, total: %d, pk: %d", count, pk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sMemDBSysTenant) DeleteData(ctx context.Context, ids []int64) error {
|
||||
_, err := db.Model(dao.SysTenant.Table()).WhereIn(dao.SysTenant.Columns().Id, ids).Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMemDBSysTenant.DeleteData err: %v, ids: %+v", err, ids)
|
||||
return err
|
||||
}
|
||||
g.Log().Debugf(ctx, "sMemDBSysTenant.DeleteData delete data success, total: %d, ids: %+v", len(ids), ids)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
// package middleware
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/util/gmeta"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/appidentity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/bcache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/packed/response"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/queue"
|
||||
)
|
||||
|
||||
type sMiddleware struct {
|
||||
}
|
||||
|
||||
func New() *sMiddleware {
|
||||
return &sMiddleware{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterMiddleware(New())
|
||||
}
|
||||
|
||||
func getContentType(r *ghttp.Request) (contentType string) {
|
||||
contentType = r.Response.Header().Get("Content-Type")
|
||||
if contentType != "" {
|
||||
return
|
||||
}
|
||||
|
||||
mime := gmeta.Get(r.GetHandlerResponse(), "mime").String()
|
||||
if mime == "" {
|
||||
contentType = consts.HTTPContentTypeJson
|
||||
} else {
|
||||
contentType = mime
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 响应处理中间件
|
||||
func (m *sMiddleware) ResponseHandler(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
if r.Response.BufferLength() > 0 {
|
||||
return
|
||||
}
|
||||
if contexts.IsWebSocket(r.GetCtx()) {
|
||||
return
|
||||
}
|
||||
contentType := getContentType(r)
|
||||
if contentType == consts.HTTPContentTypeExcel {
|
||||
r.Response.Status = http.StatusOK
|
||||
return
|
||||
}
|
||||
// g.Log().Infof(r.GetCtx(), "sMiddleware.ResponseHandler,contentType %v", contentType)
|
||||
err := r.GetError()
|
||||
res := r.GetHandlerResponse()
|
||||
var code gcode.Code = gcode.CodeOK
|
||||
|
||||
if err != nil {
|
||||
code = gerror.Code(err)
|
||||
if code == gcode.CodeNil {
|
||||
code = gcode.CodeInternalError
|
||||
}
|
||||
response.JsonExit(r, code.Code(), err.Error())
|
||||
} else {
|
||||
response.Json(r, code.Code(), "", res)
|
||||
}
|
||||
}
|
||||
|
||||
// Ctx 自定义上下文对象
|
||||
func (s *sMiddleware) Ctx(r *ghttp.Request) {
|
||||
ctx := r.GetCtx()
|
||||
r.SetCtx(r.GetNeverDoneCtx())
|
||||
|
||||
// 初始化登录用户信息
|
||||
data, err := service.SysAuth().GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
if data != nil {
|
||||
contextModel := new(model.Context)
|
||||
contextModel.User = &model.Identity{
|
||||
BaseClaims: data.BaseClaims,
|
||||
}
|
||||
contexts.Init(r, contextModel)
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
// CORS 跨域处理
|
||||
func (s *sMiddleware) CORS(r *ghttp.Request) {
|
||||
r.SetCtx(r.GetNeverDoneCtx())
|
||||
if contexts.IsWebSocket(r.GetCtx()) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
corsOptions := r.Response.DefaultCORSOptions()
|
||||
corsConfig := g.Cfg().MustGet(context.Background(), "server.allowedDomains").Strings()
|
||||
if len(corsConfig) == 0 {
|
||||
r.Response.CORSDefault()
|
||||
} else {
|
||||
corsOptions.AllowDomain = corsConfig
|
||||
r.Response.CORS(corsOptions)
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
// Auth 认证处理
|
||||
func (s *sMiddleware) Auth(r *ghttp.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
method = r.Method
|
||||
path = ""
|
||||
)
|
||||
if r.URL != nil {
|
||||
path = r.URL.Path
|
||||
}
|
||||
// 不需要验证登录的路由地址
|
||||
if s.IsExceptLogin(ctx, path) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
|
||||
userId := contexts.GetUserId(ctx)
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
g.Log().Debugf(ctx, "sMiddleware.Auth userId: %d tenantId: %s method: %s path: %s, ================>>", userId, tenantId, method, path)
|
||||
if userId == 0 {
|
||||
g.Log().Error(ctx, "sMiddleware.Auth userId is 0", "path", path, "clientIp", r.GetClientIp(), "User-Agent", utility.GetUserAgent(ctx))
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
// r.Response.WriteStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// 如果是websocket,则不进行权限验证
|
||||
// g.Log().Infof(ctx, "sMiddleware.Auth is websocket: %v", contexts.IsWebSocket(ctx))
|
||||
if contexts.IsWebSocket(ctx) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
// 是否校验强行退出用户
|
||||
forceLogout, err := mcache.GetSystemConfig(ctx, consts.ConfigOnlineForceLogout, consts.ConfigOnlineForceLogoutFalse)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth GetSystemConfig error: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if forceLogout == consts.ConfigOnlineForceLogoutTrue {
|
||||
uuid := contexts.GetUserUuid(ctx)
|
||||
token, err := bcache.GetSysAuthTokenReject(ctx, userId, uuid)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth GetSysAuthTokenReject error: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if token != "" {
|
||||
g.Log().Infof(ctx, "sMiddleware.Auth token is reject, userId: %d, uuid: %s", userId, uuid)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
}
|
||||
// 超级管理员
|
||||
if contexts.IsSuperAdmin(r.GetCtx()) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
// 权限验证
|
||||
serveHandler := r.GetServeHandler()
|
||||
userAccessCodeList, menuRoleDataAccessCodeList, err := mcache.GetUserAccessCodeList(ctx, userId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth GetUserAccessCodeList error: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if serveHandler != nil {
|
||||
g.Log().Infof(ctx, "sMiddleware.Auth serveHandler GetMetaTag(x-check-permission): %+v", serveHandler.GetMetaTag("x-check-permission"))
|
||||
accessCode := serveHandler.GetMetaTag("x-check-permission")
|
||||
if accessCode != "" {
|
||||
accessCodeList := strings.Split(accessCode, ",")
|
||||
hasPermission := false
|
||||
for _, code := range accessCodeList {
|
||||
code = strings.TrimSpace(code)
|
||||
if code == consts.SysCheckPermissionTerminalUser {
|
||||
if !isTerminalAppUser(ctx, userId) {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth terminal permission denied, userId: %d", userId)
|
||||
continue
|
||||
}
|
||||
hasPermission = true
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(code, consts.SysCheckPermissionPrefix) {
|
||||
accessCodeItemList := splitAccessCodeExpression(code)
|
||||
if len(accessCodeItemList) == 0 {
|
||||
continue
|
||||
}
|
||||
if missingItem, ok := missingAccessCodeExpressionItem(userAccessCodeList, accessCodeItemList); ok {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth userAccessCodeList not contains item: %v, userAccessCodeList: %v", missingItem, userAccessCodeList)
|
||||
continue
|
||||
}
|
||||
applyMatchedPermissionDataScope(ctx, menuRoleDataAccessCodeList, accessCodeItemList)
|
||||
hasPermission = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasPermission {
|
||||
response.JsonExit(r, consts.CodeNoPermission.Code(), consts.CodeNoPermission.Message())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
// matchAccessCode 检查权限码是否匹配,支持通配符 *
|
||||
func matchAccessCode(pattern string, code string) bool {
|
||||
pattern = normalizeAccessCode(pattern)
|
||||
code = normalizeAccessCode(code)
|
||||
if pattern == code {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(pattern, "*") {
|
||||
prefix := pattern[:len(pattern)-1]
|
||||
return strings.HasPrefix(code, prefix)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeAccessCode(code string) string {
|
||||
code = strings.TrimSpace(code)
|
||||
for strings.HasPrefix(code, consts.SysCheckPermissionMenuPrefix+consts.SysCheckPermissionMenuPrefix) {
|
||||
code = strings.TrimPrefix(code, consts.SysCheckPermissionMenuPrefix)
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func isTerminalAppUser(ctx context.Context, userId int64) bool {
|
||||
if userId <= 0 {
|
||||
return false
|
||||
}
|
||||
ok, err := appidentity.IsTerminalUserId(ctx, userId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth query app terminal identity failed, userId: %d, error: %v", userId, err)
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// hasAccessCode 检查用户权限码列表中是否存在匹配指定权限码的项。
|
||||
func hasAccessCode(userAccessCodeList []string, requiredCode string) bool {
|
||||
for _, userCode := range userAccessCodeList {
|
||||
if matchAccessCode(userCode, requiredCode) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func splitAccessCodeExpression(code string) []string {
|
||||
parts := strings.Split(code, "&")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, item := range parts {
|
||||
item = strings.TrimSpace(item)
|
||||
if item != "" {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func hasAccessCodeExpression(userAccessCodeList []string, accessCodeItemList []string) bool {
|
||||
if len(accessCodeItemList) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, item := range accessCodeItemList {
|
||||
if !hasAccessCode(userAccessCodeList, item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func missingAccessCodeExpressionItem(userAccessCodeList []string, accessCodeItemList []string) (string, bool) {
|
||||
for _, item := range accessCodeItemList {
|
||||
if !hasAccessCode(userAccessCodeList, item) {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func applyMatchedPermissionDataScope(ctx context.Context, menuRoleDataAccessCodeList []string, accessCodeItemList []string) {
|
||||
roleId, roleDataScope, ok := matchedPermissionDataScope(menuRoleDataAccessCodeList, accessCodeItemList)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
contexts.SetDataValue(ctx, "currentRoleId", roleId)
|
||||
contexts.SetDataValue(ctx, "currentRoleDataScope", roleDataScope)
|
||||
g.Log().Debugf(ctx, "sMiddleware.Auth currentRoleId: %s, currentRoleDataScope: %s", roleId, roleDataScope)
|
||||
}
|
||||
|
||||
func matchedPermissionDataScope(menuRoleDataAccessCodeList []string, accessCodeItemList []string) (roleId string, roleDataScope string, ok bool) {
|
||||
for _, item := range accessCodeItemList {
|
||||
for _, roleDataAccessCode := range menuRoleDataAccessCodeList {
|
||||
roleDataAccessCodeItemList := strings.Split(roleDataAccessCode, "|")
|
||||
if len(roleDataAccessCodeItemList) != 3 {
|
||||
continue
|
||||
}
|
||||
if !matchAccessCode(roleDataAccessCodeItemList[0], item) {
|
||||
continue
|
||||
}
|
||||
return roleDataAccessCodeItemList[1], roleDataAccessCodeItemList[2], true
|
||||
}
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// IsExceptLogin 判断是否需要验证登录
|
||||
func (s *sMiddleware) IsExceptLogin(ctx context.Context, path string) bool {
|
||||
pathList := g.Cfg().MustGet(ctx, "router.exceptLogin").Strings()
|
||||
return slices.Contains(pathList, path)
|
||||
}
|
||||
|
||||
// OperationLog 操作日志
|
||||
func (s *sMiddleware) OperationLog(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
ctx := r.Context()
|
||||
enabledDBOperationLog := g.Cfg().MustGet(ctx, "server.enabledDBOperationLog").Bool()
|
||||
if !enabledDBOperationLog {
|
||||
return
|
||||
}
|
||||
data, err := service.SysOperLog().AnalysisLog(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(context.TODO(), "sMiddleware.OperationLog error:%v", err)
|
||||
return
|
||||
}
|
||||
// 写入队列
|
||||
// logData, _ := json.Marshal(data)
|
||||
queue.Push(r.GetCtx(), consts.QueueSysOptLog, data)
|
||||
}
|
||||
|
||||
// ResponseHandlerRaw 返回处理中间件
|
||||
func (s *sMiddleware) ResponseHandlerRaw(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
// 如果已经有返回内容,那么该中间件什么也不做
|
||||
if r.Response.BufferLength() > 0 {
|
||||
g.Log().Infof(r.Context(), "ResponseHandler r.Response.BufferLength() %d buf:%s", r.Response.BufferLength(), r.Response.BufferString())
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
err = r.GetError()
|
||||
res = r.GetHandlerResponse()
|
||||
)
|
||||
if err != nil {
|
||||
r.Response.WriteJson(res)
|
||||
r.ExitAll()
|
||||
} else {
|
||||
r.Response.WriteJson(res)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package middleware
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHasAccessCodeExpressionRequiresEveryPart(t *testing.T) {
|
||||
userAccessCodeList := []string{
|
||||
"cpm:iot:tSimCardInfo:edit",
|
||||
"cpm:agent:dept:list",
|
||||
}
|
||||
|
||||
if !hasAccessCodeExpression(userAccessCodeList, splitAccessCodeExpression("cpm:iot:tSimCardInfo:edit&cpm:agent:dept:list")) {
|
||||
t.Fatal("expected AND expression to match when every permission is granted")
|
||||
}
|
||||
|
||||
if hasAccessCodeExpression(userAccessCodeList, splitAccessCodeExpression("cpm:iot:tSimCardInfo:edit&cpm:agent:tAgentEnterprise:list")) {
|
||||
t.Fatal("expected AND expression to fail when any permission is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchedPermissionDataScopeUsesFirstExpressionPermission(t *testing.T) {
|
||||
menuRoleDataAccessCodeList := []string{
|
||||
"cpm:iot:tSimCardInfo:edit|10|4",
|
||||
"cpm:agent:dept:list|20|7",
|
||||
}
|
||||
|
||||
roleId, roleDataScope, ok := matchedPermissionDataScope(
|
||||
menuRoleDataAccessCodeList,
|
||||
splitAccessCodeExpression("cpm:iot:tSimCardInfo:edit&cpm:agent:dept:list"),
|
||||
)
|
||||
if !ok {
|
||||
t.Fatal("expected data scope to be found")
|
||||
}
|
||||
if roleId != "10" || roleDataScope != "4" {
|
||||
t.Fatalf("expected first permission data scope role=10 scope=4, got role=%s scope=%s", roleId, roleDataScope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchedPermissionDataScopeFallsBackToFirstMenuBoundPermission(t *testing.T) {
|
||||
menuRoleDataAccessCodeList := []string{
|
||||
"cpm:system:sysNoticeUser:list|30|5",
|
||||
}
|
||||
|
||||
roleId, roleDataScope, ok := matchedPermissionDataScope(
|
||||
menuRoleDataAccessCodeList,
|
||||
splitAccessCodeExpression("cpc:current:user&cpm:system:sysNoticeUser:list"),
|
||||
)
|
||||
if !ok {
|
||||
t.Fatal("expected data scope to be found")
|
||||
}
|
||||
if roleId != "30" || roleDataScope != "5" {
|
||||
t.Fatalf("expected first menu-bound permission data scope role=30 scope=5, got role=%s scope=%s", roleId, roleDataScope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingAccessCodeExpressionItem(t *testing.T) {
|
||||
userAccessCodeList := []string{"cpm:iot:tSimCardInfo:edit"}
|
||||
|
||||
missingItem, ok := missingAccessCodeExpressionItem(
|
||||
userAccessCodeList,
|
||||
splitAccessCodeExpression("cpm:iot:tSimCardInfo:edit&cpm:agent:dept:list"),
|
||||
)
|
||||
if !ok {
|
||||
t.Fatal("expected a missing permission")
|
||||
}
|
||||
if missingItem != "cpm:agent:dept:list" {
|
||||
t.Fatalf("expected missing agent list permission, got %s", missingItem)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// package monitor
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
"github.com/shirou/gopsutil/v3/load"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
)
|
||||
|
||||
type sMonitorServer struct {
|
||||
}
|
||||
|
||||
func NewMonitorServer() *sMonitorServer {
|
||||
return &sMonitorServer{}
|
||||
}
|
||||
|
||||
var SysStartTime *gtime.Time
|
||||
var GoDiskSize string
|
||||
var SysRunDir string
|
||||
var LocalIP string
|
||||
var PublicIP string
|
||||
|
||||
func init() {
|
||||
service.RegisterMonitorServer(NewMonitorServer())
|
||||
SysStartTime = gtime.Now()
|
||||
SysRunDir, _ = os.Getwd()
|
||||
GoDiskSize = utility.DirSize(SysRunDir)
|
||||
LocalIP, _ = utility.GetLocalIP()
|
||||
PublicIP, _ = utility.GetPublicIP()
|
||||
}
|
||||
|
||||
func (l *sMonitorServer) GetGoInfo(ctx context.Context) (res *model.GoRunInfo) {
|
||||
SysRunDir, _ := os.Getwd()
|
||||
var gm runtime.MemStats
|
||||
runtime.ReadMemStats(&gm)
|
||||
res = &model.GoRunInfo{
|
||||
GoName: "Golang",
|
||||
GoOs: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
GoVersion: runtime.Version(),
|
||||
StartTime: SysStartTime.Format("YmdHis"),
|
||||
RunTime: gtime.Now().Timestamp() - SysStartTime.Timestamp(),
|
||||
RootPath: runtime.GOROOT(),
|
||||
Pwd: SysRunDir,
|
||||
Goroutine: strconv.Itoa(runtime.NumGoroutine()),
|
||||
GoMem: utility.FileSize(int64(gm.Sys)),
|
||||
GoSize: GoDiskSize,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetHostInfo 获取主机信息
|
||||
func (l *sMonitorServer) GetHostInfo(ctx context.Context) (data []byte) {
|
||||
hostInfo, _ := host.Info()
|
||||
timestamp, _ := host.BootTime()
|
||||
t := time.Unix(int64(timestamp), 0)
|
||||
tmpData := gconv.Map(hostInfo)
|
||||
tmpData["bootTime"] = t
|
||||
|
||||
tmpData["intranet_ip"] = LocalIP
|
||||
tmpData["public_ip"] = PublicIP
|
||||
data, _ = gjson.Encode(tmpData)
|
||||
return
|
||||
}
|
||||
|
||||
// GetSysLoad 获取系统负载信息
|
||||
func (l *sMonitorServer) GetSysLoad(ctx context.Context) (data []byte) {
|
||||
loadInfo, _ := load.Avg()
|
||||
data, _ = gjson.Encode(g.Map{
|
||||
"load1": loadInfo.Load1,
|
||||
"load5": loadInfo.Load5,
|
||||
"load15": loadInfo.Load15,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// GetCpuInfo 获取CPU信息
|
||||
func (l *sMonitorServer) GetCpuInfo(ctx context.Context) (data []byte) {
|
||||
var CpuInfoData model.CpuInfo
|
||||
cpus, _ := cpu.Info()
|
||||
for _, c := range cpus {
|
||||
CpuInfoData.Cores = CpuInfoData.Cores + c.Cores
|
||||
}
|
||||
CpuInfoData.Number = len(cpus)
|
||||
percent, _ := cpu.Percent(time.Second, false) //获取CPU使用率
|
||||
CpuInfoData.UsedPercent = percent
|
||||
if len(cpus) > 0 {
|
||||
CpuInfoData.ModelName = cpus[0].ModelName //CPU型号
|
||||
}
|
||||
|
||||
data, _ = json.Marshal(CpuInfoData)
|
||||
return
|
||||
}
|
||||
|
||||
// GetMemInfo 获取内存信息
|
||||
func (l *sMonitorServer) GetMemInfo(ctx context.Context) (data []byte) {
|
||||
hostInfo, _ := mem.VirtualMemory()
|
||||
tmpData := gconv.Map(hostInfo)
|
||||
var gomem runtime.MemStats
|
||||
runtime.ReadMemStats(&gomem)
|
||||
if tmpData == nil {
|
||||
tmpData = make(map[string]interface{})
|
||||
}
|
||||
tmpData["goUsed"] = gomem.Sys
|
||||
data, _ = gjson.Encode(tmpData)
|
||||
return
|
||||
}
|
||||
|
||||
// GetDiskInfo 获取磁盘信息
|
||||
func (l *sMonitorServer) GetDiskInfo(ctx context.Context) (data []byte) {
|
||||
diskUsed, _ := disk.Usage("/")
|
||||
data, _ = gjson.Encode(diskUsed)
|
||||
return
|
||||
}
|
||||
|
||||
// GetNetStatusInfo 获取网络信息
|
||||
func (l *sMonitorServer) GetNetStatusInfo(ctx context.Context) (data []byte) {
|
||||
IOCountersStat, _ := net.IOCounters(true)
|
||||
netWorkInfo := make([]model.NetWorkInfo, len(IOCountersStat))
|
||||
for i, n := range IOCountersStat {
|
||||
netWorkInfo[i].Name = n.Name
|
||||
netWorkInfo[i].Receive = n.BytesRecv
|
||||
netWorkInfo[i].Sent = n.BytesSent
|
||||
if netWorkInfo != nil && len(netWorkInfo) > i {
|
||||
netWorkInfo[i].ReceiveSpeed = n.BytesRecv - netWorkInfo[i].Receive
|
||||
netWorkInfo[i].SentSpeed = n.BytesSent - netWorkInfo[i].Sent
|
||||
}
|
||||
}
|
||||
data, _ = gjson.Encode(netWorkInfo)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/bcache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"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"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type sSysAuth struct {
|
||||
}
|
||||
|
||||
func NewSysAuth() *sSysAuth {
|
||||
return &sSysAuth{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysAuth(NewSysAuth())
|
||||
}
|
||||
|
||||
func (s *sSysAuth) Login(ctx context.Context, param *model.LoginParams) (res *model.LoginUserOut, token string, err error) {
|
||||
ip := g.RequestFromCtx(ctx).GetClientIp()
|
||||
logininfor := &model.SysLogininforAddModel{
|
||||
TenantId: param.TenantId,
|
||||
UserName: param.Username,
|
||||
ClientKey: "web",
|
||||
DeviceType: "web",
|
||||
Ipaddr: ip,
|
||||
LoginLocation: utility.GetCityByIp(ip),
|
||||
Browser: utility.GetBrowser(ctx),
|
||||
Os: utility.GetOs(ctx),
|
||||
Status: "0",
|
||||
Msg: "登录成功",
|
||||
LoginTime: gtime.Now(),
|
||||
}
|
||||
// 验证验证码
|
||||
err = service.SysCaptcha().VerifyCaptcha(ctx, param.CaptchaID, param.CaptchaValue)
|
||||
if err != nil {
|
||||
logininfor.Status = "1"
|
||||
logininfor.Msg = err.Error()
|
||||
return nil, "", err
|
||||
}
|
||||
// 获取用户信息
|
||||
user, err := service.SysUser().GetUserByUsernameAndPassword(ctx, param.TenantId, param.Username, param.Password)
|
||||
if err != nil {
|
||||
logininfor.Status = "1"
|
||||
logininfor.Msg = err.Error()
|
||||
return nil, "", err
|
||||
}
|
||||
userOut := &model.LoginUserOut{
|
||||
ID: user.UserId,
|
||||
Username: user.UserName,
|
||||
NickName: user.NickName,
|
||||
Avatar: user.Avatar,
|
||||
TenantId: user.TenantId,
|
||||
DeptId: user.DeptId,
|
||||
}
|
||||
// 构建临时的上下文
|
||||
identity := &model.Identity{
|
||||
BaseClaims: model.BaseClaims{
|
||||
ID: user.UserId,
|
||||
Username: user.UserName,
|
||||
NickName: user.NickName,
|
||||
TenantId: user.TenantId,
|
||||
DeptId: user.DeptId,
|
||||
Roles: []model.Role{},
|
||||
LoginAt: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
contextModel := new(model.Context)
|
||||
contextModel.User = identity
|
||||
|
||||
ctx = contexts.Set(ctx, contextModel)
|
||||
|
||||
// 生成token
|
||||
claims, token, err := s.GenerateToken(ctx, userOut)
|
||||
if err != nil {
|
||||
logininfor.Status = "1"
|
||||
logininfor.Msg = err.Error()
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if err = service.SysUser().UpdateLoginInfo(ctx, user.UserId, ip); err != nil {
|
||||
g.Log().Warningf(ctx, "sSysAuth.Login UpdateLoginInfo failed, userId: %d, ip: %s, error: %v", user.UserId, ip, err)
|
||||
}
|
||||
|
||||
// 保存登录日志
|
||||
go func() {
|
||||
id, err := service.SysLogininfor().AddLogininfor(ctx, logininfor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存在线列表
|
||||
service.SysUserOnline().Add(ctx, &model.SysUserOnlineAddModel{
|
||||
OnlineId: id,
|
||||
TenantId: user.TenantId,
|
||||
Uuid: claims.UUID,
|
||||
UserName: user.UserName,
|
||||
ClientKey: "web",
|
||||
DeviceType: "web",
|
||||
Ipaddr: ip,
|
||||
LoginLocation: utility.GetCityByIp(ip),
|
||||
Browser: utility.GetBrowser(ctx),
|
||||
Os: utility.GetOs(ctx),
|
||||
Token: token,
|
||||
LoginTime: gtime.Now(),
|
||||
ExpireTime: gtime.NewFromTime(claims.ExpiresAt.Time),
|
||||
})
|
||||
}()
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyUserLogin, user.UserId)
|
||||
return userOut, token, nil
|
||||
}
|
||||
|
||||
// 生成Token
|
||||
func (s *sSysAuth) GenerateToken(ctx context.Context, user *model.LoginUserOut) (claims *model.CustomClaims, token string, err error) {
|
||||
ets := g.Cfg().MustGet(ctx, "jwt.expiresTime", "7d").String()
|
||||
bts := g.Cfg().MustGet(ctx, "jwt.bufferTime", "1d").String()
|
||||
iss := g.Cfg().MustGet(ctx, "jwt.issuer", "XiuAdmin").String()
|
||||
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").String()
|
||||
et, _ := utility.ParseDuration(ets)
|
||||
bt, _ := utility.ParseDuration(bts)
|
||||
g.Log().Infof(ctx, "sSysAuth.GenerateToken 生成Token: user:%+v, et:%s, bt:%s, iss:%s, sk:%s", user, et, bt, iss, sk)
|
||||
roles := make([]model.Role, 0)
|
||||
if user.RoleIds != nil {
|
||||
for _, roleId := range user.RoleIds {
|
||||
roles = append(roles, model.Role{
|
||||
RoleId: roleId,
|
||||
DataScope: consts.SysRoleDataScopeAll,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
authorityIds, err := service.SysUser().GetUserRoleIds(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
rl, _, err := service.SysRole().List(ctx, &model.SysRoleListParam{
|
||||
PageInfo: request.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 5000,
|
||||
},
|
||||
RoleIds: authorityIds,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
for _, r := range rl {
|
||||
roles = append(roles, model.Role{
|
||||
RoleId: r.RoleId,
|
||||
DataScope: r.DataScope,
|
||||
})
|
||||
}
|
||||
}
|
||||
g.Log().Debugf(ctx, "sSysAuth.GenerateToken roles: %v", roles)
|
||||
uuidStr := user.UUID
|
||||
if uuidStr == "" {
|
||||
uuidStr = strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
}
|
||||
claims = &model.CustomClaims{
|
||||
BaseClaims: model.BaseClaims{
|
||||
UUID: uuidStr,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
NickName: user.NickName,
|
||||
TenantId: user.TenantId,
|
||||
DeptId: user.DeptId,
|
||||
Roles: roles,
|
||||
LoginAt: time.Now().Unix(),
|
||||
},
|
||||
BufferTime: int64(bt / time.Second),
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: jwt.ClaimStrings{"XiuAdmin"}, // 受众
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(et)), // 过期时间 7天 配置文件
|
||||
NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间
|
||||
Issuer: iss, // 发行人
|
||||
},
|
||||
}
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
token, err = t.SignedString([]byte(sk))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysAuth.GenerateToken token: %s, claims: %+v", token, claims)
|
||||
bcache.SetSysAuthToken(ctx, claims.BaseClaims.ID, claims.BaseClaims.UUID, token, et)
|
||||
return claims, token, nil
|
||||
}
|
||||
|
||||
// 解析Token
|
||||
func (s *sSysAuth) ParseToken(ctx context.Context, token string) (claims *model.CustomClaims, err error) {
|
||||
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").String()
|
||||
t, err := jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(sk), nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
if t.Claims.(*model.CustomClaims).ExpiresAt.Unix() < time.Now().Unix() {
|
||||
return nil, errors.New("token已过期")
|
||||
}
|
||||
baseClaims := t.Claims.(*model.CustomClaims).BaseClaims
|
||||
_, err = bcache.GetSysAuthToken(ctx, baseClaims.ID, baseClaims.UUID)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysAuth.ParseToken GetSysAuthToken failed, userId: %d, uuid: %s, error: %v", baseClaims.ID, baseClaims.UUID, err)
|
||||
return nil, err
|
||||
}
|
||||
return t.Claims.(*model.CustomClaims), nil
|
||||
}
|
||||
|
||||
// 删除Token
|
||||
func (s *sSysAuth) DeleteToken(ctx context.Context, token string) (err error) {
|
||||
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").String()
|
||||
t, err := jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(sk), nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
timeout := t.Claims.(*model.CustomClaims).ExpiresAt.Unix() - time.Now().Unix()
|
||||
if timeout <= 0 {
|
||||
return nil
|
||||
}
|
||||
err = bcache.DelSysAuthToken(ctx, t.Claims.(*model.CustomClaims).BaseClaims.ID, t.Claims.(*model.CustomClaims).BaseClaims.UUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bcache.SetSysAuthTokenReject(ctx, t.Claims.(*model.CustomClaims).BaseClaims.ID, t.Claims.(*model.CustomClaims).BaseClaims.UUID, token, time.Duration(timeout)*time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取token
|
||||
func (s *sSysAuth) GetAccessToken(ctx context.Context) (token string, err error) {
|
||||
authorization := g.RequestFromCtx(ctx).Header.Get("Authorization")
|
||||
authParam := g.RequestFromCtx(ctx).Get("access_token").String()
|
||||
if authorization == "" && authParam == "" {
|
||||
return "", gerror.NewCode(gcode.CodeNotImplemented)
|
||||
}
|
||||
token = strings.TrimPrefix(authorization, "Bearer ")
|
||||
if token == "" {
|
||||
token = authParam
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// 根据Token获取当前登录用户信息
|
||||
func (s *sSysAuth) GetCurrentUser(ctx context.Context) (claims *model.CustomClaims, err error) {
|
||||
// 获取token
|
||||
token, err := s.GetAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 解析token
|
||||
claims, err = s.ParseToken(ctx, token)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "sSysAuth.GetCurrentUser ParseToken failed, token: %s, error: %v", token, err)
|
||||
return nil, err
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// 获取用户权限码
|
||||
func (s *sSysAuth) GetUserAccessCodeList(ctx context.Context, userId int64) (accessCodeList []string, menuRoleAccessCodeList []string, err error) {
|
||||
accessCodeList = make([]string, 0)
|
||||
menuRoleAccessCodeList = make([]string, 0)
|
||||
roleDataScopeMap := make(map[int64]string)
|
||||
|
||||
// 角色权限码
|
||||
roleIds, err := service.SysUser().GetUserRoleIds(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(roleIds) > 0 {
|
||||
rl, _, err := service.SysRole().List(ctx, &model.SysRoleListParam{
|
||||
PageInfo: request.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 5000,
|
||||
},
|
||||
RoleIds: roleIds,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, r := range rl {
|
||||
accessCodeList = append(accessCodeList, consts.SysCheckPermissionRolePrefix+r.RoleKey)
|
||||
roleDataScopeMap[r.RoleId] = r.DataScope
|
||||
}
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList roleIds: %v", roleIds)
|
||||
// 菜单权限码
|
||||
// 获取角色菜单列表
|
||||
rmList, err := service.SysRole().GetRoleListMenu(ctx, roleIds)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
type roleMenuDataScopeRow struct {
|
||||
RoleId int64 `orm:"role_id"`
|
||||
MenuId int64 `orm:"menu_id"`
|
||||
DataScope string `orm:"data_scope"`
|
||||
}
|
||||
roleMenuDataScopeMap := make(map[string]string)
|
||||
if len(roleIds) > 0 {
|
||||
var roleMenuDataScopes []*roleMenuDataScopeRow
|
||||
roleMenuScopeModel := dao.SysRoleMenu.Ctx(ctx).
|
||||
Fields(
|
||||
dao.SysRoleMenu.Columns().RoleId,
|
||||
dao.SysRoleMenu.Columns().MenuId,
|
||||
dao.SysRoleMenu.Columns().DataScope,
|
||||
).
|
||||
WhereIn(dao.SysRoleMenu.Columns().RoleId, roleIds)
|
||||
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
|
||||
roleMenuScopeModel = roleMenuScopeModel.Where(dao.SysRoleMenu.Columns().TenantId, tenantId)
|
||||
}
|
||||
if err = roleMenuScopeModel.Scan(&roleMenuDataScopes); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, row := range roleMenuDataScopes {
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
roleMenuDataScopeMap[gconv.String(row.RoleId)+"_"+gconv.String(row.MenuId)] = row.DataScope
|
||||
}
|
||||
}
|
||||
menuIds := make([]int64, 0)
|
||||
for _, menu := range rmList {
|
||||
menuIds = append(menuIds, menu.MenuId)
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList menuIds: %v", menuIds)
|
||||
if len(menuIds) > 0 {
|
||||
// 权限码必须读取当前业务库,避免内存菜单库未刷新时拿不到新配置的按钮权限。
|
||||
var menuList []*entity.SysMenu
|
||||
err = dao.SysMenu.Ctx(ctx).
|
||||
WhereIn(dao.SysMenu.Columns().MenuId, menuIds).
|
||||
Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
|
||||
Order(dao.SysMenu.Columns().OrderNum).
|
||||
Scan(&menuList)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList menuList: %v", menuList)
|
||||
for _, m := range menuList {
|
||||
menuAccessCode := normalizeSysMenuAccessCode(m.Perms)
|
||||
if menuAccessCode == "" {
|
||||
continue
|
||||
}
|
||||
accessCodeList = append(accessCodeList, menuAccessCode)
|
||||
for _, r := range rmList {
|
||||
if r.MenuId == m.MenuId {
|
||||
roleDS := roleDataScopeMap[r.RoleId]
|
||||
if menuDataScope := roleMenuDataScopeMap[gconv.String(r.RoleId)+"_"+gconv.String(r.MenuId)]; menuDataScope != "" && menuDataScope != consts.SysRoleMenuDataScopeRole {
|
||||
roleDS = menuDataScope
|
||||
}
|
||||
if len(roleDS) < 1 {
|
||||
roleDS = consts.SysRoleDataScopeAll
|
||||
}
|
||||
menuRoleAccessCodeList = append(menuRoleAccessCodeList, menuAccessCode+"|"+gconv.String(r.RoleId)+"|"+roleDS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList accessCodeList: %v", accessCodeList)
|
||||
// 获取用户岗位信息
|
||||
postIds, err := service.SysUser().GetUserPostIds(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(postIds) > 0 {
|
||||
pl, _, err := service.SysPost().List(ctx, &model.SysPostListParam{
|
||||
PageInfo: request.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 5000,
|
||||
},
|
||||
PostIds: postIds,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, p := range pl {
|
||||
accessCodeList = append(accessCodeList, consts.SysCheckPermissionPostPrefix+p.PostCode)
|
||||
}
|
||||
}
|
||||
// 获取用户信息
|
||||
user, err := service.SysUser().GetUserById(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
accessCodeList = append(accessCodeList, consts.SysCheckPermissionUserPrefix+user.UserName)
|
||||
accessCodeList = append(accessCodeList, consts.SysCheckPermissionCurrentUser)
|
||||
accessCodeList = slices.Compact(accessCodeList)
|
||||
return accessCodeList, menuRoleAccessCodeList, nil
|
||||
}
|
||||
|
||||
func normalizeSysMenuAccessCode(perms string) string {
|
||||
perms = strings.TrimSpace(perms)
|
||||
if perms == "" {
|
||||
return ""
|
||||
}
|
||||
for strings.HasPrefix(perms, consts.SysCheckPermissionMenuPrefix+consts.SysCheckPermissionMenuPrefix) {
|
||||
perms = strings.TrimPrefix(perms, consts.SysCheckPermissionMenuPrefix)
|
||||
}
|
||||
if strings.HasPrefix(perms, consts.SysCheckPermissionMenuPrefix) {
|
||||
return perms
|
||||
}
|
||||
return consts.SysCheckPermissionMenuPrefix + perms
|
||||
}
|
||||
|
||||
// 根据openId登录
|
||||
func (s *sSysAuth) LoginByOpenId(ctx context.Context, social *model.SysSocialListModel) (res *model.LoginUserOut, token string, err error) {
|
||||
ip := g.RequestFromCtx(ctx).GetClientIp()
|
||||
logininfor := &model.SysLogininforAddModel{
|
||||
TenantId: social.User.TenantId,
|
||||
UserName: social.User.UserName,
|
||||
ClientKey: "web",
|
||||
DeviceType: "web",
|
||||
Ipaddr: ip,
|
||||
LoginLocation: utility.GetCityByIp(ip),
|
||||
Browser: utility.GetBrowser(ctx),
|
||||
Os: utility.GetOs(ctx),
|
||||
Status: "0",
|
||||
Msg: "登录成功",
|
||||
LoginTime: gtime.Now(),
|
||||
}
|
||||
user, err := service.SysUser().GetUserByUsername(ctx, social.User.UserName, social.User.TenantId)
|
||||
if err != nil {
|
||||
return nil, "用户不存在", err
|
||||
}
|
||||
userOut := &model.LoginUserOut{
|
||||
ID: user.UserId,
|
||||
Username: user.UserName,
|
||||
NickName: user.NickName,
|
||||
Avatar: user.Avatar,
|
||||
TenantId: user.TenantId,
|
||||
DeptId: user.DeptId,
|
||||
}
|
||||
// 构建临时的上下文
|
||||
identity := &model.Identity{
|
||||
BaseClaims: model.BaseClaims{
|
||||
ID: user.UserId,
|
||||
Username: user.UserName,
|
||||
NickName: user.NickName,
|
||||
TenantId: user.TenantId,
|
||||
DeptId: user.DeptId,
|
||||
Roles: []model.Role{},
|
||||
LoginAt: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
contextModel := new(model.Context)
|
||||
contextModel.User = identity
|
||||
|
||||
ctx = contexts.Set(ctx, contextModel)
|
||||
|
||||
// 生成token
|
||||
claims, token, err := s.GenerateToken(ctx, userOut)
|
||||
if err != nil {
|
||||
logininfor.Status = "1"
|
||||
logininfor.Msg = err.Error()
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if err = service.SysUser().UpdateLoginInfo(ctx, user.UserId, ip); err != nil {
|
||||
g.Log().Warningf(ctx, "sSysAuth.LoginByOpenId UpdateLoginInfo failed, userId: %d, ip: %s, error: %v", user.UserId, ip, err)
|
||||
}
|
||||
|
||||
// 保存登录日志
|
||||
go func() {
|
||||
id, err := service.SysLogininfor().AddLogininfor(ctx, logininfor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存在线列表
|
||||
service.SysUserOnline().Add(ctx, &model.SysUserOnlineAddModel{
|
||||
OnlineId: id,
|
||||
TenantId: user.TenantId,
|
||||
Uuid: claims.UUID,
|
||||
UserName: user.UserName,
|
||||
ClientKey: "web",
|
||||
DeviceType: "web",
|
||||
Ipaddr: ip,
|
||||
LoginLocation: utility.GetCityByIp(ip),
|
||||
Browser: utility.GetBrowser(ctx),
|
||||
Os: utility.GetOs(ctx),
|
||||
Token: token,
|
||||
LoginTime: gtime.Now(),
|
||||
ExpireTime: gtime.NewFromTime(claims.ExpiresAt.Time),
|
||||
})
|
||||
}()
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyUserLogin, user.UserId)
|
||||
return userOut, token, nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
var store = base64Captcha.NewMemoryStore(100, 120*time.Second)
|
||||
|
||||
type sSysCaptcha struct {
|
||||
}
|
||||
|
||||
func NewSysCaptcha() *sSysCaptcha {
|
||||
return &sSysCaptcha{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysCaptcha(NewSysCaptcha())
|
||||
}
|
||||
|
||||
type CaptchaConfig struct {
|
||||
KeyLong int
|
||||
ImgWidth int
|
||||
ImgHeight int
|
||||
OpenCaptcha int
|
||||
OpenCaptchaTimeout int
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
func (l *sSysCaptcha) GenerateCaptcha(ctx context.Context) (key string, image string, err error) {
|
||||
var captchaConfig CaptchaConfig
|
||||
g.Cfg().MustGet(ctx, "captcha").Struct(&captchaConfig)
|
||||
driver := base64Captcha.NewDriverDigit(captchaConfig.ImgHeight, captchaConfig.ImgWidth, captchaConfig.KeyLong, 0.7, 80)
|
||||
captcha := base64Captcha.NewCaptcha(driver, store)
|
||||
key, image, _, err = captcha.Generate()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
func (l *sSysCaptcha) VerifyCaptcha(ctx context.Context, key string, value string) (err error) {
|
||||
v := store.Get(key, false)
|
||||
if len(v) == 0 {
|
||||
return gerror.NewCode(consts.CodeCaptchaError, "验证码已失效")
|
||||
}
|
||||
ok := store.Verify(key, value, true)
|
||||
if !ok {
|
||||
err = gerror.NewCode(consts.CodeCaptchaError, "验证码错误")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysClient struct {
|
||||
}
|
||||
|
||||
func NewSysClient() *sSysClient {
|
||||
return &sSysClient{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysClient(NewSysClient())
|
||||
}
|
||||
|
||||
func (s *sSysClient) List(ctx context.Context, query *model.SysClientListParam) (items []*model.SysClientListModel, total int, err error) {
|
||||
|
||||
db := dao.SysClient.Ctx(ctx)
|
||||
if query.ClientId != "" {
|
||||
db = db.Where(dao.SysClient.Columns().ClientId, query.ClientId)
|
||||
}
|
||||
|
||||
if query.ClientKey != "" {
|
||||
db = db.Where(dao.SysClient.Columns().ClientKey, query.ClientKey)
|
||||
}
|
||||
|
||||
if query.ClientSecret != "" {
|
||||
db = db.Where(dao.SysClient.Columns().ClientSecret, query.ClientSecret)
|
||||
}
|
||||
|
||||
if query.Status != "" {
|
||||
db = db.Where(dao.SysClient.Columns().Status, query.Status)
|
||||
}
|
||||
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = db.Page(query.Page, query.PageSize).OrderAsc(dao.SysClient.Columns().Id).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
func (s *sSysClient) View(ctx context.Context, param *model.SysClientViewParam) (item *model.SysClientViewModel, err error) {
|
||||
db := dao.SysClient.Ctx(ctx)
|
||||
if param.Id != 0 {
|
||||
db = db.Where(dao.SysClient.Columns().Id, param.Id)
|
||||
} else if param.ClientId != "" {
|
||||
db = db.Where(dao.SysClient.Columns().ClientId, param.ClientId)
|
||||
} else {
|
||||
return nil, errors.New("未填写查询条件")
|
||||
}
|
||||
err = db.Scan(&item)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysClient) Add(ctx context.Context, param *model.SysClientAddParam) (output *model.SysClientAddModel, err error) {
|
||||
data := &do.SysClient{}
|
||||
gconv.Struct(param, data)
|
||||
data.ClientId = uuid32.S()
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
|
||||
db := dao.SysClient.Ctx(ctx)
|
||||
id, err := db.Data(data).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output = &model.SysClientAddModel{
|
||||
Id: id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysClient) Edit(ctx context.Context, param *model.SysClientEditParam) (output *model.SysClientEditModel, err error) {
|
||||
data := &do.SysClient{}
|
||||
gconv.Struct(param, data)
|
||||
data.ClientId = nil
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
|
||||
db := dao.SysClient.Ctx(ctx)
|
||||
_, err = db.Data(data).OmitNil().Where(dao.SysClient.Columns().Id, param.Id).Update()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output = &model.SysClientEditModel{
|
||||
Id: param.Id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysClient) Delete(ctx context.Context, param *model.SysClientDeleteParam) (output *model.SysClientDeleteModel, err error) {
|
||||
db := dao.SysClient.Ctx(ctx)
|
||||
data := &do.SysClient{}
|
||||
data.DeletedAt = gtime.Now()
|
||||
data.DeletedBy = contexts.GetUserId(ctx)
|
||||
_, err = db.Data(data).OmitNil().Where(dao.SysClient.Columns().Id, param.Ids).Update()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output = &model.SysClientDeleteModel{
|
||||
Ids: param.Ids,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysClient) Status(ctx context.Context, param *model.SysClientStatusParam) (output *model.SysClientStatusModel, err error) {
|
||||
db := dao.SysClient.Ctx(ctx)
|
||||
data := &do.SysClient{}
|
||||
gconv.Struct(param, data)
|
||||
data.Id = nil
|
||||
_, err = db.Data(data).OmitNil().Where(dao.SysClient.Columns().Id, param.Id).Update()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output = &model.SysClientStatusModel{
|
||||
Id: param.Id,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysConfig struct {
|
||||
}
|
||||
|
||||
func NewSysConfig() *sSysConfig {
|
||||
return &sSysConfig{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysConfig(NewSysConfig())
|
||||
}
|
||||
|
||||
func (l *sSysConfig) Model(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.SysConfig.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (l *sSysConfig) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysConfig.Table()), option...)
|
||||
}
|
||||
|
||||
func (s *sSysConfig) List(ctx context.Context, param *model.SysConfigListParam) (items []*model.SysConfigListModel, total int, err error) {
|
||||
m := s.ModelQuery(ctx)
|
||||
|
||||
if param.ConfigName != "" {
|
||||
m = m.WhereLike(dao.SysConfig.Columns().ConfigName, "%"+param.ConfigName+"%")
|
||||
}
|
||||
|
||||
if param.ConfigKey != "" {
|
||||
m = m.WhereLike(dao.SysConfig.Columns().ConfigKey, "%"+param.ConfigKey+"%")
|
||||
}
|
||||
|
||||
if param.ConfigType != "" {
|
||||
m = m.Where(dao.SysConfig.Columns().ConfigType, param.ConfigType)
|
||||
}
|
||||
|
||||
if param.ConfigValue != "" {
|
||||
m = m.WhereLike(dao.SysConfig.Columns().ConfigValue, "%"+param.ConfigValue+"%")
|
||||
}
|
||||
|
||||
if len(param.CreatedAt) > 0 {
|
||||
startTime := gtime.NewFromStr(param.CreatedAt[0])
|
||||
endTime := gtime.NewFromStr(param.CreatedAt[1])
|
||||
m = m.WhereBetween(dao.SysConfig.Columns().CreatedAt, startTime, endTime.EndOfDay())
|
||||
}
|
||||
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = m.Page(param.Page, param.PageSize).Order(dao.SysConfig.Columns().ConfigId, "ASC").Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysConfig) Add(ctx context.Context, param *model.SysConfigAddParam) (output *model.SysConfigAddModel, err error) {
|
||||
if param == nil || param.ConfigName == "" {
|
||||
return nil, errors.New("配置名称不能为空")
|
||||
}
|
||||
m := s.Model(ctx)
|
||||
|
||||
data := &do.SysConfig{}
|
||||
gconv.Struct(param, data)
|
||||
data.TenantId = contexts.GetTenantId(ctx)
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
|
||||
lastInsertId, err := m.Data(data).InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output = &model.SysConfigAddModel{
|
||||
ConfigId: lastInsertId,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigCreate, lastInsertId, data.TenantId, data.ConfigKey)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysConfig) Edit(ctx context.Context, param *model.SysConfigEditParam) (output *model.SysConfigEditModel, err error) {
|
||||
if param == nil || param.ConfigId == 0 {
|
||||
return nil, errors.New("配置ID不能为空")
|
||||
}
|
||||
|
||||
data := &do.SysConfig{}
|
||||
gconv.Struct(param, data)
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.ConfigId = nil
|
||||
|
||||
m := s.Model(ctx)
|
||||
_, err = m.Data(data).Where(dao.SysConfig.Columns().ConfigId, param.ConfigId).Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output = &model.SysConfigEditModel{
|
||||
ConfigId: param.ConfigId,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigUpdate, param.ConfigId, contexts.GetTenantId(ctx), data.ConfigKey)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysConfig) Delete(ctx context.Context, param *model.SysConfigDeleteParam) (output *model.SysConfigDeleteModel, err error) {
|
||||
m := s.Model(ctx)
|
||||
|
||||
if len(param.ConfigIds) == 0 {
|
||||
return nil, errors.New("请选择要删除的配置")
|
||||
}
|
||||
m = m.WhereIn(dao.SysConfig.Columns().ConfigId, param.ConfigIds)
|
||||
data := make([]entity.SysConfig, 0)
|
||||
err = m.Scan(&data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range data {
|
||||
if v.ConfigType == "Y" {
|
||||
return nil, errors.New("系统内置配置不能删除")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = m.Delete()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigDelete, param.ConfigIds, contexts.GetTenantId(ctx))
|
||||
return &model.SysConfigDeleteModel{
|
||||
ConfigIds: param.ConfigIds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sSysConfig) View(ctx context.Context, param *model.SysConfigViewParam) (output *model.SysConfigViewModel, err error) {
|
||||
m := s.ModelQuery(ctx)
|
||||
|
||||
err = m.Where(dao.SysConfig.Columns().ConfigId, param.ConfigId).Scan(&output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysConfig) GetConfigByKey(ctx context.Context, configKey string) (config *entity.SysConfig, err error) {
|
||||
m := s.ModelQuery(ctx)
|
||||
err = m.Where(dao.SysConfig.Columns().ConfigKey, configKey).Scan(&config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/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"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysDept struct {
|
||||
}
|
||||
|
||||
func NewSysDept() *sSysDept {
|
||||
return &sSysDept{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysDept(NewSysDept())
|
||||
}
|
||||
|
||||
func (l *sSysDept) 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.SysDept.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (l *sSysDept) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysDept.Table()), option...)
|
||||
}
|
||||
|
||||
func (l *sSysDept) GetDeptList(ctx context.Context, query model.SysDeptListParam) (items []*model.SysDeptListModel, total int, err error) {
|
||||
m := l.ModelQuery(ctx)
|
||||
if query.DeptName != "" {
|
||||
m = m.WhereLike(dao.SysDept.Columns().DeptName, "%"+query.DeptName+"%")
|
||||
}
|
||||
if query.Status != "" {
|
||||
m = m.Where(dao.SysDept.Columns().Status, query.Status)
|
||||
}
|
||||
if query.DeptType != nil {
|
||||
m = m.Where(dao.SysDept.Columns().DeptType, *query.DeptType)
|
||||
}
|
||||
// 排序:按照显示顺序OrderNum从小到大排序,如果相同则按照部门ID从小到大排序
|
||||
err = m.Page(1, 5000).Order(dao.SysDept.Columns().OrderNum, "ASC").Order(dao.SysDept.Columns().DeptId, "ASC").Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total = len(items)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysDept) GetDeptById(ctx context.Context, id int64) (dept *model.SysDeptViewModel, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
|
||||
}
|
||||
err = l.ModelQuery(ctx).Where(dao.SysDept.Columns().DeptId, id).Scan(&dept)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dept, nil
|
||||
}
|
||||
|
||||
// 构建树结构
|
||||
func (l *sSysDept) DeptTree(ctx context.Context, parentDept *model.SysDeptTreeModel, deptList []*model.SysDeptListModel, ancestors string) (data []*model.SysDeptTreeModel, err error) {
|
||||
data = make([]*model.SysDeptTreeModel, 0)
|
||||
for _, dept := range deptList {
|
||||
pId := int64(0)
|
||||
if parentDept != nil {
|
||||
pId = parentDept.DeptId
|
||||
}
|
||||
if dept.ParentId == pId {
|
||||
item := &model.SysDeptTreeModel{
|
||||
DeptId: dept.DeptId,
|
||||
ParentId: dept.ParentId,
|
||||
DeptName: dept.DeptName,
|
||||
Key: fmt.Sprintf("%d", dept.DeptId),
|
||||
Children: nil,
|
||||
Ancestors: ancestors,
|
||||
}
|
||||
subDept, err := l.DeptTree(ctx, item, deptList, ancestors+","+fmt.Sprintf("%d", dept.DeptId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Children = subDept
|
||||
data = append(data, item)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysDept) GetDeptTree(ctx context.Context, query model.SysDeptTreeParam) (items []*model.SysDeptTreeModel, err error) {
|
||||
depts, _, err := l.GetDeptList(ctx, model.SysDeptListParam{
|
||||
DeptType: query.DeptType,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(depts) == 0 {
|
||||
return make([]*model.SysDeptTreeModel, 0), nil
|
||||
}
|
||||
//找出列表中的顶级部门(可能包含多个)
|
||||
topDepts := make([]*model.SysDeptTreeModel, 0)
|
||||
deptMap := make(map[int64]*model.SysDeptTreeModel)
|
||||
for _, dept := range depts {
|
||||
deptMap[dept.DeptId] = &model.SysDeptTreeModel{
|
||||
DeptId: dept.DeptId,
|
||||
ParentId: dept.ParentId,
|
||||
DeptName: dept.DeptName,
|
||||
Key: fmt.Sprintf("%d", dept.DeptId),
|
||||
Children: nil,
|
||||
Ancestors: "0",
|
||||
}
|
||||
}
|
||||
|
||||
for _, dept := range depts {
|
||||
if _, ok := deptMap[dept.ParentId]; !ok {
|
||||
topDepts = append(topDepts, deptMap[dept.DeptId])
|
||||
}
|
||||
}
|
||||
|
||||
for _, dept := range topDepts {
|
||||
children, err := l.DeptTree(ctx, dept, depts, fmt.Sprintf("%d", dept.DeptId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dept.Children = children
|
||||
}
|
||||
|
||||
return topDepts, nil
|
||||
}
|
||||
|
||||
// 递归构建结构
|
||||
func (l *sSysDept) RecursionDeptIds(ctx context.Context, parentId int64, deptList []*model.SysDeptListModel, data *[]int64) (err error) {
|
||||
for _, dept := range deptList {
|
||||
if dept.ParentId == parentId {
|
||||
*data = append(*data, dept.DeptId)
|
||||
err := l.RecursionDeptIds(ctx, dept.DeptId, deptList, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 根据父部门id获取部门列表
|
||||
func (l *sSysDept) GetDeptIdsByParentId(ctx context.Context, parentId int64) (ids []int64, err error) {
|
||||
depts, _, err := l.GetDeptList(ctx, model.SysDeptListParam{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = make([]int64, 0)
|
||||
err = l.RecursionDeptIds(ctx, parentId, depts, &ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// g.Log().Infof(ctx, "sSysDept.GetDeptIdsByParentId 获取部门列表: ids:%+v", ids)
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (l *sSysDept) AddDept(ctx context.Context, dept *model.SysDeptAddModel) (deptId int64, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
|
||||
}
|
||||
if dept.DeptType == 1 {
|
||||
isCompany, err := l.ValidateParentIsCompany(ctx, dept.ParentId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysDept.AddDept ValidateParentIsCompany err: %v, parentId:%d, dept:%+v", err, dept.ParentId, dept)
|
||||
return 0, err
|
||||
}
|
||||
if !isCompany {
|
||||
return 0, gerror.New("上级部门必须是公司")
|
||||
}
|
||||
}
|
||||
|
||||
data := do.SysDept{}
|
||||
gconv.Struct(dept, &data)
|
||||
data.CreatedDept = user.DeptId
|
||||
data.CreatedBy = user.ID
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.UpdatedBy = user.ID
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.TenantId = user.TenantId
|
||||
|
||||
deptId, err = l.Model(ctx).Data(data).InsertAndGetId()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptCreate, deptId)
|
||||
// 刷新部门祖先
|
||||
err = l.RefreshDeptAncestors(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return deptId, nil
|
||||
}
|
||||
|
||||
func (l *sSysDept) EditDept(ctx context.Context, dept *model.SysDeptEditModel) (deptId int64, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return dept.DeptId, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
|
||||
}
|
||||
// 查询现有数据
|
||||
existingDept, err := l.GetDeptById(ctx, dept.DeptId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysDept.EditDept GetDeptById err: %v, deptId:%d, dept:%+v", err, dept.DeptId, gconv.String(dept))
|
||||
return 0, err
|
||||
}
|
||||
if existingDept == nil || existingDept.DeptId == 0 || existingDept.DeptId != dept.DeptId {
|
||||
g.Log().Warningf(ctx, "sSysDept.EditDept GetDeptById err: %v, deptId:%d, dept:%+v", err, dept.DeptId, gconv.String(dept))
|
||||
return 0, gerror.New("部门不存在")
|
||||
}
|
||||
newParentId := existingDept.ParentId
|
||||
if dept.ParentId != nil {
|
||||
newParentId = *dept.ParentId
|
||||
}
|
||||
if dept.DeptType != nil && *dept.DeptType == 1 {
|
||||
isCompany, err := l.ValidateParentIsCompany(ctx, newParentId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !isCompany {
|
||||
return 0, gerror.New("上级组织必须是公司")
|
||||
}
|
||||
}
|
||||
|
||||
if dept.DeptType != nil && *dept.DeptType != existingDept.DeptType {
|
||||
if existingDept.DeptType == 1 {
|
||||
return 0, gerror.New("组织类型不能修改")
|
||||
}
|
||||
}
|
||||
|
||||
data := do.SysDept{}
|
||||
gconv.Struct(dept, &data)
|
||||
data.UpdatedBy = user.ID
|
||||
data.UpdatedAt = gtime.Now()
|
||||
_, err = l.Model(ctx).Data(data).Where(dao.SysDept.Columns().DeptId, dept.DeptId).Update()
|
||||
if err != nil {
|
||||
return dept.DeptId, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptUpdate, dept.DeptId)
|
||||
// 刷新部门祖先
|
||||
err = l.RefreshDeptAncestors(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return dept.DeptId, nil
|
||||
}
|
||||
|
||||
func (l *sSysDept) DeleteDept(ctx context.Context, dept *model.SysDeptDeleteModel) (deptId int64, err error) {
|
||||
count, err := l.ModelQuery(ctx).Where(dao.SysDept.Columns().ParentId, dept.DeptId).Count()
|
||||
if err != nil {
|
||||
return dept.DeptId, err
|
||||
}
|
||||
if count > 0 {
|
||||
return dept.DeptId, gerror.NewCode(consts.CodeDeptHasSub, "该部门下有子部门,不能删除")
|
||||
}
|
||||
|
||||
// 部门下有用户,不能删除
|
||||
userList, _, err := service.SysUser().List(ctx, &request.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 100,
|
||||
}, &model.UserListParam{
|
||||
DeptId: dept.DeptId,
|
||||
})
|
||||
if err != nil {
|
||||
return dept.DeptId, err
|
||||
}
|
||||
if len(userList) > 0 {
|
||||
return dept.DeptId, gerror.New("该部门下有用户,不能删除")
|
||||
}
|
||||
|
||||
_, err = l.Model(ctx).Where(dao.SysDept.Columns().DeptId, dept.DeptId).Delete()
|
||||
if err != nil {
|
||||
return dept.DeptId, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptDelete, dept.DeptId)
|
||||
return dept.DeptId, nil
|
||||
}
|
||||
|
||||
// 刷新部门 ancestors
|
||||
func (l *sSysDept) RefreshDeptAncestors(ctx context.Context) (err error) {
|
||||
var depts []*model.SysDeptViewModel
|
||||
err = l.ModelQuery(ctx, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: false,
|
||||
}).Scan(&depts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, dept := range depts {
|
||||
ancestors := ""
|
||||
l.GetParentIDAncestors(ctx, depts, dept.ParentId, &ancestors)
|
||||
|
||||
if ancestors != dept.Ancestors {
|
||||
_, err = l.Model(ctx).Data(do.SysDept{
|
||||
Ancestors: ancestors,
|
||||
}).Where(dao.SysDept.Columns().DeptId, dept.DeptId).Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptUpdate, dept.DeptId)
|
||||
g.Log().Infof(ctx, "sSysDept.RefreshDeptAncestors 刷新部门祖先: deptId:%d, ancestors:%s", dept.DeptId, ancestors)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *sSysDept) GetParentIDAncestors(ctx context.Context, depts []*model.SysDeptViewModel, printId int64, ancestors *string) (err error) {
|
||||
for _, dept := range depts {
|
||||
if dept.DeptId == printId {
|
||||
*ancestors = fmt.Sprintf("%d", dept.DeptId) + "," + *ancestors
|
||||
l.GetParentIDAncestors(ctx, depts, dept.ParentId, ancestors)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
*ancestors = "0," + *ancestors
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *sSysDept) GetDeptListByIds(ctx context.Context, ids []int64) (depts []*model.SysDeptListModel, err error) {
|
||||
err = l.ModelQuery(ctx).WhereIn(dao.SysDept.Columns().DeptId, ids).Scan(&depts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return depts, nil
|
||||
}
|
||||
|
||||
// 验证上级是否是公司
|
||||
func (l *sSysDept) ValidateParentIsCompany(ctx context.Context, parentId int64) (isCompany bool, err error) {
|
||||
dept, err := l.GetDeptById(ctx, parentId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return dept.DeptType == 1 || dept.ParentId == 0, nil
|
||||
}
|
||||
|
||||
// 获取上级公司名称
|
||||
func (l *sSysDept) getParentCompanyInfo(ctx context.Context, depts []*model.SysDeptListModel, deptId int64) *model.SysDeptListModel {
|
||||
var topDept *model.SysDeptListModel
|
||||
for _, dept := range depts {
|
||||
if dept.ParentId == 0 {
|
||||
topDept = dept
|
||||
}
|
||||
if dept.DeptId == deptId {
|
||||
if dept.ParentId == 0 {
|
||||
return dept
|
||||
}
|
||||
if dept.DeptType == 1 {
|
||||
return dept
|
||||
}
|
||||
return l.getParentCompanyInfo(ctx, depts, dept.ParentId)
|
||||
}
|
||||
}
|
||||
return topDept
|
||||
}
|
||||
|
||||
// 获取组织公司Map
|
||||
func (l *sSysDept) GetDeptCompanyMap(ctx context.Context) (map[int64]string, error) {
|
||||
depts, _, err := l.GetDeptList(ctx, model.SysDeptListParam{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
companyMap := make(map[int64]string)
|
||||
for _, dept := range depts {
|
||||
if dept.ParentId == 0 {
|
||||
companyMap[0] = dept.DeptName
|
||||
}
|
||||
if dept.DeptType == 1 {
|
||||
companyMap[dept.DeptId] = dept.DeptName
|
||||
} else if dept.ParentId == 0 {
|
||||
companyMap[dept.DeptId] = dept.DeptName
|
||||
} else {
|
||||
parentDept := l.getParentCompanyInfo(ctx, depts, dept.DeptId)
|
||||
if parentDept != nil {
|
||||
companyMap[dept.DeptId] = parentDept.DeptName
|
||||
} else {
|
||||
companyMap[dept.DeptId] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
return companyMap, nil
|
||||
}
|
||||
|
||||
// 获取上级公司组织信息
|
||||
func (l *sSysDept) GetParentCompanyInfo(ctx context.Context, deptId int64) *model.SysDeptListModel {
|
||||
depts, _, err := l.GetDeptList(ctx, model.SysDeptListParam{})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysDept.GetParentCompanyInfo GetDeptList err: %v, deptId:%d", err, deptId)
|
||||
return nil
|
||||
}
|
||||
return l.getParentCompanyInfo(ctx, depts, deptId)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// package system_test
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"testing"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 数据库只读,不执行
|
||||
// g.DB().SetDryRun(true)
|
||||
g.DB().SetDebug(true)
|
||||
testing.Init()
|
||||
}
|
||||
|
||||
func TestSysDept_RefreshDeptAncestors(t *testing.T) {
|
||||
|
||||
// 设置上下文用户身份为用户
|
||||
ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
|
||||
// 为了测试只设置了hook中需要用到的数据
|
||||
User: &model.Identity{
|
||||
BaseClaims: model.BaseClaims{
|
||||
ID: 1,
|
||||
TenantId: "000000",
|
||||
DeptId: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := service.SysDept().RefreshDeptAncestors(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println("RefreshDeptAncestors success")
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysDictData struct{}
|
||||
|
||||
func NewSysDictData() *sSysDictData {
|
||||
return &sSysDictData{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysDictData(NewSysDictData())
|
||||
}
|
||||
|
||||
func (l *sSysDictData) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: false,
|
||||
FilterAuth: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.SysDictData.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (l *sSysDictData) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: false,
|
||||
FilterAuth: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysDictData.Table()), option...)
|
||||
}
|
||||
|
||||
func (s *sSysDictData) List(ctx context.Context, param *model.SysDictDataListParam) (items []model.SysDictDataListModel, total int, err error) {
|
||||
typeData := &model.SysDictTypeViewModel{}
|
||||
// 获取字典类型
|
||||
if param.DictType == "" && param.DictId != 0 {
|
||||
typeData, err = service.SysDictType().View(ctx, &model.SysDictTypeViewParam{
|
||||
DictId: param.DictId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if typeData == nil {
|
||||
return nil, 0, gerror.New("字典类型不存在")
|
||||
}
|
||||
param.DictType = typeData.DictType
|
||||
}
|
||||
if param.DictType == "" {
|
||||
return nil, 0, gerror.New("字典类型不能为空")
|
||||
}
|
||||
// 获取字典数据
|
||||
items = make([]model.SysDictDataListModel, 0)
|
||||
m := s.ModelQuery(ctx).Where(dao.SysDictData.Columns().DictType, param.DictType)
|
||||
if param.DictValue != "" {
|
||||
m = m.Where(dao.SysDictData.Columns().DictValue, param.DictValue)
|
||||
}
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = m.Order(dao.SysDictData.Columns().DictSort, "ASC").Page(param.Page, param.PageSize).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysDictData) View(ctx context.Context, param *model.SysDictDataViewParam) (data *model.SysDictDataViewModel, err error) {
|
||||
if param.DictCode == 0 {
|
||||
return nil, gerror.New("字典编码不能为空")
|
||||
}
|
||||
|
||||
m := s.ModelQuery(ctx)
|
||||
if param.DictCode != 0 {
|
||||
m = m.Where(dao.SysDictData.Columns().DictCode, param.DictCode)
|
||||
}
|
||||
err = m.Scan(&data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysDictData) Add(ctx context.Context, param *model.SysDictDataAddParam) (output *model.SysDictDataAddModel, err error) {
|
||||
m := s.Model(ctx)
|
||||
data := do.SysDictData{}
|
||||
gconv.Struct(param, &data)
|
||||
// 通过 DictType 查询字典类型列表
|
||||
listParam := &model.SysDictTypeListParam{
|
||||
DictType: param.DictType,
|
||||
}
|
||||
typeList, _, err := service.SysDictType().List(ctx, listParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(typeList) == 0 {
|
||||
return nil, gerror.New("字典类型不存在")
|
||||
}
|
||||
isSys := typeList[0].IsSys
|
||||
if isSys == "0" && !contexts.IsSuperAdmin(ctx) {
|
||||
return nil, gerror.New("非超级管理员不能添加系统内置字典数据")
|
||||
}
|
||||
data.TenantId = contexts.GetTenantId(ctx)
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
m = m.Data(data).OmitNil()
|
||||
output = &model.SysDictDataAddModel{}
|
||||
output.DictCode, err = m.InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictDataCreate, output.DictCode)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysDictData) Edit(ctx context.Context, param *model.SysDictDataEditParam) (output *model.SysDictDataEditModel, err error) {
|
||||
if param.DictCode == 0 {
|
||||
return nil, gerror.New("字典编码不能为空")
|
||||
}
|
||||
m := s.Model(ctx)
|
||||
data := do.SysDictData{}
|
||||
gconv.Struct(param, &data)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
m = m.Data(data).OmitNil().Where(dao.SysDictData.Columns().DictCode, param.DictCode)
|
||||
output = &model.SysDictDataEditModel{
|
||||
DictCode: param.DictCode,
|
||||
}
|
||||
_, err = m.Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictDataUpdate, param.DictCode)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysDictData) Delete(ctx context.Context, param *model.SysDictDataDeleteParam) (output *model.SysDictDataDeleteModel, err error) {
|
||||
if len(param.DictCodes) == 0 {
|
||||
return nil, gerror.New("字典编码不能为空")
|
||||
}
|
||||
|
||||
// 检查要删除的字典数据所属的字典类型是否为系统内置类型
|
||||
if !contexts.IsSuperAdmin(ctx) {
|
||||
// 查询字典数据所属的字典类型
|
||||
var dictTypes []string
|
||||
err = dao.SysDictData.Ctx(ctx).
|
||||
Fields(dao.SysDictData.Columns().DictType).
|
||||
WhereIn(dao.SysDictData.Columns().DictCode, param.DictCodes).
|
||||
Distinct().
|
||||
Scan(&dictTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查这些字典类型是否有系统内置类型
|
||||
var systemTypeCount int
|
||||
systemTypeCount, err = dao.SysDictType.Ctx(ctx).
|
||||
WhereIn(dao.SysDictType.Columns().DictType, dictTypes).
|
||||
Where(dao.SysDictType.Columns().IsSys, "0").
|
||||
Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if systemTypeCount > 0 {
|
||||
return nil, gerror.New("系统内置字典数据不能删除,请联系管理员")
|
||||
}
|
||||
}
|
||||
|
||||
m := s.Model(ctx)
|
||||
m = m.WhereIn(dao.SysDictData.Columns().DictCode, param.DictCodes)
|
||||
_, err = m.Delete()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysDictDataDeleteModel{
|
||||
DictCodes: param.DictCodes,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictDataDelete, param.DictCodes)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysDictData) GetDictLabel(ctx context.Context, dictType string, dictCode string) string {
|
||||
dictData, _, err := s.List(ctx, &model.SysDictDataListParam{
|
||||
DictType: dictType,
|
||||
DictValue: dictCode,
|
||||
})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if len(dictData) > 0 {
|
||||
return dictData[0].DictLabel
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *sSysDictData) GetDictListByTypes(ctx context.Context, dictTypes []string) (dictDataList []model.SysDictDataListModel, err error) {
|
||||
dictDataList = make([]model.SysDictDataListModel, 0)
|
||||
m := s.ModelQuery(ctx).WhereIn(dao.SysDictData.Columns().DictType, dictTypes)
|
||||
err = m.Scan(&dictDataList)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysDictType struct{}
|
||||
|
||||
func NewSysDictType() *sSysDictType {
|
||||
return &sSysDictType{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysDictType(NewSysDictType())
|
||||
}
|
||||
|
||||
func (l *sSysDictType) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
model := dao.SysDictType.Ctx(ctx)
|
||||
model = model.Where("is_sys = '0' OR tenant_id = ?", contexts.GetTenantId(ctx))
|
||||
return model
|
||||
}
|
||||
|
||||
func (l *sSysDictType) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
model := service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysDictType.Table())
|
||||
model = model.Where("is_sys = '0' OR tenant_id = ?", contexts.GetTenantId(ctx))
|
||||
return model
|
||||
}
|
||||
|
||||
func (s *sSysDictType) List(ctx context.Context, param *model.SysDictTypeListParam) (items []model.SysDictTypeListModel, total int, err error) {
|
||||
m := s.ModelQuery(ctx)
|
||||
if param.DictName != "" {
|
||||
m = m.WhereLike(dao.SysDictType.Columns().DictName, "%"+param.DictName+"%")
|
||||
}
|
||||
if param.DictType != "" {
|
||||
m = m.WhereLike(dao.SysDictType.Columns().DictType, "%"+param.DictType+"%")
|
||||
}
|
||||
if param.IsSys != "" {
|
||||
m = m.Where(dao.SysDictType.Columns().IsSys, param.IsSys)
|
||||
}
|
||||
if len(param.CreatedAt) == 2 {
|
||||
createdAt := gtime.NewFromStr(param.CreatedAt[0])
|
||||
createdAtEnd := gtime.NewFromStr(param.CreatedAt[1])
|
||||
m = m.WhereBetween(dao.SysDictType.Columns().CreatedAt, createdAt, createdAtEnd.EndOfDay())
|
||||
}
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = m.Order(dao.SysDictType.Columns().DictId, "DESC").Page(param.Page, param.PageSize).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysDictType) View(ctx context.Context, param *model.SysDictTypeViewParam) (data *model.SysDictTypeViewModel, err error) {
|
||||
m := s.ModelQuery(ctx)
|
||||
|
||||
if param.DictId != 0 {
|
||||
m = m.Where(dao.SysDictType.Columns().DictId, param.DictId)
|
||||
} else if param.DictType != "" {
|
||||
m = m.Where(dao.SysDictType.Columns().DictType, param.DictType)
|
||||
} else {
|
||||
return nil, errors.New("字典类型ID不能为空")
|
||||
}
|
||||
err = m.Scan(&data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysDictType) Add(ctx context.Context, param *model.SysDictTypeAddParam) (output *model.SysDictTypeAddModel, err error) {
|
||||
// 如果设置为系统字典类型,需要检查权限
|
||||
if param.IsSys == "0" {
|
||||
if !contexts.IsSuperAdmin(ctx) {
|
||||
return nil, errors.New("非超级管理员不能添加系统字典类型")
|
||||
}
|
||||
}
|
||||
m := s.Model(ctx)
|
||||
data := do.SysDictType{}
|
||||
gconv.Struct(param, &data)
|
||||
data.TenantId = contexts.GetTenantId(ctx)
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
m = m.Data(data).OmitNil()
|
||||
output = &model.SysDictTypeAddModel{}
|
||||
output.DictId, err = m.InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictTypeCreate, output.DictId)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysDictType) Edit(ctx context.Context, param *model.SysDictTypeEditParam) (output *model.SysDictTypeEditModel, err error) {
|
||||
if param.DictId == 0 {
|
||||
g.Log().Errorf(ctx, "字典类型ID不能为空,param:%+v", param)
|
||||
return nil, errors.New("字典类型ID不能为空")
|
||||
}
|
||||
|
||||
// 如果设置为系统字典类型,需要检查权限
|
||||
if param.IsSys == "0" {
|
||||
if !contexts.IsSuperAdmin(ctx) {
|
||||
return nil, errors.New("非超级管理员不能修改为系统字典类型")
|
||||
}
|
||||
}
|
||||
|
||||
m := s.Model(ctx)
|
||||
data := do.SysDictType{}
|
||||
gconv.Struct(param, &data)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
m = m.Data(data).OmitNil().Where(dao.SysDictType.Columns().DictId, param.DictId)
|
||||
output = &model.SysDictTypeEditModel{
|
||||
DictId: param.DictId,
|
||||
}
|
||||
_, err = m.Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictTypeUpdate, param.DictId)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysDictType) Delete(ctx context.Context, param *model.SysDictTypeDeleteParam) (output *model.SysDictTypeDeleteModel, err error) {
|
||||
if len(param.DictIds) == 0 {
|
||||
return nil, errors.New("字典类型ID不能为空")
|
||||
}
|
||||
|
||||
// 检查是否有系统内置字典类型
|
||||
var systemDictTypes []entity.SysDictType
|
||||
err = s.Model(ctx).WhereIn(dao.SysDictType.Columns().DictId, param.DictIds).
|
||||
Where(dao.SysDictType.Columns().IsSys, "0").
|
||||
Scan(&systemDictTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果有系统内置字典类型,检查当前用户权限
|
||||
if len(systemDictTypes) > 0 {
|
||||
if !contexts.IsSuperAdmin(ctx) {
|
||||
var dictNames []string
|
||||
for _, dict := range systemDictTypes {
|
||||
dictNames = append(dictNames, dict.DictName)
|
||||
}
|
||||
return nil, fmt.Errorf("系统内置字典类型 [%s] 只有超级管理员可以删除", strings.Join(dictNames, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
m := s.Model(ctx)
|
||||
m = m.WhereIn(dao.SysDictType.Columns().DictId, param.DictIds)
|
||||
_, err = m.Delete()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysDictTypeDeleteModel{
|
||||
DictIds: param.DictIds,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictTypeDelete, param.DictIds)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"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"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/tasks"
|
||||
)
|
||||
|
||||
func init() {
|
||||
service.RegisterSysJob(NewSysJob())
|
||||
}
|
||||
|
||||
type sSysJob struct{}
|
||||
|
||||
func NewSysJob() *sSysJob {
|
||||
return &sSysJob{}
|
||||
}
|
||||
|
||||
func (s *sSysJob) 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.SysJob.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (c *sSysJob) List(ctx context.Context, query *model.SysJobListParam, pageInfo *request.PageInfo) (Data []*model.SysJobListModel, total int, err error) {
|
||||
m := c.Model(ctx)
|
||||
if query.JobName != "" {
|
||||
m = m.WhereLike(dao.SysJob.Columns().JobName, "%"+strings.Trim(query.JobName, " ")+"%")
|
||||
}
|
||||
if query.JobGroup != "" {
|
||||
m = m.Where(dao.SysJob.Columns().JobGroup, query.JobGroup)
|
||||
}
|
||||
if query.Status != "" {
|
||||
m = m.Where(dao.SysJob.Columns().Status, query.Status)
|
||||
}
|
||||
t, err := m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
Data = make([]*model.SysJobListModel, 0)
|
||||
if err := m.Page(pageInfo.Page, pageInfo.PageSize).Order(dao.SysJob.Columns().JobId, "DESC").Scan(&Data); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return Data, t, nil
|
||||
}
|
||||
|
||||
func (c *sSysJob) View(ctx context.Context, jobId int64) (Data *model.SysJobViewModel, err error) {
|
||||
m := c.Model(ctx)
|
||||
Data = &model.SysJobViewModel{}
|
||||
if err := m.Where(dao.SysJob.Columns().JobId, jobId).Scan(&Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Data, nil
|
||||
}
|
||||
|
||||
func (c *sSysJob) Add(ctx context.Context, jobAdd *model.SysJobAddModel) (LastInsertId int64, err error) {
|
||||
claims := contexts.GetUser(ctx)
|
||||
if claims == nil {
|
||||
return 0, gerror.NewCode(gcode.CodeMissingParameter, "用户信息不存在")
|
||||
}
|
||||
|
||||
//获取task目录下是否绑定对应的方法
|
||||
_, exist := tasks.TasksInstance(ctx).CheckFuncName(jobAdd.InvokeTarget)
|
||||
if !exist {
|
||||
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", jobAdd.InvokeTarget)
|
||||
return 0, gerror.New(errInfo)
|
||||
}
|
||||
|
||||
jobAdd.CreatedDept = claims.BaseClaims.DeptId
|
||||
jobAdd.CreatedAt = gtime.Now()
|
||||
jobAdd.CreatedBy = claims.BaseClaims.ID
|
||||
jobAdd.UpdatedAt = gtime.Now()
|
||||
jobAdd.UpdatedBy = claims.BaseClaims.ID
|
||||
|
||||
data := do.SysJob{}
|
||||
gconv.Struct(jobAdd, &data)
|
||||
result, err := c.Model(ctx).Data(data).Insert()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
lastInserId, _ := result.LastInsertId()
|
||||
if lastInserId > 0 && jobAdd.Status == consts.SysJobStatusNormal {
|
||||
job := &model.SysJobViewModel{}
|
||||
if err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, lastInserId).Scan(&job); err != nil {
|
||||
return lastInserId, err
|
||||
}
|
||||
//任务启动
|
||||
err = c.taskRun(ctx, &job.SysJob)
|
||||
if err != nil {
|
||||
return lastInserId, err
|
||||
}
|
||||
}
|
||||
|
||||
return result.LastInsertId()
|
||||
}
|
||||
|
||||
func (c *sSysJob) Update(ctx context.Context, jobUpdate *model.SysJobUpdateModel) (RowsAffected int64, err error) {
|
||||
claims := contexts.GetUser(ctx)
|
||||
if claims == nil {
|
||||
return 0, gerror.NewCode(gcode.CodeMissingParameter, "用户信息不存在")
|
||||
}
|
||||
//获取task目录下是否绑定对应的方法
|
||||
_, exist := tasks.TasksInstance(ctx).CheckFuncName(jobUpdate.InvokeTarget)
|
||||
if !exist {
|
||||
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", jobUpdate.InvokeTarget)
|
||||
return 0, gerror.New(errInfo)
|
||||
}
|
||||
|
||||
jobUpdate.UpdatedAt = gtime.Now()
|
||||
jobUpdate.UpdatedBy = claims.BaseClaims.ID
|
||||
data := do.SysJob{}
|
||||
gconv.Struct(jobUpdate, &data)
|
||||
result, err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Data(data).Update()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
row, err := result.RowsAffected()
|
||||
if row > 0 && jobUpdate.Status == consts.SysJobStatusNormal {
|
||||
job := &model.SysJobViewModel{}
|
||||
if err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Scan(&job); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
//任务启动
|
||||
err = c.taskRestart(ctx, &job.SysJob)
|
||||
if err != nil {
|
||||
return row, err
|
||||
}
|
||||
}
|
||||
return row, err
|
||||
}
|
||||
|
||||
func (c *sSysJob) UpdateStatus(ctx context.Context, jobUpdate *model.SysJobUpdateStatusModel) (RowsAffected int64, err error) {
|
||||
claims := contexts.GetUser(ctx)
|
||||
if claims == nil {
|
||||
return 0, gerror.NewCode(gcode.CodeMissingParameter, "用户信息不存在")
|
||||
}
|
||||
|
||||
exists := &model.SysJobViewModel{}
|
||||
if err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Scan(&exists); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if jobUpdate.Status == consts.SysJobStatusNormal {
|
||||
//任务启动
|
||||
err = c.taskRun(ctx, &exists.SysJob)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if jobUpdate.Status == consts.SysJobStatusDisable {
|
||||
//任务停止
|
||||
if err = c.taskStop(ctx, &exists.SysJob); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
jobUpdate.UpdatedAt = gtime.Now()
|
||||
jobUpdate.UpdatedBy = claims.BaseClaims.ID
|
||||
data := do.SysJob{}
|
||||
gconv.Struct(jobUpdate, &data)
|
||||
result, err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Data(data).Update()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
row, err := result.RowsAffected()
|
||||
return row, err
|
||||
}
|
||||
|
||||
// 任务重启
|
||||
func (c *sSysJob) taskRestart(ctx context.Context, job *entity.SysJob) (err error) {
|
||||
//任务停止
|
||||
if err = c.taskStop(ctx, job); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//任务启动
|
||||
err = c.taskRun(ctx, job)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 任务启动
|
||||
func (c *sSysJob) taskRun(ctx context.Context, job *entity.SysJob) error {
|
||||
//获取task目录下是否绑定对应的方法
|
||||
actualName, exist := tasks.TasksInstance(ctx).CheckFuncName(job.InvokeTarget)
|
||||
if !exist {
|
||||
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", job.InvokeTarget)
|
||||
return gerror.New(errInfo)
|
||||
}
|
||||
|
||||
//传参解析
|
||||
paramArr, err := tasks.TasksInstance(ctx).ParseParameters(job.JobParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
taskData := tasks.Task{
|
||||
ID: fmt.Sprintf("%s-job-%d", job.InvokeTarget, job.JobId),
|
||||
TaskType: "Type-" + gconv.String(job.MisfirePolicy),
|
||||
MethodName: actualName,
|
||||
Params: paramArr,
|
||||
Explain: job.JobName,
|
||||
}
|
||||
runPayload, _ := json.Marshal(taskData)
|
||||
if job.MisfirePolicy == consts.SysJobMisfirePolicyMulty {
|
||||
// entryID, err := tasks.TasksInstance().Cron(
|
||||
// job.Concurrent,
|
||||
// worker.WithTaskCtx(context.Background()),
|
||||
// worker.WithTaskUid(taskData.ID), // 任务ID
|
||||
// worker.WithTaskGroup(taskData.MethodName), // 任务组
|
||||
// worker.WithTaskExpr(job.CronExpression),
|
||||
// worker.WithTaskTimeout(10),
|
||||
// worker.WithTaskReplace(true),
|
||||
// worker.WithTaskPayload(runPayload),
|
||||
// )
|
||||
entryID, err := tasks.TasksInstance(ctx).Cron(ctx, tasks.TaskOptions{
|
||||
Uid: taskData.ID,
|
||||
Payload: runPayload,
|
||||
Expr: &job.CronExpression,
|
||||
Concurrent: job.Concurrent,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Debug(ctx, taskData.MethodName, taskData.Explain, "启动任务失败")
|
||||
return err
|
||||
}
|
||||
g.Log().Debug(ctx, taskData.MethodName, taskData.Explain, "启动任务成功",
|
||||
"entryID", entryID, "CronExpression", job.CronExpression,
|
||||
"JobId", job.JobId,
|
||||
"JobName", job.JobName)
|
||||
} else {
|
||||
now := true
|
||||
err := tasks.TasksInstance(ctx).Once(ctx, tasks.TaskOptions{
|
||||
Uid: taskData.ID,
|
||||
Now: &now,
|
||||
Payload: runPayload,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Debug(ctx, taskData.MethodName, taskData.Explain, "启动任务失败")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sSysJob) taskStop(ctx context.Context, job *entity.SysJob) error {
|
||||
//获取task目录下是否绑定对应的方法
|
||||
_, exist := tasks.TasksInstance(ctx).CheckFuncName(job.InvokeTarget)
|
||||
if !exist {
|
||||
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", job.InvokeTarget)
|
||||
return gerror.New(errInfo)
|
||||
}
|
||||
|
||||
taskJobId := fmt.Sprintf("%s-job-%d", job.InvokeTarget, job.JobId)
|
||||
|
||||
err := tasks.TasksInstance(ctx).Remove(taskJobId)
|
||||
if err != nil {
|
||||
g.Log().Debug(ctx, job.JobName, "启动停止失败")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sSysJob) Delete(ctx context.Context, jobDelete *model.SysJobDeleteModel) (RowsAffected int64, err error) {
|
||||
claims, err := service.SysAuth().GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
exists := &[]*model.SysJobViewModel{}
|
||||
c.Model(ctx).WhereIn(dao.SysJob.Columns().JobId, jobDelete.JobIds).Where(dao.SysJob.Columns().Status, consts.SysJobStatusNormal).Scan(exists)
|
||||
if len(*exists) > 0 {
|
||||
return 0, gerror.New("存在正常运行的任务,请先停止后再删除")
|
||||
}
|
||||
|
||||
jobDelete.DeletedAt = gtime.Now()
|
||||
jobDelete.DeletedBy = claims.BaseClaims.ID
|
||||
data := do.SysJob{
|
||||
DeletedAt: jobDelete.DeletedAt,
|
||||
DeletedBy: jobDelete.DeletedBy,
|
||||
}
|
||||
result, err := c.Model(ctx).WhereIn(dao.SysJob.Columns().JobId, jobDelete.JobIds).Data(data).Update()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
func (c *sSysJob) Exec(ctx context.Context, jobId int64) error {
|
||||
exists := &model.SysJobViewModel{}
|
||||
err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobId).Scan(&exists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actualName, exist := tasks.TasksInstance(ctx).CheckFuncName(exists.InvokeTarget)
|
||||
if !exist {
|
||||
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", exists.InvokeTarget)
|
||||
return gerror.New(errInfo)
|
||||
}
|
||||
|
||||
//传参解析
|
||||
paramArr, err := tasks.TasksInstance(ctx).ParseParameters(exists.JobParams)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "sSysJob.JobRun worker.ParseParameters", err)
|
||||
return err
|
||||
}
|
||||
|
||||
taskData := tasks.Task{
|
||||
ID: fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId),
|
||||
TaskType: "Type-" + gconv.String(exists.MisfirePolicy),
|
||||
MethodName: actualName,
|
||||
Params: paramArr,
|
||||
Explain: exists.JobName,
|
||||
}
|
||||
runPayload, _ := json.Marshal(taskData)
|
||||
now := true
|
||||
err = tasks.TasksInstance(ctx).Once(ctx, tasks.TaskOptions{
|
||||
Uid: taskData.ID,
|
||||
Payload: runPayload,
|
||||
Now: &now,
|
||||
})
|
||||
// worker.WithTaskCtx(context.Background()),
|
||||
// worker.WithTaskUid(taskData.ID), // 任务ID
|
||||
// worker.WithTaskGroup(taskData.MethodName), // 任务组
|
||||
// worker.WithTaskTimeout(10),
|
||||
// worker.WithTaskNow(true),
|
||||
// worker.WithTaskReplace(true),
|
||||
// worker.WithTaskPayload(runPayload),
|
||||
// )
|
||||
|
||||
if err != nil {
|
||||
errInfo := fmt.Sprintf(exists.InvokeTarget, taskData.Explain, "启动任务失败")
|
||||
return errors.New(errInfo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询所有的状态正常的任务列表,并进行初始注册
|
||||
func (c *sSysJob) InitRegister() error {
|
||||
g.Log().Debug(context.Background(), "初始化任务注册")
|
||||
jobs := &[]*model.SysJobViewModel{}
|
||||
err := c.Model(context.Background()).Where(dao.SysJob.Columns().Status, consts.SysJobStatusNormal).Scan(jobs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, job := range *jobs {
|
||||
err = c.taskRun(context.Background(), &job.SysJob)
|
||||
if err != nil {
|
||||
g.Log().Error(context.Background(), "初始化注册任务 %s 失败", job.JobName, err)
|
||||
continue
|
||||
}
|
||||
g.Log().Debug(context.Background(), "初始化注册任务 %s 成功", job.JobName)
|
||||
}
|
||||
g.Log().Debug(context.Background(), "初始化任务注册完成")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysLogininfor struct{}
|
||||
|
||||
func NewSysLogininfor() *sSysLogininfor {
|
||||
return &sSysLogininfor{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysLogininfor(NewSysLogininfor())
|
||||
}
|
||||
|
||||
func (s *sSysLogininfor) 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.SysLogininfor.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sSysLogininfor) List(ctx context.Context, param *model.SysLogininforListParam) (items []*model.SysLogininforListModel, total int, err error) {
|
||||
m := s.Model(ctx)
|
||||
|
||||
if param.Ipaddr != "" {
|
||||
m = m.WhereLike(dao.SysLogininfor.Columns().Ipaddr, "%"+param.Ipaddr+"%")
|
||||
}
|
||||
if param.UserName != "" {
|
||||
m = m.WhereLike(dao.SysLogininfor.Columns().UserName, "%"+param.UserName+"%")
|
||||
}
|
||||
if param.Status != "" {
|
||||
m = m.Where(dao.SysLogininfor.Columns().Status, param.Status)
|
||||
}
|
||||
if len(param.LoginTime) == 2 {
|
||||
startTime := gtime.NewFromStr(param.LoginTime[0])
|
||||
endTime := gtime.NewFromStr(param.LoginTime[1])
|
||||
m = m.WhereBetween(dao.SysLogininfor.Columns().LoginTime, startTime, endTime.EndOfDay())
|
||||
}
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = m.Page(param.Page, param.PageSize).OrderDesc(dao.SysLogininfor.Columns().InfoId).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *sSysLogininfor) AddLogininfor(ctx context.Context, logininfor *model.SysLogininforAddModel) (id int64, err error) {
|
||||
data := do.SysLogininfor{}
|
||||
gconv.Struct(logininfor, &data)
|
||||
result, err := dao.SysLogininfor.Ctx(ctx).Data(data).Save()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err = result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *sSysLogininfor) Delete(ctx context.Context, param *model.SysLogininforDeleteParam) (output *model.SysLogininforDeleteModel, err error) {
|
||||
m := s.Model(ctx)
|
||||
output = &model.SysLogininforDeleteModel{
|
||||
InfoIds: param.InfoIds,
|
||||
}
|
||||
if len(param.InfoIds) == 0 {
|
||||
return nil, errors.New("请选择要删除的配置")
|
||||
}
|
||||
_, err = m.WhereIn(dao.SysLogininfor.Columns().InfoId, param.InfoIds).Delete()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"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 sSysMenu struct {
|
||||
}
|
||||
|
||||
func NewSysMenu() *sSysMenu {
|
||||
return &sSysMenu{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysMenu(NewSysMenu())
|
||||
}
|
||||
|
||||
func (s *sSysMenu) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: false,
|
||||
FilterAuth: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(dao.SysMenu.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sSysMenu) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: false,
|
||||
FilterAuth: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysMenu.Table()), option...)
|
||||
}
|
||||
|
||||
func (l *sSysMenu) List(ctx context.Context, param *model.SysMenuListParam) (data []*model.SysMenuListModel, total int, err error) {
|
||||
m := l.ModelQuery(ctx)
|
||||
if param.MenuIds != nil {
|
||||
m = m.WhereIn(dao.SysMenu.Columns().MenuId, param.MenuIds)
|
||||
}
|
||||
if param.MenuName != "" {
|
||||
m = m.WhereLike(dao.SysMenu.Columns().MenuName, "%"+param.MenuName+"%")
|
||||
}
|
||||
if param.Status != "" {
|
||||
m = m.Where(dao.SysMenu.Columns().Status, param.Status)
|
||||
}
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = m.Order(dao.SysMenu.Columns().OrderNum).Page(0, 5000).Scan(&data)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取租户菜单列表, 系统租户返回所有菜单,其他租户返回当前租户菜单
|
||||
func (l *sSysMenu) GetTenantMenu(ctx context.Context, query *model.SysMenuListParam) (data []*model.SysMenuListModel, total int, err error) {
|
||||
// 获取当前用户信息
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
m := l.ModelQuery(ctx)
|
||||
|
||||
if tenantId != consts.DefaultSystemTenantCode {
|
||||
// 获取租户信息
|
||||
tenantInfo, err := service.SysTenant().View(ctx, &model.SysTenantViewParam{
|
||||
TenantId: tenantId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// 获取租户套餐
|
||||
tenantPackage, err := service.SysTenantPackage().View(ctx, &model.SysTenantPackageViewParam{
|
||||
PackageId: tenantInfo.PackageId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if tenantPackage == nil {
|
||||
g.Log().Errorf(ctx, "sSysMenu.GetTenantMenu not found tenantPackage. TenantId:%s PackageId:%d",
|
||||
tenantId, tenantInfo.PackageId)
|
||||
return nil, 0, gerror.NewCode(gcode.CodeInternalError, "系统内部错误")
|
||||
}
|
||||
if tenantPackage.MenuIds != "" {
|
||||
menuIds := strings.Split(tenantPackage.MenuIds, ",")
|
||||
m = m.WhereIn(dao.SysMenu.Columns().MenuId, menuIds)
|
||||
} else {
|
||||
return nil, 0, nil
|
||||
}
|
||||
}
|
||||
if query.MenuName != "" {
|
||||
m = m.WhereLike(dao.SysMenu.Columns().MenuName, "%"+query.MenuName+"%")
|
||||
}
|
||||
if query.Status != "" {
|
||||
m = m.Where(dao.SysMenu.Columns().Status, query.Status)
|
||||
}
|
||||
if query.MenuIds != nil {
|
||||
m = m.WhereIn(dao.SysMenu.Columns().MenuId, query.MenuIds)
|
||||
}
|
||||
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = m.Order(dao.SysMenu.Columns().OrderNum).Page(0, 5000).Scan(&data)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建树结构
|
||||
func (l *sSysMenu) MenuTree(ctx context.Context, parentMenu *model.SysMenuTree, menuList []*entity.SysMenu) (data []*model.SysMenuTree, err error) {
|
||||
data = make([]*model.SysMenuTree, 0)
|
||||
for _, menu := range menuList {
|
||||
pId := int64(0)
|
||||
if parentMenu != nil {
|
||||
pId = parentMenu.MenuId
|
||||
}
|
||||
if menu.ParentId == pId {
|
||||
item := &model.SysMenuTree{
|
||||
SysMenu: *menu,
|
||||
Children: nil,
|
||||
}
|
||||
subMenu, err := l.MenuTree(ctx, item, menuList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Children = subMenu
|
||||
data = append(data, item)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户动态路由列表
|
||||
func (l *sSysMenu) GetUserMenu(ctx context.Context) (data []*entity.SysMenu, err error) {
|
||||
roles := contexts.GetRoles(ctx)
|
||||
roleIds := make([]int64, 0)
|
||||
for _, role := range roles {
|
||||
roleIds = append(roleIds, role.RoleId)
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysMenu.GetUserMenu roleIds: %v", roleIds)
|
||||
// 判断是否存在超级管理员角色
|
||||
if slices.Contains(roleIds, consts.SuperAdminRoleId) {
|
||||
// 获取所有菜单
|
||||
err = l.ModelQuery(ctx).Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
|
||||
Order(dao.SysMenu.Columns().OrderNum).Scan(&data)
|
||||
return
|
||||
}
|
||||
// 获取角色菜单列表
|
||||
menuList, err := service.SysRole().GetRoleListMenu(ctx, roleIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysMenu.GetUserMenu menuList: %v", menuList)
|
||||
menuIds := make([]int64, 0)
|
||||
for _, menu := range menuList {
|
||||
menuIds = append(menuIds, menu.MenuId)
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysMenu.GetUserMenu menuIds: %v", menuIds)
|
||||
// 获取用户角色菜单
|
||||
err = l.ModelQuery(ctx).WhereIn(dao.SysMenu.Columns().MenuId, menuIds).
|
||||
Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
|
||||
Order(dao.SysMenu.Columns().OrderNum).Scan(&data)
|
||||
g.Log().Infof(ctx, "sSysMenu.GetUserMenu data: %v", data)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建用户动态路由树
|
||||
func (l *sSysMenu) BuildUserMenuTree(ctx context.Context, parentMenu *v1.RouteMenu, menuList []*entity.SysMenu, allPath string) (data v1.MenuAllRes, err error) {
|
||||
data = make(v1.MenuAllRes, 0)
|
||||
for _, menu := range menuList {
|
||||
if menu.Status == consts.SysMenuStatusDisable {
|
||||
continue
|
||||
}
|
||||
if menu.MenuType == consts.SysMenuTypeBtn {
|
||||
continue
|
||||
}
|
||||
|
||||
pId := int64(0)
|
||||
if parentMenu != nil {
|
||||
pId = parentMenu.Id
|
||||
}
|
||||
if menu.ParentId == pId {
|
||||
openInNewWindow := false
|
||||
var link *string
|
||||
if menu.IsFrame == consts.SysMenuIsFrameYes {
|
||||
openInNewWindow = true
|
||||
link = &menu.Path
|
||||
}
|
||||
|
||||
query := make(map[string]string)
|
||||
if menu.QueryParam != "" {
|
||||
query2 := strings.Split(menu.QueryParam, "&")
|
||||
for _, q := range query2 {
|
||||
kv := strings.Split(q, "=")
|
||||
if len(kv) == 2 {
|
||||
query[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
hideInMenu := false
|
||||
if menu.Visible == consts.SysMenuVisibleHide {
|
||||
hideInMenu = true
|
||||
}
|
||||
keepAlive := true
|
||||
item := &v1.RouteMenu{
|
||||
Id: menu.MenuId,
|
||||
ParentId: menu.ParentId,
|
||||
Path: menu.Path,
|
||||
Redirect: nil,
|
||||
Name: fmt.Sprintf("menu_%d", menu.MenuId),
|
||||
Component: menu.Component,
|
||||
Meta: &v1.RouteMeta{
|
||||
Title: menu.MenuName,
|
||||
Icon: &menu.Icon,
|
||||
Order: &menu.OrderNum,
|
||||
OpenInNewWindow: &openInNewWindow,
|
||||
Query: query,
|
||||
HideInMenu: &hideInMenu,
|
||||
Authority: strings.Split(menu.Perms, ","),
|
||||
Link: link,
|
||||
KeepAlive: &keepAlive,
|
||||
},
|
||||
}
|
||||
if hideInMenu {
|
||||
activePath := allPath + "/"
|
||||
if strings.Contains(menu.Path, "-") {
|
||||
activePath = activePath + strings.Split(menu.Path, "-")[0]
|
||||
}
|
||||
item.Meta.ActivePath = &activePath
|
||||
}
|
||||
subMenu, err := l.BuildUserMenuTree(ctx, item, menuList, allPath+"/"+menu.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Children = subMenu
|
||||
if len(subMenu) == 0 && menu.MenuType == consts.SysMenuTypeDir {
|
||||
item.Children = []*v1.RouteMenu{}
|
||||
}
|
||||
data = append(data, item)
|
||||
}
|
||||
}
|
||||
// 排序
|
||||
sort.Slice(data, func(i, j int) bool {
|
||||
return *data[i].Meta.Order < *data[j].Meta.Order
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户动态路由树
|
||||
func (l *sSysMenu) GetUserMenuTree(ctx context.Context) (data v1.MenuAllRes, err error) {
|
||||
// 获取用户动态路由列表
|
||||
menuList, err := l.GetUserMenu(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 构建树结构
|
||||
data, err = l.BuildUserMenuTree(ctx, nil, menuList, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysMenu) GetSysMenuView(ctx context.Context, menuId int64) (data *model.SysMenuViewModel, err error) {
|
||||
menu := &entity.SysMenu{}
|
||||
err = l.ModelQuery(ctx).Where(dao.SysMenu.Columns().MenuId, menuId).Scan(menu)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = &model.SysMenuViewModel{
|
||||
SysMenu: *menu,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysMenu) UpdateSysMenu(ctx context.Context, menu *model.SysMenuUpdateModel) (data *model.SysMenuViewModel, err error) {
|
||||
menu.UpdatedAt = gtime.Now()
|
||||
menu.UpdatedBy = contexts.GetUserId(ctx)
|
||||
updateData := do.SysMenu{}
|
||||
gconv.Struct(menu, &updateData)
|
||||
updateData.MenuId = nil
|
||||
_, err = l.Model(ctx).Where(dao.SysMenu.Columns().MenuId, menu.MenuId).Data(updateData).OmitNil().Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysMenuUpdate, menu.MenuId)
|
||||
data, err = l.GetSysMenuView(ctx, menu.MenuId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysMenu) AddSysMenu(ctx context.Context, menu *model.SysMenuAddModel) (data *model.SysMenuViewModel, err error) {
|
||||
menu.CreatedDept = contexts.GetDeptId(ctx)
|
||||
menu.CreatedAt = gtime.Now()
|
||||
menu.CreatedBy = contexts.GetUserId(ctx)
|
||||
menu.UpdatedAt = gtime.Now()
|
||||
menu.UpdatedBy = contexts.GetUserId(ctx)
|
||||
insertData := do.SysMenu{}
|
||||
gconv.Struct(menu, &insertData)
|
||||
insertData.MenuId = nil
|
||||
re, err := l.Model(ctx).Data(insertData).Insert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
menuId, err := re.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysMenuCreate, menuId)
|
||||
data, err = l.GetSysMenuView(ctx, menuId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysMenu) DeleteSysMenu(ctx context.Context, menuId int64) (err error) {
|
||||
_, err = l.Model(ctx).Where(dao.SysMenu.Columns().MenuId, menuId).Delete()
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysMenuDelete, menuId)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysMenu) GetFastList(ctx context.Context) (res map[int64]*entity.SysMenu, err error) {
|
||||
var models []*entity.SysMenu
|
||||
if err = s.ModelQuery(ctx).Scan(&models); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = make(map[int64]*entity.SysMenu, len(models))
|
||||
for _, v := range models {
|
||||
res[v.MenuId] = v
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gset"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysNotice struct {
|
||||
}
|
||||
|
||||
func NewSysNotice() *sSysNotice {
|
||||
return &sSysNotice{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysNotice(NewSysNotice())
|
||||
}
|
||||
|
||||
func (l *sSysNotice) 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.SysNotice.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sSysNotice) List(ctx context.Context, param *model.SysNoticeListParam) (items []*model.SysNoticeListModel, total int, err error) {
|
||||
db := s.Model(ctx)
|
||||
|
||||
if param.NoticeTitle != "" {
|
||||
db = db.WhereLike(dao.SysNotice.Columns().NoticeTitle, "%"+param.NoticeTitle+"%")
|
||||
}
|
||||
|
||||
if param.NoticeType != "" {
|
||||
db = db.Where(dao.SysNotice.Columns().NoticeType, param.NoticeType)
|
||||
}
|
||||
|
||||
if param.CreatedBy != "" {
|
||||
db = db.Where(dao.SysNotice.Columns().CreatedBy, param.CreatedBy)
|
||||
}
|
||||
|
||||
if len(param.CreatedAt) == 2 {
|
||||
createdAt1 := gtime.NewFromStr(param.CreatedAt[0])
|
||||
createdAt2 := gtime.NewFromStr(param.CreatedAt[1])
|
||||
|
||||
db = db.WhereBetween(dao.SysNotice.Columns().CreatedAt, createdAt1, createdAt2.EndOfDay())
|
||||
}
|
||||
|
||||
db = db.OrderDesc(dao.SysNotice.Columns().NoticeId)
|
||||
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = db.Page(param.Page, param.PageSize).WithAll().Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysNotice) insertNoticeUserList(ctx context.Context, tx gdb.TX, userIds *gset.IntSet, param *entity.SysNotice) (err error) {
|
||||
|
||||
sysNotifyUserList := make([]*do.SysNoticeUser, 0, userIds.Size())
|
||||
for _, userId := range userIds.Slice() {
|
||||
sysNotifyUserList = append(sysNotifyUserList, &do.SysNoticeUser{
|
||||
NoticeId: param.NoticeId,
|
||||
NoticeTitle: param.NoticeTitle,
|
||||
NoticeType: param.NoticeType,
|
||||
NoticeContent: param.NoticeContent,
|
||||
Status: param.Status,
|
||||
UserId: int64(userId),
|
||||
IsRead: 0,
|
||||
Remark: param.Remark,
|
||||
TenantId: contexts.GetTenantId(ctx),
|
||||
CreatedDept: contexts.GetDeptId(ctx),
|
||||
CreatedBy: contexts.GetUserId(ctx),
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedBy: contexts.GetUserId(ctx),
|
||||
UpdatedAt: gtime.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
_, err = dao.SysNoticeUser.Ctx(ctx).TX(tx).Data(sysNotifyUserList).OmitNil().Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysNotice) Add(ctx context.Context, param *model.SysNoticeAddParam) (err error) {
|
||||
if param == nil || param.NoticeTitle == "" {
|
||||
return errors.New("公告标题不能为空")
|
||||
}
|
||||
if param.NoticeType == "" {
|
||||
return errors.New("公告类型不能为空")
|
||||
}
|
||||
if param.NoticeContent == "" {
|
||||
return errors.New("公告内容不能为空")
|
||||
}
|
||||
if param.NoticeRange == 0 {
|
||||
return errors.New("公告范围不能为空")
|
||||
}
|
||||
|
||||
data := do.SysNotice{}
|
||||
gconv.Struct(param, &data)
|
||||
if param.NoticeRange == consts.NoticeRangeDept {
|
||||
if len(param.DeptIdList) == 0 {
|
||||
return errors.New("通知组织ID列表不能为空")
|
||||
}
|
||||
deptIds, err := json.Marshal(param.DeptIdList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.DeptIds = string(deptIds)
|
||||
}
|
||||
|
||||
if param.NoticeRange == consts.NoticeRangeUser {
|
||||
if len(param.UserIdList) == 0 {
|
||||
return errors.New("通知用户ID列表不能为空")
|
||||
}
|
||||
userIds, err := json.Marshal(param.UserIdList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.UserIds = string(userIds)
|
||||
}
|
||||
|
||||
data.TenantId = contexts.GetTenantId(ctx)
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
err = dao.SysNotice.Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
id, err := s.Model(ctx).TX(tx).Data(data).OmitEmpty().InsertAndGetId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id <= 0 {
|
||||
return errors.New("新增公告失败")
|
||||
}
|
||||
data.NoticeId = id
|
||||
|
||||
// getUserIds
|
||||
userIds := gset.NewIntSet()
|
||||
if param.NoticeRange == consts.NoticeRangeUser {
|
||||
userIds.Add(gconv.Ints(param.UserIdList)...)
|
||||
g.Log().Debugf(ctx, "userIds: %+v", userIds.Slice())
|
||||
}
|
||||
if param.NoticeRange == consts.NoticeRangeDept {
|
||||
userList, _, err := service.SysUser().List(ctx, &request.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 10000,
|
||||
}, &model.UserListParam{
|
||||
DeptIdList: param.DeptIdList,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, user := range userList {
|
||||
userIds.Add(int(user.UserId))
|
||||
}
|
||||
}
|
||||
if param.NoticeRange == consts.NoticeRangeAll {
|
||||
userList, _, err := service.SysUser().List(ctx, &request.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 10000,
|
||||
}, &model.UserListParam{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, user := range userList {
|
||||
userIds.Add(int(user.UserId))
|
||||
}
|
||||
}
|
||||
|
||||
if userIds.Size() <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
entityData := entity.SysNotice{}
|
||||
gconv.Struct(data, &entityData)
|
||||
err = s.insertNoticeUserList(ctx, tx, userIds, &entityData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sSysNotice) Edit(ctx context.Context, param *model.SysNoticeEditParam) (err error) {
|
||||
if param == nil || param.NoticeId == 0 {
|
||||
return errors.New("公告ID不能为空")
|
||||
}
|
||||
if param.NoticeTitle == nil || *param.NoticeTitle == "" {
|
||||
return errors.New("公告标题不能为空")
|
||||
}
|
||||
if param.NoticeType == nil || *param.NoticeType == "" {
|
||||
return errors.New("公告类型不能为空")
|
||||
}
|
||||
if param.NoticeContent == nil || len(*param.NoticeContent) == 0 {
|
||||
return errors.New("公告内容不能为空")
|
||||
}
|
||||
|
||||
data := do.SysNotice{}
|
||||
gconv.Struct(param, &data)
|
||||
if param.NoticeRange == consts.NoticeRangeDept {
|
||||
if len(param.DeptIdList) == 0 {
|
||||
return errors.New("通知组织ID列表不能为空")
|
||||
}
|
||||
deptIds, err := json.Marshal(param.DeptIdList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.DeptIds = string(deptIds)
|
||||
}
|
||||
|
||||
if param.NoticeRange == consts.NoticeRangeUser {
|
||||
if len(param.UserIdList) == 0 {
|
||||
return errors.New("通知用户ID列表不能为空")
|
||||
}
|
||||
userIds, err := json.Marshal(param.UserIdList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.UserIds = string(userIds)
|
||||
}
|
||||
data.TenantId = contexts.GetTenantId(ctx)
|
||||
data.NoticeContent = []byte(*param.NoticeContent)
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
|
||||
err = dao.SysNotice.Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
m := s.Model(ctx).TX(tx)
|
||||
|
||||
m = m.Where(dao.SysNotice.Columns().NoticeId, param.NoticeId)
|
||||
g.Log().Debugf(ctx, "param: %+v", string(*param.NoticeContent))
|
||||
g.Log().Debugf(ctx, "data: %+v", string(data.NoticeContent))
|
||||
_, err = m.Data(data).OmitEmpty().Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 根据 notifyId删除,添加新的列表
|
||||
userIds := gset.NewIntSet()
|
||||
if param.NoticeRange == consts.NoticeRangeUser {
|
||||
userIds.Add(gconv.Ints(param.UserIdList)...)
|
||||
}
|
||||
if param.NoticeRange == consts.NoticeRangeDept || param.NoticeRange == consts.NoticeRangeAll {
|
||||
userList, _, err := service.SysUser().List(ctx, &request.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 10000,
|
||||
}, &model.UserListParam{
|
||||
DeptIdList: param.DeptIdList,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, user := range userList {
|
||||
userIds.Add(int(user.UserId))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
_, err = dao.SysNoticeUser.Ctx(ctx).TX(tx).Where(dao.SysNoticeUser.Columns().NoticeId, param.NoticeId).Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if userIds.Size() <= 0 {
|
||||
return
|
||||
}
|
||||
entityData := entity.SysNotice{}
|
||||
gconv.Struct(data, &entityData)
|
||||
err = s.insertNoticeUserList(ctx, tx, userIds, &entityData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sSysNotice) Delete(ctx context.Context, param *model.SysNoticeDeleteParam) (err error) {
|
||||
err = dao.SysNotice.Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
_, err = s.Model(ctx).TX(tx).WhereIn(dao.SysNotice.Columns().NoticeId, param.NoticeIds).Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除SysNoticeUser
|
||||
_, err = dao.SysNoticeUser.Ctx(ctx).TX(tx).WhereIn(dao.SysNoticeUser.Columns().NoticeId, param.NoticeIds).Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sSysNotice) View(ctx context.Context, param *model.SysNoticeViewParam) (data *model.SysNoticeViewModel, err error) {
|
||||
if param == nil || param.NoticeId == 0 {
|
||||
return nil, errors.New("公告ID不能为空")
|
||||
}
|
||||
data = &model.SysNoticeViewModel{}
|
||||
err = s.Model(ctx).Where(dao.SysNotice.Columns().NoticeId, param.NoticeId).Scan(&data)
|
||||
g.Log().Debugf(ctx, "data: %+v", string(data.NoticeContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data.UserIds != "" {
|
||||
json.Unmarshal([]byte(data.UserIds), &data.UserIdList)
|
||||
}
|
||||
if data.DeptIds != "" {
|
||||
json.Unmarshal([]byte(data.DeptIds), &data.DeptIdList)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/hook"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/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 sSysNoticeUser struct{}
|
||||
|
||||
func NewSysNoticeUser() *sSysNoticeUser {
|
||||
return &sSysNoticeUser{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysNoticeUser(NewSysNoticeUser())
|
||||
}
|
||||
|
||||
// Model 用户通知公告表ORM模型
|
||||
func (s *sSysNoticeUser) 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.SysNoticeUser.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取用户通知公告表列表
|
||||
func (s *sSysNoticeUser) List(ctx context.Context, in *model.SysNoticeUserListParam) (list []*model.SysNoticeUserListModel, totalCount int, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 根据当前用户查询
|
||||
mod = mod.Where(dao.SysNoticeUser.Columns().UserId, contexts.GetUserId(ctx))
|
||||
mod = mod.Where(dao.SysNoticeUser.Columns().Status, consts.SysStatusNormal)
|
||||
// 字段过滤
|
||||
mod = mod.Fields(model.SysNoticeUserListModel{})
|
||||
|
||||
if in.IsRead != "" {
|
||||
mod = mod.Where(dao.SysNoticeUser.Columns().IsRead, in.IsRead)
|
||||
}
|
||||
|
||||
if in.NoticeId > 0 {
|
||||
mod = mod.Where(dao.SysNoticeUser.Columns().NoticeId, in.NoticeId)
|
||||
}
|
||||
|
||||
if in.NoticeTitle != "" {
|
||||
mod = mod.WhereLike(dao.SysNoticeUser.Columns().NoticeTitle, "%"+in.NoticeTitle+"%")
|
||||
}
|
||||
|
||||
if in.NoticeType != "" {
|
||||
mod = mod.Where(dao.SysNoticeUser.Columns().NoticeType, in.NoticeType)
|
||||
}
|
||||
// 分页
|
||||
mod = mod.Page(in.Page, in.PageSize)
|
||||
|
||||
// 排序
|
||||
mod = mod.OrderDesc(dao.SysNoticeUser.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 *sSysNoticeUser) Export(ctx context.Context, in *model.SysNoticeUserListParam) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.SysNoticeUserExportModel{})
|
||||
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.SysNoticeUserExportModel
|
||||
)
|
||||
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增用户通知公告表
|
||||
func (s *sSysNoticeUser) Edit(ctx context.Context, in *model.SysNoticeUserEditParam) (err error) {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
in.UpdatedBy = contexts.GetUserId(ctx)
|
||||
in.UpdatedAt = gtime.Now()
|
||||
data := do.SysNoticeUser{}
|
||||
gconv.Struct(in, &data)
|
||||
data.Id = nil
|
||||
data.NoticeContent = []byte(in.NoticeContent)
|
||||
if _, err = s.Model(ctx).TX(tx).
|
||||
Fields(model.SysNoticeUserUpdateFields{}).
|
||||
WherePri(in.Id).Data(data).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "修改用户通知公告表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
in.TenantId = contexts.GetTenantId(ctx)
|
||||
in.CreatedDept = contexts.GetDeptId(ctx)
|
||||
in.CreatedBy = contexts.GetUserId(ctx)
|
||||
in.CreatedAt = gtime.Now()
|
||||
data := do.SysNoticeUser{}
|
||||
gconv.Struct(in, &data)
|
||||
data.Id = nil
|
||||
data.NoticeContent = []byte(in.NoticeContent)
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx).
|
||||
Fields(model.SysNoticeUserInsertFields{}).
|
||||
Data(data).Insert(); err != nil {
|
||||
err = gerror.Wrap(err, "新增用户通知公告表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Delete 删除用户通知公告表
|
||||
func (s *sSysNoticeUser) Delete(ctx context.Context, in *model.SysNoticeUserDeleteParam) (err error) {
|
||||
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
|
||||
err = gerror.Wrap(err, "删除用户通知公告表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取用户通知公告表指定信息
|
||||
func (s *sSysNoticeUser) View(ctx context.Context, in *model.SysNoticeUserViewParam) (res *model.SysNoticeUserViewModel, 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 *sSysNoticeUser) Status(ctx context.Context, in *model.SysNoticeUserStatusParam) (err error) {
|
||||
data := do.SysNoticeUser{
|
||||
Status: in.Status,
|
||||
UpdatedBy: contexts.GetUserId(ctx),
|
||||
}
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(data).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "更新用户通知公告表状态失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Read 已读
|
||||
func (s *sSysNoticeUser) Read(ctx context.Context, in *model.SysNoticeUserReadParam) (err error) {
|
||||
|
||||
data := do.SysNoticeUser{
|
||||
IsRead: 1,
|
||||
UpdatedBy: contexts.GetUserId(ctx),
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
if _, err = s.Model(ctx).WherePri(in.Ids).Data(data).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "更新用户通知公告表状态失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysOperLog struct {
|
||||
}
|
||||
|
||||
func NewSysOperLog() *sSysOperLog {
|
||||
return &sSysOperLog{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysOperLog(NewSysOperLog())
|
||||
}
|
||||
|
||||
func (s *sSysOperLog) GetOperLogList(ctx context.Context, query *model.SysOperLogListParam, page *request.PageInfo) (items []*model.SysOperLogListModel, total int, err error) {
|
||||
// 获取当前用户租户编码
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
|
||||
db := dao.SysOperLog.Ctx(ctx).Where(dao.SysOperLog.Columns().TenantId, tenantId)
|
||||
|
||||
if query.Title != "" {
|
||||
db = db.WhereLike(dao.SysOperLog.Columns().Title, "%"+query.Title+"%")
|
||||
}
|
||||
|
||||
if query.BusinessType != "" {
|
||||
db = db.Where(dao.SysOperLog.Columns().BusinessType, query.BusinessType)
|
||||
}
|
||||
|
||||
if query.Method != "" {
|
||||
db = db.WhereLike(dao.SysOperLog.Columns().Method, "%"+query.Method+"%")
|
||||
}
|
||||
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = db.Page(page.Page, page.PageSize).OrderDesc(dao.SysOperLog.Columns().OperId).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *sSysOperLog) AnalysisLog(ctx context.Context) (data *model.SysOperLogAddParam, err error) {
|
||||
ctxData := contexts.Get(ctx)
|
||||
request := ghttp.RequestFromCtx(ctx)
|
||||
handlerResponse := request.GetHandlerResponse() // 响应结果
|
||||
param := request.GetMap() // 请求参数
|
||||
path := request.URL.Path
|
||||
|
||||
res := gconv.Map(handlerResponse)
|
||||
deptName := ""
|
||||
operName := ""
|
||||
businessType := 0
|
||||
operatorType := 0
|
||||
status := 0
|
||||
costTime := gtime.Now().Sub(gtime.New(request.EnterTime)).Milliseconds()
|
||||
message := ""
|
||||
if ctxData != nil && ctxData.User != nil {
|
||||
deptName, _ = mcache.GetDeptName(ctx, ctxData.User.DeptId)
|
||||
operName = ctxData.User.Username
|
||||
}
|
||||
jsonResult := ""
|
||||
var code gcode.Code = gcode.CodeOK
|
||||
if erro := request.GetError(); erro != nil {
|
||||
status = 1
|
||||
code = gerror.Code(erro)
|
||||
message = gjson.New(g.Map{
|
||||
"code": code,
|
||||
"message": erro.Error(),
|
||||
}).String()
|
||||
} else {
|
||||
status = 0
|
||||
if b, err := gjson.Encode(res); err == nil {
|
||||
if len(b) > 1024 {
|
||||
jsonResult = "数据过大,未记录"
|
||||
} else {
|
||||
jsonResult = string(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(path, "/add") {
|
||||
businessType = 1
|
||||
}
|
||||
if strings.Contains(path, "/edit") {
|
||||
businessType = 2
|
||||
}
|
||||
if strings.Contains(path, "/delete") {
|
||||
businessType = 3
|
||||
}
|
||||
url := request.URL.String()
|
||||
if len(url) > 100 {
|
||||
url = url[:100] + "..."
|
||||
}
|
||||
operParam := gconv.String(param)
|
||||
if len(operParam) > 1024 {
|
||||
operParam = "数据过大,未记录"
|
||||
}
|
||||
data = &model.SysOperLogAddParam{
|
||||
TenantId: contexts.GetTenantId(ctx),
|
||||
Title: "",
|
||||
BusinessType: businessType,
|
||||
Method: request.URL.Path,
|
||||
RequestMethod: request.Method,
|
||||
OperatorType: operatorType,
|
||||
OperName: operName,
|
||||
DeptName: deptName,
|
||||
OperUrl: url,
|
||||
OperIp: utility.GetClientIp(ctx),
|
||||
OperLocation: utility.GetCityByIp(utility.GetClientIp(ctx)),
|
||||
OperParam: operParam,
|
||||
JsonResult: jsonResult,
|
||||
Status: status,
|
||||
ErrorMsg: message,
|
||||
OperTime: gtime.Now(),
|
||||
CostTime: costTime,
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *sSysOperLog) ClearOperationLogByDays(ctx context.Context, days int) error {
|
||||
_, err := dao.SysOperLog.Ctx(ctx).Delete("to_days(now())-to_days(`oper_time`) > ?", days+1)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sSysOperLog) RealWrite(ctx context.Context, data model.SysOperLogAddParam) (err error) {
|
||||
writeData := do.SysOperLog{}
|
||||
if err = gconv.Struct(data, &writeData); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = dao.SysOperLog.Ctx(ctx).Data(writeData).Save()
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "sSysOperLog.RealWrite error:%v,data:%+v", err, data)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/hash"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"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/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysOss struct {
|
||||
}
|
||||
|
||||
func NewSysOss() *sSysOss {
|
||||
return &sSysOss{}
|
||||
}
|
||||
|
||||
func sysOssDataFromUpload(ctx context.Context, result model.UploadResponse) do.SysOss {
|
||||
now := gtime.Now()
|
||||
return do.SysOss{
|
||||
TenantId: contexts.GetTenantId(ctx),
|
||||
FileName: result.Name,
|
||||
OriginalName: result.OriginalName,
|
||||
FileSuffix: result.Ext,
|
||||
Path: result.Path,
|
||||
Url: result.FullPath,
|
||||
CreatedDept: contexts.GetDeptId(ctx),
|
||||
CreatedAt: now,
|
||||
CreatedBy: contexts.GetUserId(ctx),
|
||||
UpdatedAt: now,
|
||||
UpdatedBy: contexts.GetUserId(ctx),
|
||||
Service: "local",
|
||||
Md5: result.Md5,
|
||||
FileSize: result.Size,
|
||||
FileCrc16: result.Crc16,
|
||||
FileSum: result.Sum16,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysOss(NewSysOss())
|
||||
}
|
||||
|
||||
func (s *sSysOss) 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.SysOss.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sSysOss) List(ctx context.Context, param *model.SysOssListParam, pageInfo *request.PageInfo) (items []*model.SysOssListModel, total int, err error) {
|
||||
|
||||
db := s.Model(ctx)
|
||||
|
||||
if param.FileName != "" {
|
||||
db = db.WhereLike(dao.SysOss.Columns().FileName, "%"+param.FileName+"%")
|
||||
}
|
||||
|
||||
if param.OriginalName != "" {
|
||||
db = db.WhereLike(dao.SysOss.Columns().OriginalName, "%"+param.OriginalName+"%")
|
||||
}
|
||||
|
||||
if param.FileSuffix != "" {
|
||||
db = db.Where(dao.SysOss.Columns().FileSuffix, param.FileSuffix)
|
||||
}
|
||||
|
||||
if param.Service != "" {
|
||||
db = db.Where(dao.SysOss.Columns().Service, param.Service)
|
||||
}
|
||||
|
||||
if len(param.CreatedAt) == 2 {
|
||||
startTime, err := gtime.StrToTime(param.CreatedAt[0])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
endTime, err := gtime.StrToTime(param.CreatedAt[1])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
db = db.WhereBetween(dao.SysOss.Columns().CreatedAt, startTime, endTime.EndOfDay())
|
||||
}
|
||||
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = db.Page(pageInfo.Page, pageInfo.PageSize).Order(dao.SysOss.Columns().OssId, "DESC").Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOss) View(ctx context.Context, param *model.SysOssViewParam) (output *model.SysOssViewModel, err error) {
|
||||
if param.OssId == 0 {
|
||||
return nil, errors.New("未选择文件")
|
||||
}
|
||||
db := s.Model(ctx).Where(dao.SysOss.Columns().OssId, param.OssId)
|
||||
err = db.Scan(&output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOss) Download(ctx context.Context, param *model.SysOssDownloadParam) (output *model.SysOssDownloadModel, err error) {
|
||||
if param.OssId == 0 {
|
||||
return nil, errors.New("未选择文件")
|
||||
}
|
||||
data := &entity.SysOss{}
|
||||
db := s.Model(ctx).Where(dao.SysOss.Columns().OssId, param.OssId)
|
||||
err = db.Scan(&data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data.Service != "local" {
|
||||
err = gerror.New("非本地文件")
|
||||
return
|
||||
}
|
||||
output = &model.SysOssDownloadModel{
|
||||
SysOss: data,
|
||||
}
|
||||
err = s.DownloadLocal(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOss) Delete(ctx context.Context, param *model.SysOssDeleteParam) (output *model.SysOssDeleteModel, err error) {
|
||||
if len(param.OssIds) == 0 {
|
||||
return nil, errors.New("未选择文件")
|
||||
}
|
||||
items := make([]*entity.SysOss, 0)
|
||||
err = s.Model(ctx).WhereIn(dao.SysOss.Columns().OssId, param.OssIds).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, errors.New("文件不存在")
|
||||
}
|
||||
_, err = s.Model(ctx).WhereIn(dao.SysOss.Columns().OssId, param.OssIds).Delete()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range items {
|
||||
if item.Service == "local" {
|
||||
s.DeleteLocal(ctx, item)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOss) Upload(ctx context.Context, param *model.SysOssUploadParam) (output *model.SysOssUploadModel, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
file := r.GetUploadFile("file")
|
||||
if file == nil {
|
||||
err = gerror.New("上传文件必须")
|
||||
return
|
||||
}
|
||||
// 检查文件类型
|
||||
err = s.CheckType(ctx, param.FileType, file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
err = s.CheckSize(ctx, param.FileType, file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO:待实现其他云存储
|
||||
if param.NewFileType == 0 {
|
||||
if param.IsDevice {
|
||||
param.NewFileType = consts.SysUploadNewFileTypeDeviceFile
|
||||
} else if param.FileType == consts.SysUploadFileTypeImg {
|
||||
param.NewFileType = consts.SysUploadNewFileTypeUserImg
|
||||
} else {
|
||||
param.NewFileType = consts.SysUploadNewFileTypeUserFile
|
||||
}
|
||||
}
|
||||
result, err := s.UploadLocal(ctx, file, param.NewFileType, param.SaveOriginalName, param.SubDirName, param.NotAddDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := s.Model(ctx).Data(sysOssDataFromUpload(ctx, result)).InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysOssUploadModel{}
|
||||
err = s.Model(ctx).Where(dao.SysOss.Columns().OssId, id).Scan(&output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOss) MoveFile(ctx context.Context, param *model.SysOssMoveFileParam) (output *model.SysOssMoveFileModel, err error) {
|
||||
result, err := s.MoveFileLocal(ctx, param.FilePath, param.NewFileType, param.SaveOriginalName, param.SubDirName, param.NotAddDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := s.Model(ctx).Data(sysOssDataFromUpload(ctx, result)).InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oss := &entity.SysOss{}
|
||||
err = s.Model(ctx).Where(dao.SysOss.Columns().OssId, id).Scan(&oss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysOssMoveFileModel{
|
||||
SysOss: oss,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sSysOss) SaveContent(ctx context.Context, param *model.SysOssSaveContentParam) (output *model.SysOssSaveContentModel, err error) {
|
||||
result, err := s.SaveContentLocal(ctx, param.Content, param.NewFileType, param.FileName, param.SubDirName, param.NotAddDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := s.Model(ctx).Data(sysOssDataFromUpload(ctx, result)).InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oss := &entity.SysOss{}
|
||||
err = s.Model(ctx).Where(dao.SysOss.Columns().OssId, id).Scan(&oss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysOssSaveContentModel{
|
||||
SysOss: oss,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (s *sSysOss) getNewFileTypePath(ctx context.Context, newFileType int) string {
|
||||
switch newFileType {
|
||||
case consts.SysUploadNewFileTypeUserFile:
|
||||
return consts.SysUploadNewFileTypePathUserFile
|
||||
case consts.SysUploadNewFileTypeUserImg:
|
||||
return consts.SysUploadNewFileTypePathUserImg
|
||||
case consts.SysUploadNewFileTypeDeviceFile:
|
||||
return consts.SysUploadNewFileTypePathDeviceFile
|
||||
default:
|
||||
key := fmt.Sprintf(consts.ConfigOssNewFileTypePath, newFileType)
|
||||
config, err := service.SysConfig().GetConfigByKey(ctx, key)
|
||||
if err == nil && config != nil {
|
||||
return config.ConfigValue
|
||||
}
|
||||
}
|
||||
return "other"
|
||||
}
|
||||
|
||||
func (s *sSysOss) GetSaveFilePathConfig(ctx context.Context, newFileType int, notAddDate int) (string, error) {
|
||||
uploadPath := g.Cfg().MustGet(ctx, "system.upload.local.path").String()
|
||||
if uploadPath == "" {
|
||||
return "", errors.New("上传路径未配置")
|
||||
}
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
if tenantId == "" {
|
||||
tenantId = "default"
|
||||
}
|
||||
fileTypePath := s.getNewFileTypePath(ctx, newFileType)
|
||||
fullDirPath := uploadPath + "/" + tenantId + "/" + fileTypePath
|
||||
if notAddDate == 0 {
|
||||
nowData := time.Now().Format("20060102")
|
||||
fullDirPath = fullDirPath + "/" + nowData
|
||||
}
|
||||
return fullDirPath, nil
|
||||
}
|
||||
|
||||
func (s *sSysOss) MoveFileLocal(ctx context.Context, filePath string, newFileType int, useOriginalName int, subDirName string, isAddDate int) (result model.UploadResponse, err error) {
|
||||
if filePath == "" {
|
||||
err = errors.New("文件路径必须")
|
||||
return
|
||||
}
|
||||
fileName := gfile.Basename(filePath)
|
||||
fullDirPath, err := s.GetSaveFilePathConfig(ctx, newFileType, isAddDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if subDirName != "" {
|
||||
fullDirPath = fullDirPath + "/" + subDirName
|
||||
}
|
||||
if !gfile.Exists(fullDirPath) {
|
||||
err = gfile.Mkdir(fullDirPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = gfile.Move(filePath, fullDirPath)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysOss MoveFileLocal move file error: %+v, filePath: %s, fullDirPath: %s, newFileType: %d, useOriginalName: %d, subDirName: %s", err, filePath, fullDirPath, newFileType, useOriginalName, subDirName)
|
||||
return
|
||||
}
|
||||
fullFilePath := fullDirPath + "/" + fileName
|
||||
url := s.buildLocalFileURL(ctx, fullFilePath)
|
||||
md5, _ := gmd5.EncryptFile(fullFilePath)
|
||||
crc16 := hash.Crc16ChecksumFile(fullFilePath)
|
||||
sum16 := hash.Sum16ChecksumFile(fullFilePath)
|
||||
result = model.UploadResponse{
|
||||
Size: gfile.Size(fullFilePath),
|
||||
Path: fullFilePath,
|
||||
FullPath: url,
|
||||
OriginalName: fileName,
|
||||
Name: fileName,
|
||||
Type: gfile.ExtName(fileName),
|
||||
Ext: gfile.ExtName(fileName),
|
||||
Md5: md5,
|
||||
Crc16: crc16,
|
||||
Sum16: sum16,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 保存内容
|
||||
func (s *sSysOss) SaveContentLocal(ctx context.Context, content []byte, newFileType int, fileName string, subDirName string, isAddDate int) (result model.UploadResponse, err error) {
|
||||
fullDirPath, err := s.GetSaveFilePathConfig(ctx, newFileType, isAddDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if subDirName != "" {
|
||||
fullDirPath = fullDirPath + "/" + subDirName
|
||||
}
|
||||
if !gfile.Exists(fullDirPath) {
|
||||
err = gfile.Mkdir(fullDirPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
fullFilePath := fullDirPath + "/" + fileName
|
||||
err = gfile.PutBytes(fullFilePath, content)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
url := s.buildLocalFileURL(ctx, fullFilePath)
|
||||
md5, _ := gmd5.EncryptFile(fullFilePath)
|
||||
crc16 := hash.Crc16ChecksumFile(fullFilePath)
|
||||
sum16 := hash.Sum16ChecksumFile(fullFilePath)
|
||||
result = model.UploadResponse{
|
||||
Size: gfile.Size(fullFilePath),
|
||||
Path: fullFilePath,
|
||||
FullPath: url,
|
||||
OriginalName: fileName,
|
||||
Name: fileName,
|
||||
Type: "application/json",
|
||||
Ext: gfile.ExtName(fileName),
|
||||
Md5: md5,
|
||||
Crc16: crc16,
|
||||
Sum16: sum16,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *sSysOss) UploadLocal(ctx context.Context, file *ghttp.UploadFile, newFileType int, useOriginalName int, subDirName string, notAddDate int) (result model.UploadResponse, err error) {
|
||||
if file == nil {
|
||||
err = errors.New("文件必须")
|
||||
return
|
||||
}
|
||||
|
||||
fullDirPath, err := s.GetSaveFilePathConfig(ctx, newFileType, notAddDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if subDirName != "" {
|
||||
fullDirPath = fullDirPath + "/" + subDirName
|
||||
}
|
||||
if !gfile.Exists(fullDirPath) {
|
||||
err = gfile.Mkdir(fullDirPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
fileName := gfile.Basename(file.Filename)
|
||||
ext := gfile.Ext(file.Filename)
|
||||
fileNewName, err := file.Save(fullDirPath, useOriginalName == 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fullFilePath := fullDirPath + "/" + fileNewName
|
||||
url := s.buildLocalFileURL(ctx, fullFilePath)
|
||||
md5, _ := gmd5.EncryptFile(fullFilePath)
|
||||
crc16 := hash.Crc16ChecksumFile(fullFilePath)
|
||||
sum16 := hash.Sum16ChecksumFile(fullFilePath)
|
||||
result = model.UploadResponse{
|
||||
Size: file.Size,
|
||||
Path: fullFilePath,
|
||||
FullPath: url,
|
||||
OriginalName: fileName,
|
||||
Name: fileNewName,
|
||||
Type: file.Header.Get("Content-type"),
|
||||
Ext: ext,
|
||||
Md5: md5,
|
||||
Crc16: crc16,
|
||||
Sum16: sum16,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOss) buildLocalFileURL(ctx context.Context, fullFilePath string) string {
|
||||
fullFilePath = strings.TrimSpace(fullFilePath)
|
||||
if fullFilePath == "" {
|
||||
return ""
|
||||
}
|
||||
configValue := ""
|
||||
configType, err := service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssUrlPath)
|
||||
if err == nil && configType != nil {
|
||||
configValue = strings.TrimSpace(configType.ConfigValue)
|
||||
}
|
||||
if url, ok := localFileURLByMapping(fullFilePath, configValue); ok {
|
||||
return url
|
||||
}
|
||||
|
||||
uploadPath := strings.TrimSpace(g.Cfg().MustGet(ctx, "system.upload.local.path").String())
|
||||
baseURL := localFileURLFallbackBase(configValue)
|
||||
if baseURL != "" {
|
||||
if url, ok := localFileURLByBase(fullFilePath, uploadPath, baseURL); ok {
|
||||
return url
|
||||
}
|
||||
}
|
||||
if url, ok := localFileURLByBase(fullFilePath, uploadPath, "/upload"); ok {
|
||||
return url
|
||||
}
|
||||
if configValue != "" && !strings.Contains(configValue, "|") {
|
||||
return strings.TrimRight(configValue, "/") + "/" + strings.TrimLeft(filepath.ToSlash(fullFilePath), "/")
|
||||
}
|
||||
return fullFilePath
|
||||
}
|
||||
|
||||
func localFileURLFallbackBase(configValue string) string {
|
||||
pairs := localFileURLMappingPairs(configValue)
|
||||
for _, pair := range pairs {
|
||||
if pair[1] != "" {
|
||||
return pair[1]
|
||||
}
|
||||
}
|
||||
configValue = strings.TrimSpace(configValue)
|
||||
if configValue == "" || strings.Contains(configValue, "|") {
|
||||
return ""
|
||||
}
|
||||
return configValue
|
||||
}
|
||||
|
||||
func localFileURLByMapping(fullFilePath string, configValue string) (string, bool) {
|
||||
for _, pair := range localFileURLMappingPairs(configValue) {
|
||||
if suffix, ok := localPathSuffix(fullFilePath, pair[0]); ok {
|
||||
return joinURLPath(pair[1], suffix), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func localFileURLByBase(fullFilePath string, uploadPath string, baseURL string) (string, bool) {
|
||||
if strings.TrimSpace(uploadPath) == "" || strings.TrimSpace(baseURL) == "" {
|
||||
return "", false
|
||||
}
|
||||
suffix, ok := localPathSuffix(fullFilePath, uploadPath)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return joinURLPath(baseURL, suffix), true
|
||||
}
|
||||
|
||||
func localFileURLMappingPairs(configValue string) [][2]string {
|
||||
configValue = strings.TrimSpace(configValue)
|
||||
if configValue == "" {
|
||||
return nil
|
||||
}
|
||||
items := strings.FieldsFunc(configValue, func(r rune) bool {
|
||||
return r == ';' || r == '\n' || r == '\r'
|
||||
})
|
||||
pairs := make([][2]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
parts := strings.SplitN(strings.TrimSpace(item), "|", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
source := strings.TrimSpace(parts[0])
|
||||
target := strings.TrimSpace(parts[1])
|
||||
if source == "" || target == "" {
|
||||
continue
|
||||
}
|
||||
pairs = append(pairs, [2]string{source, target})
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
func localPathSuffix(fullFilePath string, prefix string) (string, bool) {
|
||||
full := normalizeLocalPath(fullFilePath)
|
||||
base := normalizeLocalPath(prefix)
|
||||
if full == "" || base == "" {
|
||||
return "", false
|
||||
}
|
||||
if strings.EqualFold(full, base) {
|
||||
return "", true
|
||||
}
|
||||
prefixWithSlash := strings.TrimRight(base, "/") + "/"
|
||||
if len(full) > len(prefixWithSlash) && strings.EqualFold(full[:len(prefixWithSlash)], prefixWithSlash) {
|
||||
return full[len(prefixWithSlash):], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func normalizeLocalPath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
path = filepath.ToSlash(filepath.Clean(path))
|
||||
return strings.TrimRight(path, "/")
|
||||
}
|
||||
|
||||
func joinURLPath(baseURL string, suffix string) string {
|
||||
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
suffix = strings.TrimLeft(filepath.ToSlash(strings.TrimSpace(suffix)), "/")
|
||||
if suffix == "" {
|
||||
if baseURL == "" {
|
||||
return "/"
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
return baseURL + "/" + suffix
|
||||
}
|
||||
|
||||
func (s *sSysOss) DownloadLocal(ctx context.Context, file *entity.SysOss) (err error) {
|
||||
if file == nil {
|
||||
err = errors.New("文件必须")
|
||||
return
|
||||
}
|
||||
finalPath, err := s.resolveLocalOssPath(ctx, file.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r := ghttp.RequestFromCtx(ctx)
|
||||
if r == nil {
|
||||
err = gerror.New("ctx not http request")
|
||||
return
|
||||
}
|
||||
reader, err := os.Open(finalPath)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "打开下载文件失败")
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
fileInfo, err := reader.Stat()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取下载文件信息失败")
|
||||
}
|
||||
fileName := strings.TrimSpace(file.FileName)
|
||||
if fileName == "" {
|
||||
fileName = filepath.Base(finalPath)
|
||||
}
|
||||
contentType := appOssDownloadContentType(finalPath, file.FileSuffix)
|
||||
r.Response.Header().Set("Content-Type", contentType)
|
||||
r.Response.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
|
||||
r.Response.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, strings.ReplaceAll(fileName, `"`, "")))
|
||||
r.Response.Status = http.StatusOK
|
||||
http.ServeContent(r.Response.Writer, r.Request, fileName, fileInfo.ModTime(), reader)
|
||||
r.Response.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
func appOssDownloadContentType(filePath string, suffix string) string {
|
||||
ext := strings.TrimSpace(suffix)
|
||||
if ext == "" {
|
||||
ext = filepath.Ext(filePath)
|
||||
}
|
||||
ext = strings.TrimPrefix(ext, ".")
|
||||
if ext == "" {
|
||||
return "application/octet-stream"
|
||||
}
|
||||
if contentType := mime.TypeByExtension("." + ext); contentType != "" {
|
||||
return contentType
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
func (s *sSysOss) DeleteLocal(ctx context.Context, file *entity.SysOss) (err error) {
|
||||
if file == nil {
|
||||
err = errors.New("文件不存在")
|
||||
return
|
||||
}
|
||||
filePath, err := s.resolveLocalOssPath(ctx, file.Path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = gfile.Remove(filePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOss) resolveLocalOssPath(ctx context.Context, filePath string) (string, error) {
|
||||
filePath = strings.TrimSpace(filePath)
|
||||
if filePath == "" {
|
||||
return "", errors.New("文件路径为空")
|
||||
}
|
||||
|
||||
candidates := make([]string, 0, 4)
|
||||
addCandidate := func(path string) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
cleaned := filepath.Clean(path)
|
||||
for _, item := range candidates {
|
||||
if item == cleaned {
|
||||
return
|
||||
}
|
||||
}
|
||||
candidates = append(candidates, cleaned)
|
||||
}
|
||||
|
||||
addCandidate(filePath)
|
||||
if !filepath.IsAbs(filePath) {
|
||||
if absPath, err := filepath.Abs(filePath); err == nil {
|
||||
addCandidate(absPath)
|
||||
}
|
||||
if workDir, err := os.Getwd(); err == nil {
|
||||
addCandidate(filepath.Join(workDir, "server", filePath))
|
||||
}
|
||||
if uploadPath := strings.TrimSpace(g.Cfg().MustGet(ctx, "system.upload.local.path").String()); uploadPath != "" && !filepath.IsAbs(uploadPath) {
|
||||
if absUploadPath, err := filepath.Abs(uploadPath); err == nil {
|
||||
normalizedUploadPath := filepath.ToSlash(filepath.Clean(uploadPath))
|
||||
normalizedFilePath := filepath.ToSlash(filepath.Clean(filePath))
|
||||
if strings.HasPrefix(normalizedFilePath, normalizedUploadPath+"/") {
|
||||
suffix := strings.TrimPrefix(normalizedFilePath, normalizedUploadPath+"/")
|
||||
addCandidate(filepath.Join(absUploadPath, filepath.FromSlash(suffix)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if gfile.Exists(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", gerror.Newf("文件不存在:%s", filePath)
|
||||
}
|
||||
|
||||
func (s *sSysOss) CheckType(ctx context.Context, checkFileType string, file *ghttp.UploadFile) (err error) {
|
||||
|
||||
var (
|
||||
configType *entity.SysConfig
|
||||
)
|
||||
|
||||
if checkFileType == consts.SysUploadFileTypeFile {
|
||||
//获取上传类型配置
|
||||
configType, err = service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssFileTypeKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
} else if checkFileType == consts.SysUploadFileTypeImg {
|
||||
//获取上传类型配置
|
||||
configType, err = service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssImgTypeKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("文件检查类型错误:%s|%s, 文件类型:%s", consts.SysUploadFileTypeFile, consts.SysUploadFileTypeImg, checkFileType)
|
||||
}
|
||||
ext := gfile.ExtName(file.Filename)
|
||||
ext = strings.ToLower(ext)
|
||||
imageType := gstr.Split(configType.ConfigValue, ",")
|
||||
rightType := false
|
||||
for _, v := range imageType {
|
||||
if v == ext {
|
||||
rightType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !rightType {
|
||||
err = gerror.New("上传文件类型错误,只能包含后缀为:" + configType.ConfigValue + "的文件。")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOss) CheckSize(ctx context.Context, checkFileType string, file *ghttp.UploadFile) (err error) {
|
||||
|
||||
var (
|
||||
configType *entity.SysConfig
|
||||
)
|
||||
|
||||
if checkFileType == consts.SysUploadFileTypeFile {
|
||||
//获取上传类型配置
|
||||
configType, err = service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssFileSizeKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
} else if checkFileType == consts.SysUploadFileTypeImg {
|
||||
//获取上传类型配置
|
||||
configType, err = service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssImgSizeKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("文件检查类型错误:%s|%s", consts.SysUploadFileTypeFile, consts.SysUploadFileTypeImg)
|
||||
}
|
||||
match, err := gregex.MatchString(`^([0-9]+)(?i:([a-z]*))$`, configType.ConfigValue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(match) == 0 {
|
||||
err = gerror.New("上传文件大小未设置,请在后台配置,格式为(30M,30k,30MB)")
|
||||
return
|
||||
}
|
||||
var cfSize int64
|
||||
switch gstr.ToUpper(match[2]) {
|
||||
case "MB", "M":
|
||||
cfSize = gconv.Int64(match[1]) * 1024 * 1024
|
||||
case "KB", "K":
|
||||
cfSize = gconv.Int64(match[1]) * 1024
|
||||
case "":
|
||||
cfSize = gconv.Int64(match[1])
|
||||
}
|
||||
if cfSize == 0 {
|
||||
err = gerror.New("上传文件大小未设置,请在后台配置,格式为(30M,30k,30MB),最大单位为MB")
|
||||
return
|
||||
}
|
||||
if file.Size > cfSize {
|
||||
err = gerror.New("上传文件大小超过限制,最大为" + configType.ConfigValue)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOss) GetAllUrl(ctx context.Context, url string) (allUrl string, ossId int64, fileSize int64, OriginalName string, md5 string, hmac string, err error) {
|
||||
entity := &entity.SysOss{}
|
||||
s.Model(ctx).Where(dao.SysOss.Columns().Path, url).Scan(&entity)
|
||||
if entity.OssId <= 0 {
|
||||
return
|
||||
}
|
||||
ossId = entity.OssId
|
||||
ossConfig, err := service.SysOssConfig().View(ctx, &model.SysOssConfigViewParam{
|
||||
ConfigKey: entity.Service,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysOss GetAllUrl error: %+v, url: %s", err, url)
|
||||
return "", 0, 0, "", "", "", err
|
||||
}
|
||||
if ossConfig.IsHttps == "Y" {
|
||||
allUrl = "https://" + ossConfig.Domain + entity.Path
|
||||
} else {
|
||||
allUrl = "http://" + ossConfig.Domain + entity.Path
|
||||
}
|
||||
fileSize = gfile.Size(entity.Path)
|
||||
OriginalName = entity.OriginalName
|
||||
md5, _ = gmd5.Encrypt(entity.Path)
|
||||
hmac = ""
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"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 sSysOssConfig struct {
|
||||
}
|
||||
|
||||
func NewSysOssConfig() *sSysOssConfig {
|
||||
return &sSysOssConfig{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysOssConfig(NewSysOssConfig())
|
||||
}
|
||||
|
||||
func (s *sSysOssConfig) 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.SysOssConfig.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sSysOssConfig) List(ctx context.Context, param *model.SysOssConfigListParam) (items []*model.SysOssConfigListModel, total int, err error) {
|
||||
|
||||
db := s.Model(ctx)
|
||||
|
||||
if param.ConfigKey != "" {
|
||||
db = db.WhereLike(dao.SysOssConfig.Columns().ConfigKey, "%"+param.ConfigKey+"%")
|
||||
}
|
||||
|
||||
if param.BucketName != "" {
|
||||
db = db.WhereLike(dao.SysOssConfig.Columns().BucketName, "%"+param.BucketName+"%")
|
||||
}
|
||||
if param.Status != "" {
|
||||
db = db.Where(dao.SysOssConfig.Columns().Status, param.Status)
|
||||
}
|
||||
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = db.Page(param.Page, param.PageSize).OrderAsc(dao.SysOssConfig.Columns().OssConfigId).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOssConfig) View(ctx context.Context, param *model.SysOssConfigViewParam) (item *model.SysOssConfigViewModel, err error) {
|
||||
db := s.Model(ctx)
|
||||
if param.OssConfigId > 0 {
|
||||
db = db.Where(dao.SysOssConfig.Columns().OssConfigId, param.OssConfigId)
|
||||
} else if param.ConfigKey != "" {
|
||||
db = db.Where(dao.SysOssConfig.Columns().ConfigKey, param.ConfigKey)
|
||||
} else {
|
||||
g.Log().Errorf(ctx, "sSysOssConfig View 参数错误: %+v", param)
|
||||
return nil, gerror.New("参数错误")
|
||||
}
|
||||
err = db.Scan(&item)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOssConfig) Add(ctx context.Context, param *model.SysOssConfigAddParam) (item *model.SysOssConfigAddModel, err error) {
|
||||
db := s.Model(ctx)
|
||||
data := &do.SysOssConfig{}
|
||||
gconv.Struct(param, data)
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
data.TenantId = contexts.GetTenantId(ctx)
|
||||
if len(param.Status) == 0 {
|
||||
data.Status = "1"
|
||||
}
|
||||
|
||||
id, err := db.Data(data).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item = &model.SysOssConfigAddModel{
|
||||
OssConfigId: id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOssConfig) Edit(ctx context.Context, param *model.SysOssConfigEditParam) (item *model.SysOssConfigEditModel, err error) {
|
||||
item = &model.SysOssConfigEditModel{
|
||||
OssConfigId: param.OssConfigId,
|
||||
}
|
||||
db := s.Model(ctx)
|
||||
data := &do.SysOssConfig{}
|
||||
gconv.Struct(param, data)
|
||||
data.OssConfigId = nil
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
if param.Status == nil || *param.Status == "" {
|
||||
data.Status = nil
|
||||
}
|
||||
|
||||
_, err = db.Data(data).OmitNil().Where(dao.SysOssConfig.Columns().OssConfigId, param.OssConfigId).Update()
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOssConfig) Delete(ctx context.Context, param *model.SysOssConfigDeleteParam) (item *model.SysOssConfigDeleteModel, err error) {
|
||||
item = &model.SysOssConfigDeleteModel{
|
||||
OssConfigIds: param.OssConfigIds,
|
||||
}
|
||||
db := s.Model(ctx)
|
||||
_, err = db.WhereIn(dao.SysOssConfig.Columns().OssConfigId, param.OssConfigIds).Delete()
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysOssConfig) GetAllUrlByService(ctx context.Context, path string, tenantId string, ossService string) (allUrl string, err error) {
|
||||
ossConfig := &entity.SysOssConfig{}
|
||||
db := s.Model(ctx)
|
||||
db = db.Where(dao.SysOssConfig.Columns().ConfigKey, ossService)
|
||||
db = db.Where(dao.SysOssConfig.Columns().TenantId, tenantId)
|
||||
err = db.Scan(&ossConfig)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysOss GetAllUrlByService error: %+v, url: %s ,service: %s", err, path, ossService)
|
||||
return "", err
|
||||
}
|
||||
if ossConfig == nil {
|
||||
g.Log().Errorf(ctx, "sSysOss GetAllUrlByService error: ossConfig is empty, url: %s ,service: %s", path, ossService)
|
||||
err = gerror.New("ossConfig is empty")
|
||||
return
|
||||
}
|
||||
domain := ossConfig.Endpoint
|
||||
if ossConfig.Domain != "" {
|
||||
domain = ossConfig.Domain
|
||||
}
|
||||
if ossConfig.IsHttps == "Y" {
|
||||
allUrl = "https://" + domain + path
|
||||
} else {
|
||||
allUrl = "http://" + domain + path
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysOss GetAllUrlByService success: %s, url: %s, ossConfig: %+v", ossService, allUrl, ossConfig)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveLocalOssPathFromProjectRoot(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
relativePath := filepath.FromSlash("resource/upload/000000/user/file/test.xlsx")
|
||||
realPath := filepath.Join(tempDir, "server", relativePath)
|
||||
if err := os.MkdirAll(filepath.Dir(realPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir test oss path: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(realPath, []byte("xlsx"), 0o644); err != nil {
|
||||
t.Fatalf("write test oss file: %v", err)
|
||||
}
|
||||
|
||||
oldWd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("get wd: %v", err)
|
||||
}
|
||||
if err = os.Chdir(tempDir); err != nil {
|
||||
t.Fatalf("chdir temp root: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(oldWd); err != nil {
|
||||
t.Fatalf("restore wd: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
got, err := NewSysOss().resolveLocalOssPath(context.Background(), relativePath)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve local oss path: %v", err)
|
||||
}
|
||||
if got != realPath {
|
||||
t.Fatalf("resolved path = %q, want %q", got, realPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppOssDownloadContentTypeXlsx(t *testing.T) {
|
||||
got := appOssDownloadContentType("resource/upload/test.xlsx", "xlsx")
|
||||
if got != "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" {
|
||||
t.Fatalf("content type = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalFileURLByMappingWithProductionUploadPath(t *testing.T) {
|
||||
filePath := "/data/app/iotlcmadmin-web-svc/upload/000000/user/file/20260710/sim-card-renew/result/REN-result.xlsx"
|
||||
configValue := "/data/app/iotlcmadmin-web-svc/upload|http://iotlcm.example.com:8008/upload"
|
||||
|
||||
got, ok := localFileURLByMapping(filePath, configValue)
|
||||
if !ok {
|
||||
t.Fatal("expected production upload path mapping to match")
|
||||
}
|
||||
want := "http://iotlcm.example.com:8008/upload/000000/user/file/20260710/sim-card-renew/result/REN-result.xlsx"
|
||||
if got != want {
|
||||
t.Fatalf("url = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalFileURLByBaseWithProductionUploadPath(t *testing.T) {
|
||||
filePath := "/data/app/iotlcmadmin-web-svc/upload/000000/user/file/20260710/sim-card-renew/import/REN-import.xlsx"
|
||||
got, ok := localFileURLByBase(filePath, "/data/app/iotlcmadmin-web-svc/upload", "http://iotlcm.example.com:8008/upload")
|
||||
if !ok {
|
||||
t.Fatal("expected upload path base mapping to match")
|
||||
}
|
||||
want := "http://iotlcm.example.com:8008/upload/000000/user/file/20260710/sim-card-renew/import/REN-import.xlsx"
|
||||
if got != want {
|
||||
t.Fatalf("url = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalFileURLByMappingWithDevelopmentUploadPath(t *testing.T) {
|
||||
got, ok := localFileURLByMapping(
|
||||
filepath.FromSlash("resource/upload/000000/user/file/test.xlsx"),
|
||||
"resource/upload|/upload",
|
||||
)
|
||||
if !ok {
|
||||
t.Fatal("expected development upload path mapping to match")
|
||||
}
|
||||
if got != "/upload/000000/user/file/test.xlsx" {
|
||||
t.Fatalf("url = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/convert"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/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 sSysPost struct {
|
||||
}
|
||||
|
||||
func NewSysPost() *sSysPost {
|
||||
return &sSysPost{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysPost(NewSysPost())
|
||||
}
|
||||
|
||||
func (l *sSysPost) 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.SysPost.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (l *sSysPost) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: true,
|
||||
})
|
||||
}
|
||||
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysPost.Table()), option...)
|
||||
}
|
||||
|
||||
func (l *sSysPost) List(ctx context.Context, query *model.SysPostListParam) (items []*model.SysPostListModel, total int, err error) {
|
||||
m := l.ModelQuery(ctx)
|
||||
deptIds := make([]int64, 0)
|
||||
if query.DeptId != 0 {
|
||||
deptIds = append(deptIds, query.DeptId)
|
||||
} else if query.BelongDeptId != 0 {
|
||||
deptIds = append(deptIds, query.BelongDeptId)
|
||||
subDeptIds, err := service.SysDept().GetDeptIdsByParentId(ctx, query.BelongDeptId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
deptIds = append(deptIds, subDeptIds...)
|
||||
}
|
||||
if query.PostCode != "" {
|
||||
m = m.WhereLike(dao.SysPost.Columns().PostCode, "%"+query.PostCode+"%")
|
||||
}
|
||||
if query.PostName != "" {
|
||||
m = m.WhereLike(dao.SysPost.Columns().PostName, "%"+query.PostName+"%")
|
||||
}
|
||||
if len(deptIds) > 0 {
|
||||
m = m.WhereIn(dao.SysPost.Columns().DeptId, deptIds)
|
||||
}
|
||||
if query.Status != "" {
|
||||
m = m.Where(dao.SysPost.Columns().Status, query.Status)
|
||||
}
|
||||
if len(query.CreatedAt) == 2 {
|
||||
startTime := gtime.NewFromStr(query.CreatedAt[0])
|
||||
endTime := gtime.NewFromStr(query.CreatedAt[1])
|
||||
m = m.WhereBetween(dao.SysPost.Columns().CreatedAt, startTime, endTime.EndOfDay())
|
||||
}
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = m.WithAll().Page(query.Page, query.PageSize).OrderAsc(dao.SysPost.Columns().PostSort).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysPost) View(ctx context.Context, param *model.SysPostViewParam) (post *model.SysPostViewModel, err error) {
|
||||
err = l.ModelQuery(ctx).WithAll().Where(dao.SysPost.Columns().PostId, param.PostId).Scan(&post)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysPost) Add(ctx context.Context, param *model.SysPostAddParam) (post *model.SysPostAddModel, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
|
||||
}
|
||||
data := do.SysPost{}
|
||||
gconv.Struct(param, &data)
|
||||
data.TenantId = user.TenantId
|
||||
data.CreatedDept = user.DeptId
|
||||
data.CreatedBy = user.ID
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.UpdatedBy = user.ID
|
||||
data.UpdatedAt = gtime.Now()
|
||||
postId, err := l.Model(ctx).Data(data).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
post = &model.SysPostAddModel{
|
||||
PostId: postId,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysPostCreate, postId)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysPost) Edit(ctx context.Context, param *model.SysPostEditParam) (post *model.SysPostEditModel, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
|
||||
}
|
||||
data := do.SysPost{}
|
||||
gconv.Struct(param, &data)
|
||||
data.UpdatedBy = user.ID
|
||||
data.UpdatedAt = gtime.Now()
|
||||
_, err = l.Model(ctx).Data(data).OmitNil().Where(dao.SysPost.Columns().PostId, param.PostId).Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
post = &model.SysPostEditModel{
|
||||
PostId: param.PostId,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysPostUpdate, param.PostId)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysPost) Delete(ctx context.Context, param *model.SysPostDeleteParam) (post *model.SysPostDeleteModel, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
|
||||
}
|
||||
ids := make([]int64, 0)
|
||||
if param.PostId != 0 {
|
||||
ids = append(ids, param.PostId)
|
||||
}
|
||||
if len(param.PostIds) > 0 {
|
||||
ids = append(ids, param.PostIds...)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, gerror.New("请选择要删除的岗位")
|
||||
}
|
||||
data := do.SysPost{}
|
||||
data.DeletedBy = user.ID
|
||||
data.DeletedAt = gtime.Now()
|
||||
_, err = l.Model(ctx).Data(data).OmitNil().WhereIn(dao.SysPost.Columns().PostId, ids).Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
post = &model.SysPostDeleteModel{
|
||||
PostId: param.PostId,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysPostDelete, ids)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysPost) Export(ctx context.Context, param *model.SysPostExportParam) (post *model.SysPostExportModel, err error) {
|
||||
if param.Page == 0 {
|
||||
param.Page = 1
|
||||
}
|
||||
if param.PageSize == 0 {
|
||||
param.PageSize = 1000
|
||||
}
|
||||
items, total, err := l.List(ctx, param.SysPostListParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(model.SysPostExportModel{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = "导出CURD列表-" + gctx.CtxId(ctx)
|
||||
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", total, total+param.PageSize-1/param.PageSize, param.Page, len(items))
|
||||
exports []model.SysPostExportModel
|
||||
)
|
||||
g.Log().Info(ctx, " sSysPost.Export total", total)
|
||||
if err = gconv.Scan(items, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
g.Log().Info(ctx, " sSysPost.Export exports", len(exports))
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
g.Log().Info(ctx, " sSysPost.Export err", err)
|
||||
post = &model.SysPostExportModel{}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gset"
|
||||
"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"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type sSysQrcode struct {
|
||||
}
|
||||
|
||||
func NewSysQrcode() *sSysQrcode {
|
||||
return &sSysQrcode{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysQrcode(NewSysQrcode())
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Second * 60)
|
||||
g.Log().Debug(context.Background(), "检查二维码是否过期,过期则删除, 每分钟检查一次")
|
||||
checkQrcodeExpiredToRemove(context.Background())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type SysQrcodeConfig struct {
|
||||
ThirdId int `json:"thirdId" dc:"第三方ID"`
|
||||
SceneIdLogin int `json:"sceneIdLogin" dc:"场景ID-登录"`
|
||||
SceneIdBind int `json:"sceneIdBind" dc:"场景ID-绑定"`
|
||||
Token string `json:"token" dc:"Token"`
|
||||
ServerApi string `json:"serverApi" dc:"服务端API"`
|
||||
}
|
||||
|
||||
// 检查二维码是否过期,过期则删除
|
||||
func checkQrcodeExpiredToRemove(ctx context.Context) {
|
||||
var sysQrcodeConfig *SysQrcodeConfig
|
||||
g.TryCatch(ctx, func(ctx context.Context) {
|
||||
var err error
|
||||
sysQrcodeConfig, err = getSysQrcodeConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取系统二维码配置失败, 错误: %v", err)
|
||||
return
|
||||
}
|
||||
}, func(ctx context.Context, exception error) {
|
||||
g.Log().Errorf(ctx, "检查二维码是否过期, 获取系统二维码配置失败, 错误: %v", exception)
|
||||
})
|
||||
|
||||
if sysQrcodeConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
g.TryCatch(ctx, func(ctx context.Context) {
|
||||
redisKey := fmt.Sprintf(consts.KeyLoginScanWechat, sysQrcodeConfig.SceneIdLogin)
|
||||
err := checkQrcodeExpiredToRemoveByKey(ctx, redisKey)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "检查登录二维码是否过期失败, 错误: %v", err)
|
||||
return
|
||||
}
|
||||
}, func(ctx context.Context, exception error) {
|
||||
g.Log().Errorf(ctx, "检查登录二维码是否过期, 错误: %v", exception)
|
||||
})
|
||||
|
||||
g.TryCatch(ctx, func(ctx context.Context) {
|
||||
redisKey := fmt.Sprintf(consts.KeyUserBindWechat, sysQrcodeConfig.SceneIdBind)
|
||||
err := checkQrcodeExpiredToRemoveByKey(ctx, redisKey)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "检查绑定二维码是否过期失败, 错误: %v", err)
|
||||
return
|
||||
}
|
||||
}, func(ctx context.Context, exception error) {
|
||||
g.Log().Errorf(ctx, "检查绑定二维码是否过期, 错误: %v", exception)
|
||||
})
|
||||
}
|
||||
|
||||
// 检查指定redisKey的Hash的Field是否过期,过期则删除对应的feild
|
||||
func checkQrcodeExpiredToRemoveByKey(ctx context.Context, redisKey string) (err error) {
|
||||
val, err := g.Redis().HGetAll(ctx, redisKey)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取登录二维码缓存信息失败 redisKey:%s, 错误: %v", redisKey, err)
|
||||
return err
|
||||
}
|
||||
mapData := val.Map()
|
||||
if len(mapData) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for field, v := range mapData {
|
||||
cacheModel := &model.QrcodeCacheModel{}
|
||||
err = gconv.Struct(v, cacheModel)
|
||||
if err != nil {
|
||||
// 删除redisKey
|
||||
g.Log().Debug(ctx, "数据转换失败, 删除对应的数据 redisKey:%s, field:%s", redisKey, field)
|
||||
g.Redis().Do(ctx, "HDEL", redisKey, field)
|
||||
continue
|
||||
}
|
||||
// 判断是否过期 一分钟
|
||||
if cacheModel.ExpireTime.Before(gtime.Now().Add(time.Minute * -1)) {
|
||||
// 删除redisKey
|
||||
g.Log().Debug(ctx, "删除过期数据, redisKey:%s, field:%s", redisKey, field)
|
||||
g.Redis().Do(ctx, "HDEL", redisKey, field)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取系统二维码配置
|
||||
func getSysQrcodeConfig(ctx context.Context) (*SysQrcodeConfig, error) {
|
||||
|
||||
thirdId, err := g.Cfg().Get(ctx, "wx.qrcode.thirdId")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sceneIdLogin, err := g.Cfg().Get(ctx, "wx.qrcode.sceneIdLogin")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sceneIdBind, err := g.Cfg().Get(ctx, "wx.qrcode.sceneIdBind")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := g.Cfg().Get(ctx, "wx.qrcode.token")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverApi, err := g.Cfg().Get(ctx, "wx.qrcode.serverApi")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SysQrcodeConfig{
|
||||
ThirdId: thirdId.Int(),
|
||||
SceneIdLogin: sceneIdLogin.Int(),
|
||||
SceneIdBind: sceneIdBind.Int(),
|
||||
Token: token.String(),
|
||||
ServerApi: serverApi.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 调用二维码API
|
||||
func callQrcodeApi(ctx context.Context, sysQrcodeConfig *SysQrcodeConfig, isLogin bool) (res *model.ApiCallQrcodeModel, err error) {
|
||||
|
||||
// 调用API
|
||||
sceneId := sysQrcodeConfig.SceneIdLogin
|
||||
if !isLogin {
|
||||
sceneId = sysQrcodeConfig.SceneIdBind
|
||||
}
|
||||
request := g.Client()
|
||||
request.SetHeader("Content-Type", "application/json")
|
||||
timeStamp, sign := tokenEncode(sysQrcodeConfig.Token)
|
||||
response, err := request.Get(ctx, sysQrcodeConfig.ServerApi, g.Map{
|
||||
"thirdId": sysQrcodeConfig.ThirdId,
|
||||
"sceneId": sceneId,
|
||||
"timeStamp": timeStamp,
|
||||
"sign": sign,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "调用二维码API失败, 请求数据: %v, 错误: %v", request, err)
|
||||
return nil, err
|
||||
}
|
||||
if response.StatusCode != 200 {
|
||||
g.Log().Errorf(ctx, "调用二维码API失败, 请求数据: %v, 响应数据: %v", request, response)
|
||||
return nil, errors.New("调用二维码API失败")
|
||||
}
|
||||
|
||||
responseData := response.ReadAllString()
|
||||
|
||||
g.Log().Debugf(ctx, "调用二维码API成功, 请求数据: %v, 响应数据: %v", request, responseData)
|
||||
result := &model.ApiCallQrcodeResult{}
|
||||
err = json.Unmarshal([]byte(responseData), &result)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "调用二维码API失败, 数据转换失败, 请求数据: %v, 响应数据: %v, 错误: %v", request, responseData, err)
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
g.Log().Errorf(ctx, "调用二维码API失败, 错误信息: %v", result.Error)
|
||||
return nil, errors.New(result.Error)
|
||||
}
|
||||
return &model.ApiCallQrcodeModel{
|
||||
Ticket: result.Data.Ticket,
|
||||
ExpireTime: result.Data.ExpireTime,
|
||||
QrUrl: result.Data.QrUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取登录二维码
|
||||
func (l *sSysQrcode) GetQrcodeLogin(ctx context.Context) (res *model.QrcodeLoginModel, err error) {
|
||||
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取系统二维码配置失败, 错误: %v", err)
|
||||
return nil, gerror.New("获取系统二维码配置失败")
|
||||
}
|
||||
if sysQrcodeConfig.ServerApi == "" || sysQrcodeConfig.Token == "" || sysQrcodeConfig.ThirdId == 0 || sysQrcodeConfig.SceneIdLogin == 0 || sysQrcodeConfig.SceneIdBind == 0 {
|
||||
return nil, errors.New("系统二维码配置错误")
|
||||
}
|
||||
apiResult, err := callQrcodeApi(ctx, sysQrcodeConfig, true)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取二维码失败, 错误: %v", err)
|
||||
return nil, gerror.New("获取二维码失败")
|
||||
}
|
||||
|
||||
expireTime := gtime.Now().Add(time.Second * time.Duration(apiResult.ExpireTime))
|
||||
// 将结果保存到redis中
|
||||
tempUserId := uuid.New().String()
|
||||
redisKey := fmt.Sprintf(consts.KeyLoginScanWechat, sysQrcodeConfig.SceneIdLogin)
|
||||
cacheModel := &model.QrcodeCacheModel{
|
||||
Ticket: apiResult.Ticket,
|
||||
ExpireTime: expireTime,
|
||||
QrUrl: apiResult.QrUrl,
|
||||
Scanned: false,
|
||||
OpenId: "",
|
||||
}
|
||||
mapData := make(map[string]interface{})
|
||||
mapData[tempUserId] = cacheModel
|
||||
g.Redis().HSet(ctx, redisKey, mapData)
|
||||
|
||||
return &model.QrcodeLoginModel{
|
||||
TempUserId: tempUserId,
|
||||
QrcodeCacheModel: *cacheModel,
|
||||
Bound: false,
|
||||
Expired: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 对openId进行加密
|
||||
func openIdEncode(openId string) string {
|
||||
data := []byte(openId)
|
||||
hash := md5.Sum(data)
|
||||
md5String := hex.EncodeToString(hash[:])
|
||||
return base64.StdEncoding.EncodeToString([]byte(openId + "," + md5String))
|
||||
}
|
||||
|
||||
// 对openId进行解密
|
||||
func openIdDecode(openIdEncode string) (string, error) {
|
||||
decode, err := base64.StdEncoding.DecodeString(openIdEncode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
strs := strings.Split(string(decode), ",")
|
||||
if len(strs) != 2 {
|
||||
return "", errors.New("openId 格式错误")
|
||||
}
|
||||
|
||||
openId := strs[0]
|
||||
md5String := strs[1]
|
||||
|
||||
data := []byte(openId)
|
||||
hash := md5.Sum(data)
|
||||
md5String2 := hex.EncodeToString(hash[:])
|
||||
if md5String != md5String2 {
|
||||
return "", errors.New("openId 格式错误")
|
||||
}
|
||||
|
||||
return openId, nil
|
||||
}
|
||||
|
||||
// 对token进行加密
|
||||
func tokenEncode(token string) (timeStamp int64, md5String string) {
|
||||
nowTime := time.Now().Unix()
|
||||
tokenStr := fmt.Sprintf("%d%s", nowTime, token)
|
||||
data := []byte(tokenStr)
|
||||
hash := md5.Sum(data)
|
||||
md5String = hex.EncodeToString(hash[:])
|
||||
return nowTime, md5String
|
||||
}
|
||||
|
||||
func tokenTimeStampEncode(timeStamp int64, token string) (md5String string) {
|
||||
tokenStr := fmt.Sprintf("%d%s", timeStamp, token)
|
||||
data := []byte(tokenStr)
|
||||
hash := md5.Sum(data)
|
||||
md5String = hex.EncodeToString(hash[:])
|
||||
return md5String
|
||||
}
|
||||
|
||||
// 获取登录二维码扫码结果
|
||||
func (l *sSysQrcode) GetQrcodeLoginStatus(ctx context.Context, tempUserId string) (res *model.QrcodeLoginModel, err error) {
|
||||
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取系统二维码配置失败, 错误: %v", err)
|
||||
return nil, gerror.New("获取系统二维码配置失败")
|
||||
}
|
||||
|
||||
res = &model.QrcodeLoginModel{
|
||||
TempUserId: tempUserId,
|
||||
QrcodeCacheModel: model.QrcodeCacheModel{
|
||||
Ticket: "",
|
||||
ExpireTime: gtime.Now(),
|
||||
QrUrl: "",
|
||||
Scanned: false,
|
||||
OpenId: "",
|
||||
},
|
||||
Bound: false,
|
||||
Expired: false,
|
||||
}
|
||||
|
||||
// 获取不到缓存,统一标记为过期
|
||||
redisKey := fmt.Sprintf(consts.KeyLoginScanWechat, sysQrcodeConfig.SceneIdLogin)
|
||||
cashValue, err := g.Redis().HGet(ctx, redisKey, tempUserId)
|
||||
if err != nil || cashValue == nil || cashValue.Map() == nil {
|
||||
g.Log().Errorf(ctx, "未获取到%s的二维码扫码结果, 错误: %v", tempUserId, err)
|
||||
res.Expired = true
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 将val 的数据保存到cacheModel中'
|
||||
cacheModel := &model.QrcodeCacheModel{}
|
||||
g.Log().Debugf(ctx, "获取登录二维码扫码结果, 缓存数据: %v", cashValue.Map())
|
||||
mapData := cashValue.Map()
|
||||
err = gconv.Struct(mapData, cacheModel)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取登录二维码扫码结果失败, 错误: %v", err)
|
||||
res.Expired = true
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 将缓存结果赋值给返回结果
|
||||
res.Ticket = cacheModel.Ticket
|
||||
res.ExpireTime = cacheModel.ExpireTime
|
||||
res.QrUrl = cacheModel.QrUrl
|
||||
res.Scanned = cacheModel.Scanned
|
||||
res.OpenId = openIdEncode(cacheModel.OpenId)
|
||||
g.Log().Debugf(ctx, "获取登录二维码扫码结果, 缓存数据: %v", cacheModel)
|
||||
|
||||
//先看扫码状态
|
||||
if !cacheModel.Scanned {
|
||||
// 未扫码 需要判断是否过期
|
||||
if cacheModel.ExpireTime.Before(gtime.Now()) {
|
||||
res.Expired = true
|
||||
// 删除redisKey
|
||||
g.Redis().Do(ctx, "HDEL", redisKey, tempUserId)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
// 已扫码 需要判断是否绑定
|
||||
//根据openId和source查询数据库
|
||||
userBindList, _, err := service.SysSocial().List(ctx, &model.SysSocialListParam{
|
||||
OpenId: cacheModel.OpenId,
|
||||
Source: "wechat_mp",
|
||||
}, &request.PageInfo{Page: 1, PageSize: 100})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取社会化关系表信息失败, 错误: %v", err)
|
||||
return nil, gerror.New("获取绑定信息失败")
|
||||
}
|
||||
if len(userBindList) == 0 {
|
||||
res.Bound = false
|
||||
// 删除redisKey
|
||||
g.Redis().Do(ctx, "HDEL", redisKey, tempUserId)
|
||||
return res, nil
|
||||
}
|
||||
validBindUserList := make([]*model.SysSocialListModel, 0)
|
||||
tenantIdList := gset.NewStrSet()
|
||||
for _, bindUser := range userBindList {
|
||||
user, err := service.SysUser().GetUserByUsername(ctx, bindUser.UserName, bindUser.TenantId)
|
||||
if err != nil || user == nil {
|
||||
g.Log().Errorf(ctx, "关联信息无效, Id:%d, openId:%s, source:%s, userName:%s, tenantId:%s", bindUser.Id, cacheModel.OpenId, "wechat_mp", bindUser.UserName, bindUser.TenantId)
|
||||
continue
|
||||
}
|
||||
bindUser.User = &model.SysUserViewModel{
|
||||
UserId: user.UserId,
|
||||
UserName: user.UserName,
|
||||
NickName: user.NickName,
|
||||
Avatar: user.Avatar,
|
||||
TenantId: user.TenantId,
|
||||
}
|
||||
tenantIdList.Add(bindUser.TenantId)
|
||||
validBindUserList = append(validBindUserList, bindUser)
|
||||
}
|
||||
//租户Idmap
|
||||
tenantMap := make(map[string]*model.SysTenantListModel)
|
||||
if tenantIdList.Size() > 0 {
|
||||
// 查询租户列表
|
||||
tenantList := make([]*model.SysTenantListModel, 0)
|
||||
err := dao.SysTenant.Ctx(ctx).WhereIn(dao.SysTenant.Columns().TenantId, tenantIdList.Slice()).Scan(&tenantList)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取租户列表失败, 错误: %v", err)
|
||||
return nil, gerror.New("获取租户列表失败")
|
||||
}
|
||||
for _, tenant := range tenantList {
|
||||
tenantMap[tenant.TenantId] = tenant
|
||||
}
|
||||
}
|
||||
|
||||
if len(validBindUserList) == 1 {
|
||||
//单账号绑定 执行登录操作
|
||||
bindUser := validBindUserList[0]
|
||||
res.Bound = true
|
||||
|
||||
// 执行登录操作
|
||||
userOut, token, err := service.SysAuth().LoginByOpenId(ctx, bindUser)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "登录操作失败, 错误: %v", err)
|
||||
return nil, gerror.New("登录失败, 请稍后重试")
|
||||
}
|
||||
res.LoginUserOut = userOut
|
||||
res.Token = token
|
||||
} else {
|
||||
// 多账号绑定,返回用户列表
|
||||
userList := make([]*model.SysUserSocialModel, 0)
|
||||
|
||||
for _, bindUser := range validBindUserList {
|
||||
user := bindUser.User
|
||||
tenant := tenantMap[user.TenantId]
|
||||
tenantName := ""
|
||||
if tenant != nil {
|
||||
tenantName = tenant.CompanyName
|
||||
}
|
||||
userList = append(userList, &model.SysUserSocialModel{
|
||||
TenantId: user.TenantId,
|
||||
UserId: user.UserId,
|
||||
UserName: user.UserName,
|
||||
NickName: user.NickName,
|
||||
Phonenumber: user.Phonenumber,
|
||||
Avatar: user.Avatar,
|
||||
TenantName: tenantName,
|
||||
})
|
||||
}
|
||||
res.UserList = userList
|
||||
|
||||
}
|
||||
// 删除redisKey
|
||||
g.Redis().Do(ctx, "HDEL", redisKey, tempUserId)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 获取绑定二维码
|
||||
func (l *sSysQrcode) GetQrcodeBind(ctx context.Context) (res *model.QrcodeCacheModel, err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId == 0 {
|
||||
return nil, gerror.New("用户未登录")
|
||||
}
|
||||
|
||||
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sysQrcodeConfig.ServerApi == "" || sysQrcodeConfig.Token == "" || sysQrcodeConfig.ThirdId == 0 || sysQrcodeConfig.SceneIdLogin == 0 || sysQrcodeConfig.SceneIdBind == 0 {
|
||||
return nil, errors.New("系统二维码配置错误")
|
||||
}
|
||||
apiResult, err := callQrcodeApi(ctx, sysQrcodeConfig, false)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取绑定二维码失败, 错误: %v", err)
|
||||
return nil, gerror.New("获取绑定二维码失败")
|
||||
}
|
||||
|
||||
expireTime := gtime.Now().Add(time.Second * time.Duration(apiResult.ExpireTime))
|
||||
// 将结果保存到redis中
|
||||
res = &model.QrcodeCacheModel{
|
||||
Ticket: apiResult.Ticket,
|
||||
ExpireTime: expireTime,
|
||||
QrUrl: apiResult.QrUrl,
|
||||
Scanned: false,
|
||||
OpenId: "",
|
||||
}
|
||||
|
||||
redisKey := fmt.Sprintf(consts.KeyUserBindWechat, sysQrcodeConfig.SceneIdBind)
|
||||
mapData := make(map[string]interface{})
|
||||
g.Log().Debugf(ctx, "获取绑定二维码, 缓存数据: %v", res)
|
||||
mapData[strconv.FormatInt(userId, 10)] = res
|
||||
g.Log().Debugf(ctx, "用户 %d 设置绑定二维码缓存: key: %s field: %s value: %v", userId, redisKey, strconv.FormatInt(userId, 10), res)
|
||||
g.Redis().HSet(ctx, redisKey, mapData)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 获取绑定二维码扫码结果
|
||||
func (l *sSysQrcode) GetQrcodeBindStatus(ctx context.Context) (res *model.QrcodeBindStatusModel, err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if userId == 0 {
|
||||
return nil, gerror.New("用户未登录")
|
||||
}
|
||||
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sysQrcodeConfig.ServerApi == "" || sysQrcodeConfig.Token == "" || sysQrcodeConfig.ThirdId == 0 || sysQrcodeConfig.SceneIdLogin == 0 || sysQrcodeConfig.SceneIdBind == 0 {
|
||||
return nil, errors.New("系统二维码配置错误")
|
||||
}
|
||||
|
||||
redisKey := fmt.Sprintf(consts.KeyUserBindWechat, sysQrcodeConfig.SceneIdBind)
|
||||
val, err := g.Redis().HGet(ctx, redisKey, strconv.FormatInt(userId, 10))
|
||||
g.Log().Debugf(ctx, "获取绑定二维码扫码结果, 用户 %d 的缓存数据: %v", userId, val.Map())
|
||||
if err != nil || val == nil || val.Map() == nil {
|
||||
return &model.QrcodeBindStatusModel{
|
||||
QrcodeCacheModel: model.QrcodeCacheModel{
|
||||
Ticket: "",
|
||||
ExpireTime: gtime.Now(),
|
||||
QrUrl: "",
|
||||
},
|
||||
Expired: true,
|
||||
Bound: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
cacheModel := &model.QrcodeCacheModel{}
|
||||
mapData := val.Map()
|
||||
err = gconv.Struct(mapData, cacheModel)
|
||||
if err != nil {
|
||||
return nil, gerror.New("获取绑定二维码扫码结果失败")
|
||||
}
|
||||
res = &model.QrcodeBindStatusModel{
|
||||
QrcodeCacheModel: *cacheModel,
|
||||
Bound: false,
|
||||
Expired: false,
|
||||
}
|
||||
if !cacheModel.Scanned {
|
||||
// TODO 判断是否过期
|
||||
if cacheModel.ExpireTime.Before(gtime.Now()) {
|
||||
res.Expired = true
|
||||
// 删除redisKey
|
||||
g.Redis().Do(ctx, "HDEL", redisKey, strconv.FormatInt(userId, 10))
|
||||
}
|
||||
return res, nil
|
||||
} else {
|
||||
// 已扫码 需要判断是否绑定
|
||||
// 判断是否已绑定
|
||||
userBindList, _, err := service.SysSocial().List(ctx, &model.SysSocialListParam{
|
||||
UserId: userId,
|
||||
OpenId: cacheModel.OpenId,
|
||||
Source: "wechat_mp",
|
||||
}, &request.PageInfo{Page: 1, PageSize: 1})
|
||||
if err != nil {
|
||||
return nil, gerror.New("获取绑定信息失败")
|
||||
}
|
||||
if len(userBindList) > 0 {
|
||||
res.Bound = true
|
||||
} else {
|
||||
res.Bound = false
|
||||
}
|
||||
// 删除redisKey
|
||||
g.Redis().Do(ctx, "HDEL", redisKey, strconv.FormatInt(userId, 10))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 扫码回调
|
||||
func (l *sSysQrcode) QrcodeScanCallback(ctx context.Context, param *model.QrcodeScanCallbackParam) (err error) {
|
||||
g.Log().Debug(ctx, "扫码回调, 参数: %v", param)
|
||||
if param.TimeStamp == 0 || param.Sign == "" {
|
||||
return gerror.New("签名或时间戳不能为空")
|
||||
}
|
||||
|
||||
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sysQrcodeConfig.ServerApi == "" || sysQrcodeConfig.Token == "" || sysQrcodeConfig.ThirdId == 0 || sysQrcodeConfig.SceneIdLogin == 0 || sysQrcodeConfig.SceneIdBind == 0 {
|
||||
return errors.New("系统二维码配置错误")
|
||||
}
|
||||
|
||||
//nowTime := gtime.Now().Unix()
|
||||
sign := tokenTimeStampEncode(param.TimeStamp, sysQrcodeConfig.Token)
|
||||
// if math.Abs(float64(param.TimeStamp-nowTime)) > 60 {
|
||||
// return gerror.New("时间戳无效")
|
||||
// }
|
||||
if param.Sign != sign {
|
||||
return gerror.New("签名无效")
|
||||
}
|
||||
|
||||
if param.SceneId == sysQrcodeConfig.SceneIdLogin {
|
||||
redisKey := fmt.Sprintf(consts.KeyLoginScanWechat, param.SceneId)
|
||||
// 登录回调 更新一个扫码缓存状态为已扫码
|
||||
val, err := g.Redis().HGetAll(ctx, redisKey)
|
||||
if err != nil {
|
||||
return gerror.New("获取登录二维码扫码结果失败")
|
||||
}
|
||||
|
||||
g.Log().Debugf(ctx, "登录回调, 获取登录二维码扫码结果: %v", val)
|
||||
mapData := val.Map()
|
||||
if len(mapData) == 0 {
|
||||
return gerror.New("获取登录二维码扫码结果失败")
|
||||
}
|
||||
|
||||
for tempUserId, v := range mapData {
|
||||
cacheModel := &model.QrcodeCacheModel{}
|
||||
err = gconv.Struct(v, cacheModel)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "缓存数据转换失败,数据: %v, 错误: %v", v, err)
|
||||
continue
|
||||
}
|
||||
if cacheModel.Scanned || cacheModel.Ticket != param.Ticket {
|
||||
continue
|
||||
}
|
||||
// 未扫码
|
||||
cacheModel.Scanned = true
|
||||
cacheModel.OpenId = param.OpenId
|
||||
updateMap := make(map[string]interface{})
|
||||
updateMap[tempUserId] = cacheModel
|
||||
g.Redis().HSet(ctx, redisKey, updateMap)
|
||||
break
|
||||
}
|
||||
}
|
||||
if param.SceneId == sysQrcodeConfig.SceneIdBind {
|
||||
// 绑定回调 保存绑定结果
|
||||
redisKey := fmt.Sprintf(consts.KeyUserBindWechat, param.SceneId)
|
||||
val, err := g.Redis().HGetAll(ctx, redisKey)
|
||||
if err != nil {
|
||||
return gerror.New("获取绑定二维码扫码结果失败 redisKey: " + redisKey)
|
||||
}
|
||||
mapData := val.Map()
|
||||
if len(mapData) == 0 {
|
||||
return gerror.New("获取绑定二维码扫码结果失败 redisKey: " + redisKey)
|
||||
}
|
||||
for userId, v := range mapData {
|
||||
cacheModel := &model.QrcodeCacheModel{}
|
||||
err = gconv.Struct(v, cacheModel)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "缓存数据转换失败,数据: %v, 错误: %v", v, err)
|
||||
continue
|
||||
}
|
||||
if cacheModel.Scanned || cacheModel.Ticket != param.Ticket {
|
||||
continue
|
||||
}
|
||||
// 未扫码
|
||||
cacheModel.Scanned = true
|
||||
cacheModel.OpenId = param.OpenId
|
||||
updateMap := make(map[string]interface{})
|
||||
updateMap[userId] = cacheModel
|
||||
g.Redis().HSet(ctx, redisKey, updateMap)
|
||||
|
||||
user := &entity.SysUser{}
|
||||
// 查询用户信息
|
||||
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, gconv.Int64(userId)).Scan(&user)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查询用户信息失败, 错误: %v", err)
|
||||
return gerror.New("查询用户信息失败")
|
||||
}
|
||||
if user == nil {
|
||||
g.Log().Errorf(ctx, "用户不存在, 用户ID: %s", userId)
|
||||
return gerror.New("用户不存在")
|
||||
}
|
||||
|
||||
// 保存绑定结果
|
||||
err = service.SysSocial().Create(ctx, &model.SysSocialSaveParam{
|
||||
UserId: gconv.Int64(userId),
|
||||
AuthId: fmt.Sprintf("%s-%s", consts.SourceWechatMp, param.OpenId),
|
||||
OpenId: param.OpenId,
|
||||
Source: consts.SourceWechatMp,
|
||||
UserName: user.UserName,
|
||||
AccessToken: param.OpenId,
|
||||
TenantId: user.TenantId,
|
||||
CreatedBy: 0, // 创建者为0 表示系统自动创建
|
||||
CreatedAt: gtime.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "保存绑定结果失败, 错误: %v", err)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 扫码后选择用户登录
|
||||
func (l *sSysQrcode) QrcodeLoginSelectUserId(ctx context.Context, param *model.QrcodeLoginSelectUserIdParam) (res *model.QrcodeLoginAndBindOpenIdModel, err error) {
|
||||
openId, err := openIdDecode(param.OpenId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "openId 解密失败, 错误: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
g.Log().Debugf(ctx, "扫码后选择用户登录, 参数: %v", param)
|
||||
user, err := service.SysUser().GetUserByUsername(ctx, param.UserName, param.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, gerror.New("用户不存在")
|
||||
}
|
||||
|
||||
userOut, token, err := service.SysAuth().LoginByOpenId(ctx, &model.SysSocialListModel{
|
||||
UserId: user.UserId,
|
||||
OpenId: openId,
|
||||
Source: consts.SourceWechatMp,
|
||||
TenantId: user.TenantId,
|
||||
CreatedBy: 0, // 创建者为0 表示系统自动创建
|
||||
CreatedAt: gtime.Now(),
|
||||
User: &model.SysUserViewModel{
|
||||
UserId: user.UserId,
|
||||
UserName: user.UserName,
|
||||
NickName: user.NickName,
|
||||
Avatar: user.Avatar,
|
||||
TenantId: user.TenantId,
|
||||
},
|
||||
})
|
||||
return &model.QrcodeLoginAndBindOpenIdModel{
|
||||
LoginUserOut: userOut,
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 登录并绑定
|
||||
func (l *sSysQrcode) QrcodeLoginAndBindOpenId(ctx context.Context, param *model.QrcodeLoginAndBindOpenIdParam) (res *model.QrcodeLoginAndBindOpenIdModel, err error) {
|
||||
openId, err := openIdDecode(param.OpenId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "openId 解密失败, 错误: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// 1. 执行登录
|
||||
userOut, token, err := service.SysAuth().Login(ctx, &model.LoginParams{
|
||||
Username: param.Username,
|
||||
Password: param.Password,
|
||||
CaptchaID: param.CaptchaID,
|
||||
CaptchaValue: param.CaptchaValue,
|
||||
TenantId: param.TenantId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 2. 执行绑定
|
||||
err = service.SysSocial().Create(ctx, &model.SysSocialSaveParam{
|
||||
UserId: userOut.ID,
|
||||
AuthId: fmt.Sprintf("%s-%s", consts.SourceWechatMp, openId),
|
||||
OpenId: openId,
|
||||
Source: consts.SourceWechatMp,
|
||||
UserName: param.Username,
|
||||
AccessToken: openId,
|
||||
TenantId: param.TenantId,
|
||||
CreatedBy: 0, // 创建者为0 表示系统自动创建
|
||||
CreatedAt: gtime.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.QrcodeLoginAndBindOpenIdModel{
|
||||
LoginUserOut: userOut,
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 注册并绑定
|
||||
func (l *sSysQrcode) QrcodeRegisterAndBindOpenId(ctx context.Context, param *model.QrcodeRegisterAndBindOpenIdParam) (res *model.QrcodeLoginAndBindOpenIdModel, err error) {
|
||||
openId, err := openIdDecode(param.OpenId)
|
||||
if err != nil || len(openId) == 0 {
|
||||
g.Log().Errorf(ctx, "openId 解密失败, 错误: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// 1. 执行注册
|
||||
err = service.SysUser().Register(ctx, &model.SysUserRegisterModel{
|
||||
UserName: param.UserName,
|
||||
Password: param.Password,
|
||||
TenantId: param.TenantId,
|
||||
CaptchaID: param.CaptchaID,
|
||||
CaptchaValue: param.CaptchaValue,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := service.SysUser().GetUserByUsername(ctx, param.UserName, param.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, gerror.New("用户注册失败")
|
||||
}
|
||||
// 2. 执行绑定
|
||||
err = service.SysSocial().Create(ctx, &model.SysSocialSaveParam{
|
||||
UserId: user.UserId,
|
||||
AuthId: fmt.Sprintf("%s-%s", consts.SourceWechatMp, openId),
|
||||
OpenId: openId,
|
||||
Source: consts.SourceWechatMp,
|
||||
UserName: param.UserName,
|
||||
AccessToken: openId,
|
||||
TenantId: param.TenantId,
|
||||
CreatedBy: 0, // 创建者为0 表示系统自动创建
|
||||
CreatedAt: gtime.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 执行登录
|
||||
userOut, token, err := service.SysAuth().LoginByOpenId(ctx, &model.SysSocialListModel{
|
||||
UserId: user.UserId,
|
||||
OpenId: openId,
|
||||
Source: consts.SourceWechatMp,
|
||||
TenantId: param.TenantId,
|
||||
CreatedBy: 0, // 创建者为0 表示系统自动创建
|
||||
CreatedAt: gtime.Now(),
|
||||
User: &model.SysUserViewModel{
|
||||
UserId: user.UserId,
|
||||
UserName: user.UserName,
|
||||
NickName: user.NickName,
|
||||
Avatar: user.Avatar,
|
||||
TenantId: user.TenantId,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.QrcodeLoginAndBindOpenIdModel{
|
||||
LoginUserOut: userOut,
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
"slices"
|
||||
|
||||
"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 sSysRole struct {
|
||||
}
|
||||
|
||||
func NewSysRole() *sSysRole {
|
||||
return &sSysRole{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysRole(NewSysRole())
|
||||
}
|
||||
|
||||
func (l *sSysRole) Model(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.SysRole.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (l *sSysRole) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: true,
|
||||
FilterAuth: false, // 角色表不验证数据权限
|
||||
})
|
||||
}
|
||||
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysRole.Table()), option...)
|
||||
}
|
||||
|
||||
// 获取租户下角色列表
|
||||
func (s *sSysRole) List(ctx context.Context, param *model.SysRoleListParam) (res []*model.SysRoleListModel, total int, err error) {
|
||||
db := s.ModelQuery(ctx)
|
||||
// 注意:ModelQuery使用内存数据库,删除的记录会被物理删除,所以不需要过滤deleted_at
|
||||
if param.RoleName != "" {
|
||||
db = db.WhereLike(dao.SysRole.Columns().RoleName, "%"+param.RoleName+"%")
|
||||
}
|
||||
if param.Status != "" {
|
||||
db = db.Where(dao.SysRole.Columns().Status, param.Status)
|
||||
}
|
||||
if param.DeptId != nil {
|
||||
db = db.Where(dao.SysRole.Columns().DeptId, *param.DeptId)
|
||||
}
|
||||
if len(param.CreatedAt) == 2 {
|
||||
start, end := gtime.NewFromStr(param.CreatedAt[0]), gtime.NewFromStr(param.CreatedAt[1])
|
||||
db = db.WhereBetween(dao.SysRole.Columns().CreatedAt, start, end.EndOfDay())
|
||||
}
|
||||
if len(param.RoleIds) > 0 {
|
||||
db = db.WhereIn(dao.SysRole.Columns().RoleId, param.RoleIds)
|
||||
}
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if total == 0 {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
companyMap, err := service.SysDept().GetDeptCompanyMap(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
g.Log().Debugf(ctx, "sSysRole.List companyMap: %v", companyMap)
|
||||
err = db.Page(param.Page, param.PageSize).Scan(&res)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, role := range res {
|
||||
role.DeptName = companyMap[role.DeptId]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取角色详情
|
||||
func (s *sSysRole) View(ctx context.Context, param *model.SysRoleViewParam) (res *model.SysRoleViewModel, err error) {
|
||||
err = s.ModelQuery(ctx).Where(dao.SysRole.Columns().RoleId, param.RoleId).Scan(&res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 获取角色菜单
|
||||
roleMenus, err := s.GetRoleMenu(ctx, param.RoleId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
menuIdList := make([]int64, 0)
|
||||
for _, menu := range roleMenus {
|
||||
menuIdList = append(menuIdList, menu.MenuId)
|
||||
}
|
||||
res.MenuIds = menuIdList
|
||||
// 获取角色部门
|
||||
roleDepts, err := s.GetRoleDept(ctx, param.RoleId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deptIdList := make([]int64, 0)
|
||||
for _, dept := range roleDepts {
|
||||
deptIdList = append(deptIdList, dept.DeptId)
|
||||
}
|
||||
res.DeptIds = deptIdList
|
||||
return
|
||||
}
|
||||
|
||||
// 获取角色菜单
|
||||
func (s *sSysRole) GetRoleMenu(ctx context.Context, id int64) (res []*entity.SysMenu, err error) {
|
||||
mod := dao.SysRoleMenu.Ctx(ctx)
|
||||
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
|
||||
mod = mod.Where("tenant_id", tenantId)
|
||||
}
|
||||
err = mod.Where(dao.SysRoleMenu.Columns().RoleId, id).Scan(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取角色部门
|
||||
func (s *sSysRole) GetRoleDept(ctx context.Context, id int64) (res []*entity.SysDept, err error) {
|
||||
mod := dao.SysRoleDept.Ctx(ctx)
|
||||
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
|
||||
mod = mod.Where("tenant_id", tenantId)
|
||||
}
|
||||
err = mod.Where(dao.SysRoleDept.Columns().RoleId, id).Scan(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取角色列表对应菜单
|
||||
func (s *sSysRole) GetRoleListMenu(ctx context.Context, ids []int64) (res []*entity.SysRoleMenu, err error) {
|
||||
res = make([]*entity.SysRoleMenu, 0)
|
||||
if slices.Contains(ids, consts.SuperAdminRoleId) {
|
||||
menus, _, err := service.SysMenu().List(ctx, &model.SysMenuListParam{
|
||||
Status: consts.SysMenuStatusNormal,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, menu := range menus {
|
||||
res = append(res, &entity.SysRoleMenu{
|
||||
RoleId: consts.SuperAdminRoleId,
|
||||
MenuId: menu.MenuId,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
mod := dao.SysRoleMenu.Ctx(ctx)
|
||||
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
|
||||
mod = mod.Where("tenant_id", tenantId)
|
||||
}
|
||||
err = mod.WhereIn(dao.SysRoleMenu.Columns().RoleId, ids).Scan(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// 新增角色
|
||||
func (s *sSysRole) Add(ctx context.Context, param *model.SysRoleAddParam) (role *model.SysRoleAddModel, err error) {
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil, gerror.New("登录已过期")
|
||||
}
|
||||
data := do.SysRole{}
|
||||
gconv.Struct(param, &data)
|
||||
data.DataScope = "1"
|
||||
data.CreatedBy = user.ID
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.CreatedDept = user.DeptId
|
||||
data.TenantId = user.TenantId
|
||||
data.UpdatedBy = user.ID
|
||||
data.UpdatedAt = gtime.Now()
|
||||
|
||||
roleId, err := s.Model(ctx).Data(data).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleCreate, roleId)
|
||||
|
||||
role = &model.SysRoleAddModel{
|
||||
RoleId: roleId,
|
||||
}
|
||||
// 新增角色菜单
|
||||
err = s.RoleMenu(ctx, roleId, param.MenuIds)
|
||||
if err != nil {
|
||||
return role, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 编辑角色
|
||||
func (s *sSysRole) Edit(ctx context.Context, param *model.SysRoleEditParam) (role *model.SysRoleEditModel, err error) {
|
||||
role = &model.SysRoleEditModel{
|
||||
RoleId: param.RoleId,
|
||||
}
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return role, gerror.New("登录已过期")
|
||||
}
|
||||
data := do.SysRole{}
|
||||
gconv.Struct(param, &data)
|
||||
data.UpdatedBy = user.ID
|
||||
data.UpdatedAt = gtime.Now()
|
||||
|
||||
_, err = s.Model(ctx).Data(data).Where(dao.SysRole.Columns().RoleId, param.RoleId).OmitNil().Update()
|
||||
if err != nil {
|
||||
return role, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, param.RoleId)
|
||||
// 更新角色菜单
|
||||
err = s.RoleMenu(ctx, param.RoleId, param.MenuIds)
|
||||
if err != nil {
|
||||
return role, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
func (s *sSysRole) Delete(ctx context.Context, param *model.SysRoleDeleteParam) (role *model.SysRoleDeleteModel, err error) {
|
||||
role = &model.SysRoleDeleteModel{
|
||||
RoleId: param.RoleId,
|
||||
RoleIds: param.RoleIds,
|
||||
}
|
||||
roleIds := make([]int64, 0)
|
||||
if param.RoleId != 0 {
|
||||
roleIds = append(roleIds, param.RoleId)
|
||||
} else {
|
||||
roleIds = param.RoleIds
|
||||
}
|
||||
if len(roleIds) == 0 {
|
||||
return role, gerror.New("请选择要删除的角色")
|
||||
}
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return role, gerror.New("登录已过期")
|
||||
}
|
||||
data := do.SysRole{}
|
||||
data.DeletedBy = user.ID
|
||||
data.DeletedAt = gtime.Now()
|
||||
_, err = s.Model(ctx).WhereIn(dao.SysRole.Columns().RoleId, roleIds).Data(data).OmitNil().Update()
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleDelete, roleIds)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysRole) RoleMenu(ctx context.Context, roleId int64, menuIds []int64) (err error) {
|
||||
// 查询角色菜单
|
||||
roleMenu := make([]*entity.SysRoleMenu, 0)
|
||||
err = dao.SysRoleMenu.Ctx(ctx).Where(dao.SysRoleMenu.Columns().RoleId, roleId).Scan(&roleMenu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currMenuIds := make([]int64, 0)
|
||||
for _, menu := range roleMenu {
|
||||
currMenuIds = append(currMenuIds, menu.MenuId)
|
||||
}
|
||||
|
||||
delMenuIds := utility.ArrayRightDiff(currMenuIds, menuIds)
|
||||
addMenuIds := utility.ArrayRightDiff(menuIds, currMenuIds)
|
||||
|
||||
// 删除角色菜单
|
||||
if len(delMenuIds) > 0 {
|
||||
_, err = dao.SysRoleMenu.Ctx(ctx).Where(dao.SysRoleMenu.Columns().RoleId, roleId).WhereIn(dao.SysRoleMenu.Columns().MenuId, delMenuIds).Delete()
|
||||
}
|
||||
|
||||
// 新增角色菜单
|
||||
if len(addMenuIds) > 0 {
|
||||
addData := make([]*do.SysRoleMenu, 0, len(addMenuIds))
|
||||
for _, menuId := range addMenuIds {
|
||||
addData = append(addData, &do.SysRoleMenu{
|
||||
RoleId: roleId,
|
||||
MenuId: menuId,
|
||||
})
|
||||
}
|
||||
_, err = dao.SysRoleMenu.Ctx(ctx).Data(addData).Insert()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 自定义角色部门数据权限
|
||||
func (s *sSysRole) RoleDept(ctx context.Context, roleId int64, deptIds []int64) (err error) {
|
||||
// 查询角色部门
|
||||
roleDept := make([]*entity.SysRoleDept, 0)
|
||||
err = dao.SysRoleDept.Ctx(ctx).Where(dao.SysRoleDept.Columns().RoleId, roleId).Scan(&roleDept)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currDeptIds := make([]int64, 0)
|
||||
for _, dept := range roleDept {
|
||||
currDeptIds = append(currDeptIds, dept.DeptId)
|
||||
}
|
||||
|
||||
delDeptIds := utility.ArrayRightDiff(currDeptIds, deptIds)
|
||||
addDeptIds := utility.ArrayRightDiff(deptIds, currDeptIds)
|
||||
|
||||
// 删除角色部门
|
||||
if len(delDeptIds) > 0 {
|
||||
_, err = dao.SysRoleDept.Ctx(ctx).Where(dao.SysRoleDept.Columns().RoleId, roleId).WhereIn(dao.SysRoleDept.Columns().DeptId, delDeptIds).Delete()
|
||||
}
|
||||
|
||||
// 新增角色部门
|
||||
if len(addDeptIds) > 0 {
|
||||
addData := make([]*do.SysRoleDept, 0, len(addDeptIds))
|
||||
for _, deptId := range addDeptIds {
|
||||
addData = append(addData, &do.SysRoleDept{
|
||||
RoleId: roleId,
|
||||
DeptId: deptId,
|
||||
})
|
||||
}
|
||||
_, err = dao.SysRoleDept.Ctx(ctx).Data(addData).Insert()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 编辑角色数据权限
|
||||
func (s *sSysRole) EditRoleDataScope(ctx context.Context, model *model.SysRoleDataScopeEditParam) (err error) {
|
||||
|
||||
data := do.SysRole{}
|
||||
gconv.Struct(model, &data)
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
|
||||
_, err = s.Model(ctx).Data(data).Where(dao.SysRole.Columns().RoleId, model.RoleId).OmitNil().Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, model.RoleId)
|
||||
|
||||
// 更新角色部门
|
||||
err = s.RoleDept(ctx, model.RoleId, model.DeptIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/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/util/gconv"
|
||||
)
|
||||
|
||||
type sSysSocial struct {
|
||||
}
|
||||
|
||||
func NewSysSocial() *sSysSocial {
|
||||
return &sSysSocial{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysSocial(NewSysSocial())
|
||||
}
|
||||
|
||||
// 查询社会化关系表
|
||||
func (s *sSysSocial) List(ctx context.Context, query *model.SysSocialListParam, page *request.PageInfo) (items []*model.SysSocialListModel, total int, err error) {
|
||||
if query.UserId == 0 {
|
||||
query.UserId = contexts.GetUserId(ctx)
|
||||
}
|
||||
|
||||
db := dao.SysSocial.Ctx(ctx)
|
||||
if query.UserId != 0 {
|
||||
db = db.Where(dao.SysSocial.Columns().UserId, query.UserId)
|
||||
}
|
||||
if query.Source != "" {
|
||||
db = db.Where(dao.SysSocial.Columns().Source, query.Source)
|
||||
}
|
||||
if query.OpenId != "" {
|
||||
db = db.Where(dao.SysSocial.Columns().OpenId, query.OpenId)
|
||||
}
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = db.Page(page.Page, page.PageSize).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
// 删除社会化关系表
|
||||
func (s *sSysSocial) Delete(ctx context.Context, id int64) (err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
_, err = dao.SysSocial.Ctx(ctx).WherePri(id).Where(dao.SysSocial.Columns().UserId, userId).Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sSysSocial) Create(ctx context.Context, social *model.SysSocialSaveParam) (err error) {
|
||||
return dao.SysSocial.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// 查询是否存在
|
||||
exist, err := dao.SysSocial.Ctx(ctx).Where(dao.SysSocial.Columns().OpenId, social.OpenId).Where(dao.SysSocial.Columns().Source, social.Source).Where(dao.SysSocial.Columns().UserId, social.UserId).Exist()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
// 绑定信息已存在
|
||||
g.Log().Errorf(ctx, "绑定信息已存在, openId: %s, source: %s, userId: %d", social.OpenId, social.Source, social.UserId)
|
||||
return gerror.New("绑定信息已存在")
|
||||
}
|
||||
|
||||
data := do.SysSocial{}
|
||||
gconv.Struct(social, &data)
|
||||
_, err = dao.SysSocial.Ctx(ctx).Data(data).OmitEmpty().Save()
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysTenant struct {
|
||||
}
|
||||
|
||||
func NewSysTenant() *sSysTenant {
|
||||
return &sSysTenant{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
s := NewSysTenant()
|
||||
service.RegisterSysTenant(s)
|
||||
}
|
||||
func (l *sSysTenant) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
if len(option) == 0 {
|
||||
option = append(option, &handler.Option{
|
||||
FilterTenant: false,
|
||||
})
|
||||
}
|
||||
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysTenant.Table()), option...)
|
||||
}
|
||||
|
||||
// 获取租户信息
|
||||
func (l *sSysTenant) View(ctx context.Context, param *model.SysTenantViewParam) (data *model.SysTenantViewModel, err error) {
|
||||
db := l.ModelQuery(ctx)
|
||||
if param.Id != 0 {
|
||||
db = db.Where(dao.SysTenant.Columns().Id, param.Id)
|
||||
} else if param.TenantId != "" {
|
||||
db = db.Where(dao.SysTenant.Columns().TenantId, param.TenantId)
|
||||
} else {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
err = db.Scan(&data)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取租户列表
|
||||
func (l *sSysTenant) List(ctx context.Context, param *model.SysTenantListParam) (data []*model.SysTenantListModel, total int, err error) {
|
||||
|
||||
// db := dao.SysTenant.Ctx(ctx)
|
||||
// 内存数据库优化
|
||||
db := l.ModelQuery(ctx)
|
||||
if param.TenantId != "" {
|
||||
db = db.Where(dao.SysTenant.Columns().TenantId, param.TenantId)
|
||||
}
|
||||
if param.ContactUserName != "" {
|
||||
db = db.WhereLike(dao.SysTenant.Columns().ContactUserName, "%"+param.ContactUserName+"%")
|
||||
}
|
||||
if param.ContactPhone != "" {
|
||||
db = db.WhereLike(dao.SysTenant.Columns().ContactPhone, "%"+param.ContactPhone+"%")
|
||||
}
|
||||
if param.CompanyName != "" {
|
||||
db = db.WhereLike(dao.SysTenant.Columns().CompanyName, "%"+param.CompanyName+"%")
|
||||
}
|
||||
if param.LicenseNumber != "" {
|
||||
db = db.WhereLike(dao.SysTenant.Columns().LicenseNumber, "%"+param.LicenseNumber+"%")
|
||||
}
|
||||
if param.Status != "" {
|
||||
db = db.Where(dao.SysTenant.Columns().Status, param.Status)
|
||||
}
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = db.Page(param.Page, param.PageSize).OrderAsc(dao.SysTenant.Columns().PackageId).Scan(&data)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return data, total, nil
|
||||
}
|
||||
|
||||
func (l *sSysTenant) Add(ctx context.Context, param *model.SysTenantAddParam) (output *model.SysTenantAddModel, err error) {
|
||||
data := &do.SysTenant{}
|
||||
gconv.Struct(param, &data)
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
// 查询租户套餐
|
||||
tenantPackage, err := service.SysTenantPackage().View(ctx, &model.SysTenantPackageViewParam{
|
||||
PackageId: param.PackageId,
|
||||
})
|
||||
if err != nil || tenantPackage == nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add SysTenantPackage().View err: %v, packageId: %d, param: %+v",
|
||||
err, param.PackageId, param)
|
||||
err = errors.New("租户套餐不存在")
|
||||
return nil, err
|
||||
}
|
||||
if tenantPackage.MenuIds == "" {
|
||||
err = errors.New("租户套餐菜单为空")
|
||||
return nil, err
|
||||
}
|
||||
menuIds := strings.Split(tenantPackage.MenuIds, ",")
|
||||
|
||||
tenantId := ""
|
||||
deptId := int64(0)
|
||||
roleId := int64(0)
|
||||
userId := int64(0)
|
||||
sysConfigIds := make([]int64, 0)
|
||||
err = g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error {
|
||||
data.TenantId = "100000"
|
||||
id, err := dao.SysTenant.Ctx(ctx).TX(tx).Data(data).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add SysTenant.Table().InsertAndGetId err: %v, data: %+v", err, data)
|
||||
return err
|
||||
}
|
||||
tenantId = fmt.Sprintf("%06d", 100000+id)
|
||||
|
||||
// 创建部门
|
||||
dataDeptInsert := do.SysDept{}
|
||||
dataDeptInsert.TenantId = tenantId
|
||||
dataDeptInsert.DeptName = param.CompanyName
|
||||
dataDeptInsert.ParentId = 0
|
||||
dataDeptInsert.OrderNum = 0
|
||||
dataDeptInsert.DeptType = 1 // 0:部门 1是公司
|
||||
dataDeptInsert.Status = consts.SysDeptStatusNormal
|
||||
dataDeptInsert.CreatedBy = contexts.GetUserId(ctx)
|
||||
dataDeptInsert.CreatedAt = gtime.Now()
|
||||
dataDeptInsert.UpdatedBy = contexts.GetUserId(ctx)
|
||||
dataDeptInsert.UpdatedAt = gtime.Now()
|
||||
deptId, err = dao.SysDept.Ctx(ctx).TX(tx).Data(dataDeptInsert).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add SysDept.Table().InsertAndGetId err: %v, data: %+v", err, dataDeptInsert)
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
dataRoleInsert := do.SysRole{}
|
||||
dataRoleInsert.TenantId = tenantId
|
||||
dataRoleInsert.RoleName = "租户管理员"
|
||||
dataRoleInsert.RoleKey = "tenantAdmin"
|
||||
dataRoleInsert.RoleSort = 1
|
||||
dataRoleInsert.Status = "0"
|
||||
dataRoleInsert.DataScope = consts.SysRoleDataScopeAll
|
||||
dataRoleInsert.MenuCheckStrictly = 0
|
||||
dataRoleInsert.DeptCheckStrictly = 0
|
||||
dataRoleInsert.CreatedBy = contexts.GetUserId(ctx)
|
||||
dataRoleInsert.CreatedAt = gtime.Now()
|
||||
dataRoleInsert.UpdatedBy = contexts.GetUserId(ctx)
|
||||
dataRoleInsert.UpdatedAt = gtime.Now()
|
||||
roleId, err = dao.SysRole.Ctx(ctx).TX(tx).Data(dataRoleInsert).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add SysRole.Table().InsertAndGetId err: %v, data: %+v", err, dataRoleInsert)
|
||||
return err
|
||||
}
|
||||
|
||||
// 角色关联菜单
|
||||
dataRoleMenuInserts := []*do.SysRoleMenu{}
|
||||
for _, menuId := range menuIds {
|
||||
dataRoleMenuInserts = append(dataRoleMenuInserts, &do.SysRoleMenu{
|
||||
RoleId: roleId,
|
||||
MenuId: menuId,
|
||||
})
|
||||
}
|
||||
_, err = dao.SysRoleMenu.Ctx(ctx).TX(tx).Data(dataRoleMenuInserts).OmitNil().Insert()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add SysRoleMenu.Table().Insert err: %v, data: %+v", err, dataRoleMenuInserts)
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建系统管理员
|
||||
salt := utility.RandomString(5)
|
||||
password := utility.PasswordEncrypt(param.Password, salt)
|
||||
|
||||
dataUserInsert := do.SysUser{}
|
||||
dataUserInsert.TenantId = tenantId
|
||||
dataUserInsert.DeptId = deptId
|
||||
dataUserInsert.NickName = param.ContactUserName
|
||||
dataUserInsert.UserName = param.Username
|
||||
dataUserInsert.Password = password
|
||||
dataUserInsert.UserType = consts.SysUserTypeSys
|
||||
dataUserInsert.Salt = salt
|
||||
dataUserInsert.Phonenumber = param.ContactPhone
|
||||
dataUserInsert.Email = ""
|
||||
dataUserInsert.Sex = consts.SysUserSexUnknown
|
||||
dataUserInsert.Status = consts.SysUserStatusNormal
|
||||
dataUserInsert.CreatedDept = contexts.GetDeptId(ctx)
|
||||
dataUserInsert.CreatedBy = contexts.GetUserId(ctx)
|
||||
dataUserInsert.CreatedAt = gtime.Now()
|
||||
dataUserInsert.UpdatedBy = contexts.GetUserId(ctx)
|
||||
dataUserInsert.UpdatedAt = gtime.Now()
|
||||
|
||||
userId, err = dao.SysUser.Ctx(ctx).TX(tx).Data(dataUserInsert).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add SysUser.Table().InsertAndGetId err: %v, data: %+v", err, dataUserInsert)
|
||||
return err
|
||||
}
|
||||
// 修改租户信息
|
||||
tenantUpdate := do.SysTenant{
|
||||
TenantId: tenantId,
|
||||
AdminRoleId: roleId,
|
||||
AdminDeptId: deptId,
|
||||
AdminUserId: userId,
|
||||
}
|
||||
_, err = dao.SysTenant.Ctx(ctx).TX(tx).Data(tenantUpdate).
|
||||
Where(dao.SysTenant.Columns().Id, id).Update()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add SysTenant.Table().Update err: %v, data: %+v", err, data)
|
||||
return err
|
||||
}
|
||||
// 修改部门负责人
|
||||
dataDeptUpdate := do.SysDept{}
|
||||
dataDeptUpdate.DeptId = deptId
|
||||
dataDeptUpdate.Leader = userId
|
||||
dataDeptUpdate.UpdatedBy = contexts.GetUserId(ctx)
|
||||
dataDeptUpdate.UpdatedAt = gtime.Now()
|
||||
_, err = dao.SysDept.Ctx(ctx).TX(tx).Data(dataDeptUpdate).OmitNil().Where(dao.SysDept.Columns().DeptId, deptId).Update()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add SysDept.Table().Update err: %v, data: %+v", err, dataDeptUpdate)
|
||||
return err
|
||||
}
|
||||
output = &model.SysTenantAddModel{
|
||||
Id: id,
|
||||
}
|
||||
// 角色关联用户
|
||||
dataUserRoleInserts := []*do.SysUserRole{}
|
||||
dataUserRoleInserts = append(dataUserRoleInserts, &do.SysUserRole{
|
||||
UserId: userId,
|
||||
RoleId: roleId,
|
||||
})
|
||||
_, err = dao.SysUserRole.Ctx(ctx).TX(tx).Data(dataUserRoleInserts).OmitNil().Insert()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add SysUserRole.Table().Insert err: %v, data: %+v", err, dataUserRoleInserts)
|
||||
return err
|
||||
}
|
||||
// 插入系统参数
|
||||
sysConfigIds, err = l.BatchInsertSysParam(ctx, tx, tenantId, userId, deptId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add batchInsertSysParam err: %v, tenantId: %s, createdBy: %d, createdDept: %d", err, tenantId, contexts.GetUserId(ctx), contexts.GetDeptId(ctx))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.Add err: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// 更新缓存
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptCreate, deptId)
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleCreate, roleId)
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysTenantCreate, output.Id)
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyUserCreate, userId)
|
||||
for _, sysConfigId := range sysConfigIds {
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigCreate, sysConfigId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysTenant) Edit(ctx context.Context, param *model.SysTenantEditParam) (output *model.SysTenantEditModel, err error) {
|
||||
if param.Id == 0 {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
data := &do.SysTenant{}
|
||||
gconv.Struct(param, &data)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
db := dao.SysTenant.Ctx(ctx)
|
||||
db = db.Where(dao.SysTenant.Columns().Id, param.Id)
|
||||
_, err = db.Data(data).OmitNil().Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysTenantEditModel{
|
||||
Id: param.Id,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysTenantUpdate, param.Id)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysTenant) Delete(ctx context.Context, param *model.SysTenantDeleteParam) (output *model.SysTenantDeleteModel, err error) {
|
||||
if len(param.Ids) == 0 {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
if slices.Contains(param.Ids, 1) {
|
||||
return nil, errors.New("不能删除默认租户")
|
||||
}
|
||||
data := &do.SysTenant{}
|
||||
gconv.Struct(param, &data)
|
||||
data.DeletedAt = gtime.Now()
|
||||
data.DeletedBy = contexts.GetUserId(ctx)
|
||||
|
||||
db := dao.SysTenant.Ctx(ctx)
|
||||
db = db.WhereIn(dao.SysTenant.Columns().Id, param.Ids)
|
||||
_, err = db.Data(data).OmitNil().Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysTenantDeleteModel{
|
||||
Ids: param.Ids,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysTenantDelete, param.Ids)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysTenant) Status(ctx context.Context, param *model.SysTenantStatusParam) (output *model.SysTenantStatusModel, err error) {
|
||||
if param.Id == 0 {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
data := &do.SysTenant{}
|
||||
gconv.Struct(param, &data)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
db := dao.SysTenant.Ctx(ctx)
|
||||
db = db.Where(dao.SysTenant.Columns().Id, param.Id)
|
||||
_, err = db.Data(data).OmitNil().Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysTenantStatusModel{
|
||||
Id: param.Id,
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysTenantUpdate, param.Id)
|
||||
return
|
||||
}
|
||||
|
||||
// 新建租户插入系统参数表数据
|
||||
func (l *sSysTenant) BatchInsertSysParam(ctx context.Context, tx gdb.TX, tenantId string, createdBy int64, createdDept int64) (ids []int64, err error) {
|
||||
now := gtime.Now()
|
||||
configs := []do.SysConfig{
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "主框架页-默认皮肤样式名称",
|
||||
ConfigKey: "sys.index.skinName",
|
||||
ConfigValue: "skin-blue",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
Remark: "蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow",
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "用户管理-账号初始密码",
|
||||
ConfigKey: "sys.user.initPassword",
|
||||
ConfigValue: "123456",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
Remark: "初始化密码 123456",
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "主框架页-侧边栏主题",
|
||||
ConfigKey: "sys.index.sideTheme",
|
||||
ConfigValue: "theme-dark",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
Remark: "深色主题theme-dark,浅色主题theme-light",
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "账号自助-是否开启用户注册功能",
|
||||
ConfigKey: "sys.account.registerUser",
|
||||
ConfigValue: "false",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
Remark: "是否开启注册用户功能(true开启,false关闭)",
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "OSS预览列表资源开关",
|
||||
ConfigKey: "sys.oss.previewListResource",
|
||||
ConfigValue: "true",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
Remark: "true:开启, false:关闭",
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "上传文件类型",
|
||||
ConfigKey: "sys.oss.fileType",
|
||||
ConfigValue: "txt,md,doc,docx,pdf,xls,xlsx,ppt,pptx,txt,jpg,jpeg,png,gif,mp4,avi,mov,mp3,wav,m4a,csv,json,application/json,bin,hex,fw,zip",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "上传图片类型",
|
||||
ConfigKey: "sys.oss.imgType",
|
||||
ConfigValue: "jpg,jpeg,png,gif,webp",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "上传文件大小",
|
||||
ConfigKey: "sys.oss.fileSize",
|
||||
ConfigValue: "20M",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "上传图片大小",
|
||||
ConfigKey: "sys.oss.imgSize",
|
||||
ConfigValue: "5M",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "上传文件Url路径",
|
||||
ConfigKey: "sys.oss.urlPath",
|
||||
ConfigValue: "/data/app/iotlcmadmin-web-svc/upload|http://iotlcm.example.com:8008/upload",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
Remark: "\"http://ab.com/\" 或 \"/\" 或 \"resource/upload|http://ab.com/upload\"",
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "是否启动在线用户强制退出",
|
||||
ConfigKey: "sys.online.forceLogout",
|
||||
ConfigValue: "true",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
Remark: "true开启,false关闭",
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "系统启用的监控通道",
|
||||
ConfigKey: "iot.default.channel.gb28181",
|
||||
ConfigValue: "0",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "报警资源匹配时间",
|
||||
ConfigKey: "iot.alarm.image.timesec",
|
||||
ConfigValue: "10",
|
||||
ConfigType: "N",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "自主注册组织ID",
|
||||
ConfigKey: "iot.register.dept",
|
||||
ConfigValue: "0",
|
||||
ConfigType: "N",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "自主注册角色ID",
|
||||
ConfigKey: "iot.register.role",
|
||||
ConfigValue: "0",
|
||||
ConfigType: "N",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "系统默认MQTT渠道",
|
||||
ConfigKey: "iot.default.channel.mqtt",
|
||||
ConfigValue: "0",
|
||||
ConfigType: "N",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "系统默认TCP渠道",
|
||||
ConfigKey: "iot.default.channel.tcp",
|
||||
ConfigValue: "0",
|
||||
ConfigType: "N",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "系统默认UDP渠道",
|
||||
ConfigKey: "iot.default.channel.udp",
|
||||
ConfigValue: "0",
|
||||
ConfigType: "N",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
{
|
||||
TenantId: tenantId,
|
||||
ConfigName: "项目API默认地址",
|
||||
ConfigKey: "iot.project.api.default.addr",
|
||||
ConfigValue: "http://127.0.0.1:8510",
|
||||
ConfigType: "Y",
|
||||
CreatedDept: createdDept,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
},
|
||||
}
|
||||
|
||||
ids = make([]int64, 0, len(configs))
|
||||
configDao := dao.SysConfig.Ctx(ctx).TX(tx)
|
||||
for _, config := range configs {
|
||||
id, insertErr := configDao.Data(config).InsertAndGetId()
|
||||
if insertErr != nil {
|
||||
return nil, insertErr
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// 同步租户菜单
|
||||
func (l *sSysTenant) SyncTenantMenu(ctx context.Context, tenantId string) (err error) {
|
||||
tenantInfo, err := l.View(ctx, &model.SysTenantViewParam{
|
||||
TenantId: tenantId,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysTenant().View err: %v, tenantId: %s", err, tenantId)
|
||||
return err
|
||||
}
|
||||
tenantPackage, err := service.SysTenantPackage().View(ctx, &model.SysTenantPackageViewParam{
|
||||
PackageId: tenantInfo.PackageId,
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysTenantPackage().View err: %v, tenantId: %s, packageId: %d", err, tenantId, tenantInfo.PackageId)
|
||||
return err
|
||||
}
|
||||
if tenantPackage == nil {
|
||||
return errors.New("租户套餐不存在")
|
||||
}
|
||||
menuIds := strings.Split(tenantPackage.MenuIds, ",")
|
||||
// 获取角色对应菜单
|
||||
roleMenuList, err := service.SysRole().GetRoleMenu(ctx, tenantInfo.AdminRoleId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysRole().GetRoleMenu err: %v, tenantId: %s, roleId: %d", err, tenantId, tenantInfo.AdminRoleId)
|
||||
return err
|
||||
}
|
||||
insertMenuIds := make([]do.SysRoleMenu, 0)
|
||||
for _, menuId := range menuIds {
|
||||
found := false
|
||||
for _, roleMenu := range roleMenuList {
|
||||
if roleMenu.MenuId == gconv.Int64(menuId) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
insertMenuIds = append(insertMenuIds, do.SysRoleMenu{
|
||||
RoleId: tenantInfo.AdminRoleId,
|
||||
MenuId: gconv.Int64(menuId),
|
||||
})
|
||||
}
|
||||
}
|
||||
delMenuIds := make([]int64, 0)
|
||||
for _, roleMenu := range roleMenuList {
|
||||
if !slices.Contains(menuIds, gconv.String(roleMenu.MenuId)) {
|
||||
delMenuIds = append(delMenuIds, roleMenu.MenuId)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(delMenuIds) > 0 {
|
||||
_, err = dao.SysRoleMenu.Ctx(ctx).WhereIn(dao.SysRoleMenu.Columns().RoleId, tenantInfo.AdminRoleId).
|
||||
WhereIn(dao.SysRoleMenu.Columns().MenuId, delMenuIds).Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysRoleMenu.Ctx(ctx).Delete err: %v, tenantId: %s, roleId: %d, delMenuIds: %v", err, tenantId, tenantInfo.AdminRoleId, delMenuIds)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(insertMenuIds) > 0 {
|
||||
_, err = dao.SysRoleMenu.Ctx(ctx).Data(insertMenuIds).Insert()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysRoleMenu.Ctx(ctx).Insert err: %v, tenantId: %s, roleId: %d, insertMenuIds: %v", err, tenantId, tenantInfo.AdminRoleId, insertMenuIds)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询用户ID是否是租户管理员
|
||||
func (l *sSysTenant) IsTenantAdmin(ctx context.Context, userIds []int64) (isAdminUserIds []int64, err error) {
|
||||
entityList := make([]*entity.SysTenant, 0)
|
||||
err = dao.SysTenant.Ctx(ctx).WhereIn(dao.SysTenant.Columns().AdminUserId, userIds).Scan(&entityList)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
isAdminUserIds = make([]int64, 0)
|
||||
for _, entity := range entityList {
|
||||
isAdminUserIds = append(isAdminUserIds, entity.AdminUserId)
|
||||
}
|
||||
return isAdminUserIds, nil
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysTenantPackage struct {
|
||||
}
|
||||
|
||||
func NewSysTenantPackage() *sSysTenantPackage {
|
||||
return &sSysTenantPackage{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysTenantPackage(NewSysTenantPackage())
|
||||
}
|
||||
|
||||
// 获取租户套餐
|
||||
func (l *sSysTenantPackage) View(ctx context.Context, param *model.SysTenantPackageViewParam) (data *model.SysTenantPackageViewModel, err error) {
|
||||
if param.PackageId == 0 {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
db := dao.SysTenantPackage.Ctx(ctx)
|
||||
db = db.Where(dao.SysTenantPackage.Columns().PackageId, param.PackageId)
|
||||
err = db.Scan(&data)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysTenantPackage) List(ctx context.Context, param *model.SysTenantPackageListParam) (data []*model.SysTenantPackageListModel, total int, err error) {
|
||||
db := dao.SysTenantPackage.Ctx(ctx)
|
||||
if param.PackageName != "" {
|
||||
db = db.WhereLike(dao.SysTenantPackage.Columns().PackageName, "%"+param.PackageName+"%")
|
||||
}
|
||||
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = db.Page(param.Page, param.PageSize).OrderAsc(dao.SysTenantPackage.Columns().PackageId).Scan(&data)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return data, total, nil
|
||||
}
|
||||
|
||||
func (l *sSysTenantPackage) Add(ctx context.Context, param *model.SysTenantPackageAddParam) (output *model.SysTenantPackageAddModel, err error) {
|
||||
data := &do.SysTenantPackage{}
|
||||
gconv.Struct(param, &data)
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.CreatedBy = contexts.GetUserId(ctx)
|
||||
data.CreatedDept = contexts.GetDeptId(ctx)
|
||||
|
||||
db := dao.SysTenantPackage.Ctx(ctx)
|
||||
id, err := db.Data(data).OmitNil().InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysTenantPackageAddModel{
|
||||
PackageId: id,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysTenantPackage) Edit(ctx context.Context, param *model.SysTenantPackageEditParam) (output *model.SysTenantPackageEditModel, err error) {
|
||||
if param.PackageId == 0 {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
data := &do.SysTenantPackage{}
|
||||
gconv.Struct(param, &data)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
|
||||
db := dao.SysTenantPackage.Ctx(ctx)
|
||||
db = db.Where(dao.SysTenantPackage.Columns().PackageId, param.PackageId)
|
||||
_, err = db.Data(data).OmitNil().Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysTenantPackageEditModel{
|
||||
PackageId: param.PackageId,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysTenantPackage) Delete(ctx context.Context, param *model.SysTenantPackageDeleteParam) (output *model.SysTenantPackageDeleteModel, err error) {
|
||||
if len(param.PackageIds) == 0 {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
data := &do.SysTenantPackage{}
|
||||
gconv.Struct(param, &data)
|
||||
data.DeletedAt = gtime.Now()
|
||||
data.DeletedBy = contexts.GetUserId(ctx)
|
||||
|
||||
db := dao.SysTenantPackage.Ctx(ctx)
|
||||
db = db.WhereIn(dao.SysTenantPackage.Columns().PackageId, param.PackageIds)
|
||||
_, err = db.Data(data).OmitNil().Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysTenantPackageDeleteModel{
|
||||
PackageIds: param.PackageIds,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *sSysTenantPackage) Status(ctx context.Context, param *model.SysTenantPackageStatusParam) (output *model.SysTenantPackageStatusModel, err error) {
|
||||
if param.PackageId == 0 {
|
||||
return nil, errors.New("参数错误")
|
||||
}
|
||||
data := &do.SysTenantPackage{}
|
||||
gconv.Struct(param, &data)
|
||||
data.UpdatedAt = gtime.Now()
|
||||
data.UpdatedBy = contexts.GetUserId(ctx)
|
||||
|
||||
db := dao.SysTenantPackage.Ctx(ctx)
|
||||
db = db.Where(dao.SysTenantPackage.Columns().PackageId, param.PackageId)
|
||||
_, err = db.Data(data).OmitNil().Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = &model.SysTenantPackageStatusModel{
|
||||
PackageId: param.PackageId,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,768 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
"slices"
|
||||
|
||||
"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"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type sSysUser struct {
|
||||
}
|
||||
|
||||
func NewSysUser() *sSysUser {
|
||||
return &sSysUser{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysUser(NewSysUser())
|
||||
}
|
||||
|
||||
func (l *sSysUser) 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.SysUser.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// 根据用户名获取用户信息,不验证当前租户
|
||||
func (l *sSysUser) GetUserByUsername(ctx context.Context, username string, tenantId string) (user *entity.SysUser, err error) {
|
||||
var data *entity.SysUser
|
||||
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserName, username).Where(dao.SysUser.Columns().TenantId, tenantId).Scan(&data)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if data == nil {
|
||||
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
|
||||
}
|
||||
if data.Status == consts.SysUserStatusDisable {
|
||||
return nil, gerror.NewCode(consts.CodeUserDisabled, "账号已禁用")
|
||||
}
|
||||
if data.Status == consts.SysUserStatusLocked {
|
||||
return nil, gerror.NewCode(consts.CodeUserLocked, "账号已锁定")
|
||||
}
|
||||
if data.Status == consts.SysUserStatusExpired {
|
||||
return nil, gerror.NewCode(consts.CodeUserExpired, "账号已过期")
|
||||
}
|
||||
if data.Status == consts.SysUserStatusDeleted {
|
||||
return nil, gerror.NewCode(consts.CodeUserDeleted, "账号已删除")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 根据邮箱获取用户信息,不验证当前租户
|
||||
func (l *sSysUser) GetUserByEmail(ctx context.Context, email string, tenantId string) (user *entity.SysUser, err error) {
|
||||
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().Email, email).Where(dao.SysUser.Columns().TenantId, tenantId).Scan(&user)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
|
||||
}
|
||||
if user.Status == consts.SysUserStatusDisable {
|
||||
return nil, gerror.NewCode(consts.CodeUserDisabled, "账号已禁用")
|
||||
}
|
||||
if user.Status == consts.SysUserStatusLocked {
|
||||
return nil, gerror.NewCode(consts.CodeUserLocked, "账号已锁定")
|
||||
}
|
||||
if user.Status == consts.SysUserStatusExpired {
|
||||
return nil, gerror.NewCode(consts.CodeUserExpired, "账号已过期")
|
||||
}
|
||||
if user.Status == consts.SysUserStatusDeleted {
|
||||
return nil, gerror.NewCode(consts.CodeUserDeleted, "账号已删除")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// 根据手机号获取用户信息,不验证当前租户
|
||||
func (l *sSysUser) GetUserByPhone(ctx context.Context, phone string, tenantId string) (user *entity.SysUser, err error) {
|
||||
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().Phonenumber, phone).Where(dao.SysUser.Columns().TenantId, tenantId).Scan(&user)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
|
||||
}
|
||||
if user.Status == consts.SysUserStatusDisable {
|
||||
return nil, gerror.NewCode(consts.CodeUserDisabled, "账号已禁用")
|
||||
}
|
||||
if user.Status == consts.SysUserStatusLocked {
|
||||
return nil, gerror.NewCode(consts.CodeUserLocked, "账号已锁定")
|
||||
}
|
||||
if user.Status == consts.SysUserStatusExpired {
|
||||
return nil, gerror.NewCode(consts.CodeUserExpired, "账号已过期")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 根据用户名和密码获取用户信息
|
||||
func (l *sSysUser) GetUserByUsernameAndPassword(ctx context.Context, tenantId string, username string, password string) (user *entity.SysUser, err error) {
|
||||
// 判断用户名是否存在
|
||||
user, err = l.GetUserByUsername(ctx, username, tenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 判断密码是否正确
|
||||
password = utility.PasswordEncrypt(password, user.Salt)
|
||||
if password != user.Password {
|
||||
g.Log().Errorf(ctx, "密码错误: username:%s, password:%s, user:%+v", username, password, user)
|
||||
return nil, gerror.NewCode(consts.CodeUserPasswordError, "密码错误")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// 根据用户ID获取用户信息
|
||||
func (l *sSysUser) GetUserById(ctx context.Context, id int64) (user *model.SysUserViewModel, err error) {
|
||||
err = l.Model(ctx).WithAll().Where(dao.SysUser.Columns().UserId, id).Scan(&user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 获取用户角色
|
||||
userRoles := make([]*entity.SysUserRole, 0)
|
||||
err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().UserId, id).Scan(&userRoles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roleIds := make([]int64, 0)
|
||||
for _, userRole := range userRoles {
|
||||
roleIds = append(roleIds, userRole.RoleId)
|
||||
}
|
||||
roles := make([]*entity.SysRole, 0)
|
||||
err = dao.SysRole.Ctx(ctx).WhereIn(dao.SysRole.Columns().RoleId, roleIds).Scan(&roles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Roles = make([]*model.SysRoleMiniModel, 0)
|
||||
for _, role := range roles {
|
||||
mini := &model.SysRoleMiniModel{}
|
||||
gconv.Struct(role, mini)
|
||||
user.Roles = append(user.Roles, mini)
|
||||
}
|
||||
// 获取用户岗位
|
||||
userPosts := make([]*entity.SysUserPost, 0)
|
||||
err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().UserId, id).Scan(&userPosts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
postIds := make([]int64, 0)
|
||||
for _, userPost := range userPosts {
|
||||
postIds = append(postIds, userPost.PostId)
|
||||
}
|
||||
posts := make([]*entity.SysPost, 0)
|
||||
err = dao.SysPost.Ctx(ctx).WhereIn(dao.SysPost.Columns().PostId, postIds).Scan(&posts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Posts = make([]*model.SysPostMiniModel, 0)
|
||||
for _, post := range posts {
|
||||
mini := &model.SysPostMiniModel{}
|
||||
gconv.Struct(post, mini)
|
||||
user.Posts = append(user.Posts, mini)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
func (l *sSysUser) List(ctx context.Context, page *request.PageInfo, query *model.UserListParam) (items []*model.SysUserListModel, total int, err error) {
|
||||
deptIds := make([]int64, 0)
|
||||
if query.DeptId != 0 {
|
||||
deptIds = append(deptIds, query.DeptId)
|
||||
subDeptIds, err := service.SysDept().GetDeptIdsByParentId(ctx, query.DeptId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
deptIds = append(deptIds, subDeptIds...)
|
||||
}
|
||||
g.Log().Infof(ctx, "sSysUser.List 获取部门列表: deptIds:%+v", deptIds)
|
||||
// 实现分页查询逻辑
|
||||
var users []*model.SysUserListModel
|
||||
m := l.Model(ctx)
|
||||
if query.UserId != 0 {
|
||||
m = m.Where(dao.SysUser.Columns().UserId, query.UserId)
|
||||
}
|
||||
if len(deptIds) > 0 {
|
||||
m = m.WhereIn(dao.SysUser.Columns().DeptId, deptIds)
|
||||
}
|
||||
if query.UserName != "" {
|
||||
m = m.WhereLike(dao.SysUser.Columns().UserName, "%"+query.UserName+"%")
|
||||
}
|
||||
if query.NickName != "" {
|
||||
m = m.WhereLike(dao.SysUser.Columns().NickName, "%"+query.NickName+"%")
|
||||
}
|
||||
if query.Email != "" {
|
||||
m = m.WhereLike(dao.SysUser.Columns().Email, "%"+query.Email+"%")
|
||||
}
|
||||
if query.Phonenumber != "" {
|
||||
m = m.WhereLike(dao.SysUser.Columns().Phonenumber, "%"+query.Phonenumber+"%")
|
||||
}
|
||||
if query.Status != "" {
|
||||
m = m.Where(dao.SysUser.Columns().Status, query.Status)
|
||||
}
|
||||
if len(query.CreatedAt) == 2 {
|
||||
begin, end := gtime.NewFromStr(query.CreatedAt[0]), gtime.NewFromStr(query.CreatedAt[1])
|
||||
m = m.WhereBetween(dao.SysUser.Columns().CreatedAt, begin, end.EndOfDay())
|
||||
}
|
||||
if len(query.DeptIdList) > 0 {
|
||||
m = m.WhereIn(dao.SysUser.Columns().DeptId, query.DeptIdList)
|
||||
}
|
||||
total, err = m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = m.WithAll().Page(page.Page, page.PageSize).Scan(&users)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// g.Log().Infof(ctx, "获取用户列表成功: total:%d, users:%+v", total, users)
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// 新增用户
|
||||
func (l *sSysUser) AddUser(ctx context.Context, req *model.SysUserAddModel) (data *model.SysUserViewModel, err error) {
|
||||
// 获取当前登录用户的租户
|
||||
if req.TenantId == "" {
|
||||
req.TenantId = contexts.GetTenantId(ctx)
|
||||
}
|
||||
// 判断部门是否存在
|
||||
dept, err := service.SysDept().GetDeptById(ctx, req.DeptId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dept == nil || dept.TenantId != req.TenantId {
|
||||
return nil, gerror.NewCode(consts.CodeDeptNotFound, "部门不存在")
|
||||
}
|
||||
|
||||
// 判断用户名是否存在(在当前租户下)
|
||||
var user *entity.SysUser
|
||||
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserName, req.UserName).Where(dao.SysUser.Columns().TenantId, req.TenantId).Scan(&user)
|
||||
if err != nil {
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user != nil {
|
||||
return nil, gerror.NewCode(consts.CodeUserExists, "用户名已存在")
|
||||
}
|
||||
// // 判断邮箱是否存在
|
||||
// user, err = l.GetUserByEmail(ctx, req.Email)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// if user != nil {
|
||||
// return nil, gerror.NewCode(consts.CodeEmailExists, "邮箱已存在")
|
||||
// }
|
||||
// // 判断手机号是否存在
|
||||
// user, err = l.GetUserByPhone(ctx, req.Phone)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// if user != nil {
|
||||
// return nil, gerror.NewCode(consts.CodePhoneExists, "手机号已存在")
|
||||
// }
|
||||
|
||||
// 随机生成5位字符
|
||||
salt := utility.RandomString(5)
|
||||
password := utility.PasswordEncrypt(req.Password, salt)
|
||||
|
||||
dataInsert := do.SysUser{}
|
||||
gconv.Struct(req, &dataInsert)
|
||||
dataInsert.Password = password
|
||||
dataInsert.Salt = salt
|
||||
dataInsert.CreatedDept = contexts.GetDeptId(ctx)
|
||||
dataInsert.CreatedBy = contexts.GetUserId(ctx)
|
||||
dataInsert.CreatedAt = gtime.Now()
|
||||
dataInsert.UpdatedBy = contexts.GetUserId(ctx)
|
||||
dataInsert.UpdatedAt = gtime.Now()
|
||||
dataInsert.TenantId = req.TenantId
|
||||
result, err := dao.SysUser.Ctx(ctx).Data(dataInsert).Insert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userId, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err = l.GetUserById(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 新增用户角色
|
||||
d := make([]*do.SysUserRole, 0, len(req.RoleIds))
|
||||
if len(req.RoleIds) > 0 {
|
||||
for _, roleId := range req.RoleIds {
|
||||
d = append(d, &do.SysUserRole{
|
||||
UserId: userId,
|
||||
RoleId: roleId,
|
||||
})
|
||||
}
|
||||
_, err = dao.SysUserRole.Ctx(ctx).Data(d).Insert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// 新增用户岗位
|
||||
if len(req.PostIds) > 0 {
|
||||
postData := make([]*do.SysUserPost, 0, len(req.PostIds))
|
||||
for _, postId := range req.PostIds {
|
||||
postData = append(postData, &do.SysUserPost{
|
||||
UserId: userId,
|
||||
PostId: postId,
|
||||
})
|
||||
}
|
||||
_, err = dao.SysUserPost.Ctx(ctx).Data(postData).Insert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (l *sSysUser) Profile(ctx context.Context) (user *model.UserProfileModel, err error) {
|
||||
|
||||
u, err := l.GetUserById(ctx, contexts.GetUserId(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
companyInfo := service.SysDept().GetParentCompanyInfo(ctx, u.DeptId)
|
||||
return &model.UserProfileModel{
|
||||
SysUserViewModel: *u,
|
||||
CompanyInfo: &model.SysDeptMiniModel{
|
||||
DeptId: companyInfo.DeptId,
|
||||
DeptName: companyInfo.DeptName,
|
||||
ParentId: companyInfo.ParentId,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *sSysUser) UpdateCurrentUser(ctx context.Context, req *model.UpdateCurrentUserModel) (user *model.SysUserViewModel, err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
data := do.SysUser{}
|
||||
gconv.Struct(req, &data)
|
||||
data.UserId = nil
|
||||
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(data).OmitEmpty().Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err = l.GetUserById(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, userId)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (l *sSysUser) UpdateCurrentUserPassword(ctx context.Context, req *model.UpdateCurrentUserPasswordModel) (err error) {
|
||||
userId := contexts.GetUserId(ctx)
|
||||
var user *entity.SysUser
|
||||
err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Scan(&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return gerror.NewCode(consts.CodeUserNotFound, "用户不存在")
|
||||
}
|
||||
|
||||
oldPassword := utility.PasswordEncrypt(req.OldPassword, user.Salt)
|
||||
if oldPassword != user.Password {
|
||||
return gerror.NewCode(consts.CodeUserPasswordError, "旧密码错误")
|
||||
}
|
||||
salt := utility.RandomString(5)
|
||||
password := utility.PasswordEncrypt(req.NewPassword, salt)
|
||||
data := do.SysUser{
|
||||
Password: password,
|
||||
Salt: salt,
|
||||
UpdatedBy: userId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(data).OmitEmpty().Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *sSysUser) UpdateUser(ctx context.Context, req *model.SysUserUpdateModel) (err error) {
|
||||
currUserId := contexts.GetUserId(ctx)
|
||||
req.UpdatedAt = gtime.Now()
|
||||
req.UpdatedBy = &currUserId
|
||||
data := do.SysUser{}
|
||||
gconv.Struct(req, &data)
|
||||
data.UserId = nil
|
||||
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, req.UserId).Data(data).OmitNil().Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 用户角色
|
||||
urList := make([]*entity.SysUserRole, 0)
|
||||
err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().UserId, req.UserId).Scan(&urList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 计算删除和新增的角色
|
||||
delRoleIds := make([]int64, 0)
|
||||
addRoleIds := make([]int64, 0)
|
||||
hasRoleId := false
|
||||
for _, ur := range urList {
|
||||
hasRoleId = false
|
||||
for _, roleId := range req.RoleIds {
|
||||
if ur.RoleId == roleId {
|
||||
hasRoleId = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRoleId {
|
||||
delRoleIds = append(delRoleIds, ur.RoleId)
|
||||
}
|
||||
}
|
||||
for _, roleId := range req.RoleIds {
|
||||
hasRoleId = false
|
||||
for _, ur := range urList {
|
||||
if ur.RoleId == roleId {
|
||||
hasRoleId = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRoleId {
|
||||
addRoleIds = append(addRoleIds, roleId)
|
||||
}
|
||||
}
|
||||
// 删除多余的角色
|
||||
if len(delRoleIds) > 0 {
|
||||
_, err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().UserId, req.UserId).WhereIn(dao.SysUserRole.Columns().RoleId, delRoleIds).Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 新增角色
|
||||
d := make([]*do.SysUserRole, 0, len(addRoleIds))
|
||||
if len(addRoleIds) > 0 {
|
||||
for _, roleId := range addRoleIds {
|
||||
d = append(d, &do.SysUserRole{
|
||||
UserId: req.UserId,
|
||||
RoleId: roleId,
|
||||
})
|
||||
}
|
||||
_, err = dao.SysUserRole.Ctx(ctx).Data(d).Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 查询用户岗位
|
||||
upList := make([]*entity.SysUserPost, 0)
|
||||
err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().UserId, req.UserId).Scan(&upList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 计算删除和新增的岗位
|
||||
delPostIds := make([]int64, 0)
|
||||
addPostIds := make([]int64, 0)
|
||||
hasPostId := false
|
||||
for _, up := range upList {
|
||||
hasPostId = false
|
||||
for _, postId := range req.PostIds {
|
||||
if up.PostId == postId {
|
||||
hasPostId = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasPostId {
|
||||
delPostIds = append(delPostIds, up.PostId)
|
||||
}
|
||||
}
|
||||
for _, postId := range req.PostIds {
|
||||
hasPostId = false
|
||||
for _, up := range upList {
|
||||
if up.PostId == postId {
|
||||
hasPostId = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasPostId {
|
||||
addPostIds = append(addPostIds, postId)
|
||||
}
|
||||
}
|
||||
// 删除多余岗位
|
||||
if len(delPostIds) > 0 {
|
||||
_, err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().UserId, req.UserId).WhereIn(dao.SysUserPost.Columns().PostId, delPostIds).Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 新增岗位
|
||||
postData := make([]*do.SysUserPost, 0, len(addPostIds))
|
||||
if len(addPostIds) > 0 {
|
||||
for _, postId := range addPostIds {
|
||||
postData = append(postData, &do.SysUserPost{
|
||||
UserId: req.UserId,
|
||||
PostId: postId,
|
||||
})
|
||||
}
|
||||
_, err = dao.SysUserPost.Ctx(ctx).Data(postData).Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, req.UserId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *sSysUser) DeleteUser(ctx context.Context, userIds []int64) (err error) {
|
||||
if len(userIds) == 0 {
|
||||
g.Log().Warningf(ctx, "sSysUser.DeleteUser 用户ID列表不能为空. userIds: %v", userIds)
|
||||
return errors.New("用户ID列表不能为空")
|
||||
}
|
||||
currUserId := contexts.GetUserId(ctx)
|
||||
if slices.Contains(userIds, 1) {
|
||||
g.Log().Warningf(ctx, "sSysUser.DeleteUser 不能删除超级管理员. userIds: %v", userIds)
|
||||
return errors.New("不能删除超级管理员")
|
||||
}
|
||||
if slices.Contains(userIds, currUserId) {
|
||||
g.Log().Warningf(ctx, "sSysUser.DeleteUser 不能删除当前用户. userIds: %v, currUserId: %d", userIds, currUserId)
|
||||
return errors.New("不能删除当前用户")
|
||||
}
|
||||
isAdminUserIds, err := service.SysTenant().IsTenantAdmin(ctx, userIds)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysUser.DeleteUser IsTenantAdmin err: %v, userIds: %v", err, userIds)
|
||||
return err
|
||||
}
|
||||
if len(isAdminUserIds) > 0 {
|
||||
g.Log().Warningf(ctx, "sSysUser.DeleteUser 不能删除租户管理员. isAdminUserIds: %v, userIds: %v", isAdminUserIds, userIds)
|
||||
return errors.New(fmt.Sprintf("不能删除租户管理员ID: %v", isAdminUserIds))
|
||||
}
|
||||
|
||||
data := do.SysUser{
|
||||
DeletedAt: gtime.Now(),
|
||||
DeletedBy: currUserId,
|
||||
}
|
||||
_, err = l.Model(ctx).WhereIn(dao.SysUser.Columns().UserId, userIds).Data(data).OmitEmpty().Update()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysUser.DeleteUser Update err: %v, userIds: %v", err, userIds)
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除用户绑定关系
|
||||
_, err = dao.SysSocial.Ctx(ctx).WhereIn(dao.SysSocial.Columns().UserId, userIds).Delete()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysUser.DeleteUser SysSocial.Ctx(ctx).Delete err: %v, userIds: %v", err, userIds)
|
||||
return err
|
||||
}
|
||||
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyUserDelete, userIds)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *sSysUser) ResetPassword(ctx context.Context, userId int64, password string) (err error) {
|
||||
currUserId := contexts.GetUserId(ctx)
|
||||
salt := utility.RandomString(5)
|
||||
password = utility.PasswordEncrypt(password, salt)
|
||||
data := do.SysUser{
|
||||
Password: password,
|
||||
Salt: salt,
|
||||
UpdatedBy: currUserId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(data).OmitEmpty().Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, userId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *sSysUser) UpdateLoginInfo(ctx context.Context, userId int64, loginIp string) (err error) {
|
||||
data := do.SysUser{
|
||||
LoginIp: loginIp,
|
||||
LoginDate: gtime.Now(),
|
||||
}
|
||||
_, err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(data).Update()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sSysUser.UpdateLoginInfo err: %v, userId: %d, loginIp: %s", err, userId, loginIp)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取用户角色ID列表
|
||||
func (l *sSysUser) GetUserRoleIds(ctx context.Context, userId int64) (roleIds []int64, err error) {
|
||||
urList := make([]*entity.SysUserRole, 0)
|
||||
err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().UserId, userId).Scan(&urList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ur := range urList {
|
||||
roleIds = append(roleIds, ur.RoleId)
|
||||
}
|
||||
return roleIds, nil
|
||||
}
|
||||
|
||||
// 获取用户岗位ID列表
|
||||
func (l *sSysUser) GetUserPostIds(ctx context.Context, userId int64) (postIds []int64, err error) {
|
||||
upList := make([]*entity.SysUserPost, 0)
|
||||
err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().UserId, userId).Scan(&upList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, up := range upList {
|
||||
postIds = append(postIds, up.PostId)
|
||||
}
|
||||
return postIds, nil
|
||||
}
|
||||
|
||||
func (l *sSysUser) Register(ctx context.Context, param *model.SysUserRegisterModel) (err error) {
|
||||
// 验证验证码
|
||||
err = service.SysCaptcha().VerifyCaptcha(ctx, param.CaptchaID, param.CaptchaValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否开启用户注册功能
|
||||
registerConfig := &model.SysConfigViewModel{}
|
||||
mod := dao.SysConfig.Ctx(ctx)
|
||||
mod = mod.Where(dao.SysConfig.Columns().ConfigKey, "sys.account.registerUser")
|
||||
mod = mod.Where(dao.SysConfig.Columns().TenantId, param.TenantId)
|
||||
err = mod.Scan(®isterConfig)
|
||||
if err != nil {
|
||||
return gerror.New("请联系管理员开启用户注册功能")
|
||||
}
|
||||
if registerConfig == nil || registerConfig.ConfigValue != "true" {
|
||||
return gerror.New("请联系管理员开启用户注册功能")
|
||||
}
|
||||
|
||||
// 获取租户信息
|
||||
tenant, err := service.SysTenant().View(ctx, &model.SysTenantViewParam{
|
||||
TenantId: param.TenantId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tenant == nil {
|
||||
return gerror.New("租户无效")
|
||||
}
|
||||
|
||||
// 获取租户的默认注册组织ID
|
||||
config := &model.SysConfigViewModel{}
|
||||
mod = dao.SysConfig.Ctx(ctx)
|
||||
mod = mod.Where(dao.SysConfig.Columns().ConfigKey, consts.ConfigKeyTenantDefaultRegisterDeptId)
|
||||
mod = mod.Where(dao.SysConfig.Columns().TenantId, param.TenantId)
|
||||
err = mod.Scan(&config)
|
||||
if err != nil {
|
||||
return gerror.New("该租户不允许自主注册")
|
||||
}
|
||||
if config == nil || config.ConfigValue == "" {
|
||||
return gerror.New("该租户不允许自主注册")
|
||||
}
|
||||
|
||||
// 验证用户名是否存在
|
||||
mod1 := dao.SysUser.Ctx(ctx)
|
||||
mod1 = mod1.Where(dao.SysUser.Columns().UserName, param.UserName)
|
||||
mod1 = mod1.Where(dao.SysUser.Columns().TenantId, param.TenantId)
|
||||
total, err := mod1.Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if total > 0 {
|
||||
return gerror.New("用户名已存在")
|
||||
}
|
||||
// 随机生成5位字符
|
||||
salt := utility.RandomString(5)
|
||||
password := utility.PasswordEncrypt(param.Password, salt)
|
||||
|
||||
newUser := do.SysUser{
|
||||
UserName: param.UserName,
|
||||
NickName: "",
|
||||
Password: password,
|
||||
Salt: salt,
|
||||
TenantId: param.TenantId,
|
||||
DeptId: gconv.Int64(config.ConfigValue),
|
||||
Status: consts.SysDeptStatusNormal,
|
||||
CreatedBy: 0,
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedBy: 0,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
|
||||
userId, err := l.Model(ctx).Data(newUser).InsertAndGetId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//配置角色
|
||||
// 新增用户角色
|
||||
mod = dao.SysConfig.Ctx(ctx)
|
||||
mod = mod.Where(dao.SysConfig.Columns().ConfigKey, consts.ConfigKeyTenantDefaultRegisterRoleId)
|
||||
mod = mod.Where(dao.SysConfig.Columns().TenantId, param.TenantId)
|
||||
err = mod.Scan(&config)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "获取租户的默认角色失败", err)
|
||||
}
|
||||
if config != nil && config.ConfigValue != "" {
|
||||
roleId := gconv.Int64(config.ConfigValue)
|
||||
data := do.SysUserRole{
|
||||
UserId: userId,
|
||||
RoleId: roleId,
|
||||
}
|
||||
_, err = dao.SysUserRole.Ctx(ctx).Data(data).Insert()
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "获取租户的默认角色失败", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status 更新用户状态
|
||||
func (l *sSysUser) Status(ctx context.Context, param *model.SysUserStatusParam) (err error) {
|
||||
data := do.SysUser{
|
||||
Status: param.Status,
|
||||
}
|
||||
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, param.UserId).Data(data).OmitEmpty().Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 批量查询用户迷你信息
|
||||
func (l *sSysUser) BatchGetUserMiniInfo(ctx context.Context, userIds []int64) (users []*model.SysUserMiniModel, err error) {
|
||||
err = dao.SysUser.Ctx(ctx).WhereIn(dao.SysUser.Columns().UserId, userIds).Scan(&users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// package system
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysUserOnline struct {
|
||||
}
|
||||
|
||||
func NewSysUserOnline() *sSysUserOnline {
|
||||
return &sSysUserOnline{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysUserOnline(NewSysUserOnline())
|
||||
}
|
||||
|
||||
func (s *sSysUserOnline) 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.SysUserOnline.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sSysUserOnline) Add(ctx context.Context, userOnline *model.SysUserOnlineAddModel) (err error) {
|
||||
data := do.SysUserOnline{}
|
||||
gconv.Struct(userOnline, &data)
|
||||
_, err = s.Model(ctx).Data(data).OmitEmpty().Save()
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysUserOnline) List(ctx context.Context, query *model.SysUserOnlineListParam, page *request.PageInfo) (items []*model.SysUserOnlineListModel, total int, err error) {
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
db := s.Model(ctx).Where(dao.SysUserOnline.Columns().TenantId, tenantId)
|
||||
if query.UserName != "" {
|
||||
db = db.Where(dao.SysUserOnline.Columns().UserName, query.UserName)
|
||||
}
|
||||
if query.ClientKey != "" {
|
||||
db = db.WhereLike(dao.SysUserOnline.Columns().ClientKey, "%"+query.ClientKey+"%")
|
||||
}
|
||||
if query.DeviceType != "" {
|
||||
db = db.Where(dao.SysUserOnline.Columns().DeviceType, query.DeviceType)
|
||||
}
|
||||
if query.Ipaddr != "" {
|
||||
db = db.Where(dao.SysUserOnline.Columns().Ipaddr, query.Ipaddr)
|
||||
}
|
||||
if len(query.LoginTime) == 2 {
|
||||
startTime := gtime.NewFromStr(query.LoginTime[0])
|
||||
endTime := gtime.NewFromStr(query.LoginTime[1])
|
||||
db = db.WhereBetween(dao.SysUserOnline.Columns().LoginTime, startTime, endTime.EndOfDay())
|
||||
}
|
||||
if query.Token != "" {
|
||||
db = db.Where(dao.SysUserOnline.Columns().Token, query.Token)
|
||||
}
|
||||
total, err = db.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = db.Page(page.Page, page.PageSize).Order(dao.SysUserOnline.Columns().LoginTime, "desc").Scan(&items)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysUserOnline) Delete(ctx context.Context, ids []int64) (err error) {
|
||||
if len(ids) == 0 {
|
||||
return errors.New("ids不能为空")
|
||||
}
|
||||
var data = make([]*model.SysUserOnlineViewModel, 0)
|
||||
err = s.Model(ctx).WhereIn(dao.SysUserOnline.Columns().OnlineId, ids).Scan(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range data {
|
||||
service.SysAuth().DeleteToken(ctx, v.Token)
|
||||
}
|
||||
_, err = s.Model(ctx).WhereIn(dao.SysUserOnline.Columns().OnlineId, ids).Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysUserOnline) DeleteByToken(ctx context.Context, token string) (err error) {
|
||||
var data *model.SysUserOnlineViewModel
|
||||
err = s.Model(ctx).Where(dao.SysUserOnline.Columns().Token, token).Scan(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if data != nil && data.Token != "" {
|
||||
service.SysAuth().DeleteToken(ctx, data.Token)
|
||||
}
|
||||
_, err = s.Model(ctx).Where(dao.SysUserOnline.Columns().Token, token).Delete()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// package system_test
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 数据库只读,不执行
|
||||
// g.DB().SetDryRun(true)
|
||||
g.DB().SetDebug(true)
|
||||
testing.Init()
|
||||
}
|
||||
|
||||
func TestSysUser_UpdateUserPassword(t *testing.T) {
|
||||
// 设置上下文用户身份为用户
|
||||
ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
|
||||
// 为了测试只设置了hook中需要用到的数据
|
||||
User: &model.Identity{
|
||||
BaseClaims: model.BaseClaims{
|
||||
ID: 1,
|
||||
TenantId: "000000",
|
||||
DeptId: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
newPassword := "123456"
|
||||
salt := "123456"
|
||||
password := utility.PasswordEncrypt(utility.Md5(newPassword), salt)
|
||||
data := do.SysUser{
|
||||
Password: password,
|
||||
Salt: salt,
|
||||
UpdatedBy: 1,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
_, err := dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, 1).Data(data).OmitEmpty().Update()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println("UpdateUserPassword success")
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// // package system_test
|
||||
// // @Link https://github.com/xiujiecn/xiu-admin
|
||||
// // @Copyright Copyright (c) 2025 LiXiujie
|
||||
// // @Author Lxj <li@xiujie.cn>
|
||||
// // @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
package system_test
|
||||
|
||||
// import (
|
||||
// "context"
|
||||
// "encoding/json"
|
||||
// "fmt"
|
||||
// "testing"
|
||||
// "time"
|
||||
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/worker"
|
||||
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/tasks"
|
||||
|
||||
// _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
// "github.com/gogf/gf/v2/frame/g"
|
||||
// "github.com/gogf/gf/v2/os/gctx"
|
||||
// "github.com/gogf/gf/v2/util/gconv"
|
||||
// )
|
||||
|
||||
// func init() {
|
||||
// // 数据库只读,不执行
|
||||
// // g.DB().SetDryRun(true)
|
||||
// g.DB().SetDebug(true)
|
||||
// testing.Init()
|
||||
// }
|
||||
|
||||
// func TestTask_runOnce(t *testing.T) {
|
||||
// // 设置上下文用户身份为用户
|
||||
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
|
||||
// // 为了测试只设置了hook中需要用到的数据
|
||||
// User: &model.Identity{
|
||||
// BaseClaims: model.BaseClaims{
|
||||
// ID: 1,
|
||||
// TenantId: "000000",
|
||||
// DeptId: 1,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
// exists := &model.SysJobViewModel{}
|
||||
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
|
||||
// paramArr, _ := tasks.TasksInstance().ParseParameters(exists.JobParams)
|
||||
// taskData := tasks.Task{
|
||||
// ID: fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId),
|
||||
// TaskType: "Type-" + gconv.String(exists.MisfirePolicy),
|
||||
// MethodName: exists.InvokeTarget,
|
||||
// Params: paramArr,
|
||||
// Explain: exists.JobName,
|
||||
// }
|
||||
// runPayload, _ := json.Marshal(taskData)
|
||||
|
||||
// tasks.TasksInstance().Once(
|
||||
// worker.WithTaskCtx(context.Background()),
|
||||
// worker.WithTaskUid(taskData.ID), // 任务ID
|
||||
// worker.WithTaskGroup(taskData.MethodName), // 任务组
|
||||
// worker.WithTaskTimeout(10),
|
||||
// worker.WithTaskNow(true),
|
||||
// worker.WithTaskReplace(true),
|
||||
// worker.WithTaskPayload(runPayload),
|
||||
// )
|
||||
// }
|
||||
|
||||
// func TestTask_schedule(t *testing.T) {
|
||||
// // 设置上下文用户身份为用户
|
||||
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
|
||||
// // 为了测试只设置了hook中需要用到的数据
|
||||
// User: &model.Identity{
|
||||
// BaseClaims: model.BaseClaims{
|
||||
// ID: 1,
|
||||
// TenantId: "000000",
|
||||
// DeptId: 1,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
// exists := &model.SysJobViewModel{}
|
||||
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
|
||||
// paramArr, _ := tasks.TasksInstance().ParseParameters(exists.JobParams)
|
||||
// //获取task目录下是否绑定对应的方法
|
||||
// actualName, exist := tasks.TasksInstance().CheckFuncName(exists.InvokeTarget)
|
||||
// if !exist {
|
||||
// errInfo := fmt.Sprintf("没有绑定对应的方法:%s", exists.InvokeTarget)
|
||||
// g.Log().Error(ctx, errInfo)
|
||||
// }
|
||||
|
||||
// //传参解析
|
||||
// paramArr, err := tasks.TasksInstance().ParseParameters(exists.JobParams)
|
||||
// if err != nil {
|
||||
// g.Log().Error(ctx, err)
|
||||
// }
|
||||
|
||||
// taskData := tasks.Task{
|
||||
// ID: fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId),
|
||||
// TaskType: "Type-" + gconv.String(exists.MisfirePolicy),
|
||||
// MethodName: actualName,
|
||||
// Params: paramArr,
|
||||
// Explain: exists.JobName,
|
||||
// }
|
||||
// runPayload, _ := json.Marshal(taskData)
|
||||
// _, err2 := tasks.TasksInstance().Cron(
|
||||
// 1,
|
||||
// worker.WithTaskCtx(context.Background()),
|
||||
// worker.WithTaskUid(taskData.ID), // 任务ID
|
||||
// worker.WithTaskGroup(taskData.MethodName), // 任务组
|
||||
// worker.WithTaskExpr("0/2 * * * * *"),
|
||||
// //worker.WithTaskExpr("@every 2s"),
|
||||
// worker.WithTaskTimeout(10),
|
||||
// worker.WithTaskReplace(true),
|
||||
// worker.WithTaskPayload(runPayload),
|
||||
// )
|
||||
// if err2 != nil {
|
||||
// g.Log().Debug(ctx, taskData.MethodName, taskData.Explain, "启动任务失败")
|
||||
// }
|
||||
|
||||
// time.Sleep(10 * time.Minute)
|
||||
// }
|
||||
|
||||
// func TestTask_remove(t *testing.T) {
|
||||
// // 设置上下文用户身份为用户
|
||||
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
|
||||
// // 为了测试只设置了hook中需要用到的数据
|
||||
// User: &model.Identity{
|
||||
// BaseClaims: model.BaseClaims{
|
||||
// ID: 1,
|
||||
// TenantId: "000000",
|
||||
// DeptId: 1,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
// exists := &model.SysJobViewModel{}
|
||||
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
|
||||
// taskJobId := fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId)
|
||||
// g.Log().Debug(ctx, taskJobId)
|
||||
// err := tasks.TasksInstance().Remove(taskJobId)
|
||||
// if err != nil {
|
||||
// g.Log().Debug(ctx, exists.JobName, "启动停止失败")
|
||||
// g.Log().Error(ctx, err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestTask_getTask(t *testing.T) {
|
||||
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
|
||||
// // 为了测试只设置了hook中需要用到的数据
|
||||
// User: &model.Identity{
|
||||
// BaseClaims: model.BaseClaims{
|
||||
// ID: 1,
|
||||
// TenantId: "000000",
|
||||
// DeptId: 1,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
// exists := &model.SysJobViewModel{}
|
||||
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
|
||||
// taskJobId := fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId)
|
||||
// task, err := tasks.TasksInstance().GetTask(taskJobId)
|
||||
// if err != nil {
|
||||
// g.Log().Debug(ctx, exists.JobName, "获取任务失败")
|
||||
// g.Log().Error(ctx, err)
|
||||
// }
|
||||
// fmt.Println("task", task)
|
||||
// }
|
||||
Reference in New Issue
Block a user