feat: 接入商务用户资料和全局统计偏好
This commit is contained in:
Generated
+8
@@ -8,12 +8,14 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/go-kratos/kratos/v3"
|
||||
business2 "kra/internal/biz/business"
|
||||
"kra/internal/biz/integration"
|
||||
payment3 "kra/internal/biz/payment"
|
||||
system2 "kra/internal/biz/system"
|
||||
task2 "kra/internal/biz/task"
|
||||
"kra/internal/config"
|
||||
"kra/internal/data"
|
||||
"kra/internal/data/business"
|
||||
integration3 "kra/internal/data/integration"
|
||||
payment2 "kra/internal/data/payment"
|
||||
"kra/internal/data/system"
|
||||
@@ -33,6 +35,7 @@ import (
|
||||
"kra/internal/server"
|
||||
"kra/internal/server/handler"
|
||||
"kra/internal/server/router"
|
||||
business3 "kra/internal/service/business"
|
||||
integration2 "kra/internal/service/integration"
|
||||
payment4 "kra/internal/service/payment"
|
||||
system3 "kra/internal/service/system"
|
||||
@@ -78,6 +81,10 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg
|
||||
auditRecorderUsecase := system2.NewAuditRecorderUsecase(auditRecordRepo)
|
||||
auditRecorder := system3.NewAuditRecorder(auditRecorderUsecase)
|
||||
registry := taskRegistry()
|
||||
userPreferenceRepo := business.NewUserPreferenceRepo(dataData)
|
||||
userPreferenceUsecase := business2.NewUserPreferenceUsecase(userPreferenceRepo)
|
||||
userPreferenceService := business3.NewUserPreferenceService(userPreferenceUsecase)
|
||||
handlerBusiness := handler.NewBusiness(userPreferenceService)
|
||||
runtimeconfigStore := data.NewIntegrationRuntime(dataData)
|
||||
smsSender := sms.NewSender(runtimeconfigStore)
|
||||
smsUsecase := integration.NewSMSUsecase(smsSender)
|
||||
@@ -176,6 +183,7 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg
|
||||
integrationConfigService := integration2.NewIntegrationConfigService(integrationConfigUsecase)
|
||||
integrationConfig := handler.NewIntegrationConfig(integrationConfigService)
|
||||
set := &handler.Set{
|
||||
Business: handlerBusiness,
|
||||
SMS: handlerSMS,
|
||||
Authority: authority,
|
||||
Menu: menu,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# 商务功能迁移执行记录
|
||||
|
||||
## 基线与方法
|
||||
|
||||
- 旧后端:`D:/Users/Yvan/projects/yzyz/sw`,`main`,`f9a5e3a90`。
|
||||
- 框架基线:`d31082f4a7816d4c83544b8f54e6f42a3b2132b1`。
|
||||
- 旧前端:`D:/Users/Yvan/projects/yzyz/sw/web`,`dev/dev1.0`,`ba284011c87fd3d5730e0e3dceffb14a7389730c`。
|
||||
- 新实现分支:`codex/sw-business-migration`。
|
||||
|
||||
按基线至当前版本的最终差异提取功能,以当前 Java 实现、实际调用的前端接口和 SQL 为核对依据。提交记录用于解释修复和撤销,不逐提交搬运。旧前端中未接入的 mock、废弃接口不能作为已实现业务迁移。
|
||||
|
||||
沿用现有迁移 SQL 的 `/sw/` 路由与 `view/sw/` 菜单组件路径。认证、响应信封、权限、表格及图表采用 Kra;业务字段、统计口径、状态流转、可见范围保留旧实现。登录页、系统名称和品牌资源来自旧商务。新增代码注释与提交信息使用中文。
|
||||
|
||||
用户已有的 `configs/config.yaml` 修改与迁移 SQL 保留。未经最后阶段的数据库确认,不连接、复制或修改用户的旧数据库。
|
||||
|
||||
## 依赖顺序与完成标准
|
||||
|
||||
| 阶段 | 范围 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 商务用户字段、层级与角色、账号资料和审核、偏好、登录及找回密码 | 进行中 |
|
||||
| 2 | 平台绑定、商务/渠道可见范围、外部平台与 OA 数据源、商务列表及抽屉 | 待实现 |
|
||||
| 3 | 预警规则、记录与状态机、操作日志、附件、消息、任务注册与调度 | 待实现 |
|
||||
| 4 | 渠道列表及新增、渠道激活、推广游戏等业务接口与页面 | 待实现 |
|
||||
| 5 | 日汇总构建、充值/注册明细、渠道/游戏/区服报表、排行、画像与仪表盘 | 待实现 |
|
||||
| 6 | 静态接口核对、后端测试、前端构建和页面验证 | 随阶段推进,最终全量复核 |
|
||||
| 7 | 用户确认源数据库,复制至隔离库,执行迁移,启动并逐接口核对数据 | 未开始,必须等待用户确认 |
|
||||
|
||||
每项完成需覆盖 DTO → service → biz → data、路由目录、权限、菜单页面和有意义的行为测试。只有真实实现并通过验证的项才标为完成。整项目标在实库验证之前保持未完成。
|
||||
|
||||
## 已核实的业务约束
|
||||
|
||||
- 超管创建一级商务,一级创建二级,二级创建三级;三级不能创建下级。层级与祖先链由服务器生成。
|
||||
- 新增子商务最终版本默认正常,不采用残留注释中的“默认待审核”。
|
||||
- 一/二级商务列表只显示下级树,不显示自己;三级列表为空。报表使用独立渠道可见范围,不能套用 Kra 的部门过滤替代。
|
||||
- `includeSubPromote` 与 `idcardDedup` 无记录或数据库值非零时均为开启,新增账号同时初始化开启。
|
||||
- SDK 商务绑定数量仅计算已通过记录;渠道计数依照当前直属渠道算法,跨平台 ID 不能混用。
|
||||
- 用户审核状态有正常、停用、驳回、待审核四种,须保留 `business_status`,不能压成 `enable` 后丢失含义。
|
||||
- 金额查询固定不做身份证去重;人数查询遵守全局偏好。
|
||||
- SQL 已建立用户、角色、文件等 ID 映射;平台实体及业务主键保留。迁移验收应按映射比较,不能直接比较所有原始 ID。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- 已落地商务用户七个扩展字段的存取及增量补列;登录和既有会话同时检查商务审核状态。
|
||||
- 已落地全局偏好 GET/PUT、当前用户隔离、缺省开启、显式关闭及幂等写入;Kra 顶栏增加统计口径入口。
|
||||
- 已生成 `source-inventory.json`:现有 SQL 的 63 个接口契约、279 个变更源文件及额外找回密码控制器。此清单是范围索引,不代表已完成实现。
|
||||
- 已执行 `make generate`、相关 Go 测试、变更前端 ESLint 和生产构建。构建存在原 `arcdash` 依赖的 BigInt 浏览器目标警告。
|
||||
- 当前没有执行本轮真实数据库迁移验证。
|
||||
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -1,6 +1,7 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/biz/integration"
|
||||
"kra/internal/biz/payment"
|
||||
"kra/internal/biz/system"
|
||||
@@ -10,4 +11,4 @@ import (
|
||||
)
|
||||
|
||||
// ProviderSet is biz providers.
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"kra/internal/biz/system"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthenticated = errors.Unauthorized("SW_UNAUTHENTICATED", "请先登录")
|
||||
ErrPreferenceRequired = errors.BadRequest("SW_PREFERENCE_REQUIRED", "偏好参数不能为空")
|
||||
ErrStorage = errors.InternalServer("SW_STORAGE_ERROR", "商务数据访问失败")
|
||||
)
|
||||
|
||||
// UserPreference 使用新用户 ID,保留旧商务两个全局开关的含义。
|
||||
type UserPreference struct {
|
||||
IncludeSubPromote bool
|
||||
IDCardDedup bool
|
||||
}
|
||||
|
||||
type UserPreferenceRepo interface {
|
||||
Find(context.Context, uint) (*UserPreference, error)
|
||||
Save(context.Context, uint, UserPreference) error
|
||||
}
|
||||
|
||||
type UserPreferenceUsecase struct{ repo UserPreferenceRepo }
|
||||
|
||||
func NewUserPreferenceUsecase(repo UserPreferenceRepo) *UserPreferenceUsecase {
|
||||
return &UserPreferenceUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *UserPreferenceUsecase) Current(ctx context.Context) (*UserPreference, error) {
|
||||
actor, ok := system.ActorFromContext(ctx)
|
||||
if !ok || actor.UserID == 0 {
|
||||
return nil, ErrUnauthenticated
|
||||
}
|
||||
return uc.ForUser(ctx, actor.UserID)
|
||||
}
|
||||
|
||||
func (uc *UserPreferenceUsecase) ForUser(ctx context.Context, id uint) (*UserPreference, error) {
|
||||
if id == 0 {
|
||||
return nil, ErrUnauthenticated
|
||||
}
|
||||
pref, err := uc.repo.Find(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pref == nil {
|
||||
return &UserPreference{IncludeSubPromote: true, IDCardDedup: true}, nil
|
||||
}
|
||||
return pref, nil
|
||||
}
|
||||
|
||||
func (uc *UserPreferenceUsecase) UpdateCurrent(ctx context.Context, pref UserPreference) error {
|
||||
actor, ok := system.ActorFromContext(ctx)
|
||||
if !ok || actor.UserID == 0 {
|
||||
return ErrUnauthenticated
|
||||
}
|
||||
return uc.repo.Save(ctx, actor.UserID, pref)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/system"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type preferenceRepoFake struct {
|
||||
value *UserPreference
|
||||
userID uint
|
||||
calls int
|
||||
}
|
||||
|
||||
func (r *preferenceRepoFake) Find(_ context.Context, id uint) (*UserPreference, error) {
|
||||
r.calls++
|
||||
r.userID = id
|
||||
return r.value, nil
|
||||
}
|
||||
func (r *preferenceRepoFake) Save(_ context.Context, id uint, pref UserPreference) error {
|
||||
r.calls++
|
||||
r.userID, r.value = id, &pref
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPreferenceDefaultsAndCurrentIdentity(t *testing.T) {
|
||||
r := &preferenceRepoFake{}
|
||||
uc := NewUserPreferenceUsecase(r)
|
||||
if _, err := uc.Current(context.Background()); err != ErrUnauthenticated || r.calls != 0 {
|
||||
t.Fatalf("匿名请求访问了偏好: %v", err)
|
||||
}
|
||||
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: 7})
|
||||
pref, err := uc.Current(ctx)
|
||||
if err != nil || !pref.IncludeSubPromote || !pref.IDCardDedup {
|
||||
t.Fatalf("缺省开关应均为开启: %+v %v", pref, err)
|
||||
}
|
||||
if err = uc.UpdateCurrent(ctx, UserPreference{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.userID != 7 || r.value.IncludeSubPromote || r.value.IDCardDedup {
|
||||
t.Fatalf("未保存当前用户的关闭值: %+v", r)
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttemp
|
||||
uc.recordLogin(ctx, attempt, false, "用户名不存在或者密码错误", 0)
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
if user.Enable != 1 {
|
||||
if user.Enable != 1 || user.BusinessStatus != 0 {
|
||||
_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
|
||||
uc.recordLogin(ctx, attempt, false, "用户被禁止登录", user.ID)
|
||||
return nil, &UserDisabledError{UserID: user.ID}
|
||||
@@ -183,7 +183,7 @@ func (uc *AuthenticationUsecase) currentTokenUser(ctx context.Context, claims *A
|
||||
if err != nil || user == nil || user.ID != claims.ID || user.Username != claims.Username {
|
||||
return nil, ErrTokenDisabled
|
||||
}
|
||||
if user.Enable != 1 {
|
||||
if user.Enable != 1 || user.BusinessStatus != 0 {
|
||||
return nil, &UserDisabledError{UserID: user.ID}
|
||||
}
|
||||
if !userHasAuthority(user, claims.AuthorityID) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBusinessReviewStateBlocksPasswordAndExistingSession(t *testing.T) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("secret"), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, status := range []int{1, 2, 3} {
|
||||
user := &User{ID: 7, Username: "business", Password: string(hash), Enable: 1, BusinessStatus: status}
|
||||
security := NewSecurityUsecase(&authenticationSecurityRepo{}, &authenticationCache{}, &authenticationSettings{}, nil)
|
||||
uc := NewAuthenticationUsecase(NewUserUsecase(&authenticationUserRepo{user: user}), security, &authenticationIssuer{}, nil)
|
||||
if _, err := uc.Login(context.Background(), &LoginAttempt{Username: "business", Password: "secret", IP: "127.0.0.1"}); !errors.Is(err, ErrUserDisabled) {
|
||||
t.Fatalf("审核状态 %d 的登录应被拒绝: %v", status, err)
|
||||
}
|
||||
if _, err := uc.currentTokenUser(context.Background(), &AuthClaims{ID: 7, Username: "business", UUID: "uuid", IssuedAt: time.Now()}); !errors.Is(err, ErrUserDisabled) {
|
||||
t.Fatalf("审核状态 %d 的现有会话应被拒绝: %v", status, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,14 @@ type User struct {
|
||||
OriginSetting map[string]any
|
||||
MustChangePassword bool
|
||||
PasswordUpdatedAt *time.Time
|
||||
// 商务资料与系统身份共用主键;平台商务 ID 另由绑定关系持有。
|
||||
Level int
|
||||
ParentID uint
|
||||
IDPath string
|
||||
RealName string
|
||||
QQNumber string
|
||||
BusinessStatus int
|
||||
RejectReason string
|
||||
}
|
||||
type UserListFilter struct {
|
||||
Username, NickName, Phone, Email, OrderKey string
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"kra/pkg/database/migration"
|
||||
)
|
||||
|
||||
func Migrations() []migration.Step {
|
||||
return []migration.Step{{ID: "202611030001_business_preference", Migrate: func(db *gorm.DB) error {
|
||||
// 旧表沿用;已迁移数据库不重建或重置用户偏好。
|
||||
if err := migration.CreateMissingTables(db, &preferencePO{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if !db.Migrator().HasTable("sys_authorities") || !db.Migrator().HasTable("casbin_rule") {
|
||||
return nil
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
// 两个接口仅操作当前身份,旧系统对所有已登录用户开放。
|
||||
for _, method := range []string{"GET", "PUT"} {
|
||||
if err := tx.Exec(`INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5)
|
||||
SELECT 'p', a.authority_id, ?, ?, '', '', '' FROM sys_authorities a
|
||||
WHERE a.deleted_at IS NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM casbin_rule p WHERE p.ptype='p' AND p.v0=a.authority_id AND p.v1=? AND p.v2=?)`,
|
||||
"/sw/user/preference", method, "/sw/user/preference", method).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}}}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
// Provider 复用 Data 持有的连接,不在仓储中创建数据库客户端。
|
||||
type Provider interface{ DB() *gorm.DB }
|
||||
|
||||
type preferencePO struct {
|
||||
UserID uint `gorm:"primaryKey;autoIncrement:false"`
|
||||
IncludeSubPromote *int
|
||||
IDCardDedup *int `gorm:"column:idcard_dedup"`
|
||||
UpdateTime time.Time
|
||||
}
|
||||
|
||||
func (preferencePO) TableName() string { return "sys_user_preference" }
|
||||
|
||||
type userPreferenceRepo struct{ data Provider }
|
||||
|
||||
func NewUserPreferenceRepo(data Provider) business.UserPreferenceRepo {
|
||||
return &userPreferenceRepo{data: data}
|
||||
}
|
||||
|
||||
func (r *userPreferenceRepo) Find(ctx context.Context, id uint) (*business.UserPreference, error) {
|
||||
var po preferencePO
|
||||
err := r.data.DB().WithContext(ctx).First(&po, "user_id = ?", id).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
return &business.UserPreference{
|
||||
IncludeSubPromote: po.IncludeSubPromote == nil || *po.IncludeSubPromote != 0,
|
||||
IDCardDedup: po.IDCardDedup == nil || *po.IDCardDedup != 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *userPreferenceRepo) Save(ctx context.Context, id uint, pref business.UserPreference) error {
|
||||
include, dedup := 0, 0
|
||||
if pref.IncludeSubPromote {
|
||||
include = 1
|
||||
}
|
||||
if pref.IDCardDedup {
|
||||
dedup = 1
|
||||
}
|
||||
po := preferencePO{UserID: id, IncludeSubPromote: &include, IDCardDedup: &dedup, UpdateTime: time.Now()}
|
||||
if err := r.data.DB().WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"include_sub_promote", "idcard_dedup", "update_time"}),
|
||||
}).Create(&po).Error; err != nil {
|
||||
return business.ErrStorage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz/business"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testData struct{ db *gorm.DB }
|
||||
|
||||
func (d testData) DB() *gorm.DB { return d.db }
|
||||
|
||||
func openTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "business.db")), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
for _, step := range Migrations() {
|
||||
if err := step.Migrate(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestPreferencePreservesLegacyValuesAndUpserts(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
r := NewUserPreferenceRepo(testData{db})
|
||||
ctx := context.Background()
|
||||
if pref, err := r.Find(ctx, 5); err != nil || pref != nil {
|
||||
t.Fatalf("缺省记录错误: %+v %v", pref, err)
|
||||
}
|
||||
if err := db.Exec("INSERT INTO sys_user_preference(user_id,include_sub_promote,idcard_dedup) VALUES (5,NULL,2),(6,1,1)").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pref, err := r.Find(ctx, 5)
|
||||
if err != nil || !pref.IncludeSubPromote || !pref.IDCardDedup {
|
||||
t.Fatalf("旧 NULL 和非零值应为开启: %+v %v", pref, err)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := r.Save(ctx, 5, business.UserPreference{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, step := range Migrations() {
|
||||
if err := step.Migrate(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
pref, err = r.Find(ctx, 5)
|
||||
if err != nil || pref.IncludeSubPromote || pref.IDCardDedup {
|
||||
t.Fatalf("重复迁移不能重置偏好: %+v %v", pref, err)
|
||||
}
|
||||
other, err := r.Find(ctx, 6)
|
||||
if err != nil || !other.IncludeSubPromote || !other.IDCardDedup {
|
||||
t.Fatalf("影响其他用户: %+v %v", other, err)
|
||||
}
|
||||
var count int64
|
||||
db.Model(&preferencePO{}).Where("user_id = ?", 5).Count(&count)
|
||||
if count != 1 {
|
||||
t.Fatalf("重复写入产生多条记录: %d", count)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/config"
|
||||
databusiness "kra/internal/data/business"
|
||||
dataintegration "kra/internal/data/integration"
|
||||
datapayment "kra/internal/data/payment"
|
||||
datasystem "kra/internal/data/system"
|
||||
@@ -25,6 +26,8 @@ import (
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewData,
|
||||
wire.Bind(new(databusiness.Provider), new(*Data)),
|
||||
databusiness.NewUserPreferenceRepo,
|
||||
NewIntegrationRuntime,
|
||||
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||
|
||||
@@ -34,8 +34,8 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||
if err = db.Table(migration.TableName).Count(&versions).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if versions != 14 {
|
||||
t.Fatalf("migration versions = %d, want 14", versions)
|
||||
if versions != 16 {
|
||||
t.Fatalf("migration versions = %d, want 16", versions)
|
||||
}
|
||||
var communicationRows []dataintegration.ConfigPO
|
||||
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {
|
||||
|
||||
@@ -178,7 +178,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *system
|
||||
value.Menus = nil
|
||||
}
|
||||
}
|
||||
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
|
||||
defaults := []struct{ path, method string }{{"/sw/user/preference", "GET"}, {"/sw/user/preference", "PUT"}, {"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
|
||||
rules := make([]casbinRulePO, 0, len(defaults))
|
||||
for _, item := range defaults {
|
||||
if strict {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBusinessFieldsSurviveMigrationAndIdentityLoad(t *testing.T) {
|
||||
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = migrateAll(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
po := userPO{Username: "business", UUID: "business-uuid", Password: "original-hash", Enable: 1, Level: 2, ParentID: 11, IDPath: "0,11", RealName: "商务姓名", QQNumber: "12345", BusinessStatus: 3, RejectReason: "审核资料"}
|
||||
if err = db.Create(&po).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = ensureBusinessUserFields(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := NewUserRepo(&Data{gormDB: newReloadableDB(db, nil)})
|
||||
user, err := r.FindUserByUUID(context.Background(), "business-uuid")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.Level != 2 || user.ParentID != 11 || user.IDPath != "0,11" || user.RealName != "商务姓名" || user.QQNumber != "12345" || user.BusinessStatus != 3 || user.RejectReason != "审核资料" || user.Password != "original-hash" {
|
||||
t.Fatalf("迁移或身份加载丢失商务字段: %+v", user)
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,25 @@ func Migrations() []migration.Step {
|
||||
},
|
||||
{ID: "202608210002_communication_surface", Migrate: ensureCommunicationSurface},
|
||||
{ID: "202608210003_communication_test_surface", Migrate: ensureCommunicationTestSurface},
|
||||
{ID: "202611030002_business_user_fields", Migrate: ensureBusinessUserFields},
|
||||
}
|
||||
}
|
||||
|
||||
func ensureBusinessUserFields(db *gorm.DB) error {
|
||||
// 与已交付迁移 SQL 的字段保持一致,仅补缺列,保留导入值和密码。
|
||||
for _, field := range []string{"Level", "ParentID", "IDPath", "RealName", "QQNumber", "BusinessStatus", "RejectReason"} {
|
||||
if !db.Migrator().HasColumn(&userPO{}, field) {
|
||||
if err := db.Migrator().AddColumn(&userPO{}, field); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if !db.Migrator().HasIndex(&userPO{}, "idx_sys_users_business_parent") {
|
||||
return db.Migrator().CreateIndex(&userPO{}, "idx_sys_users_business_parent")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Kept as a migration version for existing installations; the canonical
|
||||
// communication surface seeder is idempotent and now owns the test endpoint.
|
||||
func ensureCommunicationTestSurface(db *gorm.DB) error { return ensureCommunicationSurface(db) }
|
||||
|
||||
@@ -25,6 +25,13 @@ type userPO struct {
|
||||
OriginSetting string `gorm:"type:text"`
|
||||
PasswordUpdatedAt *time.Time
|
||||
MustChangePassword bool
|
||||
Level int `gorm:"type:tinyint unsigned;not null;default:0"`
|
||||
ParentID uint `gorm:"not null;default:0;index:idx_sys_users_business_parent"`
|
||||
IDPath string `gorm:"column:id_path;size:255;not null;default:0"`
|
||||
RealName string `gorm:"size:30;not null;default:''"`
|
||||
QQNumber string `gorm:"column:qq_number;size:20;not null;default:''"`
|
||||
BusinessStatus int `gorm:"type:tinyint unsigned;not null;default:0"`
|
||||
RejectReason string `gorm:"size:200;not null;default:''"`
|
||||
}
|
||||
|
||||
func (userPO) TableName() string { return "sys_users" }
|
||||
|
||||
@@ -93,7 +93,7 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*system.User, erro
|
||||
for _, item := range positionPOs {
|
||||
positions = append(positions, *posFromPO(item))
|
||||
}
|
||||
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil
|
||||
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt, Level: po.Level, ParentID: po.ParentID, IDPath: po.IDPath, RealName: po.RealName, QQNumber: po.QQNumber, BusinessStatus: po.BusinessStatus, RejectReason: po.RejectReason}, nil
|
||||
}
|
||||
|
||||
func toBizAuthority(po authorityPO) system.Authority {
|
||||
@@ -116,7 +116,7 @@ func baseBizUser(po *userPO) (*system.User, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil
|
||||
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt, Level: po.Level, ParentID: po.ParentID, IDPath: po.IDPath, RealName: po.RealName, QQNumber: po.QQNumber, BusinessStatus: po.BusinessStatus, RejectReason: po.RejectReason}, nil
|
||||
}
|
||||
|
||||
func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*system.User, error) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
databusiness "kra/internal/data/business"
|
||||
"kra/internal/modules/surface"
|
||||
"kra/pkg/module"
|
||||
)
|
||||
|
||||
func Definition() module.Definition {
|
||||
return module.Definition{Name: "business", Migrations: databusiness.Migrations(), Surface: module.Surface{
|
||||
APIs: surface.APIsForPrefix("/sw/", 0),
|
||||
}}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
businessmodule "kra/internal/modules/business"
|
||||
integrationmodule "kra/internal/modules/integration"
|
||||
paymentmodule "kra/internal/modules/payment"
|
||||
systemmodule "kra/internal/modules/system"
|
||||
@@ -20,5 +21,6 @@ func Catalog() module.Catalog {
|
||||
integrationmodule.Definition(),
|
||||
taskmodule.Definition(),
|
||||
paymentmodule.Definition(),
|
||||
businessmodule.Definition(),
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -16,25 +16,25 @@ func TestCatalogMigrationIDsAreUnique(t *testing.T) {
|
||||
|
||||
func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) {
|
||||
catalog := Catalog()
|
||||
if len(catalog.Definitions) != 4 {
|
||||
t.Fatalf("definitions = %d, want 4", len(catalog.Definitions))
|
||||
if len(catalog.Definitions) != 5 {
|
||||
t.Fatalf("definitions = %d, want 5", len(catalog.Definitions))
|
||||
}
|
||||
want := []string{"system", "integration", "task", "payment"}
|
||||
want := []string{"system", "integration", "task", "payment", "business"}
|
||||
for index, name := range want {
|
||||
if got := catalog.Definitions[index].Name; got != name {
|
||||
t.Fatalf("definition[%d] = %q, want %q", index, got, name)
|
||||
}
|
||||
}
|
||||
if got := catalog.MigrationSteps(); len(got) != 14 {
|
||||
t.Fatalf("module migrations = %d, want 14", len(got))
|
||||
if got := catalog.MigrationSteps(); len(got) != 16 {
|
||||
t.Fatalf("module migrations = %d, want 16", len(got))
|
||||
}
|
||||
var menus, apis int
|
||||
for _, definition := range catalog.Definitions {
|
||||
menus += len(definition.Surface.Menus)
|
||||
apis += len(definition.Surface.APIs)
|
||||
}
|
||||
if menus != 4 || apis != 18 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 4/18", menus, apis)
|
||||
if menus != 4 || apis != 20 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 4/20", menus, apis)
|
||||
}
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
||||
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
||||
|
||||
@@ -41,6 +41,8 @@ type routeValue struct {
|
||||
// Gin router files still bind handlers; tests keep those registrations aligned
|
||||
// with this catalog while public, audit and API metadata live only here.
|
||||
var routes = map[string]routeValue{
|
||||
"GET /sw/user/preference": {group: "商务偏好", description: "获取当前用户商务偏好"},
|
||||
"PUT /sw/user/preference": {group: "商务偏好", description: "更新当前用户商务偏好", audit: true},
|
||||
"DELETE /api/deleteApisByIds": {group: "api", description: "批量删除api", audit: true},
|
||||
"DELETE /dataAccessLog/deleteDataAccessLogByIds": {group: "数据权限审计", description: "批量删除数据权限审计日志", audit: true},
|
||||
"DELETE /department/deleteDepartment": {group: "部门", description: "删除部门", audit: true},
|
||||
|
||||
@@ -26,6 +26,7 @@ func NewGinEngine(runtime *config.Store, access *systemservice.AccessControlServ
|
||||
|
||||
func emptyHandlers() *handler.Set {
|
||||
return &handler.Set{
|
||||
Business: &handler.Business{},
|
||||
Authority: &handler.Authority{}, Menu: &handler.Menu{}, API: &handler.API{},
|
||||
Permission: &handler.Permission{}, Organization: &handler.Organization{},
|
||||
Announcement: &handler.Announcement{}, Email: &handler.Email{}, Payment: &handler.Payment{}, Task: &handler.Task{},
|
||||
@@ -97,7 +98,7 @@ func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
||||
t.Fatalf("registered route log count = %d, want %d", got, want)
|
||||
}
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":197`) {
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":199`) {
|
||||
t.Fatalf("startup route summary is missing: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"kra/internal/service/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type Business struct {
|
||||
preference *business.UserPreferenceService
|
||||
}
|
||||
|
||||
func NewBusiness(preference *business.UserPreferenceService) *Business {
|
||||
return &Business{preference: preference}
|
||||
}
|
||||
|
||||
func (h *Business) UserPreference(c *gin.Context) {
|
||||
value, err := h.preference.Current(c.Request.Context())
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, value)
|
||||
}
|
||||
|
||||
func (h *Business) UpdateUserPreference(c *gin.Context) {
|
||||
var req dto.UserPreferenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Fail(c, "偏好参数格式错误")
|
||||
return
|
||||
}
|
||||
if err := h.preference.UpdateCurrent(c.Request.Context(), &req); err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OK(c)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import "github.com/google/wire"
|
||||
// ProviderSet wires the system HTTP handlers. Set itself is filled field by
|
||||
// field so adding a handler only means adding its constructor here.
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewBusiness,
|
||||
NewAuthority, NewMenu, NewAPI, NewPermission, NewOrganization,
|
||||
NewAnnouncement, NewEmail, NewPayment, NewTask, NewMedia, NewAudit,
|
||||
NewExport, NewVersion, NewDictionary, NewParameter, NewAPIToken,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
type Set struct {
|
||||
Business *Business
|
||||
SMS *SMS
|
||||
Authority *Authority
|
||||
Menu *Menu
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"kra/internal/server/handler"
|
||||
)
|
||||
|
||||
func RegisterBusiness(private *gin.RouterGroup, h *handler.Business) {
|
||||
private.GET("/sw/user/preference", h.UserPreference)
|
||||
private.PUT("/sw/user/preference", h.UpdateUserPreference)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ func (r *Routes) RegisterRoutes(public, private *gin.RouterGroup, engine *gin.En
|
||||
return
|
||||
}
|
||||
registrations := []func(){
|
||||
func() { RegisterBusiness(private, r.handlers.Business) },
|
||||
func() { RegisterPublic(public, engine, r.handlers.Public) },
|
||||
func() { RegisterUser(private, r.handlers.User) },
|
||||
func() { RegisterNavigation(private, r.handlers.Navigation) },
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type PreferenceUsecase interface {
|
||||
Current(context.Context) (*business.UserPreference, error)
|
||||
UpdateCurrent(context.Context, business.UserPreference) error
|
||||
}
|
||||
|
||||
type UserPreferenceService struct{ uc PreferenceUsecase }
|
||||
|
||||
func NewUserPreferenceService(uc *business.UserPreferenceUsecase) *UserPreferenceService {
|
||||
return &UserPreferenceService{uc: uc}
|
||||
}
|
||||
|
||||
func (s *UserPreferenceService) Current(ctx context.Context) (*dto.UserPreferenceResponse, error) {
|
||||
pref, err := s.uc.Current(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.UserPreferenceResponse{IncludeSubPromote: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup}, nil
|
||||
}
|
||||
|
||||
func (s *UserPreferenceService) UpdateCurrent(ctx context.Context, req *dto.UserPreferenceRequest) error {
|
||||
if req == nil || req.IncludeSubPromote == nil || req.IDCardDedup == nil {
|
||||
return business.ErrPreferenceRequired
|
||||
}
|
||||
return s.uc.UpdateCurrent(ctx, business.UserPreference{IncludeSubPromote: *req.IncludeSubPromote, IDCardDedup: *req.IDCardDedup})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type preferenceFake struct {
|
||||
calls int
|
||||
value business.UserPreference
|
||||
}
|
||||
|
||||
func (f *preferenceFake) Current(context.Context) (*business.UserPreference, error) {
|
||||
return &f.value, nil
|
||||
}
|
||||
func (f *preferenceFake) UpdateCurrent(_ context.Context, p business.UserPreference) error {
|
||||
f.calls++
|
||||
f.value = p
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPreferenceRequiresBothFieldsButAcceptsFalse(t *testing.T) {
|
||||
f := &preferenceFake{}
|
||||
s := &UserPreferenceService{uc: f}
|
||||
off := false
|
||||
for _, req := range []*dto.UserPreferenceRequest{nil, {}, {IncludeSubPromote: &off}, {IDCardDedup: &off}} {
|
||||
if err := s.UpdateCurrent(context.Background(), req); err != business.ErrPreferenceRequired {
|
||||
t.Fatalf("缺字段应被拒绝: %v", err)
|
||||
}
|
||||
}
|
||||
if f.calls != 0 {
|
||||
t.Fatal("校验失败后不应调用用例")
|
||||
}
|
||||
if err := s.UpdateCurrent(context.Background(), &dto.UserPreferenceRequest{IncludeSubPromote: &off, IDCardDedup: &off}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.calls != 1 || f.value.IncludeSubPromote || f.value.IDCardDedup {
|
||||
t.Fatalf("false 值未完整传递: %+v", f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dto
|
||||
|
||||
type UserPreferenceRequest struct {
|
||||
IncludeSubPromote *bool `json:"includeSubPromote"`
|
||||
IDCardDedup *bool `json:"idcardDedup"`
|
||||
}
|
||||
|
||||
type UserPreferenceResponse struct {
|
||||
IncludeSubPromote bool `json:"includeSubPromote"`
|
||||
IDCardDedup bool `json:"idcardDedup"`
|
||||
}
|
||||
@@ -73,6 +73,13 @@ type SwitchAuthorityRequest struct {
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
Level int `json:"level"`
|
||||
ParentID uint `json:"parentId"`
|
||||
IDPath string `json:"idPath"`
|
||||
RealName string `json:"realName"`
|
||||
QQNumber string `json:"qqNumber"`
|
||||
BusinessStatus int `json:"businessStatus"`
|
||||
RejectReason string `json:"rejectReason"`
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
businessservice "kra/internal/service/business"
|
||||
integrationservice "kra/internal/service/integration"
|
||||
paymentservice "kra/internal/service/payment"
|
||||
systemservice "kra/internal/service/system"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
// ProviderSet aggregates all service modules for Wire.
|
||||
var ProviderSet = wire.NewSet(
|
||||
businessservice.NewUserPreferenceService,
|
||||
systemservice.ProviderSet,
|
||||
paymentservice.ProviderSet,
|
||||
integrationservice.ProviderSet,
|
||||
|
||||
@@ -40,5 +40,5 @@ func convertUser(user *system.User) *dto.UserResponse {
|
||||
if user.Department != nil {
|
||||
department = departmentResponse(user.Department)
|
||||
}
|
||||
return &dto.UserResponse{ID: user.ID, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, UUID: user.UUID, Username: user.Username, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Authority: convertAuthority(user.Authority), Authorities: authorities, DeptID: user.DeptID, Department: department, Departments: departments, Positions: positions, Phone: user.Phone, Email: user.Email, Enable: user.Enable, OriginSetting: user.OriginSetting, PasswordUpdatedAt: user.PasswordUpdatedAt}
|
||||
return &dto.UserResponse{ID: user.ID, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, UUID: user.UUID, Username: user.Username, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Authority: convertAuthority(user.Authority), Authorities: authorities, DeptID: user.DeptID, Department: department, Departments: departments, Positions: positions, Phone: user.Phone, Email: user.Email, Enable: user.Enable, OriginSetting: user.OriginSetting, PasswordUpdatedAt: user.PasswordUpdatedAt, Level: user.Level, ParentID: user.ParentID, IDPath: user.IDPath, RealName: user.RealName, QQNumber: user.QQNumber, BusinessStatus: user.BusinessStatus, RejectReason: user.RejectReason}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""从指定旧仓库和现有迁移 SQL 生成可复核的迁移清单,不访问数据库。"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo, *args):
|
||||
return subprocess.check_output(["git", "-C", str(repo), *args], text=True, encoding="utf-8").strip()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", required=True, type=Path)
|
||||
parser.add_argument("--baseline", default="d31082f4a7816d4c83544b8f54e6f42a3b2132b1")
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = args.source.resolve()
|
||||
sql = (root / "docs/sw6-to-kra/sw_to_kra_migrate.sql").read_text(encoding="utf-8")
|
||||
contracts = [dict(zip(("method", "source_path", "target_path", "permission", "controller"), row))
|
||||
for row in re.findall(r"\('(GET|POST|PUT|DELETE)','([^']+)','([^']+)','([^']+)','([^']+)'\)", sql)]
|
||||
changes = []
|
||||
for path in git(source, "diff", "--name-only", args.baseline, "main").splitlines():
|
||||
# 只登记路径与摘要,不把旧配置中的凭据写入新仓库。
|
||||
if not (path.endswith(".java") or "/mapper/" in path and path.endswith(".xml")):
|
||||
continue
|
||||
file = source / path
|
||||
changes.append({"path": path, "exists": file.is_file(),
|
||||
"sha256": hashlib.sha256(file.read_bytes()).hexdigest() if file.is_file() else None})
|
||||
custom = []
|
||||
for file in sorted((source / "ruoyi-admin/src/main/java").rglob("*Controller.java")):
|
||||
text = file.read_text(encoding="utf-8")
|
||||
if file.name == "ForgotPasswordController.java":
|
||||
custom.append({"path": file.relative_to(source).as_posix(), "source": text})
|
||||
result = {"baseline": args.baseline, "backend_head": git(source, "rev-parse", "main"),
|
||||
"frontend_head": git(source / "web", "rev-parse", "dev/dev1.0"),
|
||||
"contracts": contracts, "changed_sources": changes,
|
||||
"additional_controllers": [item["path"] for item in custom]}
|
||||
target = root / "docs/sw6-to-kra/source-inventory.json"
|
||||
target.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"已登记 {len(contracts)} 个 SQL 接口契约、{len(changes)} 个变更源文件。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,4 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
export const getUserPreference = () => service({ url: '/sw/user/preference', method: 'get' })
|
||||
export const updateUserPreference = (data) => service({ url: '/sw/user/preference', method: 'put', data })
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<el-popover placement="bottom-end" :width="250" trigger="click" @show="load">
|
||||
<template #reference>
|
||||
<icon-button icon="lucide:sliders-horizontal" label="统计口径" />
|
||||
</template>
|
||||
<div v-loading="loading" class="space-y-3">
|
||||
<div class="font-medium">全局统计口径</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>包含下级渠道</span>
|
||||
<el-switch :model-value="preference.includeSubPromote" :disabled="loading || !loaded" @change="save('includeSubPromote', $event)" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>身份证去重</span>
|
||||
<el-switch :model-value="preference.idcardDedup" :disabled="loading || !loaded" @change="save('idcardDedup', $event)" />
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">用于商务数据查询,充值金额不受身份证去重影响。</p>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import IconButton from '@/components/iconButton/index.vue'
|
||||
import { getUserPreference, updateUserPreference } from '@/api/business'
|
||||
import { emitter } from '@/utils/bus'
|
||||
|
||||
const preference = ref({ includeSubPromote: true, idcardDedup: true })
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
loaded.value = false
|
||||
try {
|
||||
const res = await getUserPreference()
|
||||
if (res.code === 0) {
|
||||
preference.value = res.data
|
||||
loaded.value = true
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const save = async (key, value) => {
|
||||
if (loading.value || !loaded.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const next = { ...preference.value, [key]: value }
|
||||
const res = await updateUserPreference(next)
|
||||
if (res.code === 0) {
|
||||
preference.value = next
|
||||
ElMessage.success('统计口径已更新')
|
||||
emitter.emit('reload')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<!-- 工具按钮组:扁平图标,无独立描边/阴影,hover 时图标 morph 为文案(取代 tooltip)。 -->
|
||||
<div class="flex items-center gap-1 mx-3">
|
||||
<business-preference />
|
||||
<icon-button
|
||||
v-if="settings.header.search.visible"
|
||||
icon="lucide:search"
|
||||
@@ -41,6 +42,7 @@
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import CommandMenu from '@/components/commandMenu/index.vue'
|
||||
import IconButton from '@/components/iconButton/index.vue'
|
||||
import BusinessPreference from '@/components/businessPreference/index.vue'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { settings } = storeToRefs(themeStore)
|
||||
|
||||
Reference in New Issue
Block a user